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:
1 parent
5a0fe1e313
commit
b13fd20be4
28 files changed
+975
-101
No files matched your search
@@ -13,7 +13,9 @@ async function start(page: Page, scenario: BattleScenario = "win") {
|
||||
const source = await response.text();
|
||||
const needle = "const world = new GameWorld(assets);";
|
||||
expect(source).toContain(needle);
|
||||
await route.fulfill({ response, body: source.replace(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 { battleFixtureWorld } = await import('/tests/battle-fixture.ts');
|
||||
const world = battleFixtureWorld(assets, ${JSON.stringify(scenario)});`) });
|
||||
});
|
||||
|
||||
@@ -45,8 +45,6 @@ test("ROM gate, help and responsive layouts", async ({ page }) => {
|
||||
expect(button.width).toBeGreaterThanOrEqual(44);
|
||||
expect(button.height).toBeGreaterThanOrEqual(44);
|
||||
}
|
||||
await page.locator("#player-name").scrollIntoViewIfNeeded();
|
||||
await expect(page.locator("#player-name")).toBeInViewport({ ratio: 0.9 });
|
||||
await page.locator("#rom-label").scrollIntoViewIfNeeded();
|
||||
await expect(page.locator("#rom-label")).toBeInViewport({ ratio: 0.9 });
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@ async function start(page: Page, room: boolean | "poster" | "center" = false) {
|
||||
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(
|
||||
body: source.replace("this.world?.beginIntro();", "").replace(
|
||||
needle,
|
||||
`const { foundationFixture } = await import('/tests/foundation-fixture.ts'); const world = foundationFixture(assets, ${JSON.stringify(room)});`,
|
||||
),
|
||||
|
||||
@@ -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?",
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
/** Play a new game's introduction through the real controls: the gender
|
||||
* menu, the clock (kept at the device time) and a name. An empty name
|
||||
* picks the gender's first preset (CHRIS or KRIS). */
|
||||
export async function completeIntro(
|
||||
page: Page,
|
||||
{ girl = false, name = "" } = {},
|
||||
) {
|
||||
const screen = page.locator("#game-screen"),
|
||||
choice = page.locator("#dialogue-choice");
|
||||
await expect(screen).toHaveAttribute("data-intro", "true");
|
||||
await choice.getByRole("option", { name: girl ? "Girl" : "Boy" }).click();
|
||||
for (
|
||||
let guard = 0;
|
||||
guard < 80 && (await screen.getAttribute("data-intro")) === "true";
|
||||
guard++
|
||||
) {
|
||||
if (!(await choice.isVisible())) {
|
||||
await page.locator("#dialogue-next").click();
|
||||
continue;
|
||||
}
|
||||
const menu = choice.getByRole("option");
|
||||
if (await choice.locator("input").count()) {
|
||||
await choice.locator("input").fill(name);
|
||||
await choice.getByRole("button", { name: "OK" }).click();
|
||||
} else if (await menu.count()) await menu.nth(name ? 0 : 1).click();
|
||||
else if (await choice.getByRole("button", { name: "YES" }).count())
|
||||
await choice.getByRole("button", { name: "YES" }).click();
|
||||
else await choice.getByRole("button", { name: "OK" }).click();
|
||||
}
|
||||
await expect(screen).toHaveAttribute("data-intro", "false");
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { CrystalAssets } from "../src/game/rom/assets";
|
||||
import { GameWorld, migrateSave } from "../src/game/world/world";
|
||||
import { settle } from "./world-driver";
|
||||
|
||||
const romPath =
|
||||
process.env.CRYSTAL_ROM_PATH ??
|
||||
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
|
||||
/** InitGender's menu, then NamePlayer's first item. */
|
||||
const BOY = 0,
|
||||
GIRL = 1,
|
||||
NEW_NAME = 0,
|
||||
YES = 1,
|
||||
NO = 0;
|
||||
|
||||
describe.skipIf(!existsSync(romPath))("the new game introduction", () => {
|
||||
const bytes = existsSync(romPath)
|
||||
? new Uint8Array(readFileSync(romPath))
|
||||
: new Uint8Array();
|
||||
function intro(time = "2026-09-25T14:37:00") {
|
||||
let now = new Date(time);
|
||||
const world = new GameWorld(new CrystalAssets(bytes), () => now);
|
||||
world.beginIntro();
|
||||
return { world, setNow: (value: string) => (now = new Date(value)) };
|
||||
}
|
||||
|
||||
it("asks for gender, the clock and a name, then starts in the bedroom", () => {
|
||||
const { world } = intro();
|
||||
expect(world.intro).toBe(true);
|
||||
expect(world.dialogue?.choice).toEqual({
|
||||
kind: "menu",
|
||||
items: ["Boy", "Girl"],
|
||||
selected: 0,
|
||||
});
|
||||
// The steppers start at the device time; 5:05 PM is chosen instead.
|
||||
const shown = settle(world, [GIRL, 17, YES, 5, YES, NEW_NAME]);
|
||||
expect(shown).toContain("What?\nDAY 5 o'clock?");
|
||||
expect(shown).toContain("Whoa! 5 min.?");
|
||||
expect(shown).toContain("DAY 5:05!\nYikes! I over-\nslept!");
|
||||
expect(shown).toContain("My name is OAK.");
|
||||
expect(shown.at(-1)).toBe("YOUR NAME?");
|
||||
expect(world.portrait).toEqual({ player: true });
|
||||
expect(world.dialogue?.choice).toEqual({
|
||||
kind: "name",
|
||||
current: "KRIS",
|
||||
max: 7,
|
||||
});
|
||||
const rest = settle(world, ["KAWA"]);
|
||||
expect(rest[1]).toBe("KAWA, are you\nready?");
|
||||
expect(world.intro).toBe(false);
|
||||
expect(world.busy).toBe(false);
|
||||
expect(world.portrait).toBeUndefined();
|
||||
expect(world.name).toBe("KAWA");
|
||||
expect(world.avatar).toBe("kris");
|
||||
expect(world.state.clock.minuteOffset).toBe(17 * 60 + 5 - (14 * 60 + 37));
|
||||
expect(world.clock()).toMatchObject({ hours: 17, minutes: 5 });
|
||||
expect(world.clockTimeOfDay).toBe(1);
|
||||
expect(world.mapId).toBe("24:7");
|
||||
expect(world.save()).toMatchObject({ intro: false, name: "KAWA" });
|
||||
});
|
||||
|
||||
it("offers the gender's preset names and a blank name's default", () => {
|
||||
const { world } = intro();
|
||||
const shown = settle(world, [BOY, 14, YES, 37, YES, 2]);
|
||||
expect(shown).toContain("Zzz… Hm? Wha…?\nYou woke me up!");
|
||||
expect(world.name).toBe("MAT");
|
||||
expect(world.avatar).toBe("chris");
|
||||
expect(world.state.clock.minuteOffset).toBe(0);
|
||||
|
||||
const blank = intro().world;
|
||||
settle(blank, [GIRL, 14, YES, 37, YES, NEW_NAME, ""]);
|
||||
expect(blank.name).toBe("KRIS");
|
||||
});
|
||||
|
||||
it("repeats the hour or minutes after NO and ignores B in its menus", () => {
|
||||
const { world } = intro();
|
||||
world.choose(-1);
|
||||
expect(world.dialogue?.choice?.kind).toBe("menu");
|
||||
const shown = settle(world, [BOY, 3, NO, 22, YES, 59, NO, 0, YES, 1]);
|
||||
expect(shown).toContain("What?\nNITE 3 o'clock?");
|
||||
expect(shown).toContain("What?\nNITE 10 o'clock?");
|
||||
expect(shown).toContain("NITE 10:00!\nNo wonder it's so\ndark!");
|
||||
expect(world.name).toBe("CHRIS");
|
||||
});
|
||||
|
||||
it("answers the chosen hour as OakText_ResponseToSetTime does", () => {
|
||||
const response = (hour: number) => {
|
||||
const { world } = intro();
|
||||
const shown = settle(world, [BOY, hour, YES, 0, YES, 1]);
|
||||
return shown.find((page) => page.includes(":00"));
|
||||
};
|
||||
expect(response(3)).toBe("NITE 3:00!\nNo wonder it's so\ndark!");
|
||||
expect(response(4)).toBe("MORN 4:00!\nI overslept!");
|
||||
// DAY_HOUR + 1: ten o'clock prints DAY but still oversleeps.
|
||||
expect(response(10)).toBe("DAY 10:00!\nI overslept!");
|
||||
expect(response(11)).toBe("DAY 11:00!\nYikes! I over-\nslept!");
|
||||
expect(response(18)).toBe("NITE 6:00!\nNo wonder it's so\ndark!");
|
||||
expect(response(0)).toBe("NITE 12:00!\nNo wonder it's so\ndark!");
|
||||
});
|
||||
|
||||
it("wraps the stepper and keeps the shortest offset across midnight", () => {
|
||||
const { world, setNow } = intro("2026-09-25T23:30:00");
|
||||
world.choose(BOY);
|
||||
world.advanceDialogue();
|
||||
world.advanceDialogue();
|
||||
world.advanceDialogue();
|
||||
expect(world.dialogue?.choice).toMatchObject({
|
||||
kind: "clock",
|
||||
selected: 23,
|
||||
});
|
||||
world.moveCursor("up");
|
||||
expect(world.dialogue?.choice).toMatchObject({ selected: 0 });
|
||||
world.moveCursor("down");
|
||||
world.moveCursor("down");
|
||||
expect(world.dialogue?.choice).toMatchObject({ selected: 22 });
|
||||
world.moveCursor("up");
|
||||
world.moveCursor("up");
|
||||
settle(world, [0, YES, 10, YES, 1]);
|
||||
expect(world.state.clock.minuteOffset).toBe(40);
|
||||
// The game clock passes midnight 40 minutes before the device does.
|
||||
expect(world.clock()).toMatchObject({ hours: 0, minutes: 10 });
|
||||
setNow("2026-09-26T08:00:00");
|
||||
expect(world.clock()).toMatchObject({ hours: 8, minutes: 40 });
|
||||
|
||||
const early = intro("2026-09-25T00:10:00").world;
|
||||
settle(early, [BOY, 23, YES, 30, YES, 1]);
|
||||
expect(early.state.clock.minuteOffset).toBe(-40);
|
||||
});
|
||||
|
||||
it("starts again after a save made during it", () => {
|
||||
const { world } = intro();
|
||||
settle(world, [GIRL, 14, YES, 37, YES, NEW_NAME]);
|
||||
const mid = world.save();
|
||||
expect(mid.intro).toBe(true);
|
||||
expect(mid.name).toBe("CHRIS");
|
||||
|
||||
const restored = new GameWorld(
|
||||
new CrystalAssets(bytes),
|
||||
() => new Date("2026-09-25T14:37:00"),
|
||||
);
|
||||
restored.restore(mid);
|
||||
expect(restored.intro).toBe(true);
|
||||
expect(restored.dialogue?.pages[0]).toBe(
|
||||
"Are you a boy?\nOr are you a girl?",
|
||||
);
|
||||
settle(restored, [BOY, 14, YES, 37, YES, 3]);
|
||||
expect(restored.name).toBe("ALLAN");
|
||||
const done = restored.save();
|
||||
expect(done.intro).toBe(false);
|
||||
|
||||
// Loading a finished save leaves no introduction behind.
|
||||
const loaded = intro().world;
|
||||
loaded.restore(done);
|
||||
expect(loaded.intro).toBe(false);
|
||||
expect(loaded.busy).toBe(false);
|
||||
expect(loaded.name).toBe("ALLAN");
|
||||
});
|
||||
|
||||
it("migrates earlier saves as past the introduction on the device clock", () => {
|
||||
const world = new GameWorld(new CrystalAssets(bytes));
|
||||
const { intro: _, ...old } = world.save();
|
||||
const migrated = migrateSave(
|
||||
{ ...old, clock: { weekdayOffset: 3, dst: true } },
|
||||
() => 0,
|
||||
)!;
|
||||
expect(migrated.intro).toBe(false);
|
||||
expect(migrated.clock).toEqual({
|
||||
minuteOffset: 0,
|
||||
weekdayOffset: 3,
|
||||
dst: true,
|
||||
});
|
||||
world.restore(migrated);
|
||||
expect(world.intro).toBe(false);
|
||||
expect(() =>
|
||||
world.restore({
|
||||
...world.save(),
|
||||
clock: { minuteOffset: 720, weekdayOffset: 0, dst: false },
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("reads Oak's, Chris's and Kris's pictures and palettes", () => {
|
||||
const data = new CrystalAssets(bytes).pokemon;
|
||||
expect(data.trainerPic(10)).toHaveLength(49 * 16);
|
||||
expect(data.trainerPic(10).some((byte) => byte !== 0)).toBe(true);
|
||||
expect(data.playerPic(false)).not.toEqual(data.playerPic(true));
|
||||
expect(data.trainerPalette(0)).not.toEqual(data.trainerPalette(1));
|
||||
expect(data.trainerPalette(10)[0]).toEqual([255, 255, 255]);
|
||||
expect(() => data.trainerPic(0)).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,9 @@ async function start(page: Page) {
|
||||
const source = await response.text();
|
||||
const needle = "const world = new GameWorld(assets);";
|
||||
expect(source).toContain(needle);
|
||||
await route.fulfill({ response, body: source.replace(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 { martFixtureWorld } = await import('/tests/mart-fixture.ts');
|
||||
const world = martFixtureWorld(assets);`) });
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { GameWorld } from "../src/game/world/world";
|
||||
import type { Direction } from "../src/game/world/types";
|
||||
import { VECTORS } from "../src/game/world/types";
|
||||
import { settle } from "./world-driver";
|
||||
import { completeIntro } from "./intro-driver";
|
||||
|
||||
const romPath = resolve(
|
||||
process.env.CRYSTAL_ROM_PATH ??
|
||||
@@ -59,10 +60,13 @@ test("local maps follow a stairs transition and importing a previous position",
|
||||
await page.goto("/");
|
||||
await page.locator("#rom-file").setInputFiles(romPath);
|
||||
await expect(page.locator("#status")).toContainText("verified");
|
||||
await page.locator("#play").click();
|
||||
await completeIntro(page);
|
||||
await page.locator("#tab-saves").click();
|
||||
const downloaded = page.waitForEvent("download");
|
||||
await page.locator("#backup").click();
|
||||
const backup = await downloaded;
|
||||
await page.locator("#tab-game").click();
|
||||
await page.locator("#play").click();
|
||||
for (const direction of path!) {
|
||||
const [dx, dy] = VECTORS[direction];
|
||||
@@ -128,6 +132,7 @@ test("Mom's opening scene runs with native keyboard and pointer choices", async
|
||||
await page.locator("#rom-file").setInputFiles(romPath);
|
||||
await expect(page.locator("#status")).toContainText("verified");
|
||||
await page.locator("#play").click();
|
||||
await completeIntro(page);
|
||||
const screen = page.locator("#game-screen");
|
||||
const step = async (key: string, map: string, x: number, y: number) => {
|
||||
await page.keyboard.down(key);
|
||||
@@ -437,17 +442,18 @@ test("native player loads the actual ROM without loading an emulator and persist
|
||||
page.on("request", (request) => requests.push(request.url()));
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.goto("/");
|
||||
await page.locator("#player-name").fill("KAWA");
|
||||
await page.locator("#player-avatar").selectOption("kris");
|
||||
await page.locator("#rom-file").setInputFiles(romPath);
|
||||
await expect(page.locator("#status")).toContainText("verified");
|
||||
await expect(page.locator("#game-screen")).toHaveAttribute(
|
||||
"data-map",
|
||||
"24:7",
|
||||
);
|
||||
await expect(page.locator("#minimap")).toBeVisible();
|
||||
await expect(page.locator("#minimap")).toHaveAttribute("data-map", "24:7");
|
||||
await page.locator("#play").click();
|
||||
// The introduction covers the world and its minimap until it ends.
|
||||
await expect(page.locator("#minimap")).toBeHidden();
|
||||
await completeIntro(page, { girl: true, name: "KAWA" });
|
||||
await expect(page.locator("#minimap")).toBeVisible();
|
||||
await expect(page.locator("#game-screen")).toHaveAttribute("data-x", "3");
|
||||
await page.keyboard.down("ArrowUp");
|
||||
await expect(page.locator("#game-screen")).toHaveAttribute("data-y", "2");
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("native ROM formats", () => {
|
||||
sceneOwner: () => undefined,
|
||||
timeMask: () => 2,
|
||||
clock: () => ({ weekday: 0, hours: 12, minutes: 0 }),
|
||||
deviceWeekday: () => 0,
|
||||
clockDay: () => 0,
|
||||
text: () => defaultTextContext("KAWA"),
|
||||
};
|
||||
const yes = new ScriptRun(new RomReader(bytes), 0, state, host);
|
||||
@@ -260,7 +260,7 @@ describe.skipIf(!existsSync(romPath))(
|
||||
expect(save.scenes).toEqual({ "24:6": 1 });
|
||||
expect(save.phone).toEqual([1]);
|
||||
// 2026-09-22 is a Tuesday; the chosen Monday is six days ahead.
|
||||
expect(save.clock).toEqual({ weekdayOffset: 6, dst: true });
|
||||
expect(save.clock).toEqual({ minuteOffset: 0, weekdayOffset: 6, dst: true });
|
||||
expect(world.clock().weekday).toBe(1);
|
||||
expect(world.state.flags.size).toBe(flags + 2);
|
||||
expect(world.state.events.has(1735)).toBe(true);
|
||||
@@ -839,7 +839,7 @@ describe.skipIf(!existsSync(romPath))(
|
||||
sceneOwner: () => undefined,
|
||||
timeOfDay: () => 1,
|
||||
clock: () => ({ weekday: 0, hours: 12, minutes: 0 }),
|
||||
deviceWeekday: () => 0,
|
||||
clockDay: () => 0,
|
||||
text: () => defaultTextContext("CHRIS"),
|
||||
data: assets.pokemon,
|
||||
player: () => ({ id: 0, gender: 0 as const }),
|
||||
|
||||
@@ -14,7 +14,9 @@ async function start(
|
||||
const source = await response.text();
|
||||
const needle = "const world = new GameWorld(assets);";
|
||||
expect(source).toContain(needle);
|
||||
await route.fulfill({ response, body: source.replace(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 { ${fixture} } = await import('/tests/quest-fixture.ts');
|
||||
const world = ${fixture}(assets);`) });
|
||||
});
|
||||
|
||||
@@ -12,7 +12,9 @@ test("trainer UI refuses RUN, wins against Joey and saves the reward", async ({
|
||||
const source = await response.text();
|
||||
const needle = "const world = new GameWorld(assets);";
|
||||
expect(source).toContain(needle);
|
||||
await route.fulfill({ response, body: source.replace(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 { trainerFixtureWorld } = await import('/tests/trainer-fixture.ts');
|
||||
const world = trainerFixtureWorld(assets, undefined, false, true);`) });
|
||||
});
|
||||
@@ -68,7 +70,9 @@ test("Route 30: Bug Catcher Don spots the player, walks over and sends out both
|
||||
const source = await response.text();
|
||||
const needle = "const world = new GameWorld(assets);";
|
||||
expect(source).toContain(needle);
|
||||
await route.fulfill({ response, body: source.replace(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 { route30FixtureWorld } = await import('/tests/trainer-fixture.ts');
|
||||
const world = route30FixtureWorld(assets);`) });
|
||||
});
|
||||
|
||||
@@ -2,7 +2,8 @@ import type { GameWorld } from "../src/game/world/world";
|
||||
import type { Direction } from "../src/game/world/types";
|
||||
|
||||
/** Drive a world through scenes the way a player would: acknowledge pages,
|
||||
* answer choices in order (default YES / first weekday) and wait out timed
|
||||
* answer choices in order (default YES, the first weekday or menu item, or
|
||||
* the clock's shown value) and wait out timed
|
||||
* effects. Returns every page that was shown. */
|
||||
export function settle(
|
||||
world: GameWorld,
|
||||
@@ -29,7 +30,9 @@ export function settle(
|
||||
? answer
|
||||
: dialogue.choice.kind === "yesno"
|
||||
? 1
|
||||
: 0,
|
||||
: dialogue.choice.kind === "clock"
|
||||
? dialogue.choice.selected
|
||||
: 0,
|
||||
);
|
||||
} else if (last && dialogue.holdTicks !== undefined) {
|
||||
if (page !== lastHeld) shown.push(page);
|
||||
|
||||
Reference in new issue
Block a user