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 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 . 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: and two digits, or three digits over the . 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]); // , № 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 { 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; } } } }