i did it for greg

This commit is contained in:
bryanthaboi
2026-08-14 17:07:06 -04:00
parent 4395792226
commit c8f6c7241b
9 changed files with 205 additions and 12 deletions
+7
View File
@@ -929,6 +929,10 @@ mod.content.sfx:register("SFX_MOD_CHIME", { file = "chime.ogg" })
| field | type | required | | field | type | required |
|---|---|---| |---|---|---|
| `anchorX` | number | no |
| `anchorY` | number | no |
| `frameHeight` | integer >= 1 | no |
| `frameWidth` | integer >= 1 | no |
| `frames` | integer >= 1 | yes | | `frames` | integer >= 1 | yes |
| `id` | string | no | | `id` | string | no |
| `image` | file path | yes | | `image` | file path | yes |
@@ -1100,6 +1104,7 @@ mod.content.tokens:register("CLOCK", function(game) return "12" end)
| `paletteSource` | string | no | | `paletteSource` | string | no |
| `parties` | list of list of {level, species} | yes | | `parties` | list of list of {level, species} | yes |
| `pic` | file path | no | | `pic` | file path | no |
| `trueColor` | boolean | no |
```lua ```lua
mod.content.trainers:patch("OPP_BROCK", { baseMoney = 99 }) mod.content.trainers:patch("OPP_BROCK", { baseMoney = 99 })
@@ -1122,7 +1127,9 @@ do not.
| `index` | integer 0..255 | no | | `index` | integer 0..255 | no |
| `items` | list of items id | no | | `items` | list of items id | no |
| `name` | string | yes | | `name` | string | yes |
| `pic` | file path | no |
| `trainers` | list of {id?, index?, name, party, trainerType?} | yes | | `trainers` | list of {id?, index?, name, party, trainerType?} | yes |
| `trueColor` | boolean | no |
```lua ```lua
mod.content.trainers:patch("BEAUTY", { baseMoney = 99 }) mod.content.trainers:patch("BEAUTY", { baseMoney = 99 })
+23 -3
View File
@@ -316,6 +316,27 @@ function BattleState.trainerPicPath(data, trainer, oppClass, partyIndex)
return base and base.pic or nil return base and base.pic or nil
end end
-- trueColor on the trainer record, or on the basePic it reuses when the
-- subclass does not set the flag itself. Explicit false stays false.
function BattleState.trainerTrueColor(data, trainer)
if not trainer then return false end
if trainer.trueColor ~= nil then
return trainer.trueColor and true or false
end
local base = trainer.basePic and data and data.trainers
and data.trainers[trainer.basePic]
return (base and base.trueColor) and true or false
end
-- Load a trainer frontpic through getImage so a trueColor portrait skips
-- the 4-shade quantize the same way a species pic does.
function BattleState.trainerSprite(data, trainer, oppClass, partyIndex)
return getImage(
BattleState.trainerPicPath(data, trainer, oppClass, partyIndex),
BattleState.trainerPalette(data, trainer),
BattleState.trainerTrueColor(data, trainer))
end
-- The battle-BGP fade variant of a pic (AnimationFlashScreen and the -- The battle-BGP fade variant of a pic (AnimationFlashScreen and the
-- SetAnimationBGPalette effects remap the four BG shades; on the SGB -- SetAnimationBGPalette effects remap the four BG shades; on the SGB
-- the colorizer then colors the REMAPPED shade, so a faded pic shows -- the colorizer then colors the REMAPPED shade, so a faded pic shows
@@ -756,9 +777,8 @@ function BattleState.newTrainer(game, oppClass, partyIndex)
-- MonsterPalettes[0] = PAL_MEWMON -- InitBattleCommon zeroes -- MonsterPalettes[0] = PAL_MEWMON -- InitBattleCommon zeroes
-- wEnemyMonSpecies2 before the intro's SET_PAL_BATTLE -- wEnemyMonSpecies2 before the intro's SET_PAL_BATTLE
-- (engine/battle/core.asm:6682, engine/gfx/palettes.asm SetPal_Battle) -- (engine/battle/core.asm:6682, engine/gfx/palettes.asm SetPal_Battle)
self.trainerPic = getImage( self.trainerPic = BattleState.trainerSprite(
BattleState.trainerPicPath(game.data, self.trainer, oppClass, partyIndex), game.data, self.trainer, oppClass, partyIndex)
BattleState.trainerPalette(game.data, self.trainer))
self.introText = Strings("%s wants\nto fight!", self.trainer.name) self.introText = Strings("%s wants\nto fight!", self.trainer.name)
return self return self
end end
+4 -2
View File
@@ -1797,7 +1797,8 @@ local function buildBattleState()
"throwBall", "ballChain", "tossAnimFor", "ballFlicker", "ballMissMessage", "throwBall", "ballChain", "tossAnimFor", "ballFlicker", "ballMissMessage",
"storeCaughtMon", "safariAction", "safariEnemyTurn", "drawBallRow", "storeCaughtMon", "safariAction", "safariEnemyTurn", "drawBallRow",
"drawClassic", "isWideBattleLayout", "wideLayout", "bgMode", "uiSize", "drawClassic", "isWideBattleLayout", "wideLayout", "bgMode", "uiSize",
"sgbPalettes", "trainerPalette", "trainerPicPath", "invalidate", "sgbPalettes", "trainerPalette", "trainerPicPath", "trainerTrueColor",
"trainerSprite", "invalidate",
"imageBattleScale", "resolveBattleScale", "backPlacement", "imageBattleScale", "resolveBattleScale", "backPlacement",
"frontPlacement", "StatBox", "enter", "exit", "frontPlacement", "StatBox", "enter", "exit",
}) do }) do
@@ -1884,7 +1885,8 @@ COVERAGE["src.battle.BattleState"] = {
absent = "newWild newTrainer makeSafari makeGhost makeBattler resolveTurn " absent = "newWild newTrainer makeSafari makeGhost makeBattler resolveTurn "
.. "computeDamage catchAttempt runRoll enter exit sgbPalettes " .. "computeDamage catchAttempt runRoll enter exit sgbPalettes "
.. "isWideBattleLayout wideLayout bgMode uiSize letterboxWhite " .. "isWideBattleLayout wideLayout bgMode uiSize letterboxWhite "
.. "holdsUIAnchors BG_WORLD_DIM trainerPalette trainerPicPath invalidate " .. "holdsUIAnchors BG_WORLD_DIM trainerPalette trainerPicPath "
.. "trainerTrueColor trainerSprite invalidate "
.. "backPlacement frontPlacement StatBox drawClassic drawBallRow " .. "backPlacement frontPlacement StatBox drawClassic drawBallRow "
.. "safariAction safariEnemyTurn throwBall storeCaughtMon field ruleset " .. "safariAction safariEnemyTurn throwBall storeCaughtMon field ruleset "
.. "rng oppClass partyIndex aiUses introText dead", .. "rng oppClass partyIndex aiUses introText dead",
+8
View File
@@ -1017,6 +1017,9 @@ R.trainers = {
index = f.opt(f.int(0, 255)), index = f.opt(f.int(0, 255)),
-- unused vanilla classes ship without a pic, so it cannot be required -- unused vanilla classes ship without a pic, so it cannot be required
pic = f.opt(f.path), pic = f.opt(f.path),
-- Full-color portrait: skip the 4-shade SGB/GBC remap, same flag pokemon
-- and sprites already carry.
trueColor = f.opt(f.bool),
-- Optional Advanced-mode OBJ palette source for a custom trainer portrait. -- Optional Advanced-mode OBJ palette source for a custom trainer portrait.
-- It follows the same ROM crosswalk form as sprites.paletteSource. -- It follows the same ROM crosswalk form as sprites.paletteSource.
paletteSource = f.opt(f.str), paletteSource = f.opt(f.str),
@@ -1068,6 +1071,11 @@ R.trainers = {
gen2Fields = { gen2Fields = {
id = f.opt(f.str), name = f.str, id = f.opt(f.str), name = f.str,
index = f.opt(f.int(0, 255)), index = f.opt(f.int(0, 255)),
-- class frontpic; when set, this wins over menu_gfx.battleHud.trainerPics
pic = f.opt(f.path),
-- Full-color portrait: skip the GBC 4-shade remap, same flag Gen 1
-- trainers and pokemon already carry.
trueColor = f.opt(f.bool),
baseMoney = f.opt(f.int(0)), baseMoney = f.opt(f.int(0)),
-- the class's battle theme; Gen 1 spells the same idea `battleTheme`, -- the class's battle theme; Gen 1 spells the same idea `battleTheme`,
-- but this is the extractor's own key and a strict rename would reject -- but this is the extractor's own key and a strict rename would reject
+7 -3
View File
@@ -98,14 +98,14 @@ function OakSpeech.resolvePic(game, desc, speech)
local t = desc.type local t = desc.type
if t == "trainer" then if t == "trainer" then
if speech and desc.id == "OPP_PROF_OAK" and speech.oakPic then if speech and desc.id == "OPP_PROF_OAK" and speech.oakPic then
return speech.oakPic, false, false return speech.oakPic, false, speech.oakTrueColor or false
end end
if speech and desc.id == "OPP_RIVAL1" and speech.rivalPic then if speech and desc.id == "OPP_RIVAL1" and speech.rivalPic then
return speech.rivalPic, false, false return speech.rivalPic, false, speech.rivalTrueColor or false
end end
local trainers = game.data.trainers or {} local trainers = game.data.trainers or {}
local tr = trainers[desc.id] local tr = trainers[desc.id]
return tryImage(tr and tr.pic), false, false return tryImage(tr and tr.pic), false, tr and tr.trueColor or false
elseif t == "pokemon" then elseif t == "pokemon" then
if speech and desc.id == speech.demoSpecies and speech.demoPic then if speech and desc.id == speech.demoSpecies and speech.demoPic then
return speech.demoPic, desc.flip and true or false, speech.demoTrueColor return speech.demoPic, desc.flip and true or false, speech.demoTrueColor
@@ -241,7 +241,11 @@ function OakSpeech.new(game, onDone)
self.answers = {} self.answers = {}
local trainers = game.data.trainers or {} local trainers = game.data.trainers or {}
self.oakPic = tryImage(trainers.OPP_PROF_OAK and trainers.OPP_PROF_OAK.pic) self.oakPic = tryImage(trainers.OPP_PROF_OAK and trainers.OPP_PROF_OAK.pic)
self.oakTrueColor = self.oakPic
and trainers.OPP_PROF_OAK and trainers.OPP_PROF_OAK.trueColor or false
self.rivalPic = tryImage(trainers.OPP_RIVAL1 and trainers.OPP_RIVAL1.pic) self.rivalPic = tryImage(trainers.OPP_RIVAL1 and trainers.OPP_RIVAL1.pic)
self.rivalTrueColor = self.rivalPic
and trainers.OPP_RIVAL1 and trainers.OPP_RIVAL1.trueColor or false
local oakGfx = (game.data.field and game.data.field.oakSpeech) or {} local oakGfx = (game.data.field and game.data.field.oakSpeech) or {}
self.cfg = oakGfx self.cfg = oakGfx
-- the show-off mon and the name length cap come from data; the vanilla -- the show-off mon and the name length cap come from data; the vanilla
+23 -4
View File
@@ -185,6 +185,19 @@ function BattleState:statusHUDVisible()
self) ~= false self) ~= false
end end
-- Class frontpic for the battle intro. A trainers-registry `pic` wins over
-- the extracted menu_gfx sheet; `trueColor` skips the GBC 4-shade remap.
-- Returns path, trueColor.
function BattleState.trainerArt(data, classId)
if not classId then return nil, false end
local classes = data and data.gen2Trainers and data.gen2Trainers.classes
local classDef = classes and classes[classId]
local hud = data and data.gen2MenuGfx and data.gen2MenuGfx.battleHud
local path = (classDef and classDef.pic)
or (hud and hud.trainerPics and hud.trainerPics[classId])
return path, (classDef and classDef.trueColor) and true or false
end
-- opts: battle (a Battle), onDone(outcome), save -- opts: battle (a Battle), onDone(outcome), save
function BattleState.new(game, opts) function BattleState.new(game, opts)
opts = opts or {} opts = opts or {}
@@ -308,6 +321,7 @@ function BattleState.new(game, opts)
-- pic is a cache asset, so an import made before the extractor grew that -- pic is a cache asset, so an import made before the extractor grew that
-- stage has none and the mon stands in for the whole intro. -- stage has none and the mon stands in for the whole intro.
self.showEnemyTrainer = false self.showEnemyTrainer = false
self.enemyTrainerTrueColor = false
-- The CLASS CONSTANT (BUG_CATCHER), which is what both tables this looks the -- The CLASS CONSTANT (BUG_CATCHER), which is what both tables this looks the
-- pic up in are keyed by: menu_gfx's trainerPics is written out of -- pic up in are keyed by: menu_gfx's trainerPics is written out of
-- constants.trainerClassOrder, and palettes.trainers out of the same names. -- constants.trainerClassOrder, and palettes.trainers out of the same names.
@@ -317,17 +331,19 @@ function BattleState.new(game, opts)
-- no palette for every trainer the world starts, which is all of them. -- no palette for every trainer the world starts, which is all of them.
-- `classId` is the trainers.lua key, i.e. the constant; `className` is the -- `classId` is the trainers.lua key, i.e. the constant; `className` is the
-- DISPLAY name ("BUG CATCHER", with the space) and is not a key at all. -- DISPLAY name ("BUG CATCHER", with the space) and is not a key at all.
-- A class record's own `pic` / `trueColor` (the trainers registry) wins
-- over the extracted sheet, so a mod can drop in full-color art.
local enemyTrainer = self.battle and self.battle.trainer local enemyTrainer = self.battle and self.battle.trainer
self.enemyTrainerClass = enemyTrainer self.enemyTrainerClass = enemyTrainer
and (enemyTrainer.classId or enemyTrainer.class) and (enemyTrainer.classId or enemyTrainer.class)
local trainerPics = hudGfx and hudGfx.trainerPics local trainerPath, trainerTrueColor =
local trainerPath = self.enemyTrainerClass and trainerPics BattleState.trainerArt(data, self.enemyTrainerClass)
and trainerPics[self.enemyTrainerClass]
if trainerPath then if trainerPath then
local ok, image = pcall(Assets.image, trainerPath) local ok, image = pcall(Assets.image, trainerPath)
if ok and image then if ok and image then
self.enemyTrainerImage = image self.enemyTrainerImage = image
self.enemyTrainerPath = trainerPath self.enemyTrainerPath = trainerPath
self.enemyTrainerTrueColor = trainerTrueColor and true or false
self.showEnemyTrainer = true self.showEnemyTrainer = true
end end
end end
@@ -588,7 +604,10 @@ function BattleState:drawPic(mon, back)
-- slides it out (InitEnemyTrainer, engine/battle/core.asm:7848). -- slides it out (InitEnemyTrainer, engine/battle/core.asm:7848).
local enemyTrainer = (not back) and self.showEnemyTrainer local enemyTrainer = (not back) and self.showEnemyTrainer
and self.enemyTrainerImage and self.enemyTrainerImage
if enemyTrainer then image, path = enemyTrainer, self.enemyTrainerPath end if enemyTrainer then
image, path = enemyTrainer, self.enemyTrainerPath
trueColor = self.enemyTrainerTrueColor
end
if not image then return end if not image then return end
local side = back and "player" or "enemy" local side = back and "player" or "enemy"
local anim = self:animPicState(side) local anim = self:animPicState(side)
+92
View File
@@ -0,0 +1,92 @@
-- trainers.trueColor: the same 4-shade opt-out pokemon and sprites already
-- carry, now on the trainers registry. ROM-free.
-- luajit tests/engine/trainer_true_color.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local BattleState = require("src.battle.BattleState")
local Gen2Battle = require("src.ui.gen2.BattleState")
local Schemas = require("src.mods.Schemas")
local OakSpeech = require("src.ui.OakSpeech")
local spec = Schemas.REGISTRIES.trainers
T.check(spec.fields.trueColor ~= nil,
"Gen 1 trainers schema lists trueColor")
T.check(Schemas.check(spec, "trainers", "OPP_BROCK",
{ trueColor = true }, "patch"),
"a trueColor patch validates")
T.check(not Schemas.check(spec, "trainers", "OPP_BROCK",
{ trueColor = "yes" }, "patch"),
"trueColor rejects a non-boolean")
local gen2 = Schemas.shapeFor("trainers", spec, 2)
T.check(gen2.fields.trueColor ~= nil and gen2.fields.pic ~= nil,
"Gold trainers schema lists pic and trueColor")
T.check(Schemas.check(spec, "trainers", "BEAUTY",
{ pic = "mods/x/beauty.png", trueColor = true },
"patch", 2),
"a Gold pic+trueColor patch validates")
T.eq(BattleState.trainerTrueColor(nil, nil), false,
"no trainer is not trueColor")
T.eq(BattleState.trainerTrueColor(nil, { pic = "a.png" }), false,
"a vanilla portrait is not trueColor")
T.eq(BattleState.trainerTrueColor(nil, { trueColor = true }), true,
"the record's own flag wins")
T.eq(BattleState.trainerTrueColor(nil, { trueColor = false }), false,
"explicit false stays false")
local data = {
trainers = {
BASE = { pic = "base.png", trueColor = true },
VANILLA = { pic = "vanilla.png" },
},
}
T.eq(BattleState.trainerTrueColor(data, { basePic = "BASE" }), true,
"a basePic reuse inherits the base flag")
T.eq(BattleState.trainerTrueColor(data,
{ basePic = "BASE", trueColor = false }), false,
"an explicit false on the subclass beats the base")
T.eq(BattleState.trainerTrueColor(data, { basePic = "VANILLA" }), false,
"reusing a vanilla base stays unshaded-off")
local goldData = {
gen2Trainers = {
classes = {
BEAUTY = { pic = "mods/x/beauty.png", trueColor = true },
BUG_CATCHER = {},
},
},
gen2MenuGfx = {
battleHud = {
trainerPics = {
BEAUTY = "assets/generated/trainers/beauty.png",
BUG_CATCHER = "assets/generated/trainers/bug_catcher.png",
},
},
},
}
local beautyPath, beautyTc = Gen2Battle.trainerArt(goldData, "BEAUTY")
T.eq(beautyPath, "mods/x/beauty.png",
"a class pic wins over the extracted sheet")
T.eq(beautyTc, true, "and keeps trueColor")
local bugPath, bugTc = Gen2Battle.trainerArt(goldData, "BUG_CATCHER")
T.eq(bugPath, "assets/generated/trainers/bug_catcher.png",
"a class without pic keeps the extracted sheet")
T.eq(bugTc, false, "and is not trueColor")
T.eq(select(1, Gen2Battle.trainerArt(goldData, nil)), nil,
"no class is no pic")
local game = {
data = {
trainers = {
OPP_BROCK = { pic = "brock.png", trueColor = true },
OPP_PROF_OAK = { pic = "oak.png", trueColor = true },
},
},
}
local _, _, oakTc = OakSpeech.resolvePic(game,
{ type = "trainer", id = "OPP_BROCK" })
T.eq(oakTc, true, "OakSpeech reports a trainer record's trueColor")
T.finish("trainer true color")
+15
View File
@@ -23,6 +23,7 @@ local MoveEffects = require("src.battle.MoveEffects")
local Pokemon = require("src.pokemon.Pokemon") local Pokemon = require("src.pokemon.Pokemon")
local Runtime = require("src.mods.Runtime") local Runtime = require("src.mods.Runtime")
local SaveData = require("src.core.SaveData") local SaveData = require("src.core.SaveData")
local Schemas = require("src.mods.Schemas")
local Status = require("src.battle.Status") local Status = require("src.battle.Status")
local TrainerAI = require("src.battle.TrainerAI") local TrainerAI = require("src.battle.TrainerAI")
local TurnOrder = require("src.battle.TurnOrder") local TurnOrder = require("src.battle.TurnOrder")
@@ -346,6 +347,20 @@ do
check(BattleState.trainerPicPath(Data, { basePic = "OPP_ENGINEER" }) check(BattleState.trainerPicPath(Data, { basePic = "OPP_ENGINEER" })
== Data.trainers.OPP_ENGINEER.pic, == Data.trainers.OPP_ENGINEER.pic,
"a custom trainer can reuse a base trainer portrait by id") "a custom trainer can reuse a base trainer portrait by id")
check(BattleState.trainerTrueColor(Data, { trueColor = true }) == true,
"a trainer record's trueColor flag is readable")
check(BattleState.trainerTrueColor(Data, { trueColor = false }) == false,
"explicit false stays false")
check(BattleState.trainerTrueColor(Data, { basePic = "OPP_ENGINEER" })
== false,
"a vanilla base portrait is not trueColor")
check(Schemas.check(Schemas.REGISTRIES.trainers, "trainers", "OPP_BROCK",
{ trueColor = true }, "patch"),
"a trueColor trainers patch validates against the catalog schema")
check(Schemas.check(Schemas.REGISTRIES.trainers, "trainers", "BEAUTY",
{ pic = "mods/x/beauty.png", trueColor = true },
"patch", 2),
"a Gold trainers patch can carry pic and trueColor")
end end
do do
+26
View File
@@ -370,6 +370,32 @@ check(math.abs(r - 0.4) < 1e-6 and math.abs(g - 0.7) < 1e-6
and math.abs(b - 0.9) < 1e-6, and math.abs(b - 0.9) < 1e-6,
"a trueColor pic keeps a pixel no 4-shade palette contains") "a trueColor pic keeps a pixel no 4-shade palette contains")
-- trainers.trueColor is the same opt-out on a class portrait
BattleState.invalidate()
local trainerPicData = {
trainers = {
SHADED = { pic = "assets/generated/battle/front/shaded.png" },
FULLCOLOR = { pic = "assets/generated/battle/front/full.png",
trueColor = true },
REUSED = { basePic = "FULLCOLOR" },
},
palettes = { palettes = { MEWMON = monPalette }, pokemon = {} },
}
local shadedTrainer = BattleState.trainerSprite(trainerPicData,
trainerPicData.trainers.SHADED)
r, g, b = shadedTrainer.data:getPixel(0, 0)
check(r == 0 and g == 0 and b == 1,
"a 4-shade trainer pic is palette-quantized onto its shade bucket")
local fullTrainer = BattleState.trainerSprite(trainerPicData,
trainerPicData.trainers.FULLCOLOR)
r, g, b = fullTrainer.data:getPixel(0, 0)
check(math.abs(r - 0.4) < 1e-6 and math.abs(g - 0.7) < 1e-6
and math.abs(b - 0.9) < 1e-6,
"a trueColor trainer pic keeps a pixel no 4-shade palette contains")
check(BattleState.trainerTrueColor(trainerPicData,
trainerPicData.trainers.REUSED) == true,
"a basePic reuse inherits the base portrait's trueColor flag")
-- ------- trueColor: the colors == false zone sentinel -- ------- trueColor: the colors == false zone sentinel
check(PaletteFX.zone(nil, 0, 0, 1, 1) == nil, "nil colors is still no zone") check(PaletteFX.zone(nil, 0, 0, 1, 1) == nil, "nil colors is still no zone")