Open Routes 38 to 41, Olivine and Cianwood to the Mineral Badge (#32)
Enable Route 38 and its Ecruteak gate, Route 39 with the MooMoo farm, Olivine City and its buildings, the Lighthouse's six floors, Routes 40 and 41, and Cianwood City with its gym, pharmacy, Photo Studio, Mania's house, the Poke Seer and the Lugia speech house. Olivine's port, the Battle Tower gate and Whirl Islands stay closed. The story runs from the rival in Olivine through Jasmine and Amphy, Suicune and Eusine, the SecretPotion, Chuck's boulders, the STORMBADGE, TM01 and HM02, back to Amphy's cure, Jasmine, the MINERALBADGE and TM23. Port GiveShuckle and ReturnShuckie with every return code and Mania's wrong last-Pokemon speech, PokeSeer with its high-byte-only OT check and reused caught level, PhotoStudio's no-printer path (Printer Error 2), and PlaySlowCry. wMooMooBerries joins the saved script bytes. Port trainer item AI: the 13 healing, status and X items, the highest-level gate, item use while Mean Look or Wrap prevents switching, and the original's random and state quirks, checked against the ROM over 5,632 states. Leaders with items are no longer refused. Game Corner sprites match the original frame by frame: cursor and reel positions, Golem's whole-frame flips, frame timing, the frame a removed sprite is still drawn, and the Golem roll's screen shake. The Burned Tower rival's garbage trainer header is documented as a boundary: its seen text dispatches text command $7f into home code before any battle. The playthrough continues for six stages to Jasmine; its driver can Surf, fish and travel through floors joined by holes. New tests cover trainer items, Olivine and Cianwood side content, and reference checks for trainer items, trainer sight and Game Corner animations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c4e8521d4
commit
b5c2b19094
29 files changed
+2708
-139
No files matched your search
@@ -0,0 +1,326 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { existsSync } from "node:fs";
|
||||
import { romPath, symbolsPath, loadSymbols, openReference } from "./reference-fixture";
|
||||
|
||||
test.skip(!existsSync(romPath) || !existsSync(symbolsPath), "Needs private ROM and reference symbols.");
|
||||
|
||||
// TrainerClassAttributes AI_ITEM_SWITCH bits (constants/trainer_data_constants.asm).
|
||||
const ALWAYS_USE = 1 << 4,
|
||||
UNKNOWN_USE = 1 << 5,
|
||||
CONTEXT_USE = 1 << 6;
|
||||
|
||||
/**
|
||||
* Compares the native AI_TryItem (engine/battle/ai/items.asm) against the
|
||||
* unmodified routine in the reference emulator, called directly (not
|
||||
* through AI_SwitchOrTryItem, whose switch-side dispatch predates this
|
||||
* port and is exercised by the existing switch tests/playthrough). Random
|
||||
* is redirected to a fixed byte stream shared by both runs, so the number
|
||||
* of draws is part of the comparison alongside the modeled state: the two item slots, the base reward (the misread-item bug), the
|
||||
* enemy's HP/status/substatuses, its stat levels, the turn-ending
|
||||
* substatus resets and wEnemyGoesFirst.
|
||||
*
|
||||
* The AI context/switch flags (CONTEXT_USE, ALWAYS_USE, UNKNOWN_USE) are
|
||||
* read live from TrainerClassAttributes by class, so eight reserved
|
||||
* classes (1-8) have that one byte patched into the ROM copy with every
|
||||
* combination; every other table this routine reads (AI_Items' item ids)
|
||||
* is exercised as native code, not re-read from ROM.
|
||||
*/
|
||||
test("AI_TryItem matches the original across items, flags, HP, status and turn state", async ({ page }) => {
|
||||
test.setTimeout(180000);
|
||||
const { at, routine } = loadSymbols();
|
||||
const names = [
|
||||
"hROMBank", "wInBattleTowerBattle", "wEnemyTrainerItem1", "wEnemyTrainerItem2", "wEnemyTrainerBaseReward",
|
||||
"wTrainerClass", "wOTPartyCount", "wOTPartyMon1Level", "wOTPartyMon1Status", "wOTPartyMon1HP",
|
||||
"wCurOTMon", "wEnemyMonStatus", "wEnemyMonHP", "wEnemyMonMaxHP",
|
||||
"wEnemySubStatus3", "wEnemySubStatus4", "wEnemySubStatus5", "wEnemyToxicCount", "wEnemyTurnsTaken",
|
||||
"wEnemyGoesFirst", "wEnemyFuryCutterCount", "wEnemyProtectCount", "wEnemyRageCounter",
|
||||
"wLastEnemyCounterMove", "wEnemyStatLevels", "wEnemyStats", "hBattleTurn", "wLinkMode", "wBattleMode",
|
||||
"wAttackMissed", "wEffectFailed",
|
||||
];
|
||||
const addresses = Object.fromEntries(names.map((name) => [name, at(name)]));
|
||||
const commands = { tryItem: routine("AI_TryItem"), random: routine("Random") };
|
||||
const trainerClassAttributes = routine("TrainerClassAttributes");
|
||||
// bank*0x4000 + address-0x4000: the ROM file offset (scripts/check-reference.mjs's formula).
|
||||
const flat = ({ bank, address }: { bank: number; address: number }) =>
|
||||
address < 0x4000 ? address : bank * 0x4000 + address - 0x4000;
|
||||
const attributesFlat = flat(trainerClassAttributes);
|
||||
// A used item prints text and plays a sound, and the heal items animate
|
||||
// the HP bar and redraw the HUD; all four wait on VBlank-driven input or
|
||||
// timing that this isolated, interrupt-disabled call never provides, so
|
||||
// they are neutered like Random. None of their own effects (the screen,
|
||||
// audio and the HP bar's visual fill) are part of this comparison; the
|
||||
// WRAM state AI_TryItem changes before and around them is.
|
||||
const presentationFlats = ["PrintText", "PlaySFX", "AnimateHPBar", "UpdateEnemyHUD"].map((name) => flat(routine(name)));
|
||||
|
||||
await openReference(page);
|
||||
const result = await page.evaluate(
|
||||
async ({ a, commands, attributesFlat, bits, presentationFlats }) => {
|
||||
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
|
||||
const { CrystalAssets } = await import("/src/game/rom/assets.ts");
|
||||
const { Battle } = await import("/src/game/battle/battle.ts");
|
||||
const rom = new Uint8Array(await (document.querySelector("#reference-fixture") as HTMLInputElement).files![0].arrayBuffer());
|
||||
const assets = new CrystalAssets(rom);
|
||||
// Patch Random (reference-smart-ai.spec.ts's stub and stream) and, for
|
||||
// eight reserved trainer classes (1-8), the AI_ITEM_SWITCH byte each
|
||||
// flag combination needs; AI_TryItem reads it live from ROM by class.
|
||||
const patched = rom.slice();
|
||||
patched.set([0xc3, 0x80, 0xc0], commands.random.address);
|
||||
for (const p of presentationFlats) patched[p] = 0xc9; // ret
|
||||
const NUM_TRAINER_ATTRIBUTES = 7, TRNATTR_AI_ITEM_SWITCH = 5;
|
||||
bits.forEach((flags: number, i: number) => {
|
||||
const p = attributesFlat + i * NUM_TRAINER_ATTRIBUTES + TRNATTR_AI_ITEM_SWITCH;
|
||||
patched[p] = flags;
|
||||
patched[p + 1] = 0;
|
||||
});
|
||||
const reference = new BinjgbEmulator();
|
||||
await reference.load(patched, document.createElement("canvas"));
|
||||
const write = (p: number, v: number) => reference.writeMemory(p, v & 255);
|
||||
const word = (p: number, v: number) => { write(p, v >> 8); write(p + 1, v); };
|
||||
const read = (p: number) => reference.readMemory(p);
|
||||
const readWord = (p: number) => (read(p) << 8) | read(p + 1);
|
||||
const stub = [0xe5, 0xd5, 0xfa, 0xe0, 0xc0, 0x5f, 0x16, 0, 0x21, 0, 0xc1, 0x19, 0x3c, 0xea, 0xe0, 0xc0, 0x7e, 0xd1, 0xe1, 0xc9];
|
||||
stub.forEach((v, i) => write(0xc080 + i, v));
|
||||
const { bank, address } = commands.tryItem;
|
||||
const program = [0xf3, 0x31, 0xfe, 0xc7, 0x3e, bank, 0xe0, a.hROMBank & 255, 0xea, 0, 0x20, 0xcd, address & 255, address >> 8, 0x3e, 0x42, 0xea, 0xf0, 0xc0, 0x18, 0xfe];
|
||||
|
||||
let seed = 0x9a7c3;
|
||||
const next = (limit = 256) => { seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; return (seed >>> 16) % limit; };
|
||||
|
||||
// Items in AI_Items' scan order, with 0 standing for NO_ITEM.
|
||||
const I = { FR: 0x0e, MP: 0x0f, HP: 0x10, SP: 0x11, PO: 0x12, XAC: 0x21, FH: 0x26, GS: 0x29, DH: 0x2c, XAT: 0x31, XDE: 0x33, XSP: 0x34, XSC: 0x35 };
|
||||
const itemPairs: [number, number][] = [
|
||||
[0, 0], [I.FR, 0], [I.MP, 0], [I.HP, I.SP], [I.PO, I.XAC], [I.FH, I.GS], [I.DH, I.XAT],
|
||||
[I.XDE, 0], [I.XSP, 0], [I.XSC, 0], [I.FH, I.HP], // last pair: the stuck-pointer scenario
|
||||
];
|
||||
const hpCases = [[100, 100], [30, 100], [10, 100], [25, 100], [26, 100], [50, 100], [51, 100], [25, 101]];
|
||||
const statusCases: { status: number; toxic?: number }[] = [
|
||||
{ status: 0 }, { status: 1 << 5 /* FRZ */ }, { status: 3 /* asleep */ },
|
||||
{ status: 1 << 3 /* PSN */, toxic: 5 },
|
||||
];
|
||||
const failures: unknown[] = [];
|
||||
let compared = 0, maxDraws = 0;
|
||||
for (let flagIndex = 0; flagIndex < bits.length; flagIndex++)
|
||||
for (const [item1, item2] of itemPairs)
|
||||
for (const [hp, maxHp] of hpCases)
|
||||
for (const { status, toxic } of statusCases)
|
||||
for (const turnsTaken of [0, 1]) {
|
||||
const classId = flagIndex + 1;
|
||||
const [attackMissed, effectFailed] = [[0, 0], [1, 0], [0, 1]][compared % 3];
|
||||
const bytes = Array.from({ length: 64 }, () => next());
|
||||
let draws = 0;
|
||||
const party = [{ level: 30, status: 0, hp: 50 }];
|
||||
const trainer = {
|
||||
name: "", className: "", party: party as never, aiFlags: 0, switchFlags: bits[flagIndex],
|
||||
items: [item1, item2] as [number, number], baseReward: 25, reward: 0,
|
||||
};
|
||||
const battle = new Battle({
|
||||
data: assets.pokemon, party: [] as never, playerId: 1, playerName: "CHRIS", timeOfDay: 1,
|
||||
random: () => bytes[draws++], text: () => ({}) as never,
|
||||
pokedex: { seen: new Set(), caught: new Set() },
|
||||
trainer,
|
||||
});
|
||||
battle.turn = 1;
|
||||
battle.attackMissed = attackMissed;
|
||||
battle.effectFailed = effectFailed;
|
||||
battle.curOTMon = 0;
|
||||
battle.enemy.mon = {
|
||||
species: 19, item: 0, moves: [33, 0, 0, 0], pp: [35, 0, 0, 0], dvs: 0, happiness: 70,
|
||||
level: 30, status, hp, maxHp, stats: [30, 30, 30, 30, 30], types: [0, 0], nickname: "RATTATA",
|
||||
} as never;
|
||||
battle.enemy.levels = [7, 7, 7, 7, 7, 7, 7];
|
||||
battle.enemy.sub = [0, 0, 0, (1 << 0) | (1 << 7) /* Bide, Confused */, 1 << 6 /* Rage */, toxic ? 1 : 0];
|
||||
battle.enemy.toxicCount = toxic ?? 0;
|
||||
battle.enemy.turnsTaken = turnsTaken;
|
||||
battle.enemy.furyCutterCount = 3;
|
||||
battle.enemy.protectCount = 2;
|
||||
battle.enemy.rageCounter = 4;
|
||||
battle.enemy.lastCounterMove = 33;
|
||||
battle.enemyGoesFirst = 0;
|
||||
const internal = battle as unknown as { aiTryItem(): Generator<unknown, boolean, unknown> };
|
||||
const gen = internal.aiTryItem();
|
||||
for (let step = gen.next(); !step.done; step = gen.next());
|
||||
const nativeDraws = draws;
|
||||
|
||||
write(a.wInBattleTowerBattle, 0);
|
||||
write(a.wTrainerClass, classId);
|
||||
write(a.wEnemyTrainerItem1, item1);
|
||||
write(a.wEnemyTrainerItem2, item2);
|
||||
write(a.wEnemyTrainerBaseReward, 25);
|
||||
write(a.wOTPartyCount, 1);
|
||||
write(a.wOTPartyMon1Level, 30);
|
||||
write(a.wOTPartyMon1Status, 0);
|
||||
word(a.wOTPartyMon1HP, 50);
|
||||
write(a.wCurOTMon, 0);
|
||||
write(a.wEnemyMonStatus, status);
|
||||
word(a.wEnemyMonHP, hp);
|
||||
word(a.wEnemyMonMaxHP, maxHp);
|
||||
write(a.wEnemySubStatus3, (1 << 0) | (1 << 7));
|
||||
write(a.wEnemySubStatus4, 1 << 6);
|
||||
write(a.wEnemySubStatus5, toxic ? 1 : 0);
|
||||
write(a.wEnemyToxicCount, toxic ?? 0);
|
||||
write(a.wEnemyTurnsTaken, turnsTaken);
|
||||
write(a.wEnemyGoesFirst, 0);
|
||||
write(a.wEnemyFuryCutterCount, 3);
|
||||
write(a.wEnemyProtectCount, 2);
|
||||
write(a.wEnemyRageCounter, 4);
|
||||
write(a.wLastEnemyCounterMove, 33);
|
||||
for (let i = 0; i < 8; i++) write(a.wEnemyStatLevels + i, 7);
|
||||
for (let i = 0; i < 5; i++) word(a.wEnemyStats + i * 2, 30);
|
||||
write(a.hBattleTurn, 1);
|
||||
write(a.wLinkMode, 0);
|
||||
write(a.wBattleMode, 2);
|
||||
// RaiseStat (not part of this port) gates on these without
|
||||
// AI_TryItem ever clearing them; both runs start a turn
|
||||
// with the same stale flags; successful, missed and failed
|
||||
// previous moves all occur in this sweep.
|
||||
write(a.wAttackMissed, attackMissed);
|
||||
write(a.wEffectFailed, effectFailed);
|
||||
bytes.forEach((v, i) => write(0xc100 + i, v));
|
||||
write(0xc0e0, 0);
|
||||
write(0xc0f0, 0);
|
||||
program.forEach((v, i) => write(0xc000 + i, v));
|
||||
reference.setProgramCounter(0xc000);
|
||||
for (let frame = 0; frame < 60 && read(0xc0f0) !== 0x42; frame++) reference.advanceFrames(1);
|
||||
if (read(0xc0f0) !== 0x42) throw new Error("AI_TryItem did not return");
|
||||
|
||||
const original = {
|
||||
used: read(a.wEnemyGoesFirst) === 1,
|
||||
item1: read(a.wEnemyTrainerItem1), item2: read(a.wEnemyTrainerItem2),
|
||||
baseReward: read(a.wEnemyTrainerBaseReward),
|
||||
status: read(a.wEnemyMonStatus), hp: readWord(a.wEnemyMonHP),
|
||||
partyStatus: read(a.wOTPartyMon1Status), partyHP: readWord(a.wOTPartyMon1HP),
|
||||
sub3: read(a.wEnemySubStatus3), sub4: read(a.wEnemySubStatus4), sub5: read(a.wEnemySubStatus5),
|
||||
furyCutterCount: read(a.wEnemyFuryCutterCount), protectCount: read(a.wEnemyProtectCount),
|
||||
rageCounter: read(a.wEnemyRageCounter), lastCounterMove: read(a.wLastEnemyCounterMove),
|
||||
levels: Array.from({ length: 7 }, (_, i) => read(a.wEnemyStatLevels + i)),
|
||||
draws: read(0xc0e0),
|
||||
};
|
||||
const native = {
|
||||
used: battle.enemyGoesFirst === 1,
|
||||
item1: trainer.items[0], item2: trainer.items[1],
|
||||
baseReward: trainer.baseReward,
|
||||
status: battle.enemy.mon.status, hp: battle.enemy.mon.hp,
|
||||
partyStatus: party[0].status, partyHP: party[0].hp,
|
||||
sub3: battle.enemy.sub[3], sub4: battle.enemy.sub[4], sub5: battle.enemy.sub[5],
|
||||
furyCutterCount: battle.enemy.furyCutterCount, protectCount: battle.enemy.protectCount,
|
||||
rageCounter: battle.enemy.rageCounter, lastCounterMove: battle.enemy.lastCounterMove,
|
||||
levels: battle.enemy.levels.slice(),
|
||||
draws: nativeDraws,
|
||||
};
|
||||
maxDraws = Math.max(maxDraws, native.draws, original.draws);
|
||||
if (JSON.stringify(original) !== JSON.stringify(native))
|
||||
failures.push({ flagIndex, item1, item2, hp, maxHp, status, toxic, turnsTaken, original, native });
|
||||
compared++;
|
||||
}
|
||||
reference.dispose();
|
||||
return { compared, maxDraws, failures, count: failures.length };
|
||||
},
|
||||
{ a: addresses, commands, attributesFlat, presentationFlats, bits: [0, ALWAYS_USE, UNKNOWN_USE, CONTEXT_USE, ALWAYS_USE | UNKNOWN_USE, ALWAYS_USE | CONTEXT_USE, UNKNOWN_USE | CONTEXT_USE, ALWAYS_USE | UNKNOWN_USE | CONTEXT_USE] },
|
||||
);
|
||||
expect(result.compared).toBe(8 * 11 * 8 * 4 * 2);
|
||||
expect(result, JSON.stringify(result.failures.slice(0, 3))).toMatchObject({ count: 0, failures: [] });
|
||||
});
|
||||
|
||||
/**
|
||||
* The base-reward-misread bug (docs/bugs_and_glitches.md "AI might use its
|
||||
* base reward value as an item"): no real trainer's base reward aliases an
|
||||
* AI_Items id (every two-item trainer's is 25), so this sets one up by
|
||||
* hand and checks both engines agree the AI "uses" Full Heal by zeroing
|
||||
* the reward rather than either real item slot.
|
||||
*/
|
||||
for (const entry of ["direct", "no-switch-style", "mean-look", "wrap"] as const)
|
||||
test(`Trainer item base-reward collision matches the original through ${entry}`, async ({ page }) => {
|
||||
const { at, routine } = loadSymbols();
|
||||
const names = ["hROMBank", "wInBattleTowerBattle", "wEnemyTrainerItem1", "wEnemyTrainerItem2", "wEnemyTrainerBaseReward", "wTrainerClass", "wOTPartyCount", "wOTPartyMon1Level", "wOTPartyMon1Status", "wCurOTMon", "wEnemyMonStatus", "wEnemyMonHP", "wEnemyMonMaxHP", "wEnemySubStatus3", "wEnemySubStatus4", "wEnemySubStatus5", "wEnemyToxicCount", "wEnemyTurnsTaken", "wEnemyGoesFirst", "hBattleTurn", "wLinkMode", "wBattleMode", "wPlayerSubStatus5", "wEnemyWrapCount", "wEnemySubStatus1"];
|
||||
const addresses = Object.fromEntries(names.map((name) => [name, at(name)]));
|
||||
const commands = { tryItem: routine(entry === "direct" ? "AI_TryItem" : "AI_SwitchOrTryItem"), random: routine("Random") };
|
||||
const trainerClassAttributes = routine("TrainerClassAttributes");
|
||||
const flat = ({ bank, address }: { bank: number; address: number }) => (address < 0x4000 ? address : bank * 0x4000 + address - 0x4000);
|
||||
const attributesFlat = flat(trainerClassAttributes);
|
||||
// See the main sweep above: these four are presentation-only and would
|
||||
// otherwise wait on input/timing this isolated call never provides.
|
||||
const presentationFlats = ["PrintText", "PlaySFX", "AnimateHPBar", "UpdateEnemyHUD"].map((name) => flat(routine(name)));
|
||||
await openReference(page);
|
||||
const result = await page.evaluate(async ({ a, commands, attributesFlat, presentationFlats, entry }) => {
|
||||
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
|
||||
const { CrystalAssets } = await import("/src/game/rom/assets.ts");
|
||||
const { Battle } = await import("/src/game/battle/battle.ts");
|
||||
const rom = new Uint8Array(await (document.querySelector("#reference-fixture") as HTMLInputElement).files![0].arrayBuffer());
|
||||
const assets = new CrystalAssets(rom);
|
||||
const patched = rom.slice();
|
||||
patched.set([0xc3, 0x80, 0xc0], commands.random.address);
|
||||
for (const p of presentationFlats) patched[p] = 0xc9; // ret
|
||||
// Class 1's AI_ITEM_SWITCH byte: CONTEXT_USE.
|
||||
patched[attributesFlat + 5] = 0x40;
|
||||
patched[attributesFlat + 6] = 0;
|
||||
const reference = new BinjgbEmulator();
|
||||
await reference.load(patched, document.createElement("canvas"));
|
||||
const write = (p: number, v: number) => reference.writeMemory(p, v & 255);
|
||||
const word = (p: number, v: number) => { write(p, v >> 8); write(p + 1, v); };
|
||||
const read = (p: number) => reference.readMemory(p);
|
||||
const readWord = (p: number) => (read(p) << 8) | read(p + 1);
|
||||
const stub = [0xe5, 0xd5, 0xfa, 0xe0, 0xc0, 0x5f, 0x16, 0, 0x21, 0, 0xc1, 0x19, 0x3c, 0xea, 0xe0, 0xc0, 0x7e, 0xd1, 0xe1, 0xc9];
|
||||
stub.forEach((v, i) => write(0xc080 + i, v));
|
||||
const { bank, address } = commands.tryItem;
|
||||
const program = [0xf3, 0x31, 0xfe, 0xc7, 0x3e, bank, 0xe0, a.hROMBank & 255, 0xea, 0, 0x20, 0xcd, address & 255, address >> 8, 0x3e, 0x42, 0xea, 0xf0, 0xc0, 0x18, 0xfe];
|
||||
for (let i = 0; i < 16; i++) write(0xc100 + i, 0); // never drawn, but kept tidy
|
||||
const item1 = 0, item2 = 0x10 /* HYPER_POTION */, baseReward = 0x26; /* FULL_HEAL */
|
||||
const trainer = { name: "", className: "", party: [{ level: 30, status: 0, hp: 50 }] as never, aiFlags: 0, switchFlags: 0x40, items: [item1, item2] as [number, number], baseReward, reward: 0 };
|
||||
const battle = new Battle({
|
||||
data: assets.pokemon, party: [] as never, playerId: 1, playerName: "CHRIS", timeOfDay: 1,
|
||||
random: () => 0, text: () => ({}) as never, pokedex: { seen: new Set(), caught: new Set() },
|
||||
trainer,
|
||||
});
|
||||
battle.turn = 1;
|
||||
battle.curOTMon = 0;
|
||||
battle.enemy.mon = { species: 19, item: 0, moves: [33, 0, 0, 0], pp: [35, 0, 0, 0], dvs: 0, happiness: 70, level: 30, status: 1 << 5, hp: 100, maxHp: 100, stats: [30, 30, 30, 30, 30], types: [0, 0], nickname: "RATTATA" } as never;
|
||||
battle.enemy.levels = [7, 7, 7, 7, 7, 7, 7];
|
||||
battle.enemyGoesFirst = 0;
|
||||
if (entry === "mean-look") battle.player.set(5, 7);
|
||||
if (entry === "wrap") battle.enemy.wrapCount = 2;
|
||||
const internal = battle as unknown as {
|
||||
aiTryItem(): Generator<unknown, boolean, unknown>;
|
||||
aiSwitchOrTryItem(): Generator<unknown, boolean, unknown>;
|
||||
};
|
||||
const gen = entry === "direct" ? internal.aiTryItem() : internal.aiSwitchOrTryItem();
|
||||
for (let step = gen.next(); !step.done; step = gen.next());
|
||||
|
||||
write(a.wInBattleTowerBattle, 0);
|
||||
write(a.wTrainerClass, 1);
|
||||
write(a.wEnemyTrainerItem1, item1);
|
||||
write(a.wEnemyTrainerItem2, item2);
|
||||
write(a.wEnemyTrainerBaseReward, baseReward);
|
||||
write(a.wOTPartyCount, 1);
|
||||
write(a.wOTPartyMon1Level, 30);
|
||||
write(a.wOTPartyMon1Status, 0);
|
||||
write(a.wCurOTMon, 0);
|
||||
write(a.wEnemyMonStatus, 1 << 5);
|
||||
word(a.wEnemyMonHP, 100);
|
||||
word(a.wEnemyMonMaxHP, 100);
|
||||
write(a.wEnemySubStatus3, 0);
|
||||
write(a.wEnemySubStatus4, 0);
|
||||
write(a.wEnemySubStatus5, 0);
|
||||
write(a.wEnemyToxicCount, 0);
|
||||
write(a.wEnemyTurnsTaken, 0);
|
||||
write(a.wEnemyGoesFirst, 0);
|
||||
write(a.hBattleTurn, 1);
|
||||
write(a.wLinkMode, 0);
|
||||
write(a.wBattleMode, 2);
|
||||
write(a.wPlayerSubStatus5, entry === "mean-look" ? 0x80 : 0);
|
||||
write(a.wEnemyWrapCount, entry === "wrap" ? 2 : 0);
|
||||
write(a.wEnemySubStatus1, 0);
|
||||
program.forEach((v, i) => write(0xc000 + i, v));
|
||||
write(0xc0f0, 0);
|
||||
reference.setProgramCounter(0xc000);
|
||||
for (let frame = 0; frame < 60 && read(0xc0f0) !== 0x42; frame++) reference.advanceFrames(1);
|
||||
if (read(0xc0f0) !== 0x42) throw new Error("AI_TryItem did not return");
|
||||
const final = {
|
||||
original: { used: read(a.wEnemyGoesFirst) === 1, item1: read(a.wEnemyTrainerItem1), item2: read(a.wEnemyTrainerItem2), baseReward: read(a.wEnemyTrainerBaseReward), status: read(a.wEnemyMonStatus) },
|
||||
native: { used: battle.enemyGoesFirst === 1, item1: trainer.items[0], item2: trainer.items[1], baseReward: trainer.baseReward, status: battle.enemy.mon.status },
|
||||
};
|
||||
reference.dispose();
|
||||
return final;
|
||||
}, { a: addresses, commands, attributesFlat, presentationFlats, entry });
|
||||
expect(result.native).toEqual(result.original);
|
||||
expect(result.native).toEqual({ used: true, item1: 0, item2: 0x10, baseReward: 0, status: 0 });
|
||||
});
|
||||
Reference in new issue
Block a user