Checks / web (push) Canceled after 0s
A new game now runs NewGame's InitGender and OakSpeech natively before the bedroom: Boy or Girl, InitClock's hour and minute steppers with their YES/NO confirmations, Oak's speech with Wooper, and NamePlayer's preset names or naming field (a blank name takes CHRIS or KRIS). Texts, menus and time strings come from a new INTRO profile table verified by check:reference; Oak's and the player's pictures decode from TrainerPicPointers, ChrisPic/KrisPic and TrainerPalettes. The chosen time is kept as a minute offset from the device clock, which time of day, the weekday, NPC hours and the daily reset now read. The intro runs only when nothing is saved; a save made during it starts it again. Saves gain intro and clock.minuteOffset, migrated by absence. The shell's name and character fields are gone. Intentional change: the steppers start at the device time rather than 10:00. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
187 lines
7.4 KiB
TypeScript
187 lines
7.4 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, 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);
|
||
// Fixture worlds stand in for a saved session, so no introduction runs.
|
||
expect(source).toContain("this.world?.beginIntro();");
|
||
await route.fulfill({
|
||
response,
|
||
body: source.replace("this.world?.beginIntro();", "").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 }]);
|
||
});
|