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>
140 lines
5.6 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|
|
}
|