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:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-10-01 13:32:14 +02:00
1 parent 38b9562bd3
commit 47698565ab
24 files changed
+954 -30

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
import type { CrystalAssets } from "../rom/assets";
import { TRAINER_CARD } from "../rom/profile";
import { OW } from "../world/constants";
import { drawTile } from "./pixels";
const CARD_PIC_TILES = 35;
const LEADER_TILES = 86;
const BADGE_TILES = 44;
/** CardRightCornerGFX replaces the picture's tile $1c, which the card's
* border corner covers. */
const CARD_CORNER_TILE = 0x1c;
/** _CGB_TrainerCard's palettes 1-7 by trainer class; Clair uses palette 1. */
const LEADER_PALETTES = [
OW.FALKNER,
OW.BUGSY,
OW.WHITNEY,
OW.MORTY,
OW.CHUCK,
OW.JASMINE,
OW.PRYCE,
OW.FALKNER,
];
function context(canvas: HTMLCanvasElement, width: number, height: number) {
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d")!;
const image = context.createImageData(width, height);
image.data.fill(255);
return { context, image };
}
/** GetCardPic's Chris or Kris picture, placed by PlaceGraphic column by
* column, in the player's own palette. */
export function drawCardPic(
canvas: HTMLCanvasElement,
assets: CrystalAssets,
female: boolean,
) {
const { context: target, image } = context(canvas, 40, 56);
const tiles = assets.reader.slice(
female ? TRAINER_CARD.KrisCardPic : TRAINER_CARD.ChrisCardPic,
CARD_PIC_TILES * 16,
);
const palette = assets.pokemon.trainerPalette(female ? 1 : 0);
for (let tile = 0; tile < CARD_PIC_TILES; tile++)
if (tile !== CARD_CORNER_TILE)
drawTile(
image.data,
40,
56,
tiles,
tile,
Math.floor(tile / 7) * 8,
(tile % 7) * 8,
palette,
);
target.putImageData(image, 0, 0);
}
/** Page 2's leaders and badges: tiles 2-17 of rows 10-15, as
* TrainerCard_Page2_3_InitObjectsAndStrings places the faces, with the
* attribute map of _CGB_TrainerCard and the badge sprites of
* TrainerCard_Page2_3_OAMUpdate for the badges owned at `frame`. */
export function drawTrainerCardBadges(
canvas: HTMLCanvasElement,
assets: CrystalAssets,
female: boolean,
badges: number,
frame: number,
) {
const r = assets.reader;
const width = 128,
height = 48;
const { context: target, image } = context(canvas, width, height);
const leaders = r.slice(TRAINER_CARD.LeaderGFX, LEADER_TILES * 16);
// The card fills the screen with the other gender's palette.
const border = assets.pokemon.trainerPalette(female ? 0 : 1);
for (let leader = 0; leader < 8; leader++) {
const base = (leader < 4 ? 0x29 : 0x51) + (leader % 4) * 10 - 0x29;
const x = (leader % 4) * 32,
y = leader < 4 ? 0 : 24;
const face = assets.pokemon.trainerPalette(LEADER_PALETTES[leader]);
// TrainerCard_Page2_3_PlaceLeadersFaces: four tiles, then two rows
// of three one tile to the right.
for (let i = 0; i < 4; i++)
drawTile(image.data, width, height, leaders, base + i, x + i * 8, y, border);
for (let row = 1; row <= 2; row++)
for (let i = 0; i < 3; i++)
drawTile(
image.data,
width,
height,
leaders,
base + 1 + row * 3 + i,
x + 8 + i * 8,
y + row * 8,
face,
);
}
const graphics = r.slice(TRAINER_CARD.BadgeGFX, BADGE_TILES * 16);
const palette = assets.palette(
TRAINER_CARD.PredefPals + OW.PREDEFPAL_CGB_BADGE * 8,
);
// dw wJohtoBadges, then y, x, palette and eight tile steps per badge.
for (let badge = 0; badge < OW.NUM_JOHTO_BADGES; badge++) {
if (!(badges & (1 << badge))) continue;
const entry = TRAINER_CARD.TrainerCard_JohtoBadgesOAM + 2 + badge * 11;
// OAM coordinates, less the hardware offsets and this area's origin.
const y = r.byte(entry) - 16 - 10 * 8,
x = r.byte(entry + 1) - 8 - 2 * 8;
const id = r.byte(entry + 3 + frame);
const flip = (id & 0x80) !== 0,
tile = id & 0x7f;
// .facing1, or .facing2's mirrored order.
const order = flip ? [1, 0, 3, 2] : [0, 1, 2, 3];
order.forEach((offset, i) =>
drawTile(
image.data,
width,
height,
graphics,
tile + offset,
x + (i % 2) * 8,
y + Math.floor(i / 2) * 8,
palette,
{ transparent: true, flipX: flip },
),
);
}
target.putImageData(image, 0, 0);
}