Files
Crystal/tests/foundation-browser.spec.ts
T
Sébastien QUEROLandClaude Opus 5.5 3bdca0099e
Checks / web (push) Canceled after 0s
Port the PC and boxes
Read TileCollisionStdScripts from the ROM so the Pokémon Center PC runs
PokemonCenterPC: Bill's PC (deposit, withdraw, release, change and name
boxes), the player's item storage (withdraw, deposit, toss, mail box) and
Oak's Pokédex rating. The house PC now opens the full player's PC menu.

A catch with a full party goes to the current box through SendMonIntoBox;
a full box keeps the ball, and filling it queues Bill's call as a memory
script after the battle. Saves gain boxes, box names and PC items,
migrated by absence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:12:26 +02:00

185 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, room: boolean | "poster" | "center" = false) {
await page.route("**/src/game/runtime.ts", async (route) => {
const response = await route.fetch(),
source = await response.text();
const needle = "const world = new GameWorld(assets);";
expect(source).toContain(needle);
await route.fulfill({
response,
body: source.replace(
needle,
`const { foundationFixture } = await import('/tests/foundation-fixture.ts'); const world = foundationFixture(assets, ${JSON.stringify(room)});`,
),
});
});
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 backup(page: Page) {
await page.locator("#tab-saves").click();
const download = page.waitForEvent("download");
await page.locator("#backup").click();
const path = (await (await download).path())!;
const record = JSON.parse(readFileSync(path, "utf8"));
return {
path,
state: JSON.parse(Buffer.from(record.state, "base64").toString("utf8")),
};
}
test("Start opens Key Items, freezes movement, and uses the Bicycle with keyboard and touch", async ({
page,
}) => {
await start(page);
await page.keyboard.press("Enter");
await expect(page.locator("#field-menu")).toBeVisible();
await page.keyboard.down("ArrowDown");
await page.clock.runFor(1000);
await page.keyboard.up("ArrowDown");
await expect(page.locator("#game-screen")).toHaveAttribute("data-y", "6");
await page
.locator("#field-items")
.getByRole("button", { name: "BICYCLE", exact: true })
.click();
await page.keyboard.press("KeyX");
await page.clock.runFor(500);
await expect(page.locator("#field-menu")).toBeHidden();
await expect(page.locator("#game-notice")).toHaveText("");
expect((await backup(page)).state.playerState).toBe(1);
});
test("the bedroom PC puts away a decoration and saves the result", async ({
page,
}) => {
await start(page, true);
await page.keyboard.press("KeyX");
await expect(page.locator("#game-notice")).toHaveText("");
await expect(page.locator("#dialogue-text")).toContainText(
"turned on the PC.",
);
await page.keyboard.press("KeyX");
const choices = page.locator("#dialogue-choice");
await choices
.getByRole("option", { name: "DECORATION", exact: true })
.click();
await expect(page.locator("#dialogue-text")).toHaveText("DECORATION");
await choices.getByRole("option", { name: "BED", exact: true }).click();
await choices
.getByRole("option", { name: "PUT IT AWAY", exact: true })
.click();
await page.keyboard.press("KeyX");
await choices.getByRole("option", { name: "EXIT", exact: true }).click();
await page.clock.runFor(500);
await expect(page.locator("#game-notice")).toHaveText("");
expect((await backup(page)).state.decorations.bed).toBe(0);
});
test("B cancels the pack and an indoor Bicycle attempt leaves the player walking", async ({
page,
}) => {
await start(page, true);
await page.keyboard.press("Enter");
await page.keyboard.press("KeyZ");
await expect(page.locator("#field-menu")).toBeHidden();
await page.keyboard.press("Enter");
await page
.locator("#field-items")
.getByRole("button", { name: "BICYCLE", exact: true })
.click();
await expect(page.locator("#game-notice")).toContainText("can’t use");
expect((await backup(page)).state.playerState).toBe(0);
});
test("the Town Map poster opens a native map and closes with B", async ({
page,
}) => {
await page.setViewportSize({ width: 320, height: 568 });
await start(page, "poster");
await page.keyboard.press("KeyX");
await expect(page.locator("#dialogue-text")).toContainText("TOWN MAP");
await page.keyboard.press("KeyX");
await expect(page.locator("#town-map-view")).toBeVisible();
const colors = await page
.locator("#town-map-canvas")
.evaluate(
(canvas: HTMLCanvasElement) =>
new Set(canvas.getContext("2d")!.getImageData(0, 0, 160, 144).data)
.size,
);
expect(colors).toBeGreaterThan(8);
await page.keyboard.press("KeyZ");
await expect(page.locator("#town-map-view")).toBeHidden();
await expect(page.locator("#game-notice")).toHaveText("");
});
test("the Pokémon Center PC deposits a Pokémon and an item", async ({
page,
}) => {
await start(page, "center");
const text = page.locator("#dialogue-text"),
choices = page.locator("#dialogue-choice");
await page.keyboard.press("KeyX");
await expect(text).toContainText("turned on the PC.");
await page.keyboard.press("KeyX");
await choices.getByRole("option", { name: "BILL's PC", exact: true }).click();
await expect(text).toContainText("BILL's PC");
await page.keyboard.press("KeyX");
await page.keyboard.press("KeyX");
await choices
.getByRole("option", { name: "DEPOSIT POKéMON", exact: true })
.click();
await choices.getByRole("option", { name: "PIDGEY :L4" }).click();
await choices.getByRole("option", { name: "DEPOSIT", exact: true }).click();
await expect(text).toHaveText("Stored PIDGEY!");
await page.clock.runFor(2000);
await expect(choices.getByRole("option", { name: "PIDGEY :L4" })).toHaveCount(
0,
);
// B leaves the party list, then Bill's PC.
await page.keyboard.press("KeyZ");
await page.keyboard.press("KeyZ");
await expect(choices.getByRole("option", { name: "TURN OFF" })).toBeVisible();
await choices
.getByRole("option", { name: /'s PC$/ })
.last()
.click();
await page.keyboard.press("KeyX");
await page.keyboard.press("KeyX");
await choices
.getByRole("option", { name: "DEPOSIT ITEM", exact: true })
.click();
const storage = page.locator("#mart");
await expect(storage).toHaveAttribute("aria-label", "Item storage");
await expect(page.locator("#mart-money")).toBeHidden();
await storage.getByRole("button", { name: /POTION/ }).click();
await storage.getByRole("button", { name: "Plus 1", exact: true }).click();
await storage.getByRole("button", { name: "DEPOSIT", exact: true }).click();
await expect(text).toContainText("Deposited 2 POTION(S).");
await page.keyboard.press("KeyX");
await storage.getByRole("button", { name: "BACK", exact: true }).click();
await choices.getByRole("option", { name: "LOG OFF", exact: true }).click();
await choices.getByRole("option", { name: "TURN OFF", exact: true }).click();
await page.keyboard.press("KeyX");
await page.clock.runFor(500);
await expect(page.locator("#game-notice")).toHaveText("");
const { state } = await backup(page);
expect(state.party).toHaveLength(1);
expect(
state.storage.boxes[0].map((mon: { species: number }) => mon.species),
).toEqual([16]);
expect(state.pcItems).toEqual([{ item: 18, quantity: 2 }]);
expect(state.bag.items).toEqual([{ item: 18, quantity: 3 }]);
});