POKéGEAR in the start menu runs the clock, Town Map and phone cards with the original joypad rules. Calls to Elm and Mom run their callee scripts, with the out-of-area and out-of-hours cases and HangUp's corrected timing. Mom saves a quarter of trainer prizes once asked, talking to her at home opens BankOfMom (GET, SAVE, CHANGE and the DST checks) with its digit amount box, and MomTriesToBuySomething buys items and dolls with her savings. Saves gain Mom's money, her next item and its trigger balance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3456aa4e11
commit
209778cabe
29 files changed
+2726
-47
No files matched your search
+140
-1
@@ -5,6 +5,7 @@ import { bindTabs } from "./ui/tabs";
|
||||
import { NativeRuntime } from "./game/runtime";
|
||||
import { LocalMapView } from "./game/rendering/minimap";
|
||||
import type { Choice, GameWorld } from "./game/world/world";
|
||||
import type { PokegearCard } from "./game/scripts/pokegear";
|
||||
import { drawTile } from "./game/rendering/pixels";
|
||||
import { PokemonData } from "./game/rom/pokemon";
|
||||
import type { Direction, Palette } from "./game/world/types";
|
||||
@@ -47,6 +48,7 @@ function showWorld(world: GameWorld) {
|
||||
get("game-notice").textContent = world.notice;
|
||||
showBattle(world);
|
||||
showStatsScreen(world);
|
||||
showPokegear(world);
|
||||
get("town-map-view").hidden = !world.townMap;
|
||||
if (world.townMap)
|
||||
drawTownMap(get<HTMLCanvasElement>("town-map-canvas"), world);
|
||||
@@ -60,6 +62,8 @@ function showWorld(world: GameWorld) {
|
||||
get("native-dialogue").hidden =
|
||||
!dialogue ||
|
||||
pictureWait ||
|
||||
// The Pokégear's map and radio cards have no textbox.
|
||||
(dialogue.choice?.kind === "pokegear" && !dialogue.pages[dialogue.index]) ||
|
||||
Boolean(mart) ||
|
||||
world.townMap ||
|
||||
Boolean(world.statsScreen);
|
||||
@@ -113,7 +117,11 @@ function showChoice(choice: Choice | undefined) {
|
||||
return element;
|
||||
};
|
||||
if (!choice) container.replaceChildren();
|
||||
else if (choice.kind === "yesno")
|
||||
else if (choice.kind === "pokegear") {
|
||||
// The Pokégear reads the controls directly.
|
||||
container.replaceChildren();
|
||||
container.hidden = true;
|
||||
} else if (choice.kind === "yesno")
|
||||
container.replaceChildren(
|
||||
button("YES", choice.selected === 1, () => runtime.choose(1)),
|
||||
button("NO", choice.selected === 0, () => runtime.choose(0)),
|
||||
@@ -156,6 +164,51 @@ function showChoice(choice: Choice | undefined) {
|
||||
);
|
||||
container.firstElementChild?.setAttribute("aria-label", "Earlier");
|
||||
container.children[2]?.setAttribute("aria-label", "Later");
|
||||
} else if (choice.kind === "mombank") {
|
||||
// Mom_ContinueMenuSetup's box: SAVED, HELD and the amount, whose digit
|
||||
// under the cursor blinks. The buttons send the joypad's own presses.
|
||||
const { view } = choice;
|
||||
const box = document.createElement("div");
|
||||
box.className = "mom-bank";
|
||||
const row = (label: string, value: HTMLElement | string) => {
|
||||
const line = document.createElement("p"),
|
||||
name = document.createElement("span"),
|
||||
amount = document.createElement("output");
|
||||
name.textContent = label;
|
||||
amount.append(value);
|
||||
line.append(name, amount);
|
||||
return line;
|
||||
};
|
||||
const digits = document.createElement("span");
|
||||
digits.setAttribute("aria-label", `¥${view.amount}`);
|
||||
digits.append(
|
||||
"¥",
|
||||
...[...String(view.amount).padStart(6, "0")].map((digit, index) => {
|
||||
const cell = document.createElement("span");
|
||||
cell.textContent = digit;
|
||||
if (index === view.cursor) cell.className = "mom-bank-cursor";
|
||||
return cell;
|
||||
}),
|
||||
);
|
||||
box.append(
|
||||
row(view.labels[0], `¥${view.saved}`),
|
||||
row(view.labels[1], `¥${view.held}`),
|
||||
row(view.labels[2], digits),
|
||||
);
|
||||
const press = (label: string, aria: string, name: string) => {
|
||||
const element = button(label, undefined, () => runtime.choose(name));
|
||||
element.setAttribute("aria-label", aria);
|
||||
return element;
|
||||
};
|
||||
container.replaceChildren(
|
||||
box,
|
||||
press("◀", "Previous digit", "left"),
|
||||
press("▼", "Decrease digit", "down"),
|
||||
press("▲", "Increase digit", "up"),
|
||||
press("▶", "Next digit", "right"),
|
||||
press("OK", "OK", "a"),
|
||||
press("CANCEL", "Cancel", "b"),
|
||||
);
|
||||
} else if (choice.kind === "name")
|
||||
container.replaceChildren(
|
||||
...nameField(choice.current, choice.max, (name) => runtime.choose(name)),
|
||||
@@ -807,6 +860,92 @@ function showBattle(world: GameWorld) {
|
||||
);
|
||||
highlightBattleChoice();
|
||||
}
|
||||
let pokegearKey = "";
|
||||
const POKEGEAR_CARDS: Record<PokegearCard, string> = {
|
||||
clock: "CLOCK",
|
||||
map: "MAP",
|
||||
phone: "PHONE",
|
||||
radio: "RADIO",
|
||||
};
|
||||
/** The Pokégear's cards (engine/pokegear/pokegear.asm): the card icons,
|
||||
* then the clock, the Town Map with its cursor's landmark, the phone list
|
||||
* or the radio. Its textbox is the native dialogue. */
|
||||
function showPokegear(world: GameWorld) {
|
||||
const view = world.pokegear;
|
||||
get("pokegear").hidden = !view;
|
||||
if (!view) {
|
||||
pokegearKey = "";
|
||||
return;
|
||||
}
|
||||
const key = JSON.stringify(view);
|
||||
if (key === pokegearKey) return;
|
||||
pokegearKey = key;
|
||||
const element = <K extends keyof HTMLElementTagNameMap>(
|
||||
tag: K,
|
||||
text = "",
|
||||
className = "",
|
||||
) => {
|
||||
const node = document.createElement(tag);
|
||||
node.textContent = text;
|
||||
if (className) node.className = className;
|
||||
return node;
|
||||
};
|
||||
get("pokegear-cards").replaceChildren(
|
||||
...view.cards.map((card) => {
|
||||
const icon = element("span", POKEGEAR_CARDS[card]);
|
||||
icon.setAttribute("aria-current", String(card === view.card));
|
||||
return icon;
|
||||
}),
|
||||
);
|
||||
const body = get("pokegear-card");
|
||||
if (view.clock) {
|
||||
const clock = element("div", "", "pokegear-clock");
|
||||
clock.append(element("span", view.clock.day), element("strong", view.clock.time));
|
||||
body.replaceChildren(clock);
|
||||
} else if (view.map) {
|
||||
const map = element("div", "", "pokegear-map"),
|
||||
canvas = element("canvas");
|
||||
canvas.setAttribute("role", "img");
|
||||
drawTownMap(canvas, world, view.map);
|
||||
map.append(element("p", `▶ ${view.map.name}`), canvas);
|
||||
body.replaceChildren(map);
|
||||
} else if (view.phone) {
|
||||
const phone = view.phone,
|
||||
card = element("div", "", "pokegear-phone"),
|
||||
list = element("ol");
|
||||
list.setAttribute("aria-label", "Phone numbers");
|
||||
list.replaceChildren(
|
||||
...phone.rows.map((row, index) => {
|
||||
const item = element("li", row);
|
||||
item.dataset.mark =
|
||||
phone.chosen === index
|
||||
? "▷"
|
||||
: phone.chosen === undefined && phone.cursor === index
|
||||
? "▶"
|
||||
: " ";
|
||||
if (item.dataset.mark !== " ") item.setAttribute("aria-current", "true");
|
||||
return item;
|
||||
}),
|
||||
);
|
||||
card.append(
|
||||
element("span", phone.signal ? "Signal ▮▮▮" : "Out of service", "pokegear-signal"),
|
||||
list,
|
||||
);
|
||||
if (phone.submenu) {
|
||||
const submenu = element("ol", "", "pokegear-submenu");
|
||||
submenu.replaceChildren(
|
||||
...phone.submenu.items.map((item, index) => {
|
||||
const entry = element("li", item);
|
||||
entry.dataset.mark = index === phone.submenu!.selected ? "▶" : " ";
|
||||
return entry;
|
||||
}),
|
||||
);
|
||||
card.append(submenu);
|
||||
}
|
||||
body.replaceChildren(card);
|
||||
} else body.replaceChildren();
|
||||
}
|
||||
|
||||
/** The stats screen's pages (engine/pokemon/stats_screen.asm), with its
|
||||
* strings, type names and stat labels read from the ROM. */
|
||||
function showStatsScreen(world: GameWorld) {
|
||||
|
||||
Reference in new issue
Block a user