import { RomReader } from "./reader"; import { decompress } from "./lz"; import { NUM_POKEMON, POKEDEX } from "./profile"; import { defaultTextContext, readString } from "../scripts/text"; import type { Palette } from "../world/types"; /** A Pokédex entry (data/pokemon/dex_entries): the species category, * height and weight, and the two pages of text. */ export interface DexEntry { /** The species name DisplayDexEntry prints under the name, as "SEED". */ category: string; /** Feet and inches as stored: 204 is 2'04". Zero leaves "?'??". */ height: number; /** Tenths of a pound. Zero leaves "???". */ weight: number; /** Each page's lines, joined by "\n" as breaks them. */ pages: [string, string]; } /** The Pokédex font's own tiles in the strings Pokedex_PlaceString draws * (gfx/pokedex/pokedex.png): the foot and inch marks and the bottom bar's * left edge. */ const DEX_TILES: Record = { 0x3b: "", 0x5e: "'", 0x5f: '"', }; /** The Pokédex's ROM data: its strings, the New mode order, the entries, * footprints and the question mark picture. */ export class PokedexData { private entries = new Map(); private order?: number[]; constructor(readonly reader: RomReader) {} private checkSpecies(species: number) { if (!Number.isInteger(species) || species < 1 || species > NUM_POKEMON) throw new Error("Unknown Pokémon species."); } /** A "@"-terminated string in the standard font. */ private string(address: number, limit = 32) { return readString(this.reader, address, defaultTextContext(""), limit); } /** A string Pokedex_PlaceString draws, ended by -1, mixing the standard * font with the Pokédex's tiles. */ dexString(label: keyof typeof POKEDEX): string { const r = this.reader; let output = ""; for (let p = POKEDEX[label], end = p + 32; p < end; p++) { const code = r.byte(p); if (code === 0xff) return output; output += DEX_TILES[code] ?? readString( new RomReader(new Uint8Array([code, 0x50])), 0, defaultTextContext(""), ); } throw new Error("Unterminated Pokédex string."); } /** Pokedex_PlaceDefaultStringIfNotSeen.NameNotSeen. */ notSeenName(): string { return this.string( POKEDEX["Pokedex_PlaceDefaultStringIfNotSeen.NameNotSeen"], ); } /** NewPokedexOrder: the species in New mode's (Johto) order. */ newOrder(): readonly number[] { if (!this.order) { const order = [ ...this.reader.slice(POKEDEX.NewPokedexOrder, NUM_POKEMON), ]; const sorted = [...order].sort((a, b) => a - b); if (sorted.some((species, index) => species !== index + 1)) throw new Error("The New Pokédex order is not a permutation."); this.order = order; } return this.order; } /** GetDexEntryPointer: the bank comes from the species' top two bits, * one bank per 64 species. */ private entryAddress(species: number) { const r = this.reader; const index = species - 1; const bank = r.byte( POKEDEX["GetDexEntryPointer.PokedexEntryBanks"] + ((index >> 6) & 3), ); return r.near(POKEDEX.PokedexDataPointerTable + index * 2, bank); } /** DisplayDexEntry and GetDexEntryPagePointer: the category, the height * and weight words, then each page up to its "@". */ entry(species: number): DexEntry { this.checkSpecies(species); const cached = this.entries.get(species); if (cached) return cached; const r = this.reader; let p = this.entryAddress(species); const category = this.string(p, 16); while (r.byte(p) !== 0x50) p++; p++; const height = r.word(p), weight = r.word(p + 2); p += 4; const page1 = p; while (r.byte(p) !== 0x50) p++; const page2 = p + 1; const entry: DexEntry = { category, height, weight, pages: [this.string(page1, 128), this.string(page2, 128)], }; this.entries.set(species, entry); return entry; } /** Pokedex_LoadAnyFootprint: two 1bpp tiles from the footprint sheet's * row for the species, then the two below them, 16 tiles further. */ footprint(species: number): Uint8Array { this.checkSpecies(species); const index = species - 1; const top = POKEDEX.Footprints + (index >> 3) * 0x100 + (index & 7) * 0x10; const tiles = new Uint8Array(32); tiles.set(this.reader.slice(top, 16), 0); tiles.set(this.reader.slice(top + 0x80, 16), 16); return tiles; } /** LoadQuestionMarkPic: the 7x7 picture of an unseen Pokémon, column by * column like a front picture. */ questionMarkPic(): Uint8Array { const tiles = decompress( this.reader, POKEDEX["LoadQuestionMarkPic.QuestionMarkLZ"], 0x2000, ); if (tiles.length < 49 * 16) throw new Error("Picture data is truncated."); return tiles.slice(0, 49 * 16); } /** PokedexQuestionMarkPalette: _CGB_Pokedex's picture palette while the * list shows (wCurPartySpecies is -1). */ questionMarkPalette(): Palette { const r = this.reader; return [0, 1, 2, 3].map((i) => { const value = r.word(POKEDEX.PokedexQuestionMarkPalette + i * 2); return [ Math.round(((value & 31) * 255) / 31), Math.round((((value >> 5) & 31) * 255) / 31), Math.round((((value >> 10) & 31) * 255) / 31), ] as const; }); } } /** DisplayDexEntry's height: PrintNum with two of four digits before the * point, which becomes the foot mark, written over the ".Height" string. */ export function dexHeight(template: string, height: number): string { if (!height) return template; const feet = String(Math.floor(height / 100)).padStart(2, " "); const inches = String(height % 100).padStart(2, "0"); // The number starts three tiles into the string (hlcoord 12, 7 after 9, 7). return template.slice(0, 3) + `${feet}'${inches}` + template.slice(8); } /** DisplayDexEntry's weight: PrintNum with four of five digits before the * point, written over the ".Weight" string's question marks. */ export function dexWeight(template: string, weight: number): string { if (!weight) return template; const pounds = String(Math.floor(weight / 10)).padStart(4, " "); // The number starts two tiles into the string (hlcoord 11, 9 after 9, 9). return template.slice(0, 2) + `${pounds}.${weight % 10}` + template.slice(8); }