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

+46 -23
View File
@@ -42,8 +42,7 @@ function showWorld(world: GameWorld) {
`You are at ${world.x}, ${world.y}. Red marks your position.`;
get("minimap-position").textContent = `${world.x}, ${world.y}`;
get("map-empty").hidden = true;
get<HTMLInputElement>("player-name").value = world.name;
get<HTMLSelectElement>("player-avatar").value = world.avatar;
get("intro-screen").hidden = !world.intro;
get("location").textContent = world.map.name;
get("tiles-walked").textContent = `${world.tilesWalked} tiles walked`;
get("game-notice").textContent = world.notice;
@@ -81,6 +80,7 @@ function showWorld(world: GameWorld) {
canvas.dataset.x = String(world.x);
canvas.dataset.y = String(world.y);
canvas.dataset.activity = world.activity;
canvas.dataset.intro = String(world.intro);
canvas.setAttribute(
"aria-label",
`${world.map.name}. Position ${world.x}, ${world.y}. Arrow keys move. X interacts. Z continues dialogue.`,
@@ -134,6 +134,25 @@ function showChoice(choice: Choice | undefined) {
);
container.firstElementChild?.setAttribute("aria-label", "Previous day");
container.children[2]?.setAttribute("aria-label", "Next day");
} else if (choice.kind === "clock") {
// InitClock's hour or minutes: Up for later, Down for earlier.
const value = document.createElement("output");
value.textContent = choice.labels[choice.selected];
value.setAttribute("aria-live", "polite");
container.replaceChildren(
button("▼", undefined, () => {
runtime.world?.moveCursor("down");
if (runtime.world) showWorld(runtime.world);
}),
value,
button("▲", undefined, () => {
runtime.world?.moveCursor("up");
if (runtime.world) showWorld(runtime.world);
}),
button("OK", true, () => runtime.choose(choice.selected)),
);
container.firstElementChild?.setAttribute("aria-label", "Earlier");
container.children[2]?.setAttribute("aria-label", "Later");
} else if (choice.kind === "name")
container.replaceChildren(
...nameField(choice.current, choice.max, (name) => runtime.choose(name)),
@@ -687,20 +706,37 @@ function setHp(bar: HTMLElement, hp: number, max: number) {
bar.classList.toggle("low", fraction <= 0.5 && fraction > 0.2);
bar.classList.toggle("critical", fraction <= 0.2);
}
let shownPicture: number | undefined;
/** Draw a pokepic from the ROM's front picture tiles and palette. */
let shownPicture = "";
/** Draw a pokepic, or the introduction's trainer or player picture, from
* the ROM's picture tiles and palette. */
function showPicture(world: GameWorld) {
const box = get("pokepic");
box.hidden = world.picture === undefined;
if (world.picture === shownPicture) return;
shownPicture = world.picture;
if (world.picture === undefined) return;
const portrait = world.portrait;
box.hidden = world.picture === undefined && !portrait;
const key = JSON.stringify([world.picture, portrait]);
if (key === shownPicture) return;
shownPicture = key;
if (box.hidden) return;
const data = world.assets.pokemon;
const picture = get<HTMLCanvasElement>("pokepic-canvas");
const context = picture.getContext("2d")!;
const image = context.createImageData(56, 56);
const tiles = data.frontPic(world.picture),
let tiles: Uint8Array, palette: Palette, label: string;
if (world.picture !== undefined) {
tiles = data.frontPic(world.picture);
palette = data.palette(world.picture);
label = data.speciesName(world.picture);
} else if (portrait && "player" in portrait) {
// DrawIntroPlayerPic: CHRIS's palette, or KRIS's (class 1).
tiles = data.playerPic(portrait.player);
palette = data.trainerPalette(portrait.player ? 1 : 0);
label = portrait.player ? "Kris" : "Chris";
} else {
const trainer = (portrait as { trainer: number }).trainer;
tiles = data.trainerPic(trainer);
palette = data.trainerPalette(trainer);
label = "Professor Oak";
}
// Picture tiles are stored column by column.
for (let tile = 0; tile < 49; tile++)
drawTile(
@@ -714,7 +750,7 @@ function showPicture(world: GameWorld) {
palette,
);
context.putImageData(image, 0, 0);
picture.setAttribute("aria-label", data.speciesName(world.picture));
picture.setAttribute("aria-label", label);
}
const runtime = new NativeRuntime(
showWorld,
@@ -724,17 +760,6 @@ const runtime = new NativeRuntime(
showError(error);
update();
},
() => ({
name:
get<HTMLInputElement>("player-name")
.value.trim()
.replace(/[^A-Za-z0-9 -]/g, "")
.slice(0, 7) || "CHRIS",
avatar:
get<HTMLSelectElement>("player-avatar").value === "kris"
? "kris"
: "chris",
}),
handleBattleInput,
);
const session = new GameSession(
@@ -793,8 +818,6 @@ function update() {
get("pause-overlay").hidden = session.state !== "paused";
get<HTMLInputElement>("rom-file").disabled = busy;
get("rom-label").classList.toggle("disabled", busy);
get<HTMLInputElement>("player-name").disabled = busy || ready();
get<HTMLSelectElement>("player-avatar").disabled = busy || ready();
get<HTMLButtonElement>("dialogue-next").disabled =
busy || session.state !== "playing";
}