- Day-Care and breeding: deposits, withdrawal pricing and growth, compatibility, egg species, DVs and moves, DayCareStep, egg collection and the Odd Egg, compared with the cartridge's routines. - CountStep: walking happiness, DoEggStep, DayCareStep and DoBikeStep in the original's order; HatchEggs with the ROM's hatch animation, compared frame by frame with the original. - MAIL: all ten stationery types and the Portrait MAIL picture, compared with the cartridge's tilemaps, graphics and palettes. - Printing: Bill's PC boxes, Pokedex PRNT, MAIL and the Photo Studio take the original's absent-printer path. - Hall of Fame records (saved apart from the League flag, ready for the League to call) and the Hall of Fame PC, compared with the cartridge's _HallOfFamePC on 255 pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
202 lines
6.2 KiB
TypeScript
202 lines
6.2 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("all ten MAIL screens match cartridge tilemaps, graphics, message placement and palettes", async ({
|
|
page,
|
|
}) => {
|
|
const { at, routine } = loadSymbols();
|
|
const labels = [
|
|
"Flower",
|
|
"Surf",
|
|
"LiteBlue",
|
|
"Portrait",
|
|
"Lovely",
|
|
"Eon",
|
|
"Morph",
|
|
"BlueSky",
|
|
"Music",
|
|
"Mirage",
|
|
];
|
|
const defs = labels.map((name) => routine(`Load${name}MailGFX`));
|
|
const a = Object.fromEntries(
|
|
[
|
|
"hROMBank",
|
|
"hCGB",
|
|
"wTilemap",
|
|
"wAttrmap",
|
|
"wCurPartySpecies",
|
|
"wUnownLetter",
|
|
"wCurMailIndex",
|
|
"wBoxAlignment",
|
|
"wBGPals1",
|
|
"sPartyMon1Mail",
|
|
"sPartyMon1MailAuthor",
|
|
].map((n) => [n, at(n)]),
|
|
);
|
|
const skip = ["ApplyPals", "ApplyAttrmap"].map(routine);
|
|
const palette = routine("LoadMailPalettes");
|
|
await openReference(page);
|
|
const result = await page.evaluate(
|
|
async ({ defs, a, skip, palette }) => {
|
|
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
|
|
const { CrystalAssets } = await import("/src/game/rom/assets.ts");
|
|
const { mailScreen } = await import("/src/game/scripts/mail.ts");
|
|
const { textTiles } = await import("/src/game/rom/font.ts");
|
|
const { MAIL } = await import("/src/game/rom/profile.ts");
|
|
const rom = new Uint8Array(
|
|
await (
|
|
document.querySelector("#reference-fixture") as HTMLInputElement
|
|
).files![0].arrayBuffer(),
|
|
);
|
|
const assets = new CrystalAssets(rom);
|
|
// Exclude hardware palette transfer only. The original graphics,
|
|
// message/author placement and palette selection execute.
|
|
for (const { bank, address } of skip)
|
|
rom[bank * 0x4000 + address - 0x4000] = 0xc9;
|
|
const ref = new BinjgbEmulator();
|
|
await ref.load(rom, document.createElement("canvas"));
|
|
const write = (p: number, v: number) => ref.writeMemory(p, v & 255);
|
|
const read = (p: number) => ref.readMemory(p);
|
|
const call = (routine: { bank: number; address: number }, e = 0) => {
|
|
const program = [
|
|
0xf3,
|
|
0x31,
|
|
0xfe,
|
|
0xc7,
|
|
0xaf,
|
|
0xe0,
|
|
0x40,
|
|
0xe0,
|
|
0x4f,
|
|
0x3e,
|
|
routine.bank,
|
|
0xe0,
|
|
a.hROMBank & 255,
|
|
0xea,
|
|
0,
|
|
0x20,
|
|
0x01,
|
|
a.sPartyMon1Mail & 255,
|
|
a.sPartyMon1Mail >> 8,
|
|
0x1e,
|
|
e,
|
|
0xcd,
|
|
routine.address & 255,
|
|
routine.address >> 8,
|
|
0x3e,
|
|
0x42,
|
|
0xea,
|
|
0xf0,
|
|
0xc0,
|
|
0x18,
|
|
0xfe,
|
|
];
|
|
write(0xc0f0, 0);
|
|
program.forEach((v, i) => write(0xc000 + i, v));
|
|
ref.setProgramCounter(0xc000);
|
|
for (let f = 0; f < 60 && read(0xc0f0) !== 0x42; f++)
|
|
ref.advanceFrames(1);
|
|
if (read(0xc0f0) !== 0x42)
|
|
throw Error(
|
|
`MAIL routine ${routine.address.toString(16)} did not return`,
|
|
);
|
|
};
|
|
const failures: unknown[] = [];
|
|
for (let index = 0; index < defs.length; index++) {
|
|
write(0xff40, 0);
|
|
write(0xff4f, 0);
|
|
write(0xff70, 1);
|
|
write(a.hCGB, 1);
|
|
for (let p = 0x8000; p < 0xa000; p++) write(p, 0);
|
|
for (let i = 0; i < 360; i++) write(a.wTilemap + i, 0x7f);
|
|
write(a.wCurMailIndex, index);
|
|
write(a.wBoxAlignment, 0);
|
|
write(a.wCurPartySpecies, 201);
|
|
write(a.wUnownLetter, 1);
|
|
const message = "“HELLO…” FRIEND! ";
|
|
write(0x0000, 0x0a);
|
|
write(0x4000, 0);
|
|
const bytes = [
|
|
...textTiles(message),
|
|
0x4e,
|
|
...textTiles("SEE YOU SOON!"),
|
|
0x50,
|
|
];
|
|
for (let i = 0; i < 47; i++) write(a.sPartyMon1Mail + i, 0x50);
|
|
bytes.forEach((v, i) => write(a.sPartyMon1Mail + i, v));
|
|
textTiles("CHRIS").forEach((v, i) =>
|
|
write(a.sPartyMon1MailAuthor + i, v),
|
|
);
|
|
call(defs[index]);
|
|
const native = mailScreen(assets.pokemon, {
|
|
message: message + "\nSEE YOU SOON!",
|
|
author: "CHRIS",
|
|
authorId: 1,
|
|
species: 201,
|
|
type: assets.reader.byte(MAIL.MailGFXPointers + index * 3),
|
|
});
|
|
const map = Array.from({ length: 360 }, (_, i) => read(a.wTilemap + i));
|
|
const mapDiff = map.flatMap((v, i) =>
|
|
v === native.map[i]
|
|
? []
|
|
: [{ i, original: v, native: native.map[i] }],
|
|
);
|
|
const tileDiff: unknown[] = [];
|
|
for (const tile of new Set(map)) {
|
|
if (tile >= 0x60) continue; // font is loaded by ReadAnyMail, before these routines
|
|
for (let b = 0; b < 16; b++)
|
|
if (read(0x9000 + tile * 16 + b) !== native.tiles[tile * 16 + b]) {
|
|
tileDiff.push({
|
|
tile,
|
|
b,
|
|
original: read(0x9000 + tile * 16 + b),
|
|
native: native.tiles[tile * 16 + b],
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
call(palette, index);
|
|
write(0xff70, 5);
|
|
const colors = Array.from(
|
|
{ length: 4 },
|
|
(_, i) =>
|
|
read(a.wBGPals1 + i * 2) | (read(a.wBGPals1 + i * 2 + 1) << 8),
|
|
);
|
|
const rgb = colors.map((v) =>
|
|
[v & 31, (v >> 5) & 31, (v >> 10) & 31].map((c) =>
|
|
Math.round((c * 255) / 31),
|
|
),
|
|
);
|
|
write(0xff70, 1);
|
|
if (
|
|
mapDiff.length ||
|
|
tileDiff.length ||
|
|
JSON.stringify(rgb) !== JSON.stringify(native.palettes[0])
|
|
)
|
|
failures.push({
|
|
index,
|
|
mapDiff: mapDiff.slice(0, 12),
|
|
tileDiff: tileDiff.slice(0, 12),
|
|
rgb,
|
|
nativePalette: native.palettes[0],
|
|
});
|
|
}
|
|
ref.dispose();
|
|
return { count: defs.length, failures };
|
|
},
|
|
{ defs, a, skip, palette },
|
|
);
|
|
expect(result.count).toBe(10);
|
|
expect(result.failures).toEqual([]);
|
|
});
|