Add scripted trainer battle foundation and native move AI
Checks / web (push) Canceled after 0s

This commit is contained in:
Sébastien QUEROL committed 2026-09-24 09:33:33 +02:00
1 parent 6a74f59e46
commit 3e92936cb9
20 files changed
+1417 -32

No files matched your search

+43 -3
View File
@@ -154,7 +154,12 @@ export type ScriptEffect =
selected: number;
}
/** startbattle: answered with wBattleResult. */
| { kind: "battle"; wild: { species: number; level: number } }
| {
kind: "battle";
wild?: { species: number; level: number };
trainer?: { classId: number; id: number; winText?: number; lossText?: number };
canLose?: boolean;
}
/** newloadmap: the world enters this map after the script ends. */
| { kind: "warp"; map: string; x: number; y: number };
@@ -266,6 +271,10 @@ export class ScriptRun {
value = 0,
battleResult = 0,
spawn = 0;
let wild = this.options.wild;
let trainer: { classId: number; id: number } | undefined;
let winText: number | undefined, lossText: number | undefined;
let canLose = false;
// One array for the whole run: specials append to and clear it in place.
const pages: string[] = [];
const byte = () => r.byte(p++);
@@ -463,18 +472,49 @@ export class ScriptRun {
p = SYMBOLS.fruitTreeScript;
break;
}
case 0x1e: {
// Only VAR_BATTLETYPE's normal and CANLOSE modes are translated.
const variable = byte(), setting = byte();
if (variable !== 3 || (setting !== 0 && setting !== 1))
throw new UnsupportedScript(offset, opcode, " (battle type or variable)");
canLose = setting === 1;
break;
}
case 0x5b:
trainer = undefined;
wild = this.options.wild;
break; // randomwildmon clears wBattleScriptFlags.
case 0x5d:
wild = { species: byte(), level: byte() };
trainer = undefined;
break;
case 0x5e:
trainer = { classId: byte(), id: byte() };
wild = undefined;
break;
case 0x64: {
// Null pointers request the battle engine's default end text.
const bank = r.bank(p), won = word(), lost = word();
winText = won ? r.address(bank, won) : undefined;
lossText = lost ? r.address(bank, lost) : undefined;
break;
}
case 0x5f: {
if (!this.options.wild) throw new UnsupportedScript(offset, opcode, " (trainer battle)");
const result = yield synchronous({ kind: "battle", wild: this.options.wild });
if (!wild && !trainer) throw new UnsupportedScript(offset, opcode, " (no opponent)");
yield* flush();
const result = yield synchronous({
kind: "battle", wild,
trainer: trainer && { ...trainer, winText, lossText }, canLose,
});
if (typeof result !== "number") throw new Error("Invalid battle result.");
battleResult = result;
value = result & 0x3f; // Script_startbattle also updates wScriptVar.
break;
}
case 0x60:
// Script_reloadmapafterbattle: a loss continues with the whiteout.
if ((battleResult & 0x3f) === 1) p = SYMBOLS.battleWhiteoutScript;
canLose = false;
break;
case 0x8a: {
byte(); // newloadmap MAPSETUP_WARP
+2
View File
@@ -71,6 +71,7 @@ const CHARACTERS: Record<number, string> = {
export interface TextContext {
player: string;
rival: string;
trainer?: string;
mom: string;
/** Native strings stored at the RAM addresses named by text_ram. */
strings: Readonly<Record<number, string>>;
@@ -126,6 +127,7 @@ function character(code: number, context: TextContext): string | undefined {
if (code === 0x5a) return battler(context, context.battle?.turn === 1);
if (code === 0x52 || code === 0x14) return context.player;
if (code === 0x53) return context.rival;
if (code === 0x3f) return context.trainer;
if (code === 0x49) return context.mom;
if (code >= 0x80 && code <= 0x99)
return String.fromCharCode(65 + code - 0x80);