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:
1 parent
397dcef44c
commit
e2dc5ba520
9 files changed
+362
-34
No files matched your search
@@ -517,6 +517,7 @@ export const MENU = {
|
||||
"MenuHeader_HoldableKeyItem.MenuData": 0x102d5,
|
||||
"MenuHeader_HoldableItem.MenuData": 0x102fa,
|
||||
AskThrowAwayText: 0x10ae4,
|
||||
AskItemMoveText: 0x10b07,
|
||||
AskQuantityThrowAwayText: 0x10ae9,
|
||||
ThrewAwayText: 0x10aee,
|
||||
YouDontHaveAMonText: 0x10af8,
|
||||
|
||||
@@ -352,6 +352,8 @@ export type ScriptEffect =
|
||||
/** Secondary text per item, such as a level or a box's count. */
|
||||
details?: string[];
|
||||
selected: number;
|
||||
/** Left and right answer "left" or "right" (MoveScreenLoop). */
|
||||
sides?: boolean;
|
||||
}
|
||||
/** A native message for presentation the port does not have yet; the
|
||||
* script continues. */
|
||||
@@ -377,12 +379,16 @@ export type ScriptEffect =
|
||||
| { kind: "evolve"; from: number; to: number; cancelable: boolean }
|
||||
/** The pack's pockets (Pack and DepositSellPack), opened on `pocket`;
|
||||
* left and right change pockets. Answered with the pocket index × 256
|
||||
* plus the entry's index, or -1 for B. */
|
||||
* plus the entry's index, or -1 for B; "select:" before that number is
|
||||
* the Select button on the entry. While `switching` (an entry of the
|
||||
* current pocket) moves with Select, `prompt` asks where to. */
|
||||
| {
|
||||
kind: "pack";
|
||||
pockets: { name: string; entries: MartEntry[] }[];
|
||||
pocket: number;
|
||||
selected: number;
|
||||
switching?: number;
|
||||
prompt?: string;
|
||||
}
|
||||
/** warpcheck: the player's tile events run again after the script. */
|
||||
| { kind: "warpcheck" }
|
||||
|
||||
+122
-15
@@ -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;
|
||||
|
||||
+30
-6
@@ -258,7 +258,14 @@ export type Choice =
|
||||
/** The naming screen: an empty answer keeps `current`. */
|
||||
| { kind: "name"; current: string; max: number; selected?: undefined }
|
||||
/** A vertical menu; `selected` answers with that item's index. */
|
||||
| { kind: "menu"; items: string[]; details?: string[]; selected: number }
|
||||
| {
|
||||
kind: "menu";
|
||||
items: string[];
|
||||
details?: string[];
|
||||
selected: number;
|
||||
/** Left and right answer "left" or "right". */
|
||||
sides?: boolean;
|
||||
}
|
||||
/** The mart's BUY/SELL/QUIT box. */
|
||||
| { kind: "martmenu"; prompt: string; selected: number }
|
||||
/** The mart's item list; `selected` answers with that entry's index. */
|
||||
@@ -268,12 +275,15 @@ export type Choice =
|
||||
entries: MartEntry[];
|
||||
selected: number;
|
||||
}
|
||||
/** The pack's pockets; `selected` is an entry of pocket `pocket`. */
|
||||
/** The pack's pockets; `selected` is an entry of pocket `pocket`, and
|
||||
* `switching` the one being moved with Select. */
|
||||
| {
|
||||
kind: "pack";
|
||||
pockets: { name: string; entries: MartEntry[] }[];
|
||||
pocket: number;
|
||||
selected: number;
|
||||
switching?: number;
|
||||
prompt?: string;
|
||||
}
|
||||
/** A Pokégear card, answered with the button pressed. */
|
||||
| { kind: "pokegear"; view: PokegearView; selected: 0 }
|
||||
@@ -2134,6 +2144,10 @@ export class GameWorld {
|
||||
this.choose("select");
|
||||
return true;
|
||||
}
|
||||
if (this.dialogue?.choice?.kind === "pack") {
|
||||
this.choosePackEntry(undefined, undefined, true);
|
||||
return true;
|
||||
}
|
||||
return this.openStartMenu(true);
|
||||
}
|
||||
/** Start in PlayerEvents (StartMenuScript): the start menu opens while
|
||||
@@ -2554,6 +2568,7 @@ export class GameWorld {
|
||||
items: effect.items,
|
||||
...(effect.details && { details: effect.details }),
|
||||
selected: effect.selected,
|
||||
...(effect.sides && { sides: true }),
|
||||
},
|
||||
};
|
||||
return true;
|
||||
@@ -2615,6 +2630,10 @@ export class GameWorld {
|
||||
pockets: effect.pockets,
|
||||
pocket: effect.pocket,
|
||||
selected: effect.selected,
|
||||
...(effect.switching !== undefined && {
|
||||
switching: effect.switching,
|
||||
prompt: effect.prompt,
|
||||
}),
|
||||
},
|
||||
};
|
||||
return true;
|
||||
@@ -4005,6 +4024,8 @@ export class GameWorld {
|
||||
if (direction === "up") choice.selected = (choice.selected + 2) % 3;
|
||||
if (direction === "down") choice.selected = (choice.selected + 1) % 3;
|
||||
} else if (choice.kind === "menu") {
|
||||
if ((direction === "left" || direction === "right") && choice.sides)
|
||||
return this.choose(direction);
|
||||
if (direction === "up")
|
||||
choice.selected = Math.max(0, choice.selected - 1);
|
||||
if (direction === "down")
|
||||
@@ -4018,9 +4039,11 @@ export class GameWorld {
|
||||
if (direction === "down")
|
||||
choice.selected = (choice.selected + length - 1) % length;
|
||||
} else if (choice.kind === "pack") {
|
||||
// Pack_InterpretJoypad: left and right change pockets.
|
||||
// Pack_InterpretJoypad: left and right change pockets, except while
|
||||
// an item is being moved (.switching_item).
|
||||
const pockets = choice.pockets.length;
|
||||
if (direction === "left" || direction === "right") {
|
||||
if (choice.switching !== undefined) return;
|
||||
choice.pocket =
|
||||
(choice.pocket + (direction === "left" ? pockets - 1 : 1)) % pockets;
|
||||
choice.selected = 0;
|
||||
@@ -4054,14 +4077,15 @@ export class GameWorld {
|
||||
} else if (direction === "up") choice.selected = (choice.selected + 1) % 7;
|
||||
else if (direction === "down") choice.selected = (choice.selected + 6) % 7;
|
||||
}
|
||||
/** Choose the highlighted pack entry, if its pocket has one. */
|
||||
choosePackEntry(pocket?: number, index?: number) {
|
||||
/** Choose the highlighted pack entry, if its pocket has one, with A or
|
||||
* (`select`) with the Select button. */
|
||||
choosePackEntry(pocket?: number, index?: number, select = false) {
|
||||
const choice = this.dialogue?.choice;
|
||||
if (choice?.kind !== "pack") return;
|
||||
pocket ??= choice.pocket;
|
||||
index ??= choice.selected;
|
||||
if (!choice.pockets[pocket]?.entries[index]) return;
|
||||
this.choose(pocket * 256 + index);
|
||||
this.choose(select ? `select:${pocket * 256 + index}` : pocket * 256 + index);
|
||||
}
|
||||
/** Answer the current choice: 1/0 for yes/no, a weekday index, or a
|
||||
* name ("" keeps the current one). */
|
||||
|
||||
Reference in new issue
Block a user