The Route 40 gate, the Battle Tower and its rooms are open with the offline challenge: the rules, the save before entering, the level menu with its level and uber checks, seven trainers drawn from BattleTowerTrainers with Pokémon from BattleTowerMons, BattleTowerText, save and quit with the receptionist resuming it, cancelling, losing and the vitamin reward. After each battle the party and Pokédex return to the quick save's, as LoadPokemonData does. Battle Tower battles turn off obedience, badge boosts, experience, items, AI items and prize money, use Falkner's AI attributes, keep Sleep to one to three turns and never fail the AI's status moves at random. New script command battletowertext; new specials BattleTowerAction (offline actions), BattleTowerRoomMenu, CheckForBattleTowerRules, BattleTowerBattle, LoadOpponentTrainerAndPokemonWithOTSprite, Menu_ChallengeExplanationCancel, TryQuickSave and Reset. DoBadgeTypeBoosts was missing from every battle; it now adds an eighth of the damage for the first badge matching the move's type on the player's turn, checked against the original routine. This completes Johto from Azalea to Blackthorn: Kanto, the League and the S.S. Aqua continue on #33; radio, rematches and Buena's password on #30. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
7.1 KiB
TypeScript
141 lines
7.1 KiB
TypeScript
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.");
|
|
test("badge stat boosts match the original for every Johto badge byte", async ({ page }) => {
|
|
const { at, routine } = loadSymbols();
|
|
const addresses = {
|
|
romBank: at("hROMBank"),
|
|
badges: at("wJohtoBadges"),
|
|
linkMode: at("wLinkMode"),
|
|
tower: at("wInBattleTowerBattle"),
|
|
stats: at("wBattleMonAttack"),
|
|
};
|
|
const boosts = routine("BadgeStatBoosts");
|
|
await openReference(page);
|
|
const result = await page.evaluate(async ({ a, boosts }) => {
|
|
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 reference = new BinjgbEmulator();
|
|
await reference.load(rom, document.createElement("canvas"));
|
|
const write = (p: number, v: number) => reference.writeMemory(p, v & 255);
|
|
const read = (p: number) => reference.readMemory(p);
|
|
// DI; SP=$c7fe; select the bank; CALL BadgeStatBoosts; sentinel; loop.
|
|
// The sentinel lives in audio RAM, away from the battle variables.
|
|
const program = [0xf3, 0x31, 0xfe, 0xc7, 0x3e, boosts.bank, 0xe0, a.romBank & 255, 0xea, 0, 0x20,
|
|
0xcd, boosts.address & 255, boosts.address >> 8, 0x3e, 0x42, 0xea, 0xf0, 0xc0, 0x18, 0xfe];
|
|
// Around the cap, a range of Special Attack values for the Glacier
|
|
// Badge's register bug, and small stats.
|
|
const statSets = [
|
|
[1, 2, 3, 4, 5],
|
|
[10, 20, 30, 40, 50],
|
|
[887, 888, 889, 900, 998],
|
|
[999, 999, 999, 999, 999],
|
|
...Array.from({ length: 24 }, (_, i) => [100, 150, 200, 180 + i * 30, 300]),
|
|
];
|
|
const failures: unknown[] = [];
|
|
let compared = 0;
|
|
for (let badges = 0; badges < 256; badges++)
|
|
for (const stats of statSets) {
|
|
const battle = new Battle({
|
|
data: assets.pokemon, party: [], pokedex: { seen: new Set(), caught: new Set() }, playerId: 1,
|
|
playerName: "CHRIS", timeOfDay: 1, random: () => 0, text: () => ({}) as never, johtoBadges: badges,
|
|
});
|
|
battle.player.mon = { stats: [...stats] } as never;
|
|
(battle as unknown as { badgeStatBoosts(): void }).badgeStatBoosts();
|
|
write(a.badges, badges);
|
|
write(a.linkMode, 0);
|
|
write(a.tower, 0);
|
|
stats.forEach((value, i) => { write(a.stats + i * 2, value >> 8); write(a.stats + i * 2 + 1, value); });
|
|
write(0xc0f0, 0);
|
|
program.forEach((value, i) => write(0xc000 + i, value));
|
|
reference.setProgramCounter(0xc000);
|
|
reference.advanceFrames(1);
|
|
if (read(0xc0f0) !== 0x42) throw Error("BadgeStatBoosts did not return");
|
|
const original = stats.map((_, i) => (read(a.stats + i * 2) << 8) | read(a.stats + i * 2 + 1));
|
|
const native = battle.player.mon.stats;
|
|
if (original.some((value, i) => value !== native[i])) failures.push({ badges, stats, original, native });
|
|
compared++;
|
|
}
|
|
reference.dispose();
|
|
return { compared, failures: failures.slice(0, 5), count: failures.length };
|
|
}, { a: addresses, boosts });
|
|
expect(result.compared).toBe(256 * 28);
|
|
expect(result, JSON.stringify(result)).toMatchObject({ count: 0 });
|
|
});
|
|
|
|
test("badge type boosts match the original for badges, types, damage, turn and the Battle Tower", async ({ page }) => {
|
|
const { at, routine } = loadSymbols();
|
|
const addresses = {
|
|
romBank: at("hROMBank"),
|
|
johto: at("wJohtoBadges"),
|
|
kanto: at("wKantoBadges"),
|
|
linkMode: at("wLinkMode"),
|
|
tower: at("wInBattleTowerBattle"),
|
|
turn: at("hBattleTurn"),
|
|
type: at("wCurType"),
|
|
damage: at("wCurDamage"),
|
|
};
|
|
const boosts = routine("DoBadgeTypeBoosts");
|
|
await openReference(page);
|
|
const result = await page.evaluate(async ({ a, boosts }) => {
|
|
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 reference = new BinjgbEmulator();
|
|
await reference.load(rom, document.createElement("canvas"));
|
|
const write = (p: number, v: number) => reference.writeMemory(p, v & 255);
|
|
const read = (p: number) => reference.readMemory(p);
|
|
const program = [0xf3, 0x31, 0xfe, 0xc7, 0x3e, boosts.bank, 0xe0, a.romBank & 255, 0xea, 0, 0x20,
|
|
0xcd, boosts.address & 255, boosts.address >> 8, 0x3e, 0x42, 0xea, 0xf0, 0xc0, 0x18, 0xfe];
|
|
const failures: unknown[] = [];
|
|
let compared = 0;
|
|
const cases: [number, number, number, number, number, number][] = [];
|
|
for (let johto = 0; johto < 256; johto++)
|
|
for (const type of [0, 2, 7, 8, 9, 22, 23, 24, 25, 26, 27])
|
|
cases.push([johto, 0, type, johto % 3 === 0 ? 1 : 100 + johto * 37, 0, 0]);
|
|
for (const kanto of [0, 1, 2, 4, 8, 16, 32, 64, 128, 255])
|
|
for (const type of [0, 3, 5, 20, 21, 22, 23, 24])
|
|
for (const damage of [0, 1, 7, 8, 9, 997, 65000, 65535])
|
|
cases.push([0x80, kanto, type, damage, 0, 0]);
|
|
for (const [turn, tower] of [[1, 0], [0, 1], [1, 1]])
|
|
for (const type of [2, 7, 8, 26])
|
|
cases.push([0xff, 0xff, type, 640, turn, tower]);
|
|
for (const [johto, kanto, type, damage, turn, tower] of cases) {
|
|
const battle = new Battle({
|
|
data: assets.pokemon, party: [], pokedex: { seen: new Set(), caught: new Set() }, playerId: 1,
|
|
playerName: "CHRIS", timeOfDay: 1, random: () => 0, text: () => ({}) as never,
|
|
johtoBadges: johto, kantoBadges: kanto, battleTower: Boolean(tower),
|
|
});
|
|
(battle as unknown as { turn: number }).turn = turn;
|
|
const native = (battle as unknown as { badgeTypeBoost(d: number, t: number): number }).badgeTypeBoost(damage, type);
|
|
write(a.johto, johto);
|
|
write(a.kanto, kanto);
|
|
write(a.linkMode, 0);
|
|
write(a.tower, tower);
|
|
write(a.turn, turn);
|
|
write(a.type, type);
|
|
write(a.damage, damage >> 8);
|
|
write(a.damage + 1, damage);
|
|
write(0xc0f0, 0);
|
|
program.forEach((value, i) => write(0xc000 + i, value));
|
|
reference.setProgramCounter(0xc000);
|
|
reference.advanceFrames(1);
|
|
if (read(0xc0f0) !== 0x42) throw Error("DoBadgeTypeBoosts did not return");
|
|
const original = (read(a.damage) << 8) | read(a.damage + 1);
|
|
if (original !== native) failures.push({ johto, kanto, type, damage, turn, tower, original, native });
|
|
compared++;
|
|
}
|
|
reference.dispose();
|
|
return { compared, failures: failures.slice(0, 5), count: failures.length };
|
|
}, { a: addresses, boosts });
|
|
expect(result.compared).toBeGreaterThan(3000);
|
|
expect(result, JSON.stringify(result)).toMatchObject({ count: 0 });
|
|
});
|