Checks / web (push) Canceled after 0s
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>
102 lines
3.3 KiB
TypeScript
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]),
|
|
);
|
|
});
|