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 }); });