Files
Crystal/tests/quest-browser.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

138 lines
5.9 KiB
TypeScript

import { test, expect, type Page } from "@playwright/test";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
const romPath = resolve(process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc");
test.beforeEach(() => test.skip(!existsSync(romPath), "Requires the local user ROM."));
async function start(
page: Page,
fixture: "questFixtureWorld" | "questRivalFixtureWorld" | "questLabFixtureWorld",
) {
await page.route("**/src/game/runtime.ts", async (route) => {
const response = await route.fetch();
const source = await response.text();
const needle = "const world = new GameWorld(assets);";
expect(source).toContain(needle);
await route.fulfill({ response, body: source.replace(needle,
`const { ${fixture} } = await import('/tests/quest-fixture.ts');
const world = ${fixture}(assets);`) });
});
await page.clock.install({ time: new Date("2026-09-22T12:00:00") });
await page.goto("/");
await page.locator("#rom-file").setInputFiles(romPath);
await expect(page.locator("#status")).toContainText("verified");
await page.clock.pauseAt(new Date("2026-09-22T12:05:00"));
await page.locator("#play").click();
}
async function step(page: Page, direction: string) {
await page.keyboard.down(`Arrow${direction}`);
await page.clock.runFor(70);
await page.keyboard.up(`Arrow${direction}`);
await page.clock.runFor(300);
}
async function drive(page: Page, name = "") {
const messages: string[] = [];
for (let guard = 0; guard < 180; guard++) {
if (await page.locator("#battle").isVisible()) {
messages.push(await page.locator("#battle-text").innerText());
const menu = page.locator("#battle-menu");
const fight = menu.getByRole("button", { name: "FIGHT", exact: true });
if (await fight.count()) {
await fight.click();
await menu.getByRole("button", { name: /SCRATCH/ }).click();
} else if (await menu.getByRole("button", { name: /Continue/ }).count()) {
await menu.getByRole("button", { name: /Continue/ }).click();
} else await page.clock.runFor(1200);
} else if (await page.locator("#dialogue-choice input").isVisible()) {
messages.push(await page.locator("#dialogue-text").innerText());
const input = page.locator("#dialogue-choice input");
await expect(input).toBeFocused();
await input.fill(name);
await input.press("Enter");
await page.clock.runFor(500);
} else if (await page.locator("#native-dialogue").isVisible()) {
messages.push(await page.locator("#dialogue-text").innerText());
await page.keyboard.press("KeyX");
await page.clock.runFor(500);
} else if (await page.locator("#game-screen").getAttribute("data-activity") !== "idle") {
await page.clock.runFor(500);
} else {
// A scene script starts a tick after the tick that ends a warp, so a
// single idle observation is not the end of the scene.
await page.clock.runFor(200);
if (
(await page.locator("#game-screen").getAttribute("data-activity")) !== "idle" ||
(await page.locator("#native-dialogue").isVisible())
)
continue;
await expect(page.locator("#game-notice")).toHaveText("");
return messages.join("\n");
}
}
throw new Error("Quest scene did not finish.");
}
async function backup(page: Page) {
await page.locator("#tab-saves").click();
const downloaded = page.waitForEvent("download");
await page.locator("#backup").click();
const path = (await (await downloaded).path())!;
const record = JSON.parse(readFileSync(path, "utf8"));
return { path, state: JSON.parse(Buffer.from(record.state, "base64").toString("utf8")) };
}
test("Mr. Pokémon's ROM scene grants the Egg and Pokédex, heals and survives reload", async ({ page }) => {
await start(page, "questFixtureWorld");
await step(page, "Up");
await expect(page.locator("#game-screen")).toHaveAttribute("data-map", "26:10");
const shown = await drive(page);
expect(shown).toContain("MYSTERY EGG");
expect(shown).toContain("POKéDEX");
const { path, state } = await backup(page);
expect(state.events).toContain(30);
expect(state.bag.keyItems).toContain(0x45);
expect(state.scenes).toMatchObject({ "26:10": 1, "26:3": 1, "24:5": 3 });
expect(state.party[0].hp).toBe(state.party[0].stats[0]);
expect(state.specialPhoneCall).toBe(2); // SPECIALCALL_ROBBED
page.once("dialog", (dialog) => dialog.accept());
await page.locator("#save-file").setInputFiles(path);
await expect(page.locator("#status")).toContainText("Backup restored");
await page.reload();
await page.locator("#rom-file").setInputFiles(romPath);
await expect(page.locator("#status")).toContainText("saved session");
const restored = await backup(page);
expect(restored.state).toEqual(state);
});
test("the real Cherrygrove return scene battles the rival and commits its exit", async ({ page }) => {
await start(page, "questRivalFixtureWorld");
await drive(page); // a pending outdoor call may run before the encounter
await step(page, "Right");
const shown = await drive(page);
expect(shown).toContain("CHIKORITA");
expect(shown).toContain("???");
await expect(page.locator("#battle")).toBeHidden();
const { state } = await backup(page);
expect(state.scenes["26:3"]).toBe(0);
expect(state.pokedex.seen).toContain(152);
expect(state.party[0].hp).toBe(state.party[0].stats[0]);
expect(state.events).toContain(30);
});
test("the officer in Elm's lab names the rival through the naming field", async ({ page }) => {
await start(page, "questLabFixtureWorld");
await step(page, "Up");
await expect(page.locator("#game-screen")).toHaveAttribute("data-map", "24:5");
await drive(page);
for (let y = 11; y > 5; y--) await step(page, "Up");
const shown = await drive(page, "BLUE");
expect(shown).toContain("RIVAL'S NAME?");
expect(shown).toContain("BLUE");
const { state } = await backup(page);
expect(state.rivalName).toBe("BLUE");
expect(state.scenes["24:5"]).toBe(2);
});