Files
Crystal/tests/overworld-routines.spec.ts
T
Sébastien QUEROLandClaude Opus 5.5 451464f7c6
Checks / web (push) Canceled after 0s
Complete the overworld foundation
Port forced movement (turn delays, bumps, ice, currents, pits), dynamic
return warps, elevators, Surf, Cut, Strength and cycling rules, working
map blocks with changeblock/changemapblocks, and the remaining NPC
movement types and movement commands.

Add tileset animation from the ROM's command tables, grass sprite
priority and special map palettes; room decorations with ownership, the
bedroom PC and save migration; and Key Items access to the Bicycle.

Fourteen original-engine traces compare positions, facing, actors, event
flags and scripted poses. Tile animation matches original VRAM for four
tilesets, and LoadMapPals matches 192 palette cases. skyfall_top now
keeps the $60 sprite offset its step function leaves behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:47:29 +02:00

102 lines
3.3 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { existsSync, readFileSync } from "node:fs";
import { CrystalAssets } from "../src/game/rom/assets";
const romPath =
process.env.CRYSTAL_ROM_PATH ??
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
const symbolsPath = ".reference/pokecrystal/pokecrystal11.sym";
test("all tileset palettes match the original LoadMapPals routine", async ({
page,
}, info) => {
test.skip(
info.project.name !== "chromium" ||
!existsSync(romPath) ||
!existsSync(symbolsPath),
"Requires the local ROM/reference; engine comparison runs in Chromium.",
);
const symbols: Record<string, { bank: number; address: number }> = {};
for (const line of readFileSync(symbolsPath, "utf8").split("\n")) {
const m = /^([0-9a-f]{2}):([0-9a-f]{4}) (\S+)$/.exec(line);
if (m)
symbols[m[3]] = { bank: parseInt(m[1], 16), address: parseInt(m[2], 16) };
}
const bytes = new Uint8Array(readFileSync(romPath));
const cases = Array.from({ length: 36 * 4 }, (_, i) => ({
tileset: 1 + Math.floor(i / 4),
time: i % 4,
environment: 3,
group: 24,
}));
for (const environment of [1, 2, 4, 7])
for (const time of [0, 1, 2, 3])
for (const tileset of [1, 29, 30])
cases.push({ tileset, time, environment, group: 24 });
await page.goto("/reference.html");
const original = await page.evaluate(
async ({ rom, symbols, cases }) => {
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
const emu = new BinjgbEmulator();
await emu.load(new Uint8Array(rom), document.createElement("canvas"));
const write = (name: string, value: number) =>
emu.writeMemory(symbols[name].address, value);
const target = symbols.LoadMapPals;
const program = [
0xf3,
0x31,
0xfe,
0xc7,
0x3e,
target.bank,
0xe0,
0x9d,
0xea,
0,
0x20,
0xcd,
target.address & 255,
target.address >> 8,
0x3e,
0x42,
0xea,
0xff,
0xc6,
0x18,
0xfe,
];
const output = cases.map((c) => {
emu.writeMemory(0xff70, 1);
write("wMapTileset", c.tileset);
write("wEnvironment", c.environment);
write("wTimeOfDayPal", c.time);
write("wMapGroup", c.group);
emu.writeMemory(0xc6ff, 0);
program.forEach((v, i) => emu.writeMemory(0xc000 + i, v));
emu.setProgramCounter(0xc000);
emu.advanceFrames(2);
if (emu.readMemory(0xc6ff) !== 0x42)
throw new Error("LoadMapPals did not return");
emu.writeMemory(0xff70, symbols.wBGPals1.bank);
return Array.from({ length: 32 }, (_, i) => {
const p = symbols.wBGPals1.address + i * 2;
const value = emu.readMemory(p) | (emu.readMemory(p + 1) << 8);
return [0, 5, 10].map((shift) =>
Math.round((((value >> shift) & 31) * 255) / 31),
);
});
});
emu.dispose();
return output;
},
{ rom: [...bytes], symbols, cases },
);
const assets = new CrystalAssets(bytes),
base = assets.map("24:6");
cases.forEach((c, i) =>
expect(
assets.palettes({ ...base, ...c }, c.time).flat(),
JSON.stringify(c),
).toEqual(original[i]),
);
});