Files
Crystal/src/game/scripts/hall-of-fame.ts
T
kawaandClaude Opus 5.5 f05fd8ebee
Checks / web (push) Failing after 18s
Private ROM checks / private-rom (push) Failing after 25m31s
Open the League: Victory Road, the Elite Four, the Hall of Fame and the credits (#33)
Routes 26 and 27 with their houses, Tohjo Falls, the Victory Road gate,
Victory Road, Route 23, Indigo Plateau, the Elite Four's rooms, Lance's
room and the Hall of Fame are open.

- halloffame (game/scripts/league.ts): the music fades as SAVING RECORD
  shows, the win and the team (without Eggs) are recorded, each Pokémon's
  back picture passes and its front picture slides in for its "New Hall of
  Famer!" page and cry, then the player's card with the play time and
  Professor Oak's rating; the play time stops meanwhile. The world then
  continues at New Bark as Continue does after the cartridge's reset.
- credits and the credits after the Hall of Fame (CreditsMachine): the
  ROM's script, strings, banner Pokémon and THE END, frame by frame as
  VBlank_Credits presents them, with the frames each command group runs
  over measured on the cartridge. B shortens credits seen before.
- wait, appear/disappear of the player, and rival names in battle texts
  (PlaceEnemysName prints wRivalName alone: "???" before he is named).
- The tile-screen renderer wraps hSCX/hSCY at 256 pixels and gives rows
  their own hSCX.
- Reference: the credits match the cartridge on every frame (BG map,
  banner tiles, CRAM palettes, LY overrides, parser), with and without B;
  the ceremony's pages match for every species and the player's card for
  Chris and Kris.
- Playthrough: five stages from Tin Tower to the Hall of Fame (HM07 from
  Ice Path, Tohjo Falls' waterfall, Victory Road's ladders, the Elite Four
  and Lance); the driver follows warps within a map and fights carefully
  from here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:52:14 +02:00

140 lines
5.6 KiB
TypeScript

import type { HallOfFameMon, HallOfFameTeam } from "../party/hall-of-fame";
import { PokemonData } from "../rom/pokemon";
import { JOHTO } from "../rom/profile";
import { fontTiles, FRAME_TILE, SPACE_TILE, textTiles } from "../rom/font";
import type { ScriptEffect } from "./interpreter";
import { blankScreen, SCREEN_BUTTONS, type TileScreen } from "./screen";
import { defaultTextContext, readString } from "./text";
import type { GameSound } from "../audio/sound";
/** Place text on a screen as tile numbers; '"' is <PK> in a nickname. */
export function placeText(screen: TileScreen, x: number, y: number, value: string) {
const tiles = (value.match(/ᴾᴷ|ᴹᴺ|[\s\S]/gu) ?? []).flatMap((character) =>
character === '"' ? [0xe1] : textTiles(character),
);
screen.map.splice(y * 20 + x, tiles.length, ...tiles);
}
/** Textbox: a frame with `width` interior columns and `height` interior
* rows whose top-left corner is at (x, y). */
export function placeTextbox(
screen: TileScreen,
x: number,
y: number,
height: number,
width = 18,
) {
const put = (column: number, row: number, values: readonly number[]) =>
screen.map.splice(row * 20 + column, values.length, ...values);
put(x, y, [FRAME_TILE, ...Array(width).fill(FRAME_TILE + 1), FRAME_TILE + 2]);
for (let row = y + 1; row <= y + height; row++) {
put(x, row, [FRAME_TILE + 3, ...Array(width).fill(SPACE_TILE), FRAME_TILE + 3]);
}
put(x, y + height + 1, [
FRAME_TILE + 4,
...Array(width).fill(FRAME_TILE + 1),
FRAME_TILE + 5,
]);
}
/** The Hall of Fame screens' tiles: the standard font, the textbox frame
* and _LoadFontsBattleExtra's $60-$78 (InitDisplayForHallOfFame and
* _HallOfFamePC load the same). */
export function hallOfFameTiles(data: PokemonData, frame: number) {
const tiles = fontTiles(data.reader, frame);
tiles.set(data.reader.slice(JOHTO.FontBattleExtra, 25 * 16), 0x60 * 16);
return tiles;
}
/** DisplayHOFMon: one Pokémon's page, over the Hall of Fame tiles, in
* SCGB_PLAYER_OR_MON_FRONTPIC_PALS's single palette, without the title
* line its callers print at (1, 2). The picture is shown at rest:
* HOF_AnimateFrontpic's ANIM_MON_HOF animation is not ported. */
export function displayHOFMon(
data: PokemonData,
mon: HallOfFameMon,
frame: number,
label: string,
): TileScreen {
const tiles = hallOfFameTiles(data, frame);
tiles.set(data.frontPic(mon.species, PokemonData.unownLetter(mon.dvs)));
const screen = blankScreen(
tiles,
`hof-${mon.species}-${mon.dvs}-${frame}`,
[data.palette(mon.species, PokemonData.shiny(mon.dvs))],
`${label}: ${mon.nickname}, ${data.speciesName(mon.species)}, level ${mon.level}, ID ${String(mon.otId).padStart(5, "0")}`,
);
screen.map.fill(SPACE_TILE);
const put = (x: number, y: number, values: readonly number[]) =>
screen.map.splice(y * 20 + x, values.length, ...values);
const text = (x: number, y: number, value: string) => placeText(screen, x, y, value);
placeTextbox(screen, 0, 0, 3);
placeTextbox(screen, 0, 12, 4);
for (let x = 0; x < 7; x++)
for (let y = 0; y < 7; y++) put(x + 6, y + 5, [x * 7 + y]);
put(1, 13, [0x74, 0xf2]); // № and <DOT>.
text(3, 13, String(mon.species).padStart(3, "0"));
// GetBasePokemonName drops the Nidorans' gender sign.
text(7, 13, data.speciesName(mon.species).split(/[♂♀]/)[0]);
const gender = data.gender(mon.species, mon.dvs);
text(18, 13, gender === "male" ? "♂" : gender === "female" ? "♀" : " ");
text(8, 14, `/${mon.nickname}`);
// PrintLevel: <LV> and two digits, or three digits over the <LV>.
if (mon.level < 100) {
put(1, 16, [0x6e]);
text(2, 16, String(mon.level));
} else text(1, 16, String(mon.level));
put(7, 16, [0x73, 0x74, 0xf3]); // <ID>, № and /.
text(10, 16, String(mon.otId).padStart(5, "0"));
return screen;
}
/** _HallOfFamePC.DisplayMonAndStrings: DisplayHOFMon's page for one
* Pokémon of a team with the team's "-Time Famer" title. */
export function hallOfFameScreen(
data: PokemonData,
team: HallOfFameTeam,
slot: number,
frame: number,
): TileScreen {
const title = readString(
data.reader,
JOHTO["_HallOfFamePC.TimeFamer"],
defaultTextContext(""),
);
const screen = displayHOFMon(data, team.mons[slot], frame, `${team.win}${title.trimStart()}`);
// "HOF Master!" needs a count of HOF_MASTER_COUNT + 1, which the capped
// count never reaches: the original's bug (docs/bugs_and_glitches.md)
// leaves every team "-Time Famer".
placeText(screen, 1, 2, title);
placeText(screen, 2, 2, String(team.win).padStart(3, " "));
return screen;
}
/** _HallOfFamePC: LoadHOFTeam takes the teams newest first until an absent
* one (or NUM_HOF_TEAMS); A shows the team's next Pokémon, and after the
* last one the next team, Start skips to the next team and B leaves. Each
* page cries as ANIM_MON_HOF's CryNoWait does. */
export function* hallOfFamePC(
data: PokemonData,
teams: readonly HallOfFameTeam[],
frame: number,
sound?: GameSound,
): Generator<ScriptEffect, void, number | string | undefined> {
for (const team of teams) {
for (let slot = 0; slot < team.mons.length; slot++) {
sound?.playMonCry2(team.mons[slot].species);
const screen = hallOfFameScreen(data, team, slot, frame);
for (;;) {
const button = yield { kind: "screen", screen };
if (typeof button !== "string" || !SCREEN_BUTTONS.includes(button))
throw new Error("Invalid Hall of Fame button.");
if (button === "b") return;
if (button === "start") {
slot = team.mons.length;
break;
}
if (button === "a") break;
}
}
}
}