Port the Pokédex list, entries and tab (#35)
Checks / web (push) Failing after 21s
Private ROM checks / private-rom (push) Failing after 14m27s

The start menu's POKéDEX runs Pokedex in New mode: the list up to the
last seen Pokémon, its cursor and page moves, and DisplayDexEntry with
the category, number, footprint, and the height, weight and pages once
caught. The Pokédex tab shows the same records from the loaded game.
Order, entries and footprints are compared with the pinned PRET source.

AREA, CRY, PRNT, the option and search screens, Old and A to Z modes
and Unown mode remain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
samyandClaude Opus 5.5 committed 2026-10-03 20:19:24 +02:00
1 parent 03aa913a94
commit cdee576818
20 files changed
+1624 -29

No files matched your search

+3
View File
@@ -12,6 +12,7 @@ import {
} from "./profile";
import { defaultTextContext, readString } from "../scripts/text";
import { PokemonData } from "./pokemon";
import { PokedexData } from "./pokedex";
import { OW } from "../world/constants";
import type {
Direction,
@@ -27,6 +28,7 @@ import type {
export class CrystalAssets {
readonly reader: RomReader;
readonly pokemon: PokemonData;
readonly pokedex: PokedexData;
private maps = new Map<string, GameMap>();
private tilesets = new Map<string, Tileset>();
private sprites = new Map<number, Sprite>();
@@ -35,6 +37,7 @@ export class CrystalAssets {
constructor(bytes: Uint8Array) {
this.reader = new RomReader(bytes);
this.pokemon = new PokemonData(this.reader);
this.pokedex = new PokedexData(this.reader);
}
/** Areas whose rules and scripts are ported. Other maps can be decoded,
+174
View File
@@ -0,0 +1,174 @@
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 <NEXT> 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<number, string> = {
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<number, DexEntry>();
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);
}
+18
View File
@@ -999,6 +999,24 @@ export const TRAINER_CARD = {
PredefPals: 0x9df6,
} as const;
/** Pokedex (engine/pokedex/pokedex.asm, pokedex_2.asm, pokedex_3.asm):
* its strings, the New mode order, the entries and their graphics, keyed
* by their PRET labels. */
export const POKEDEX = {
String_SEEN: 0x407e1,
String_OWN: 0x407e6,
"Pokedex_DrawDexEntryScreenBG.Height": 0x40852,
"Pokedex_DrawDexEntryScreenBG.Weight": 0x4085c,
"Pokedex_DrawDexEntryScreenBG.MenuItems": 0x40867,
"Pokedex_PlaceDefaultStringIfNotSeen.NameNotSeen": 0x40b9a,
NewPokedexOrder: 0x40d60,
"GetDexEntryPointer.PokedexEntryBanks": 0x44351,
PokedexDataPointerTable: 0x44378,
Footprints: 0xf9434,
"LoadQuestionMarkPic.QuestionMarkLZ": 0x1de0e1,
PokedexQuestionMarkPalette: 0x8fba,
} as const;
/** _Option (engine/menus/options_menu.asm): its screen and setting
* strings, keyed by their PRET labels. */
export const OPTIONS = {
+2
View File
@@ -59,6 +59,7 @@ import type { PokegearView } from "./pokegear";
import type { FlyMapView } from "./fly-map";
import { rockMonEncounter, treeMonEncounter } from "./treemons";
import type { TrainerCardView } from "./trainer-card";
import type { PokedexView } from "./pokedex";
import {
defaultOptions,
type GameOptions,
@@ -441,6 +442,7 @@ export type ScriptEffect =
/** A trainer card page, answered with the button pressed; without a
* view the card closes. */
| { kind: "trainercard"; view?: TrainerCardView }
| { kind: "pokedex"; view?: PokedexView }
/** The options screen, answered with the button pressed; without a
* view it closes. */
| { kind: "options"; view?: OptionsView }
+327
View File
@@ -0,0 +1,327 @@
import { NUM_POKEMON } from "../rom/profile";
import type { PokedexData } from "../rom/pokedex";
import { dexHeight, dexWeight } from "../rom/pokedex";
import type { PokemonData } from "../rom/pokemon";
import type { ScriptEffect } from "./interpreter";
type Input = number | string | undefined;
type Gen<T = void> = Generator<ScriptEffect, T, Input>;
/** The buttons the Pokédex reads from hJoyPressed and hJoyLast. */
export const POKEDEX_BUTTONS: readonly string[] = [
"a",
"b",
"start",
"select",
"up",
"down",
"left",
"right",
];
/** wDexListingHeight on the main screen. */
const LISTING_HEIGHT = 7;
/** DexEntryScreen_ArrowCursorData: PAGE, AREA, CRY and PRNT. */
const ENTRY_ACTIONS = 4;
/** One line of the main screen's list. Rows past the last seen Pokémon
* still print, as "-----". */
export interface PokedexRow {
name: string;
seen: boolean;
caught: boolean;
}
/** What the Pokédex shows. The original draws its own tilemap; here the
* text is presented natively and the pictures come from the ROM. */
export interface PokedexView {
screen: "list" | "entry";
/** The species whose front picture shows, or undefined for the question
* mark. The list draws it in PokedexQuestionMarkPalette, the entry in
* the species' palette. */
picture?: number;
/** wPokedexSeen and wPokedexCaught's set bits. */
seen: number;
own: number;
/** wDexListingHeight rows from wDexListingScrollOffset. */
rows: PokedexRow[];
/** wDexListingCursor. */
cursor: number;
/** wDexListingScrollOffset and wDexListingEnd, for the scroll bar. */
scroll: number;
end: number;
entry?: PokedexEntryView;
/** String_SEEN and String_OWN from the ROM. */
labels: { seen: string; own: string };
}
/** DisplayDexEntry: the number, name and category always; the height,
* weight and page text only once the species is caught. */
export interface PokedexEntryView {
species: number;
name: string;
category: string;
/** The national number with leading zeros. */
number: string;
/** ".Height" and ".Weight", with the numbers printed over them. */
height: string;
weight: string;
caught: boolean;
/** wPokedexStatus: page 1 or 2. */
page: 1 | 2;
text?: string;
/** ".MenuItems": PAGE, AREA, CRY and PRNT. */
actions: string[];
/** wDexArrowCursorPosIndex. */
action: number;
}
/** What the Pokédex needs from the world. */
export interface PokedexHost {
data: PokemonData;
dex: PokedexData;
seen: ReadonlySet<number>;
caught: ReadonlySet<number>;
}
/** Pokedex (engine/pokedex/pokedex.asm) in New mode: the main list and
* the entry screen. B leaves the list; A opens a seen Pokémon's entry,
* where left and right move the arrow cursor, up and down step through
* seen Pokémon, A runs the action under the cursor and B returns. Only
* PAGE acts: AREA, CRY and PRNT, the option (SELECT) and search (START)
* screens and the Unown mode are not ported. */
export class Pokedex {
/** wPokedexOrder, in DEXMODE_NEW. */
private order: readonly number[];
/** wDexListingEnd: the position of the last seen Pokémon, plus one. */
private end: number;
private cursor = 0;
private scroll = 0;
/** wPrevDexEntry: the last entry shown, which places the cursor when
* the Pokédex opens again. */
prevEntry: number;
constructor(
private host: PokedexHost,
prevEntry = 0,
) {
this.prevEntry = prevEntry;
this.order = host.dex.newOrder();
this.end = this.findLastSeen();
this.initCursorPosition();
}
*run(): Gen {
for (;;) {
const button = yield* this.button(this.listView());
if (button === "b") break;
if (button === "a") {
if (!this.host.seen.has(this.selected())) continue;
yield* this.entry();
continue;
}
// SELECT opens the option screen and START the search screen.
if (button === "select" || button === "start") continue;
this.listingInput(button);
}
yield { kind: "pokedex" };
}
private *button(view: PokedexView): Gen<string> {
const button = yield { kind: "pokedex", view };
if (typeof button !== "string" || !POKEDEX_BUTTONS.includes(button))
throw new Error("Invalid Pokédex button.");
return button;
}
/** Pokedex_InitDexEntryScreen and Pokedex_UpdateDexEntryScreen. */
private *entry(): Gen {
let page: 1 | 2 = 1;
let action = 0;
this.prevEntry = this.selected();
for (;;) {
const button = yield* this.button(this.entryView(page, action));
// Pokedex_MoveArrowCursor reads left and right only.
if (button === "left" && action > 0) action--;
else if (button === "right" && action < ENTRY_ACTIONS - 1) action++;
if (button === "b") return;
if (button === "a") {
// Pokedex_Page; .Area, .Cry and .Print are not ported.
if (action === 0) {
page = page === 1 ? 2 : 1;
this.prevEntry = this.selected();
}
continue;
}
if (
(button === "up" || button === "down") &&
this.nextOrPrevious(button)
) {
// Pokedex_ReinitDexEntryScreen: page 1 and the cursor on PAGE.
page = 1;
action = 0;
this.prevEntry = this.selected();
}
}
}
/** Pokedex_GetSelectedMon. */
private selected() {
return this.order[this.cursor + this.scroll] ?? 0;
}
/** Pokedex_OrderMonsByMode.FindLastSeen. */
private findLastSeen() {
let end = NUM_POKEMON;
while (end > 0 && !this.host.seen.has(this.order[end - 1])) end--;
return end;
}
/** Pokedex_InitCursorPosition: scroll, then move the cursor, until the
* row holds wPrevDexEntry. It gives up after the last page's start and
* seven rows, as the original's counters do. */
private initCursorPosition() {
const species = this.prevEntry;
if (!species || species > NUM_POKEMON) return;
let index = 0;
if (this.end >= 8) {
for (let c = this.end - 7; c > 0; c--) {
if (this.order[index] === species) return;
index++;
this.scroll++;
}
}
for (let c = LISTING_HEIGHT; c > 0; c--) {
if (this.order[index] === species) return;
index++;
this.cursor++;
}
}
/** Pokedex_ListingHandleDPadInput: up and down move a row, left and
* right a page, but only when the list is longer than one. */
private listingInput(button: string): boolean {
if (button === "up") return this.moveUp();
if (button === "down") return this.moveDown();
if (LISTING_HEIGHT >= this.end) return false;
if (button === "left") {
// Pokedex_ListingMoveUpOnePage.
if (!this.scroll) return false;
this.scroll =
this.scroll < LISTING_HEIGHT ? 0 : this.scroll - LISTING_HEIGHT;
return true;
}
if (button === "right") {
// Pokedex_ListingMoveDownOnePage, whose sum wraps at 256.
const sum = 2 * LISTING_HEIGHT + this.scroll;
this.scroll =
sum > 0xff || sum >= this.end
? this.end - LISTING_HEIGHT
: this.scroll + LISTING_HEIGHT;
return true;
}
return false;
}
/** Pokedex_ListingMoveCursorUp. */
private moveUp() {
if (this.cursor) this.cursor--;
else if (this.scroll) this.scroll--;
else return false;
return true;
}
/** Pokedex_ListingMoveCursorDown. */
private moveDown() {
const next = this.cursor + 1;
if (next >= this.end) return false;
if (next < LISTING_HEIGHT) this.cursor = next;
else if (next + this.scroll < this.end) this.scroll++;
else return false;
return true;
}
/** Pokedex_NextOrPreviousDexEntry: move until a seen Pokémon, or stay
* where the cursor was. */
private nextOrPrevious(button: "up" | "down"): boolean {
const cursor = this.cursor,
scroll = this.scroll;
for (;;) {
const moved = button === "up" ? this.moveUp() : this.moveDown();
if (!moved) {
this.cursor = cursor;
this.scroll = scroll;
return false;
}
if (this.host.seen.has(this.selected())) return true;
}
}
private common() {
const { dex } = this.host;
return {
seen: this.host.seen.size,
own: this.host.caught.size,
cursor: this.cursor,
scroll: this.scroll,
end: this.end,
labels: {
seen: dex.dexString("String_SEEN"),
own: dex.dexString("String_OWN"),
},
};
}
/** Pokedex_PrintListing and Pokedex_LoadSelectedMonTiles. */
private listView(): PokedexView {
const { data, dex, seen, caught } = this.host;
const rows: PokedexRow[] = [];
for (let row = 0; row < LISTING_HEIGHT; row++) {
const species = this.order[this.scroll + row];
if (!species) continue;
const shown = seen.has(species);
rows.push({
name: shown ? data.speciesName(species) : dex.notSeenName(),
seen: shown,
caught: shown && caught.has(species),
});
}
const species = this.selected();
return {
screen: "list",
...(seen.has(species) && { picture: species }),
rows,
...this.common(),
};
}
/** DisplayDexEntry. */
private entryView(page: 1 | 2, action: number): PokedexView {
const { data, dex, caught } = this.host;
const species = this.selected();
const entry = dex.entry(species);
const owned = caught.has(species);
const height = dex.dexString("Pokedex_DrawDexEntryScreenBG.Height"),
weight = dex.dexString("Pokedex_DrawDexEntryScreenBG.Weight");
return {
screen: "entry",
picture: species,
rows: [],
...this.common(),
entry: {
species,
name: data.speciesName(species),
category: entry.category,
number: String(species).padStart(3, "0"),
height: owned ? dexHeight(height, entry.height) : height,
weight: owned ? dexWeight(weight, entry.weight) : weight,
caught: owned,
page,
...(owned && { text: entry.pages[page - 1] }),
actions: dex
.dexString("Pokedex_DrawDexEntryScreenBG.MenuItems")
.trim()
.split(/\s+/),
action,
},
};
}
}
+23 -2
View File
@@ -1,5 +1,6 @@
import type { RomReader } from "../rom/reader";
import type { PokemonData } from "../rom/pokemon";
import type { PokedexData } from "../rom/pokedex";
import { BATTLE, ENGINE_FLAGS, MENU, OVERWORLD, RAM } from "../rom/profile";
import { C } from "../battle/constants";
import {
@@ -22,6 +23,7 @@ import {
} from "./interpreter";
import { Pokegear } from "./pokegear";
import { TrainerCard } from "./trainer-card";
import { Pokedex } from "./pokedex";
import { OptionsMenu, menuAccount } from "./options";
import { flyMap } from "./fly-map";
import { OW } from "../world/constants";
@@ -102,11 +104,13 @@ const FACING_VECTORS = [
/** Menu cursors in work RAM, which CleanUpBattleRAM clears after every
* battle: wBattleMenuCursorPosition (the start menu's 1-based position,
* shared with the battle menu), wPartyMenuCursor (1-based) and wLastPocket. */
* shared with the battle menu), wPartyMenuCursor (1-based) and wLastPocket.
* wPrevDexEntry, the Pokédex's last entry, survives battles. */
export interface MenuCursors {
start: number;
party: number;
pocket: number;
dex: number;
}
/** The world's verdict on a field move chosen from the party menu: a
@@ -131,6 +135,7 @@ export type MenuFieldMove =
export interface StartMenuHost {
reader: RomReader;
data: PokemonData;
dex: PokedexData;
text(): TextContext;
/** BikeFunction: the script that getting on or off the Bicycle queues
* here, or undefined when .CheckEnvironment refuses it. */
@@ -187,7 +192,7 @@ export class StartMenuRun {
constructor(
state: ScriptState,
private host: StartMenuHost,
cursors: MenuCursors = { start: 0, party: 0, pocket: 0 },
cursors: MenuCursors = { start: 0, party: 0, pocket: 0, dex: 0 },
select = false,
) {
this.state = copyScriptState(state);
@@ -323,6 +328,22 @@ export class StartMenuRun {
this.cursors.start = choice + 1;
const item = items[choice];
switch (item) {
case STARTMENUITEM_POKEDEX: {
// StartMenu_Pokedex needs a party and returns to the start menu.
if (!this.state.party.length) break;
const dex = new Pokedex(
{
data: this.data,
dex: this.host.dex,
seen: this.state.pokedex.seen,
caught: this.state.pokedex.caught,
},
this.cursors.dex,
);
yield* dex.run();
this.cursors.dex = dex.prevEntry;
break;
}
case STARTMENUITEM_POKEMON:
if (yield* this.pokemon()) return;
break;
+23 -3
View File
@@ -56,6 +56,7 @@ import {
} from "../scripts/interpreter";
import { IntroRun } from "../scripts/intro";
import { FLY_MAP_BUTTONS, type FlyMapView } from "../scripts/fly-map";
import { POKEDEX_BUTTONS, type PokedexView } from "../scripts/pokedex";
import {
StartMenuRun,
type MenuCursors,
@@ -311,6 +312,8 @@ export type Choice =
| { kind: "mombank"; view: MomBankView; selected: 0 }
/** A trainer card page, answered with the button pressed. */
| { kind: "trainercard"; view: TrainerCardView; selected: 0 }
/** A Pokédex screen, answered with the button pressed. */
| { kind: "pokedex"; view: PokedexView; selected: 0 }
/** The options screen, answered with the button pressed. */
| { kind: "options"; view: OptionsView; selected: 0 }
/** The Fly map, answered with the button pressed. */
@@ -332,6 +335,7 @@ const buttonChoice = (choice: Choice) =>
choice.kind === "pokegear" ||
choice.kind === "mombank" ||
choice.kind === "trainercard" ||
choice.kind === "pokedex" ||
choice.kind === "options" ||
choice.kind === "flymap";
export interface Dialogue {
@@ -683,7 +687,7 @@ export class GameWorld {
private turnTo?: Direction;
private turnWait = 0;
/** Start menu, party menu and pack cursors (work RAM, not saved). */
private menuCursors: MenuCursors = { start: 0, party: 0, pocket: 0 };
private menuCursors: MenuCursors = { start: 0, party: 0, pocket: 0, dex: 0 };
/** OBJECT_ACTION_BUMP this tick: walking in place against an obstacle. */
private bumping = false;
/** A screen shake object (earthquake, step_shake) in progress. */
@@ -2230,6 +2234,7 @@ export class GameWorld {
{
reader: this.assets.reader,
data: this.assets.pokemon,
dex: this.assets.pokedex,
text: () => this.host(PLAYER).text(),
bicycleScript: (state, select) => this.bicycleScript(state, select),
timeOfDay: () => this.clockTimeOfDay,
@@ -2619,6 +2624,14 @@ export class GameWorld {
choice: { kind: "trainercard", view: effect.view, selected: 0 },
};
return true;
case "pokedex":
if (!effect.view) return false;
this.dialogue = {
pages: [""],
index: 0,
choice: { kind: "pokedex", view: effect.view, selected: 0 },
};
return true;
case "options":
if (!effect.view) return false;
this.dialogue = {
@@ -3168,7 +3181,12 @@ export class GameWorld {
this.battleRun = undefined;
// ExitBattle: EvolveAfterBattle after a win, then CleanUpBattleRAM.
const { result, evolvable } = next.value;
this.menuCursors = { start: 0, party: 0, pocket: 0 };
this.menuCursors = {
start: 0,
party: 0,
pocket: 0,
dex: this.menuCursors.dex,
};
if (this.script && evolvable && !(result & 0x0f)) {
this.script.interlude = {
run: new PartyEvolution(
@@ -4202,6 +4220,8 @@ export class GameWorld {
!MOM_BANK_BUTTONS.includes(answer as never)) ||
(dialogue.choice.kind === "trainercard" &&
!TRAINER_CARD_BUTTONS.includes(answer as never)) ||
(dialogue.choice.kind === "pokedex" &&
!POKEDEX_BUTTONS.includes(answer as never)) ||
(dialogue.choice.kind === "options" &&
!OPTIONS_BUTTONS.includes(answer as never)) ||
(dialogue.choice.kind === "flymap" &&
@@ -4538,7 +4558,7 @@ export class GameWorld {
this.battle = undefined;
this.battleRun = undefined;
this.memScript = undefined;
this.menuCursors = { start: 0, party: 0, pocket: 0 };
this.menuCursors = { start: 0, party: 0, pocket: 0, dex: 0 };
this.clearNotice();
this.introPending = false;
// A save made during the introduction starts it again.