Read TileCollisionStdScripts from the ROM so the Pokémon Center PC runs PokemonCenterPC: Bill's PC (deposit, withdraw, release, change and name boxes), the player's item storage (withdraw, deposit, toss, mail box) and Oak's Pokédex rating. The house PC now opens the full player's PC menu. A catch with a full party goes to the current box through SendMonIntoBox; a full box keeps the ball, and filling it queues Bill's call as a memory script after the battle. Saves gain boxes, box names and PC items, migrated by absence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
451464f7c6
commit
3bdca0099e
21 files changed
+1999
-131
No files matched your search
@@ -0,0 +1,647 @@
|
||||
import type { RomReader } from "../rom/reader";
|
||||
import type { PokemonData } from "../rom/pokemon";
|
||||
import { ENGINE_FLAGS, PC, RAM } from "../rom/profile";
|
||||
import { receiveItem, removeFromSlot, type ItemSlot } from "../party/bag";
|
||||
import { BOX_NAME_PATTERN, PARTY_LENGTH } from "../party/pokemon";
|
||||
import {
|
||||
NUM_BOXES,
|
||||
currentBox,
|
||||
depositMon,
|
||||
receivePCItem,
|
||||
withdrawnMon,
|
||||
} from "../party/storage";
|
||||
import { readString, readText, type TextContext } from "./text";
|
||||
import type { MartEntry, ScriptEffect, ScriptState } from "./interpreter";
|
||||
|
||||
type Input = number | string | undefined;
|
||||
type Gen<T = void> = Generator<ScriptEffect, T, Input>;
|
||||
|
||||
/** What the PC routines need from the running script. */
|
||||
export interface PcHost {
|
||||
state: ScriptState;
|
||||
reader: RomReader;
|
||||
data: PokemonData;
|
||||
text(): TextContext;
|
||||
/** text_ram and text_decimal values, keyed by RAM address. */
|
||||
strings: Record<number, string>;
|
||||
numbers: Record<number, number>;
|
||||
/** _PlayerDecorationMenu: true if a decoration changed. */
|
||||
decorationMenu(): Gen<boolean>;
|
||||
}
|
||||
|
||||
/** PokemonCenterPC's menu items (PCPCITEM_*). */
|
||||
const PCPCITEM_PLAYERS_PC = 0,
|
||||
PCPCITEM_BILLS_PC = 1,
|
||||
PCPCITEM_OAKS_PC = 2,
|
||||
PCPCITEM_TURN_OFF = 4;
|
||||
/** _PlayersPC's menu items (PLAYERSPCITEM_*). */
|
||||
const PLAYERSPCITEM_WITHDRAW_ITEM = 0,
|
||||
PLAYERSPCITEM_DEPOSIT_ITEM = 1,
|
||||
PLAYERSPCITEM_TOSS_ITEM = 2,
|
||||
PLAYERSPCITEM_MAIL_BOX = 3,
|
||||
PLAYERSPCITEM_DECORATION = 4;
|
||||
/** _BillsPC's menu items. */
|
||||
const BILLSPC_WITHDRAW = 0,
|
||||
BILLSPC_DEPOSIT = 1,
|
||||
BILLSPC_CHANGE_BOX = 2,
|
||||
BILLSPC_MOVE = 3;
|
||||
/** The box submenus: DEPOSIT or WITHDRAW, STATS, RELEASE, CANCEL; and
|
||||
* SWITCH, NAME, PRINT, QUIT for a box. */
|
||||
const SUBMENU_TAKE = 0,
|
||||
SUBMENU_STATS = 1,
|
||||
SUBMENU_RELEASE = 2;
|
||||
const BOX_SWITCH = 0,
|
||||
BOX_NAME = 1,
|
||||
BOX_PRINT = 2;
|
||||
/** DelayFrames after a Bill's PC message, in overworld ticks (two frames). */
|
||||
const MESSAGE_TICKS = 25,
|
||||
RELEASED_TICKS = 40;
|
||||
const EGG = 0xfd;
|
||||
|
||||
/** A pack entry and where it came from, for TossItem's wCurItemQuantity. */
|
||||
interface PackSlot extends ItemSlot {
|
||||
pocket: "items" | "balls" | "keyItems";
|
||||
index: number;
|
||||
}
|
||||
|
||||
/** The Pokémon Center PC (engine/events/pokecenter_pc.asm), Bill's PC
|
||||
* (engine/pokemon/bills_pc_top.asm and bills_pc.asm) and Oak's PC
|
||||
* (engine/events/prof_oaks_pc.asm). The original draws its own screens;
|
||||
* here menus, lists and messages are presented natively. */
|
||||
export class PcSession {
|
||||
constructor(private host: PcHost) {}
|
||||
|
||||
private get state() {
|
||||
return this.host.state;
|
||||
}
|
||||
private string(address: number) {
|
||||
return readString(this.host.reader, address, this.host.text());
|
||||
}
|
||||
private pages(address: number) {
|
||||
return readText(this.host.reader, address, this.host.text()).split("\n\n");
|
||||
}
|
||||
private *print(address: number): Gen {
|
||||
yield { kind: "text", pages: this.pages(address) };
|
||||
}
|
||||
/** BillsPC_PlaceString followed by DelayFrames: a message that stays
|
||||
* for a fixed time without waiting for a button. */
|
||||
private *placeString(text: string, ticks = MESSAGE_TICKS): Gen {
|
||||
yield { kind: "text", pages: [text], holdTicks: ticks };
|
||||
}
|
||||
/** A menu of `items`; the chosen index, or -1 for B. */
|
||||
private *menu(
|
||||
pages: string[],
|
||||
items: string[],
|
||||
details?: string[],
|
||||
selected = 0,
|
||||
): Gen<number> {
|
||||
const choice = yield { kind: "menu", pages, items, details, selected };
|
||||
return typeof choice === "number" && choice >= 0 && choice < items.length
|
||||
? choice
|
||||
: -1;
|
||||
}
|
||||
/** Consecutive "@"-terminated strings, as static menus store them. */
|
||||
private strings(address: number, count: number) {
|
||||
const r = this.host.reader,
|
||||
result: string[] = [];
|
||||
let p = address;
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(this.string(p));
|
||||
while (r.byte(p++) !== 0x50);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/** A WhichPC list: a count, the item ids, then -1. */
|
||||
private indexSet(address: number, set: number) {
|
||||
const r = this.host.reader;
|
||||
let p = address;
|
||||
for (let i = 0; i < set; i++) p += r.byte(p) + 2;
|
||||
const count = r.byte(p);
|
||||
return Array.from({ length: count }, (_, i) => r.byte(p + 1 + i));
|
||||
}
|
||||
/** A jumptable of (routine, string) pointers: the item's string. */
|
||||
private itemString(table: number, item: number) {
|
||||
const r = this.host.reader,
|
||||
entry = table + item * 4 + 2;
|
||||
return this.string(r.near(entry, r.bank(entry)));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ PC menus
|
||||
|
||||
/** PokemonCenterPC: a Pokémon is needed, then the PCs listed for the
|
||||
* player's progress until TURN OFF or B. */
|
||||
*pokemonCenterPC(): Gen {
|
||||
if (!this.state.party.length) {
|
||||
yield* this.print(PC["PC_CheckPartyForPokemon.PokecenterPCCantUseText"]);
|
||||
return;
|
||||
}
|
||||
yield* this.print(PC.PokecenterPCTurnOnText);
|
||||
const whose = this.pages(PC.PokecenterPCWhoseText);
|
||||
let cursor = 0;
|
||||
for (;;) {
|
||||
// .ChooseWhichPCListToUse: PCPC_BEFORE_POKEDEX or PCPC_BEFORE_HOF.
|
||||
// No Hall of Fame entry exists before the League is ported.
|
||||
const set = this.state.flags.has(ENGINE_FLAGS.pokedex) ? 1 : 0;
|
||||
const items = this.indexSet(PC["PokemonCenterPC.WhichPC"], set);
|
||||
const choice = yield* this.menu(
|
||||
whose,
|
||||
items.map((item) =>
|
||||
this.itemString(PC["PokemonCenterPC.Jumptable"], item),
|
||||
),
|
||||
undefined,
|
||||
cursor,
|
||||
);
|
||||
if (choice < 0) return;
|
||||
cursor = choice;
|
||||
switch (items[choice]) {
|
||||
case PCPCITEM_BILLS_PC:
|
||||
yield* this.print(PC.PokecenterBillsPCText);
|
||||
yield* this.billsPC();
|
||||
break;
|
||||
case PCPCITEM_PLAYERS_PC:
|
||||
yield* this.print(PC.PokecenterPlayersPCText);
|
||||
yield* this.playersPC(false);
|
||||
break;
|
||||
case PCPCITEM_OAKS_PC:
|
||||
yield* this.print(PC.PokecenterOaksPCText);
|
||||
yield* this.oaksPC();
|
||||
break;
|
||||
case PCPCITEM_TURN_OFF:
|
||||
yield* this.print(PC.PokecenterPCOaksClosedText);
|
||||
return;
|
||||
default:
|
||||
throw new Error("The Hall of Fame PC is not ported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** _PlayersHousePC: returns true when a decoration changed, which makes
|
||||
* the script reload the room. */
|
||||
*playersHousePC(): Gen<boolean> {
|
||||
yield* this.print(PC.PlayersPCTurnOnText);
|
||||
return yield* this.playersPC(true);
|
||||
}
|
||||
|
||||
/** _PlayersPC: PLAYERSPC_NORMAL or PLAYERSPC_HOUSE. A changed
|
||||
* decoration closes the PC. */
|
||||
private *playersPC(house: boolean): Gen<boolean> {
|
||||
const items = this.indexSet(PC["PlayersPCMenuData.WhichPC"], house ? 1 : 0);
|
||||
const names = items.map((item) =>
|
||||
this.itemString(PC["PlayersPCMenuData.PlayersPCMenuPointers"], item),
|
||||
);
|
||||
const ask = this.pages(PC.PlayersPCAskWhatDoText);
|
||||
let cursor = 0;
|
||||
for (;;) {
|
||||
const choice = yield* this.menu(ask, names, undefined, cursor);
|
||||
if (choice < 0) return false;
|
||||
cursor = choice;
|
||||
switch (items[choice]) {
|
||||
case PLAYERSPCITEM_WITHDRAW_ITEM:
|
||||
yield* this.withdrawItems();
|
||||
break;
|
||||
case PLAYERSPCITEM_DEPOSIT_ITEM:
|
||||
yield* this.depositItems();
|
||||
break;
|
||||
case PLAYERSPCITEM_TOSS_ITEM:
|
||||
yield* this.tossItems();
|
||||
break;
|
||||
case PLAYERSPCITEM_MAIL_BOX:
|
||||
// _PlayerMailBoxMenu: no MAIL can be stored yet.
|
||||
yield* this.print(PC["_PlayerMailBoxMenu.EmptyMailboxText"]);
|
||||
break;
|
||||
case PLAYERSPCITEM_DECORATION:
|
||||
if (yield* this.host.decorationMenu()) return true;
|
||||
break;
|
||||
default:
|
||||
// LOG OFF and TURN OFF.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------ item storage
|
||||
|
||||
private itemEntries(slots: ItemSlot[]): MartEntry[] {
|
||||
const data = this.host.data;
|
||||
return slots.map((slot) => ({
|
||||
item: slot.item,
|
||||
name: data.itemName(slot.item),
|
||||
price: 0,
|
||||
owned: slot.quantity,
|
||||
}));
|
||||
}
|
||||
/** The item named by PartyMonItemName (wStringBuffer1 and 2) and the
|
||||
* quantity text_decimal shows (wItemQuantityChange). */
|
||||
private nameItem(item: number, quantity?: number) {
|
||||
const name = this.host.data.itemName(item);
|
||||
this.host.strings[RAM.stringBuffer1] = name;
|
||||
this.host.strings[RAM.stringBuffer2] = name;
|
||||
if (quantity !== undefined)
|
||||
this.host.numbers[RAM.itemQuantityChange] = quantity;
|
||||
}
|
||||
/** SelectQuantityToToss for a stack, unless _CheckTossableItem makes it
|
||||
* always one. Returns 0 if cancelled. */
|
||||
private *quantity(
|
||||
mode: "withdraw" | "deposit" | "toss",
|
||||
slot: ItemSlot,
|
||||
prompt: number,
|
||||
): Gen<number> {
|
||||
if (this.host.data.itemUnsellable(slot.item)) return 1;
|
||||
this.nameItem(slot.item);
|
||||
const chosen = yield {
|
||||
kind: "martquantity",
|
||||
mode,
|
||||
item: slot.item,
|
||||
name: this.host.data.itemName(slot.item),
|
||||
price: 0,
|
||||
max: slot.quantity,
|
||||
prompt: readText(this.host.reader, prompt, this.host.text()),
|
||||
selected: 1,
|
||||
};
|
||||
return typeof chosen === "number" &&
|
||||
Number.isInteger(chosen) &&
|
||||
chosen >= 1 &&
|
||||
chosen <= slot.quantity
|
||||
? chosen
|
||||
: 0;
|
||||
}
|
||||
|
||||
/** PlayerWithdrawItemMenu: into the matching pack pocket. */
|
||||
private *withdrawItems(): Gen {
|
||||
const items = this.state.pcItems;
|
||||
for (;;) {
|
||||
const index = yield {
|
||||
kind: "martlist",
|
||||
mode: "withdraw",
|
||||
entries: this.itemEntries(items),
|
||||
selected: 0,
|
||||
};
|
||||
const slot = typeof index === "number" ? items[index] : undefined;
|
||||
if (!slot) return;
|
||||
const quantity = yield* this.quantity(
|
||||
"withdraw",
|
||||
slot,
|
||||
PC["PlayerWithdrawItemMenu.PlayersPCHowManyWithdrawText"],
|
||||
);
|
||||
if (!quantity) continue;
|
||||
const item = slot.item;
|
||||
if (!receiveItem(this.host.data, this.state.bag, item, quantity)) {
|
||||
yield* this.print(
|
||||
PC["PlayerWithdrawItemMenu.PlayersPCNoRoomWithdrawText"],
|
||||
);
|
||||
continue;
|
||||
}
|
||||
removeFromSlot(items, index as number, item, quantity);
|
||||
this.nameItem(item, quantity);
|
||||
yield* this.print(
|
||||
PC["PlayerWithdrawItemMenu.PlayersPCWithdrewItemsText"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** The pack as DepositSellPack lists it: items, balls and key items,
|
||||
* with the pocket and index each entry comes from. */
|
||||
private packSlots(): PackSlot[] {
|
||||
const bag = this.state.bag;
|
||||
return [
|
||||
...bag.items.map((slot, index) => ({
|
||||
...slot,
|
||||
pocket: "items" as const,
|
||||
index,
|
||||
})),
|
||||
...bag.balls.map((slot, index) => ({
|
||||
...slot,
|
||||
pocket: "balls" as const,
|
||||
index,
|
||||
})),
|
||||
...bag.keyItems.map((item, index) => ({
|
||||
item,
|
||||
quantity: 1,
|
||||
pocket: "keyItems" as const,
|
||||
index,
|
||||
})),
|
||||
];
|
||||
}
|
||||
/** PlayerDepositItemMenu: from any pocket into the PC. */
|
||||
private *depositItems(): Gen {
|
||||
if (!this.packSlots().length) {
|
||||
yield* this.print(PC["PlayerDepositItemMenu.PlayersPCNoItemsText"]);
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
const slots = this.packSlots();
|
||||
const choice = yield {
|
||||
kind: "martlist",
|
||||
mode: "deposit",
|
||||
entries: this.itemEntries(slots),
|
||||
selected: 0,
|
||||
};
|
||||
const slot = typeof choice === "number" ? slots[choice] : undefined;
|
||||
if (!slot) return;
|
||||
const quantity = yield* this.quantity(
|
||||
"deposit",
|
||||
slot,
|
||||
PC["PlayerDepositItemMenu.PlayersPCHowManyDepositText"],
|
||||
);
|
||||
if (!quantity) continue;
|
||||
if (!receivePCItem(this.state.pcItems, slot.item, quantity)) {
|
||||
yield* this.print(
|
||||
PC["PlayerDepositItemMenu.PlayersPCNoRoomDepositText"],
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const bag = this.state.bag;
|
||||
if (slot.pocket === "keyItems") bag.keyItems.splice(slot.index, 1);
|
||||
else removeFromSlot(bag[slot.pocket], slot.index, slot.item, quantity);
|
||||
this.nameItem(slot.item, quantity);
|
||||
yield* this.print(PC["PlayerDepositItemMenu.PlayersPCDepositItemsText"]);
|
||||
}
|
||||
}
|
||||
|
||||
/** PlayerTossItemMenu and TossItemFromPC. */
|
||||
private *tossItems(): Gen {
|
||||
const items = this.state.pcItems;
|
||||
for (;;) {
|
||||
const index = yield {
|
||||
kind: "martlist",
|
||||
mode: "toss",
|
||||
entries: this.itemEntries(items),
|
||||
selected: 0,
|
||||
};
|
||||
const slot = typeof index === "number" ? items[index] : undefined;
|
||||
if (!slot) return;
|
||||
if (this.host.data.itemUnsellable(slot.item)) {
|
||||
yield* this.print(PC["TossItemFromPC.ItemsTooImportantText"]);
|
||||
continue;
|
||||
}
|
||||
const quantity = yield* this.quantity(
|
||||
"toss",
|
||||
slot,
|
||||
PC["TossItemFromPC.ItemsTossOutHowManyText"],
|
||||
);
|
||||
if (!quantity) continue;
|
||||
const item = slot.item;
|
||||
this.nameItem(item, quantity);
|
||||
const confirmed = yield {
|
||||
kind: "yesno",
|
||||
pages: this.pages(PC["TossItemFromPC.ItemsThrowAwayText"]),
|
||||
};
|
||||
if (!confirmed) continue;
|
||||
removeFromSlot(items, index as number, item, quantity);
|
||||
this.nameItem(item, quantity);
|
||||
yield* this.print(PC["TossItemFromPC.ItemsDiscardedText"]);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- Bill's PC
|
||||
|
||||
/** GetBoxName: the saved name, or SetDefaultBoxNames' "BOX" and number. */
|
||||
boxName(box: number) {
|
||||
return (
|
||||
this.state.storage.names[box] ??
|
||||
`${this.string(PC["SetDefaultBoxNames.Box"])}${box + 1}`
|
||||
);
|
||||
}
|
||||
private monDetails(mons: { level: number }[]) {
|
||||
return [...mons.map((mon) => `:L${mon.level}`), ""];
|
||||
}
|
||||
|
||||
/** _BillsPC: the top menu until SEE YA! or B. */
|
||||
private *billsPC(): Gen {
|
||||
if (!this.state.party.length) {
|
||||
yield* this.print(PC["_BillsPC.PCGottaHavePokemonText"]);
|
||||
return;
|
||||
}
|
||||
const names = this.strings(PC["_BillsPC.strings"], 5);
|
||||
const what = this.pages(PC["_BillsPC.PCWhatText"]);
|
||||
let cursor = 0;
|
||||
for (;;) {
|
||||
const choice = yield* this.menu(what, names, undefined, cursor);
|
||||
if (choice < 0) return;
|
||||
cursor = choice;
|
||||
if (choice === BILLSPC_WITHDRAW) yield* this.withdrawMons();
|
||||
else if (choice === BILLSPC_DEPOSIT) yield* this.depositMons();
|
||||
else if (choice === BILLSPC_CHANGE_BOX) yield* this.changeBox();
|
||||
else if (choice === BILLSPC_MOVE)
|
||||
yield {
|
||||
kind: "notice",
|
||||
text: "Moving Pokémon between boxes is not ported yet.",
|
||||
};
|
||||
else return;
|
||||
}
|
||||
}
|
||||
|
||||
/** BillsPC_CheckMail_PreventBlackout for a party Pokémon: the last one,
|
||||
* or the last that has not fainted, stays. No MAIL exists yet. */
|
||||
private *partyMonMayLeave(index: number): Gen<boolean> {
|
||||
const party = this.state.party;
|
||||
let text: number | undefined;
|
||||
if (party.length < 2) text = PC.PCString_ItsYourLastPKMN;
|
||||
else if (party.every((mon, i) => i === index || !mon.hp))
|
||||
text = PC.PCString_NoMoreUsablePKMN;
|
||||
if (text === undefined) return true;
|
||||
yield* this.placeString(this.string(text));
|
||||
return false;
|
||||
}
|
||||
/** ReleasePKMN_ByePKMN: the species name, not the nickname. */
|
||||
private *released(species: number): Gen {
|
||||
yield* this.placeString(
|
||||
this.string(PC.PCString_ReleasedPKMN),
|
||||
RELEASED_TICKS,
|
||||
);
|
||||
yield* this.placeString(
|
||||
`${this.string(PC.PCString_Bye)} ${this.host.data.speciesName(species)}!`,
|
||||
);
|
||||
}
|
||||
private *confirmRelease(): Gen<boolean> {
|
||||
return Boolean(
|
||||
yield { kind: "yesno", pages: [this.string(PC.PCString_ReleasePKMN)] },
|
||||
);
|
||||
}
|
||||
|
||||
/** _DepositPKMN: choose a party Pokémon, then DEPOSIT, STATS or RELEASE. */
|
||||
private *depositMons(): Gen {
|
||||
const party = this.state.party,
|
||||
options = this.strings(PC["BillsPCDepositMenuHeader.MenuData"] + 2, 4);
|
||||
for (;;) {
|
||||
const index = yield* this.menu(
|
||||
[
|
||||
`${this.string(PC["BillsPC_BoxName.PartyPKMN"])}\n${this.string(PC.PCString_ChooseaPKMN)}`,
|
||||
],
|
||||
[
|
||||
...party.map((mon) => mon.nickname),
|
||||
this.string(PC["BillsPC_RefreshTextboxes.CancelString"]),
|
||||
],
|
||||
this.monDetails(party),
|
||||
);
|
||||
if (index < 0 || index >= party.length) return;
|
||||
for (;;) {
|
||||
const action = yield* this.menu(
|
||||
[this.string(PC.PCString_WhatsUp)],
|
||||
options,
|
||||
);
|
||||
const mon = party[index];
|
||||
if (action === SUBMENU_TAKE) {
|
||||
if (!(yield* this.partyMonMayLeave(index))) break;
|
||||
if (!depositMon(this.host.data, this.state.storage, mon)) {
|
||||
yield* this.placeString(this.string(PC.PCString_BoxFull));
|
||||
continue;
|
||||
}
|
||||
party.splice(index, 1);
|
||||
yield* this.placeString(
|
||||
`${this.string(PC.PCString_Stored)}${mon.nickname}!`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (action === SUBMENU_STATS) {
|
||||
yield { kind: "notice", text: "The stats screen is not ported yet." };
|
||||
continue;
|
||||
}
|
||||
if (action !== SUBMENU_RELEASE) break;
|
||||
if (!(yield* this.partyMonMayLeave(index))) break;
|
||||
if (mon.species === EGG) {
|
||||
yield* this.placeString(this.string(PC.PCString_NoReleasingEGGS));
|
||||
break;
|
||||
}
|
||||
if (!(yield* this.confirmRelease())) continue;
|
||||
party.splice(index, 1);
|
||||
yield* this.released(mon.species);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** _WithdrawPKMN: choose a Pokémon in the current box, then WITHDRAW,
|
||||
* STATS or RELEASE. */
|
||||
private *withdrawMons(): Gen {
|
||||
const options = this.strings(PC["BillsPC_Withdraw.MenuData"] + 2, 4);
|
||||
for (;;) {
|
||||
const box = currentBox(this.state.storage);
|
||||
const index = yield* this.menu(
|
||||
[
|
||||
`${this.boxName(this.state.storage.current)}\n${this.string(PC.PCString_ChooseaPKMN)}`,
|
||||
],
|
||||
[
|
||||
...box.map((mon) => mon.nickname),
|
||||
this.string(PC["BillsPC_RefreshTextboxes.CancelString"]),
|
||||
],
|
||||
this.monDetails(box),
|
||||
);
|
||||
if (index < 0 || index >= box.length) return;
|
||||
for (;;) {
|
||||
const action = yield* this.menu(
|
||||
[this.string(PC.PCString_WhatsUp)],
|
||||
options,
|
||||
);
|
||||
const mon = box[index];
|
||||
if (action === SUBMENU_TAKE) {
|
||||
if (this.state.party.length >= PARTY_LENGTH) {
|
||||
yield* this.placeString(this.string(PC.PCString_PartyFull));
|
||||
continue;
|
||||
}
|
||||
box.splice(index, 1);
|
||||
this.state.party.push(withdrawnMon(this.host.data, mon));
|
||||
yield* this.placeString(
|
||||
`${this.string(PC.PCString_Got)}${mon.nickname}!`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (action === SUBMENU_STATS) {
|
||||
yield { kind: "notice", text: "The stats screen is not ported yet." };
|
||||
continue;
|
||||
}
|
||||
if (action !== SUBMENU_RELEASE) break;
|
||||
if (mon.species === EGG) {
|
||||
yield* this.placeString(this.string(PC.PCString_NoReleasingEGGS));
|
||||
continue;
|
||||
}
|
||||
if (!(yield* this.confirmRelease())) continue;
|
||||
box.splice(index, 1);
|
||||
yield* this.released(mon.species);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** _ChangeBox: choose a box, then SWITCH, NAME, PRINT or QUIT. */
|
||||
private *changeBox(): Gen {
|
||||
const storage = this.state.storage,
|
||||
options = this.strings(PC["BillsPC_ChangeBoxSubmenu.MenuData"] + 2, 4),
|
||||
current = this.string(PC["BillsPC_PrintBoxName.Current"]),
|
||||
outOf = this.string(PC["BillsPC_PrintBoxCountAndCapacity.OutOf20"]);
|
||||
let cursor = 0;
|
||||
for (;;) {
|
||||
const names = Array.from({ length: NUM_BOXES }, (_, box) =>
|
||||
this.boxName(box),
|
||||
);
|
||||
const box = yield* this.menu(
|
||||
[
|
||||
`${current}\n${this.boxName(storage.current)}\n${this.string(PC["BillsPC_PlaceChooseABoxString.ChooseABox"])}`,
|
||||
],
|
||||
names,
|
||||
storage.boxes.map((mons) => `${mons.length}${outOf}`),
|
||||
cursor,
|
||||
);
|
||||
if (box < 0) return;
|
||||
cursor = box;
|
||||
const action = yield* this.menu(
|
||||
[this.string(PC.PCString_WhatsUp)],
|
||||
options,
|
||||
);
|
||||
if (action === BOX_SWITCH) {
|
||||
if (box === storage.current) continue;
|
||||
// ChangeBoxSaveGame: the native session saves continuously, so the
|
||||
// switch itself is what the confirmation commits.
|
||||
const confirmed = yield {
|
||||
kind: "yesno",
|
||||
pages: this.pages(PC.ChangeBoxSaveText),
|
||||
};
|
||||
if (confirmed) storage.current = box;
|
||||
} else if (action === BOX_NAME) {
|
||||
const name = yield {
|
||||
kind: "name",
|
||||
pages: [this.string(PC["NamingScreenJumptable.BoxNameString"])],
|
||||
current: names[box],
|
||||
max: 8,
|
||||
};
|
||||
if (typeof name !== "string" || (name && !BOX_NAME_PATTERN.test(name)))
|
||||
throw new Error("Invalid box name.");
|
||||
// InitString: a blank name keeps the box's name.
|
||||
if (!/^ *$/.test(name)) storage.names[box] = name;
|
||||
} else if (action === BOX_PRINT) {
|
||||
if (!storage.boxes[box].length)
|
||||
yield* this.placeString(
|
||||
this.string(PC["BillsPC_PlaceEmptyBoxString_SFX.NoMonString"]),
|
||||
);
|
||||
else
|
||||
yield {
|
||||
kind: "notice",
|
||||
text: "Printing needs a Game Boy Printer, which is not supported.",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- Oak's PC
|
||||
|
||||
/** ProfOaksPC: an optional Pokédex rating from OakRatings. */
|
||||
private *oaksPC(): Gen {
|
||||
const r = this.host.reader;
|
||||
const rate = yield {
|
||||
kind: "yesno",
|
||||
pages: this.pages(PC.OakPCText1),
|
||||
};
|
||||
if (rate) {
|
||||
yield* this.print(PC.OakPCText2);
|
||||
const { seen, caught } = this.state.pokedex;
|
||||
this.host.strings[RAM.stringBuffer3] = String(seen.size);
|
||||
this.host.strings[RAM.stringBuffer4] = String(caught.size);
|
||||
yield* this.print(PC.OakPCText3);
|
||||
// FindOakRating: the first entry whose count reaches the owned total.
|
||||
let entry = PC.OakRatings;
|
||||
while (r.byte(entry) < caught.size) entry += 5;
|
||||
yield* this.print(r.near(entry + 3, r.bank(entry)));
|
||||
}
|
||||
yield* this.print(PC.OakPCText4);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user