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
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user