在 Pokémon 反编译项目中,"trainer" 相关的脚本指令用于触发训练家对战。核心指令是 trainerbattle ,它通过一系列宏封装来简化使用。下面从底层到上层逐一说明。
一、底层操作码
在 script_commands.h 中定义了 4 个相关的操作码:

二、核心宏 trainerbattle (最底层)
定义在 asm/macros/event.inc 。
.macro trainerbattle type:req, localIdA:req, trainer_a:req, intro_text_a:req,
lose_text_a:req, event_script_a:req, localIdB:req,
trainer_b:req, intro_text_b:req, lose_text_b:req,
event_script_b:req, victory_text:req, cannot_battle:req,
isDouble:req, playMusicA:req, playMusicB:req,
isRematch:req, rival_battle_flags=0它编码成一个二进制结构,C 端的对应结构体定义在 battle_setup.h :
typedef union PACKED TrainerBattleParameter{
struct PACKED _TrainerBattleParameter
{
u8 isDoubleBattle:1; // 位0:是否双打
u8 isRematch:1; // 位1:是否再战
u8 playMusicA:1; // 位2:是否播放训练家A的音乐
u8 playMusicB:1; // 位3:是否播放训练家B的音乐
u8 mode:4; // 位4-7:战斗模式
u8 objEventLocalIdA; // 训练家A的地图对象ID
u16 opponentA; // 训练家A的ID(TRAINER_xxx)
u8 *introTextA; // 训练家A的出战文本指针
u8 *defeatTextA; // 训练家A的战败文本指针
u8 *battleScriptRetAddrA;// 训练家A的战后脚本指针
u8 objEventLocalIdB; // 训练家B的地图对象ID
u16 opponentB; // 训练家B的ID
u8 *introTextB; // 训练家B的出战文本
u8 *defeatTextB; // 训练家B的战败文本
u8 *battleScriptRetAddrB;// 训练家B的战后脚本
u8 *victoryText; // 胜利文本(早期劲敌使用)
u8 *cannotBattleText; // 无法对战时的文本(双打时PM不足)
u8 rivalBattleFlags; // 劲敌战斗标志位
} params;
u8 data[sizeof(struct _TrainerBattleParameter)];
} TrainerBattleParameter;
三、战斗模式常量
定义在 battle_setup.h :
#define TRAINER_BATTLE_SINGLE 0 // 普通单打define TRAINER_BATTLE_CONTINUE_SCRIPT_NO_MUSIC 1 // 单打+继续脚本(无音乐切换)
define TRAINER_BATTLE_CONTINUE_SCRIPT 2 // 单打+继续脚本
define TRAINER_BATTLE_SINGLE_NO_INTRO_TEXT 3 // 单打(无出战文本)
define TRAINER_BATTLE_DOUBLE 4 // 双打
define TRAINER_BATTLE_REMATCH 5 // 再战(单打)
define TRAINER_BATTLE_CONTINUE_SCRIPT_DOUBLE 6 // 双打+继续脚本
define TRAINER_BATTLE_REMATCH_DOUBLE 7 // 再战(双打)
define TRAINER_BATTLE_CONTINUE_SCRIPT_DOUBLE_NO_MUSIC 8 // 双打+继续脚本(无音乐切换)
define TRAINER_BATTLE_TWO_TRAINERS_NO_INTRO 13 // 双打(两名独立训练家,无出场文本)
define TRAINER_BATTLE_EARLY_RIVAL 14 // 早期劲敌战斗
四、常用封装的宏(实际开发中使用的)
4.1 trainerbattle_single — 普通单打对战
.macro trainerbattle_single trainer:req, intro_text:req, lose_text:req, event_script=FALSE, music=TRUE</code></pre><p>参数说明:</p><p>- trainer — 训练家 ID,如 TRAINER_ROXANNE<br>- intro_text — 训练家出战时的对话文本<br>- lose_text — 训练家战败时的对话文本<br>- event_script — 选填。战后执行的脚本(不需要则填 FALSE )<br>- music — 选填。是否切换为训练家战斗音乐(默认 TRUE )<br>实际用法示例:</p><p>最简单的用法 (战后不执行额外脚本):</p><pre class="enlighter-pre"><code class="gl">Route127_EventScript_Henry::
trainerbattle_single TRAINER_HENRY, Route127_Text_HenryIntro, Route127_Text_HenryDefeat
msgbox Route127_Text_HenryPostBattle, MSGBOX_AUTOCLOSE
end</code></pre><p>战后流程:战斗结束后 → 显示战后消息 → 结束。</p><p>带战后脚本的用法 (用于注册 Match Call 等):</p><pre class="enlighter-pre"><code class="gl">Route127_EventScript_Koji::
trainerbattle_single TRAINER_KOJI_1, Route127_Text_KojiIntro,
Route127_Text_KojiDefeat, Route127_EventScript_RegisterKoji
specialvar VAR_RESULT, ShouldTryRematchBattle
goto_if_eq VAR_RESULT, TRUE, Route127_EventScript_RematchKoji
msgbox Route127_Text_KojiPostBattle, MSGBOX_DEFAULT
release
end</code></pre><p>战后流程:战斗结束 → 执行 Route127_EventScript_RegisterKoji (注册对战寻呼)→ 回到继续执行后续脚本。</p><p>trainerbattle_single 的内部逻辑:</p><p>- 不传 event_script → 使用 TRAINER_BATTLE_SINGLE (模式0)<br>- 传了 event_script + music=TRUE → 使用 TRAINER_BATTLE_CONTINUE_SCRIPT (模式2)<br>- 传了 event_script + music=FALSE → 使用 TRAINER_BATTLE_CONTINUE_SCRIPT_NO_MUSIC (模式1)</p><p><strong>4.2 trainerbattle_double — 双打对战(与同一训练家)</strong></p><pre class="enlighter-pre"><code class="gl">.macro trainerbattle_double trainer:req, intro_text:req, lose_text:req,
not_enough_pkmn_text:req, event_script=FALSE,
music=TRUE</code></pre><p>参数说明:</p><p>- 相较单打多了一个 not_enough_pkmn_text — 玩家 PM 不足 2 只时显示的文本<br>实际用法示例:</p><pre class="enlighter-pre"><code class="gl">Route103_EventScript_Amy::
trainerbattle_double TRAINER_AMY_AND_LIV_1, Route103_Text_AmyIntro,
Route103_Text_AmyDefeated, Route103_Text_AmyNotEnoughPokemon,
Route102_EventScript_AmyRegisterMatchCallAfterBattle
specialvar VAR_RESULT, ShouldTryRematchBattle
goto_if_eq VAR_RESULT, TRUE, Route102_EventScript_AmyRematch
msgbox Route103_Text_AmyPostBattle, MSGBOX_AUTOCLOSE
end</code></pre><p>注意这里的 TRAINER_AMY_AND_LIV_1 是 一个 训练家数据条目,但其队伍设置中包含多只 PM,战斗时强制双打。</p><p><strong>4.3 trainerbattle_rematch — 再战(单打)</strong></p><pre class="enlighter-pre"><code class="gl">.macro trainerbattle_rematch trainer:req, intro_text:req, lose_text:req</code></pre><p>实际用法示例:</p><pre class="enlighter-pre"><code class="gl">Route127_EventScript_RematchKoji::
trainerbattle_rematch TRAINER_KOJI_1, Route127_Text_KojiRematchIntro,
Route127_Text_KojiRematchDefeat
msgbox Route127_Text_KojiPostRematch, MSGBOX_AUTOCLOSE
end</code></pre><p>它会自动将 isRematch 标志位置 1,并且 BattleSetup_ConfigureTrainerBattle 中会调用 GetRematchTrainerId() 来获取强化后的训练家数据。</p><p><strong>4.4 trainerbattle_rematch_double — 再战(双打)</strong></p><pre class="enlighter-pre"><code class="gl">.macro trainerbattle_rematch_double trainer:req, intro_text:req, lose_text:req,
not_enough_pkmn_text:req</code></pre><p>与再战单打类似,但强制双打。</p><p><strong>4.5 trainerbattle_no_intro — 无开场白的单打</strong></p><pre class="enlighter-pre"><code class="gl">.macro trainerbattle_no_intro trainer:req, lose_text:req</code></pre><p>不显示训练家的出场对话,直接进入战斗。</p><p><strong>4.6 trainerbattle_two_trainers — 两名独立训练家的双打</strong></p><pre class="enlighter-pre"><code class="gl">.macro trainerbattle_two_trainers trainer_a:req, lose_text_a:req,
trainer_b:req, lose_text_b:req</code></pre><p>用于两名独立的 NPC 组队与玩家双打的情况。 localIdA 和 localIdB 分别对应两个地图对象。</p><p><strong>4.7 trainerbattle_earlyrival — 早期劲敌战斗</strong></p><pre class="enlighter-pre"><code class="gl">.macro trainerbattle_earlyrival trainer:req, flags:req, lose_text:req, victory_text:req</code></pre><p>- flags — 劲敌标志位(控制治疗后是否复活等行为)<br>- victory_text — 玩家胜利后的文本</p><h2>五、执行流程(数据流)</h2><pre class="enlighter-pre"><code class="gl">脚本引擎执行 trainerbattle 指令
│
▼
ScrCmd_trainerbattle() ← [src/scrcmd.c]
- TrainerBattleLoadArgs() — 解析二进制参数 → gTrainerBattleParameter
- BattleSetup_ConfigureTrainerBattle()
根据 mode 返回对应的脚本入口
│
▼
进入战斗流程脚本 ← [data/scripts/trainer_battle.inc]
EventScript_TryDoNormalTrainerBattle:
lock → faceplayer → 训练家转身动画
→ 检查是否已打过(GetTrainerFlag)
→ 播放音乐(PlayTrainerEncounterMusic)
→ 显示训练家出场文本
→ dotrainerbattle ← 真正触发战斗!
│
▼
ScrCmd_dotrainerbattle() → BattleSetup_StartTrainerBattle()
│
▼
战斗结束
│
▼
gotobeatenscript → ScrCmd_gotobeatenscript()
跳转到战斗结束后继续执行脚本
六、总结——如何选择正确的宏

核心要点是: event_script 参数决定了战斗结束后是否返回原来的脚本继续执行后面的指令 。如果传了 event_script ,战斗结束后会先跳转到该脚本执行,然后再回到 trainerbattle 的下一条指令继续。如果不传,则战斗结束后直接继续执行下一条指令。
发表评论