Breed at the Day-Care, hatch Eggs, draw MAIL stationery and browse the Hall of Fame (Closes #29)
- 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>
This commit is contained in:
1 parent
7d03f5c5ca
commit
e49bc6de9a
49 files changed
+6028
-88
No files matched your search
@@ -0,0 +1,201 @@
|
||||
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([]);
|
||||
});
|
||||
Reference in new issue
Block a user