Move items with Select and browse the party's moves

In the start menu's pack, Select marks an item and asks where it should
go; A or Select on another row moves it there, merging stacks of the
same item unless either holds 99, and B ends the move. The packs that
GIVE and the PC open ignore Select, as in the original.

Left and right on the MOVE screen show the previous or next party
member's moves, skipping Eggs, unless a move is being moved.

Part of #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-10-03 00:56:51 +02:00
1 parent 397dcef44c
commit e2dc5ba520
9 files changed
+362 -34

No files matched your search

+122 -15
View File
@@ -78,6 +78,8 @@ const {
PP_UP_ONE,
} = C;
const NOT_PORTED = "is not ported yet.";
/** MAX_ITEM_STACK */
const MAX_ITEM_STACK = 99;
/** MAP_RADIO_TOWER_3F and MAP_GOLDENROD_UNDERGROUND
* (constants/map_constants.asm). */
const RADIO_TOWER_3F = "3:19",
@@ -687,14 +689,17 @@ export class StartMenuRun {
}
/** ManagePokemonMoves (MoveScreenLoop): choose a move, then "Where?";
* the moves and their PP trade places. Left and right, which move to
* another party member's moves, are not offered. */
* the moves and their PP trade places. Left and right show the previous
* or next party member's moves, skipping Eggs, unless a move is being
* moved. */
private *managePokemonMoves(slot: number): Gen {
const mon = this.state.party[slot];
if (mon.species === EGG) return;
if (this.state.party[slot].species === EGG) return;
const where = this.string(MENU.String_MoveWhere);
let cursor = 0;
for (;;) {
// MoveScreenLoop puts the party menu cursor on wCurPartyMon.
this.cursors.party = slot + 1;
const mon = this.state.party[slot];
const count = mon.moves.filter(Boolean).length;
const names = mon.moves.slice(0, count).map((move) => this.data.moveName(move));
const details = mon.moves
@@ -704,7 +709,27 @@ export class StartMenuRun {
`PP ${mon.pp[index] & C.PP_MASK}/${maxPP(this.data, move, mon.pp[index] >> 6) & C.PP_MASK}`,
);
const heading = `${mon.nickname} :L${mon.level}`;
const first = yield* this.menu([heading], names, details, cursor);
const answer = yield {
kind: "menu",
pages: [heading],
items: names,
details,
selected: cursor,
sides: true,
};
if (answer === "left" || answer === "right") {
const next = this.cycleMoveScreen(slot, answer);
// Load2DMenuData puts the cursor back on the first move.
if (next !== slot) [slot, cursor] = [next, 0];
continue;
}
const first =
typeof answer === "number" &&
Number.isInteger(answer) &&
answer >= 0 &&
answer < names.length
? answer
: -1;
if (first < 0) return;
const second = yield* this.menu([where], names, details, first);
if (second < 0) {
@@ -717,6 +742,31 @@ export class StartMenuRun {
}
}
/** MoveScreenLoop's .cycle_right and .cycle_left: the next party member
* that is not an Egg; past the last one, back to the last that is not.
* The original's routines call each other, so a step can come back to
* where it started. */
private cycleMoveScreen(slot: number, direction: "left" | "right") {
const party = this.state.party;
let current = slot;
const egg = (index: number) => party[index].species === EGG;
let step: "left" | "right" | "done" = direction;
for (let guard = 0; step !== "done" && guard < 64; guard++) {
if (step === "right") {
current++;
if (current >= party.length) step = "left";
else if (!egg(current)) step = "done";
} else {
// .cycle_left
if (current === 0) break;
current--;
if (!egg(current)) step = "done";
else if (current === 0) step = "right";
}
}
return current;
}
// --------------------------------------------------------------- pack
private pocketSlots(pocket: PocketIndex): ItemSlot[] | undefined {
@@ -739,10 +789,14 @@ export class StartMenuRun {
}));
}
/** The pack's pockets from wLastPocket; the chosen pocket, index and
* item, or undefined for B. */
private *choosePackItem(): Gen<
{ pocket: PocketIndex; index: number; item: number } | undefined
> {
* item, or undefined for B. With `reorder` (the start menu's PACK,
* Pack_InterpretJoypad), Select moves items within a pocket. */
private *choosePackItem(
reorder = false,
): Gen<{ pocket: PocketIndex; index: number; item: number } | undefined> {
let selected = 0;
/** wSwitchItem: the entry being moved, if any. */
let switching: number | undefined;
for (;;) {
const answer = yield {
kind: "pack",
@@ -751,24 +805,77 @@ export class StartMenuRun {
entries: this.pocketEntries(index as PocketIndex),
})),
pocket: this.cursors.pocket,
selected: 0,
selected,
...(switching !== undefined && {
switching,
prompt: readText(this.reader, MENU.AskItemMoveText, this.text()),
}),
};
if (typeof answer !== "number" || !Number.isInteger(answer) || answer < 0)
return undefined;
const pocket = (answer >> 8) as PocketIndex,
index = answer & 0xff;
const select = typeof answer === "string" && /^select:\d+$/.test(answer);
// DepositSellPack's joypad ignores Select.
if (select && !reorder) continue;
const value = select ? Number((answer as string).slice(7)) : answer;
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
// .switching_item: B ends the move and stays in the pack.
if (switching === undefined) return undefined;
switching = undefined;
continue;
}
const pocket = (value >> 8) as PocketIndex,
index = value & 0xff;
if (pocket > TM_HM_POCKET) continue;
selected = index;
if (switching !== undefined) {
// .place_insert: A or Select on the destination.
if (pocket === this.cursors.pocket)
this.switchItems(pocket, switching, index);
switching = undefined;
continue;
}
this.cursors.pocket = pocket;
if (select) {
// SwitchItemsInBag's .init; the TM pocket has its own joypad.
if (pocket !== TM_HM_POCKET) switching = index;
continue;
}
const item = this.pocketEntries(pocket)[index]?.item;
if (item !== undefined) return { pocket, index, item };
}
}
/** SwitchItemsInBag placing the entry at `from` on `to`: the same item
* merges into the destination's stack unless either holds 99 (the rest
* stays behind when they add up to more); otherwise the entry moves to
* the destination's row and the ones between shift. */
private switchItems(pocket: PocketIndex, from: number, to: number) {
if (from === to) return;
const list: (ItemSlot | number)[] | undefined =
pocket === KEY_ITEM_POCKET
? this.state.bag.keyItems
: this.pocketSlots(pocket);
if (!list || from >= list.length || to >= list.length) return;
const source = list[from],
target = list[to];
if (
typeof source === "object" &&
typeof target === "object" &&
source.item === target.item &&
source.quantity !== MAX_ITEM_STACK &&
target.quantity !== MAX_ITEM_STACK
) {
const total = source.quantity + target.quantity;
target.quantity = Math.min(total, MAX_ITEM_STACK);
if (total > MAX_ITEM_STACK) source.quantity = total - MAX_ITEM_STACK;
else list.splice(from, 1);
return;
}
list.splice(to, 0, ...list.splice(from, 1));
}
/** StartMenu_Pack (Pack): returns true when an item closes the menus to
* run a script (PACKSTATE_QUITRUNSCRIPT). */
private *pack(): Gen<boolean> {
for (;;) {
const chosen = yield* this.choosePackItem();
const chosen = yield* this.choosePackItem(true);
if (!chosen) return false;
if (yield* this.itemSubmenu(chosen.pocket, chosen.index, chosen.item))
return true;