Checks / web (push) Canceled after 0s
Mr. Pokémon's ROM scene gives the Mystery Egg and Pokédex, heals the party, sets the Cherrygrove spawn with blackoutmod and queues Elm's call. Cherrygrove's rival scene runs both triggers and all three RIVAL1_1 parties as a CANLOSE battle; reloadmap returns to the script. Smart AI is accepted for effects with no AI_Smart_EffectHandlers entry (Normal Hit, Attack Down, Defense Down) and still refuses others before any random draw, including for reserve Pokémon. Special calls read SpecialPhoneCallList: the entry's condition decides, and the ROM's CheckSpecialPhoneCall.script and Script_ReceivePhoneCall run its caller script through memcall, ending with HangUp's timed "Click!". Ringing and the caller box are not presented yet. In Elm's lab, NameRival shows the ROM's "RIVAL'S NAME?" title (the prompt previously had no page and soft-locked) and applies InitName's default to blank names. Handing over the Egg and the aide's Poké Balls run from the ROM. Saves gain rivalName and specialPhoneCall. The quest fixtures use a seeded RNG (a constant byte never ends the damage-variation loop) and no longer carry the house's moved objects into Cherrygrove. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
130 lines
5.6 KiB
TypeScript
130 lines
5.6 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 {
|
|
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);
|
|
});
|