Port the introduction's naming and clock screens
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>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-30 09:53:52 +02:00
1 parent 5a0fe1e313
commit b13fd20be4
28 files changed
+975 -101

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
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) {
await page.clock.install({ time: new Date("2026-09-25T14:37: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-25T14:37:05"));
await page.locator("#play").click();
}
/** Press A until the dialogue shows `text`. */
async function advanceTo(page: Page, text: string) {
const dialogue = page.locator("#dialogue-text");
for (let guard = 0; guard < 40; guard++) {
if ((await dialogue.textContent())?.includes(text)) return;
await page.keyboard.press("KeyX");
}
await expect(dialogue).toContainText(text);
}
test("a new game asks for gender, the clock and a name before the bedroom", async ({
page,
}) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await start(page);
const screen = page.locator("#game-screen"),
choice = page.locator("#dialogue-choice"),
text = page.locator("#dialogue-text"),
picture = page.locator("#pokepic-canvas");
await expect(screen).toHaveAttribute("data-intro", "true");
await expect(page.locator("#intro-screen")).toBeVisible();
await expect(text).toHaveText("Are you a boy?\nOr are you a girl?");
// Down, then A chooses "Girl"; B is ignored.
await page.keyboard.press("KeyZ");
await page.keyboard.press("ArrowDown");
await expect(choice.getByRole("option", { name: "Girl" })).toHaveAttribute(
"aria-selected",
"true",
);
await page.keyboard.press("KeyX");
await advanceTo(page, "What time is it?");
// The hour starts at the device time; Up three times is 5 PM.
const value = choice.locator("output");
await expect(value).toHaveText("DAY 2 o'clock");
for (let i = 0; i < 3; i++) await page.keyboard.press("ArrowUp");
await expect(value).toHaveText("DAY 5 o'clock");
await page.keyboard.press("KeyX");
await expect(text).toHaveText("What?\nDAY 5 o'clock?");
await choice.getByRole("button", { name: "YES" }).click();
await expect(text).toHaveText("How many minutes?");
await expect(value).toHaveText("37 min.");
await choice.getByRole("button", { name: "Earlier" }).click();
await expect(value).toHaveText("36 min.");
await choice.getByRole("button", { name: "OK" }).click();
await expect(text).toHaveText("Whoa! 36 min.?");
await page.keyboard.press("KeyX");
await expect(text).toHaveText("DAY 5:36!\nYikes! I over-\nslept!");
await page.keyboard.press("KeyX");
await expect(picture).toHaveAttribute("aria-label", "Professor Oak");
await advanceTo(page, "This world is in-");
await expect(picture).toHaveAttribute("aria-label", "WOOPER");
await advanceTo(page, "Now, what did you");
await expect(picture).toHaveAttribute("aria-label", "Kris");
await page.keyboard.press("KeyX");
await expect(choice.getByRole("option")).toHaveText([
"NEW NAME",
"KRIS",
"AMANDA",
"JUANA",
"JODI",
]);
await page.keyboard.press("KeyX");
await expect(text).toHaveText("YOUR NAME?");
await choice.locator("input").fill("LYRA");
await choice.locator("input").press("Enter");
await expect(text).toHaveText("LYRA, are you\nready?");
await advanceTo(page, "I'll be seeing you");
await page.keyboard.press("KeyX");
await expect(screen).toHaveAttribute("data-intro", "false");
await expect(page.locator("#intro-screen")).toBeHidden();
await expect(page.locator("#pokepic")).toBeHidden();
await expect(screen).toHaveAttribute("data-map", "24:7");
await page.locator("#tab-saves").click();
const download = page.waitForEvent("download");
await page.locator("#backup").click();
const record = JSON.parse(
readFileSync((await (await download).path())!, "utf8"),
);
const save = JSON.parse(Buffer.from(record.state, "base64").toString("utf8"));
expect(save).toMatchObject({
intro: false,
name: "LYRA",
avatar: "kris",
clock: { minuteOffset: 17 * 60 + 36 - (14 * 60 + 37) },
});
expect(errors).toEqual([]);
});
test("a session saved during the introduction starts it again", async ({
page,
}) => {
await start(page);
await page.keyboard.press("KeyX");
await advanceTo(page, "What time is it?");
// Leaving the game tab pauses and saves.
await page.locator("#tab-saves").click();
await expect(page.locator("#save-status")).toContainText("Session saved");
await page.reload();
await page.locator("#rom-file").setInputFiles(romPath);
await expect(page.locator("#status")).toContainText("saved session");
await page.locator("#play").click();
await expect(page.locator("#game-screen")).toHaveAttribute(
"data-intro",
"true",
);
await expect(page.locator("#dialogue-text")).toHaveText(
"Are you a boy?\nOr are you a girl?",
);
});