Port field moves from the menu, SEL with Select, and moving Pokémon in Bill's PC
Checks / web (push) Canceled after 0s

Field moves chosen in the party menu run their original checks and the
ROM's from-menu scripts (Cut, Surf, Strength, Flash, Whirlpool, Waterfall,
Sweet Scent), and Softboiled and Milk Drink share HP. SEL registers a key
item that Select then uses, replacing the Key Items shortcut, and Bill's
PC moves Pokémon between the party and every box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-30 17:22:56 +02:00
1 parent 45275ba83b
commit 3456aa4e11
25 files changed
+1129 -186

No files matched your search

+129 -7
View File
@@ -1,14 +1,23 @@
import type { RomReader } from "../rom/reader";
import type { PokemonData } from "../rom/pokemon";
import { ENGINE_FLAGS, PC, RAM } from "../rom/profile";
import { ENGINE_FLAGS, MENU, PC, RAM } from "../rom/profile";
import { receiveItem, removeFromSlot, type ItemSlot } from "../party/bag";
import { BOX_NAME_PATTERN, PARTY_LENGTH } from "../party/pokemon";
import {
BOX_NAME_PATTERN,
PARTY_LENGTH,
type PartyMon,
} from "../party/pokemon";
import {
MONS_PER_BOX,
NUM_BOXES,
bufferMon,
currentBox,
depositMon,
receivePCItem,
restorePP,
toBoxMon,
withdrawnMon,
type BoxMon,
} from "../party/storage";
import { readString, readText, type TextContext } from "./text";
import type { MartEntry, ScriptEffect, ScriptState } from "./interpreter";
@@ -422,15 +431,128 @@ export class PcSession {
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 if (choice === BILLSPC_MOVE) yield* this.movePKMNMenu();
else return;
}
}
// ------------------------------------------------ MOVE PKMN W/O MAIL
/** BillsPC_MovePKMNMenu: refused while a party Pokémon holds MAIL
* (IsAnyMonHoldingMail), then StartMoveMonWOMail_SaveGame's question. The
* native session saves by itself, so no save messages follow. */
private *movePKMNMenu(): Gen {
const r = this.host.reader;
const mail = new Set<number>();
for (let p = MENU.MailItems; r.byte(p) !== 0xff; p++) mail.add(r.byte(p));
if (this.state.party.some((mon) => mail.has(mon.item)))
return yield* this.print(PC["BillsPC_MovePKMNMenu.PCMonHoldingMailText"]);
const ok = yield { kind: "yesno", pages: this.pages(PC.MoveMonWOMailSaveText) };
if (ok) yield* this.moveWithoutMail();
}
/** wBillsPC_LoadedBox: 0 is the party, 1-14 the boxes. */
private loadedBox(box: number): (PartyMon | BoxMon)[] {
return box ? this.state.storage.boxes[box - 1] : this.state.party;
}
private loadedBoxName(box: number) {
return box
? this.boxName(box - 1)
: this.string(PC["BillsPC_BoxName.PartyPKMN"]);
}
/** Choose the party or a box (left and right in the original). */
private *chooseLoadedBox(prompt: string, selected: number): Gen<number> {
const boxes = Array.from({ length: NUM_BOXES + 1 }, (_, box) => box);
return yield* this.menu(
[prompt],
boxes.map((box) => this.loadedBoxName(box)),
boxes.map((box) => `${this.loadedBox(box).length}/${box ? MONS_PER_BOX : PARTY_LENGTH}`),
selected,
);
}
/** _MovePKMNWithoutMail: choose a Pokémon in the party or a box, MOVE it
* (STATS shows it) and choose where it goes; the list is presented per
* box instead of with left and right. */
private *moveWithoutMail(): Gen {
const data = this.host.data;
const choose = this.string(PC.PCString_ChooseaPKMN),
where = this.string(PC.PCString_MoveToWhere),
cancel = this.string(PC["BillsPC_RefreshTextboxes.CancelString"]),
options = this.strings(PC["_MovePKMNWithoutMail.MenuData"] + 2, 3);
let loaded = this.state.storage.current + 1;
for (;;) {
const from = yield* this.chooseLoadedBox(choose, loaded);
if (from < 0) return;
loaded = from;
const list = this.loadedBox(from);
const index = yield* this.menu(
[`${this.loadedBoxName(from)}\n${choose}`],
[...list.map((mon) => mon.nickname), cancel],
this.monDetails(list),
);
if (index < 0 || index >= list.length) continue;
const action = yield* this.menu([this.string(PC.PCString_WhatsUp)], options);
if (action === 1) {
const mon = list[index];
yield {
kind: "stats",
mon: from ? bufferMon(data, mon as BoxMon) : structuredClone(mon as PartyMon),
};
continue;
}
if (action !== 0) continue;
// BillsPC_CheckMail_PreventBlackout, even within the party.
if (!from && !(yield* this.partyMonMayLeave(index))) continue;
// Where: a box, then the position to insert before.
for (;;) {
const to = yield* this.chooseLoadedBox(where, from);
if (to < 0) break;
const destination = this.loadedBox(to);
const position = yield* this.menu(
[`${this.loadedBoxName(to)}\n${where}`],
[...destination.map((mon) => mon.nickname), "▶"],
this.monDetails(destination),
destination.length,
);
if (position < 0) continue;
// BillsPC_CheckSpaceInDestination
if (
to !== from &&
destination.length >= (to ? MONS_PER_BOX : PARTY_LENGTH)
) {
yield* this.placeString(this.string(PC.PCString_TheresNoRoom));
continue;
}
yield* this.placeString(
this.string(PC["MovePKMNWithoutMail_InsertMon.Saving_LeaveOn"]),
10,
);
this.insertMon(from, index, to, position);
loaded = to;
break;
}
}
}
/** MovePKMNWithoutMail_InsertMon: remove, then insert before `position`
* (one earlier when moving down the same list, .CheckTrivialMove). A
* boxed Pokémon joining the party gets CalcBufferMonStats' full HP;
* InsertPokemonIntoBox restores the PP. */
private insertMon(from: number, index: number, to: number, position: number) {
const data = this.host.data;
const source = this.loadedBox(from);
const [moved] = source.splice(index, 1);
if (from === to && index < position) position--;
if (to) {
const stored = from ? (moved as BoxMon) : toBoxMon(moved as PartyMon);
restorePP(data, stored);
this.state.storage.boxes[to - 1].splice(position, 0, stored);
} else
this.state.party.splice(
position,
0,
from ? bufferMon(data, moved as BoxMon) : (moved as PartyMon),
);
}
/** 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> {