Honor trainer battleTheme override (fixes #945)

trainers.battleTheme validated and merged onto the trainer record but was
never read: battle music came solely from data.audio.battle[kind] where
kind is computeMusicKind()'s final/gym/trainer/wild.  Route both battle-
theme start sites through a single choke point:

- BattleState:playBattleTheme() cues Music.playBattle with the override
  (self.trainer.battleTheme via battleTheme()), defaulting to the kind
  when unset, so vanilla fights and #782's non-gym Giovanni are unchanged.
- BattleState:enter() and OverworldController:pushBattle() both call it.
- Music.playBattle gains an optional 4th song arg that overrides the kind
  default, and real call sites now populate the music.select trainerId.
- Victory jingles stay kind-based: a custom battle theme has no derivable
  win-variant.

New ROM-free T2 suite tests/engine/trainer_battle_theme_bug945.lua covers
mod load, override resolution, the choke point, and the nil-override
parity gate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Shane McGovern
2026-08-07 10:53:42 +01:00
parent 112120e8fe
commit ed8a89c5ce
5 changed files with 220 additions and 7 deletions
+21 -2
View File
@@ -1411,6 +1411,26 @@ function BattleState:computeMusicKind()
return "wild"
end
-- a mod-set per-trainer battle theme (trainers.battleTheme, an audio.songs
-- id); nil for vanilla trainers, so the kind default is untouched (#782)
function BattleState:battleTheme()
local trainer = self.trainer
if trainer and trainer.battleTheme then return trainer.battleTheme end
return nil
end
-- the battle-theme cue for this battle: the mod-set trainer battleTheme
-- when the class has one, else the kind default. The single choke point
-- both the transition-wipe start (OverworldController:pushBattle) and
-- enter() route through, so a per-trainer override can't drift between
-- them. self.musicKind is set by enter(); pushBattle runs before that,
-- so compute it here when absent.
function BattleState:playBattleTheme()
require("src.core.Music").playBattle(self.data,
self.musicKind or self:computeMusicKind(),
self.trainer and self.trainer.id, self:battleTheme())
end
-- side tables mirror the singles battlers; called before every
-- battler-switch notification so sides[i].battlers[1] stays honest
function BattleState:syncSides()
@@ -1462,7 +1482,6 @@ function BattleState:enter()
.. Strings("%s blacked\nout!", name), blackedOut))
return
end
local Music = require("src.core.Music")
self.musicKind = self:computeMusicKind()
if self.isGymLeader then
require("src.world.PikachuFollower")
@@ -1472,7 +1491,7 @@ function BattleState:enter()
-- (audio/play_battle_music.asm runs before the transition, and
-- Music.play no-ops on the same song); this covers battles pushed
-- without a transition (link battles, scripted pushes)
Music.playBattle(self.data, self.musicKind)
self:playBattleTheme()
-- intro presentation (SlidePlayerAndEnemySilhouettesOnScreen): both
-- sides slide in; the trainer pics stay up until the send-outs
-- BATTLE BG "world" drops this battle's opacity so StateStack keeps drawing