Port the trainer card and the play time
The player's name in the start menu opens TrainerCard: page 1 with the name, ID, money, Pokédex count and play time, page 2 with the Johto leaders and the badges owned, turned and closed with the original buttons. The card picture, leader faces and animated badges come from the ROM with _CGB_TrainerCard's palettes. GameTimer now counts the play time from the end of the introduction, capped at 999:59:59, and saves keep it. Closes #23 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
38b9562bd3
commit
47698565ab
24 files changed
+954
-30
No files matched your search
+85
-2
@@ -1,4 +1,8 @@
|
||||
import { drawTownMap } from "./game/rendering/town-map";
|
||||
import {
|
||||
drawCardPic,
|
||||
drawTrainerCardBadges,
|
||||
} from "./game/rendering/trainer-card";
|
||||
import "./ui/styles.css";
|
||||
import { renderShell } from "./ui/shell";
|
||||
import { bindTabs } from "./ui/tabs";
|
||||
@@ -49,6 +53,7 @@ function showWorld(world: GameWorld) {
|
||||
showBattle(world);
|
||||
showStatsScreen(world);
|
||||
showPokegear(world);
|
||||
showTrainerCard(world);
|
||||
get("town-map-view").hidden = !world.townMap;
|
||||
if (world.townMap)
|
||||
drawTownMap(get<HTMLCanvasElement>("town-map-canvas"), world);
|
||||
@@ -64,6 +69,7 @@ function showWorld(world: GameWorld) {
|
||||
pictureWait ||
|
||||
// The Pokégear's map and radio cards have no textbox.
|
||||
(dialogue.choice?.kind === "pokegear" && !dialogue.pages[dialogue.index]) ||
|
||||
dialogue.choice?.kind === "trainercard" ||
|
||||
Boolean(mart) ||
|
||||
world.townMap ||
|
||||
Boolean(world.statsScreen);
|
||||
@@ -117,8 +123,8 @@ function showChoice(choice: Choice | undefined) {
|
||||
return element;
|
||||
};
|
||||
if (!choice) container.replaceChildren();
|
||||
else if (choice.kind === "pokegear") {
|
||||
// The Pokégear reads the controls directly.
|
||||
else if (choice.kind === "pokegear" || choice.kind === "trainercard") {
|
||||
// The Pokégear and the trainer card read the controls directly.
|
||||
container.replaceChildren();
|
||||
container.hidden = true;
|
||||
} else if (choice.kind === "yesno")
|
||||
@@ -946,6 +952,83 @@ function showPokegear(world: GameWorld) {
|
||||
} else body.replaceChildren();
|
||||
}
|
||||
|
||||
let trainerCardKey = "";
|
||||
/** TrainerCard (engine/menus/trainer_card.asm): the top half with the
|
||||
* name, ID, money and card picture, then page 1's Pokédex count and play
|
||||
* time or page 2's leaders and badges. */
|
||||
function showTrainerCard(world: GameWorld) {
|
||||
const view = world.trainerCard;
|
||||
const panel = get("trainer-card");
|
||||
panel.hidden = !view;
|
||||
if (!view) {
|
||||
trainerCardKey = "";
|
||||
return;
|
||||
}
|
||||
const key = JSON.stringify(view);
|
||||
if (key === trainerCardKey) return;
|
||||
const first = !trainerCardKey;
|
||||
trainerCardKey = key;
|
||||
const element = <K extends keyof HTMLElementTagNameMap>(
|
||||
tag: K,
|
||||
text = "",
|
||||
) => {
|
||||
const node = document.createElement(tag);
|
||||
node.textContent = text;
|
||||
return node;
|
||||
};
|
||||
const rows = (list: [string, string][]) => {
|
||||
const dl = element("dl");
|
||||
for (const [term, value] of list)
|
||||
dl.append(element("dt", term), element("dd", value));
|
||||
return dl;
|
||||
};
|
||||
if (first) {
|
||||
// The frame in the other gender's palette, the picture in the player's.
|
||||
const frame = world.assets.pokemon.trainerPalette(view.female ? 0 : 1)[1];
|
||||
panel.style.setProperty("--trainer-card-frame", `rgb(${frame.join(" ")})`);
|
||||
const pic = get<HTMLCanvasElement>("trainer-card-pic");
|
||||
drawCardPic(pic, world.assets, view.female);
|
||||
pic.setAttribute("aria-label", view.female ? "Kris" : "Chris");
|
||||
}
|
||||
const { labels } = view;
|
||||
get("trainer-card-info").replaceChildren(
|
||||
...rows([
|
||||
[labels.name, view.name],
|
||||
["ID NO", view.id],
|
||||
[labels.money, `¥${view.money}`],
|
||||
]).childNodes,
|
||||
);
|
||||
const page = get("trainer-card-page");
|
||||
if (view.page === 1) {
|
||||
const { hours, minutes } = view.time;
|
||||
const time = `${String(hours).padStart(4, " ")}${view.colon ? ":" : " "}${String(minutes).padStart(2, "0")}`;
|
||||
const list: [string, string][] = [];
|
||||
if (view.pokedex !== undefined)
|
||||
list.push([labels.pokedex, String(view.pokedex).padStart(3, " ")]);
|
||||
list.push([labels.playTime, time]);
|
||||
const status = rows(list);
|
||||
status.setAttribute(
|
||||
"aria-label",
|
||||
`Play time ${hours} hours ${minutes} minutes`,
|
||||
);
|
||||
page.replaceChildren(element("h3", "STATUS"), status, element("p", labels.badges));
|
||||
} else {
|
||||
const canvas = element("canvas");
|
||||
canvas.setAttribute("role", "img");
|
||||
let owned = 0;
|
||||
for (let bit = 0; bit < 8; bit++) if (view.badges & (1 << bit)) owned++;
|
||||
canvas.setAttribute("aria-label", `${owned} of 8 Johto badges`);
|
||||
drawTrainerCardBadges(
|
||||
canvas,
|
||||
world.assets,
|
||||
view.female,
|
||||
view.badges,
|
||||
view.badgeFrame,
|
||||
);
|
||||
page.replaceChildren(element("h3", labels.badges.replace("▶", "")), canvas);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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