Port field moves from the menu, SEL with Select, and moving Pokémon in Bill's PC
Checks / web (push) Canceled after 0s
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:
1 parent
45275ba83b
commit
3456aa4e11
25 files changed
+1129
-186
No files matched your search
+155
-22
@@ -1,6 +1,6 @@
|
||||
import type { RomReader } from "../rom/reader";
|
||||
import type { PokemonData } from "../rom/pokemon";
|
||||
import { BATTLE, ENGINE_FLAGS, MENU, RAM } from "../rom/profile";
|
||||
import { BATTLE, ENGINE_FLAGS, MENU, OVERWORLD, RAM } from "../rom/profile";
|
||||
import { C } from "../battle/constants";
|
||||
import {
|
||||
changedHappiness,
|
||||
@@ -79,6 +79,22 @@ export interface MenuCursors {
|
||||
pocket: number;
|
||||
}
|
||||
|
||||
/** The world's verdict on a field move chosen from the party menu: a
|
||||
* script to run once the menu closes (with the context the world checked),
|
||||
* the messages of a failed attempt, or a notice for what is not ported. */
|
||||
export type MenuFieldMove =
|
||||
| {
|
||||
script: number;
|
||||
fieldMove: {
|
||||
mon: number;
|
||||
surfState?: number;
|
||||
block?: { x: number; y: number; block: number };
|
||||
};
|
||||
wild?: { species: number; level: number };
|
||||
}
|
||||
| { texts: number[] }
|
||||
| { notice: string };
|
||||
|
||||
/** What the start menu needs from the world. */
|
||||
export interface StartMenuHost {
|
||||
reader: RomReader;
|
||||
@@ -86,11 +102,13 @@ export interface StartMenuHost {
|
||||
text(): TextContext;
|
||||
/** BikeFunction: the script that getting on or off the Bicycle queues
|
||||
* here, or undefined when .CheckEnvironment refuses it. */
|
||||
bicycleScript(state: ScriptState): number | undefined;
|
||||
bicycleScript(state: ScriptState, select: boolean): number | undefined;
|
||||
/** wTimeOfDay: morning 0, day 1, night 2. */
|
||||
timeOfDay(): number;
|
||||
/** GetWorldMapLocation for the current map (LevelUpHappinessMod). */
|
||||
landmark(): number;
|
||||
/** A field move's checks for party slot `mon`, on the menu's draft. */
|
||||
fieldMove(state: ScriptState, mon: number, move: number): MenuFieldMove;
|
||||
}
|
||||
|
||||
/** The overworld start menu (engine/menus/start_menu.asm) with its
|
||||
@@ -105,6 +123,8 @@ export class StartMenuRun {
|
||||
* closes, with the item named as UseItem names it. */
|
||||
deferred?: number;
|
||||
deferredItem?: number;
|
||||
/** The field move context and wild Pokémon a queued script needs. */
|
||||
deferredMove?: Extract<MenuFieldMove, { script: number }>;
|
||||
/** The menu cursors kept in work RAM between openings. */
|
||||
readonly cursors: MenuCursors;
|
||||
private strings: Record<number, string> = {};
|
||||
@@ -112,14 +132,19 @@ export class StartMenuRun {
|
||||
private iterator: Gen;
|
||||
private done = false;
|
||||
|
||||
/** wUsingItemWithSelect: the registered item is being used. */
|
||||
private usingSelect = false;
|
||||
|
||||
/** `select` runs SelectMenu (the Select button) instead of StartMenu. */
|
||||
constructor(
|
||||
state: ScriptState,
|
||||
private host: StartMenuHost,
|
||||
cursors: MenuCursors = { start: 0, party: 0, pocket: 0 },
|
||||
select = false,
|
||||
) {
|
||||
this.state = copyScriptState(state);
|
||||
this.cursors = { ...cursors };
|
||||
this.iterator = this.run();
|
||||
this.iterator = select ? this.selectMenu() : this.run();
|
||||
}
|
||||
|
||||
/** Advance until the next effect, or undefined when the menu closed. */
|
||||
@@ -270,6 +295,55 @@ export class StartMenuRun {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------- select menu
|
||||
|
||||
/** SelectMenu: use the registered item, or suggest registering one. */
|
||||
private *selectMenu(): Gen {
|
||||
const registered = this.checkRegisteredItem();
|
||||
if (!registered) return yield* this.print(MENU.MayRegisterItemText);
|
||||
const { item, index } = registered;
|
||||
// UseRegisteredItem by the item's field menu.
|
||||
const menu = this.data.itemFieldMenu(item);
|
||||
if (menu < ITEMMENU_CURRENT) return yield* this.print(MENU.ItemsOakWarningText);
|
||||
this.usingSelect = menu === ITEMMENU_CLOSE;
|
||||
const used = yield* this.doItemEffect(index, item);
|
||||
this.usingSelect = false;
|
||||
if (menu === ITEMMENU_CLOSE && used === false)
|
||||
yield* this.print(MENU.ItemsOakWarningText);
|
||||
}
|
||||
/** CheckRegisteredItem: the registered item if it is still in its slot
|
||||
* (or, for key items, anywhere in the pocket); otherwise the
|
||||
* registration is cleared. A registered ball is never found, because
|
||||
* .CheckBall's slot test is inverted. */
|
||||
private checkRegisteredItem(): { item: number; index: number } | undefined {
|
||||
const registered = this.state.registeredItem,
|
||||
bag = this.state.bag;
|
||||
const pocket = registered.which >> 6,
|
||||
index = (registered.which & 0x3f) - 1;
|
||||
let found: { item: number; index: number } | undefined;
|
||||
if (registered.which) {
|
||||
if (pocket === 0) {
|
||||
if (index < bag.items.length && bag.items[index].item === registered.item)
|
||||
found = { item: registered.item, index };
|
||||
} else if (pocket === KEY_ITEM_POCKET) {
|
||||
if (bag.keyItems.includes(registered.item))
|
||||
found = { item: registered.item, index: -1 };
|
||||
}
|
||||
}
|
||||
if (!found) this.state.registeredItem = { which: 0, item: 0 };
|
||||
return found;
|
||||
}
|
||||
/** RegisterItem: CANT_SELECT items cannot be registered. */
|
||||
private *registerItem(pocket: PocketIndex, index: number, item: number): Gen {
|
||||
if (this.data.itemUnselectable(item)) return yield* this.print(MENU.CantRegisterText);
|
||||
this.state.registeredItem = {
|
||||
which: ((pocket << 6) & 0xc0) | ((index + 1) & 0x3f),
|
||||
item,
|
||||
};
|
||||
this.nameItem(item);
|
||||
yield* this.print(MENU.RegisteredItemText);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- party menu
|
||||
|
||||
/** The party menu with PlacePartyMenuText's prompt. Returns the party
|
||||
@@ -314,7 +388,7 @@ export class StartMenuRun {
|
||||
for (;;) {
|
||||
const slot = yield* this.partyMenu(PARTYMENUACTION_CHOOSE_POKEMON);
|
||||
if (slot < 0) return false;
|
||||
yield* this.pokemonActionSubmenu(slot);
|
||||
if (yield* this.pokemonActionSubmenu(slot)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,8 +427,9 @@ export class StartMenuRun {
|
||||
return this.stringList(MENU.MonMenuOptionStrings, value)[value - 1];
|
||||
}
|
||||
|
||||
/** PokemonActionSubmenu (MonSubmenu); B is CANCEL. */
|
||||
private *pokemonActionSubmenu(slot: number): Gen {
|
||||
/** PokemonActionSubmenu (MonSubmenu); B is CANCEL. Returns true when a
|
||||
* field move closes every menu to run its script. */
|
||||
private *pokemonActionSubmenu(slot: number): Gen<boolean> {
|
||||
const mon = this.state.party[slot];
|
||||
const items = this.monSubmenuItems(mon);
|
||||
const choice = yield* this.menu(
|
||||
@@ -366,22 +441,77 @@ export class StartMenuRun {
|
||||
case MONMENUITEM_STATS:
|
||||
// OpenPartyStats
|
||||
yield { kind: "stats", mon };
|
||||
return;
|
||||
break;
|
||||
case MONMENUITEM_SWITCH:
|
||||
return yield* this.switchPartyMons(slot);
|
||||
yield* this.switchPartyMons(slot);
|
||||
break;
|
||||
case MONMENUITEM_ITEM:
|
||||
return yield* this.giveTakePartyMonItem(slot);
|
||||
yield* this.giveTakePartyMonItem(slot);
|
||||
break;
|
||||
case MONMENUITEM_MOVE:
|
||||
return yield* this.managePokemonMoves(slot);
|
||||
yield* this.managePokemonMoves(slot);
|
||||
break;
|
||||
case MONMENUITEM_CANCEL:
|
||||
return;
|
||||
break;
|
||||
case MONMENUITEM_MAIL:
|
||||
return yield* this.notice(`MAIL ${NOT_PORTED}`);
|
||||
yield* this.notice(`MAIL ${NOT_PORTED}`);
|
||||
break;
|
||||
default:
|
||||
return yield* this.notice(
|
||||
`Using ${this.monMenuString(item)} from the menu ${NOT_PORTED}`,
|
||||
);
|
||||
return yield* this.fieldMove(slot, item);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** MonMenu_Cut and the other field moves: Softboiled and Milk Drink run
|
||||
* here; the others ask the world, which checks the map. */
|
||||
private *fieldMove(slot: number, item: number): Gen<boolean> {
|
||||
const r = this.reader;
|
||||
let p = MENU.MonMenuOptions;
|
||||
while (r.byte(p + 1) !== item) p += 3;
|
||||
const move = r.byte(p + 2);
|
||||
if (move === C.SOFTBOILED || move === C.MILK_DRINK) {
|
||||
yield* this.softboiledMilkDrink(slot);
|
||||
return false;
|
||||
}
|
||||
const result = this.host.fieldMove(this.state, slot, move);
|
||||
if ("script" in result) {
|
||||
this.deferred = result.script;
|
||||
this.deferredMove = result;
|
||||
return true;
|
||||
}
|
||||
if ("notice" in result) yield* this.notice(result.notice);
|
||||
else for (const text of result.texts) yield* this.print(text);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** MonMenu_Softboiled_MilkDrink: the user needs more than a fifth of
|
||||
* its maximum HP, and gives that much to another Pokémon that has not
|
||||
* fainted and is not at full HP. */
|
||||
private *softboiledMilkDrink(user: number): Gen {
|
||||
const party = this.state.party,
|
||||
source = party[user];
|
||||
const fifth = Math.floor(source.stats[0] / 5);
|
||||
if (source.hp <= fifth) {
|
||||
yield* this.print(MENU["MonMenu_Softboiled_MilkDrink.PokemonNotEnoughHPText"]);
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
const target = yield* this.partyMenu(PARTYMENUACTION_HEALING_ITEM);
|
||||
if (target < 0) break;
|
||||
const mon = party[target];
|
||||
if (target === user || !mon.hp || mon.hp >= mon.stats[0]) {
|
||||
yield* this.print(
|
||||
OVERWORLD["Softboiled_MilkDrinkFunction.ItemCantUseOnMonText"],
|
||||
);
|
||||
continue;
|
||||
}
|
||||
source.hp = Math.max(0, source.hp - fifth);
|
||||
this.setHP(mon, restoreHealth(mon.hp, mon.stats[0], fifth));
|
||||
yield* this.itemActionText(target, C.PARTYMENUTEXT_HEAL_HP);
|
||||
break;
|
||||
}
|
||||
// .skip: the cursor returns to the user.
|
||||
this.cursors.party = user + 1;
|
||||
}
|
||||
|
||||
/** SwitchPartyMons and _SwitchPartyMons: "Move to where?" without a
|
||||
@@ -591,7 +721,7 @@ export class StartMenuRun {
|
||||
yield* this.tossMenu(pocket, index, item);
|
||||
return false;
|
||||
case "sel":
|
||||
yield* this.notice(`Registering items for SELECT ${NOT_PORTED}`);
|
||||
yield* this.registerItem(pocket, index, item);
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
@@ -660,13 +790,16 @@ export class StartMenuRun {
|
||||
}
|
||||
const used = yield* this.doItemEffect(index, item);
|
||||
if (menu !== ITEMMENU_CLOSE) return false;
|
||||
if (!used) yield* this.print(BATTLE.OakThisIsntTheTimeText);
|
||||
return used;
|
||||
if (used === false) yield* this.print(BATTLE.OakThisIsntTheTimeText);
|
||||
return used === true;
|
||||
}
|
||||
|
||||
/** _DoItemEffect: ItemEffects by ROM address. Returns
|
||||
* wItemEffectSucceeded. */
|
||||
private *doItemEffect(index: number, item: number): Gen<boolean> {
|
||||
* wItemEffectSucceeded, or "unported" after a notice. */
|
||||
private *doItemEffect(
|
||||
index: number,
|
||||
item: number,
|
||||
): Gen<boolean | "unported"> {
|
||||
const r = this.reader;
|
||||
this.nameItem(item);
|
||||
const effect = r.near(
|
||||
@@ -722,7 +855,7 @@ export class StartMenuRun {
|
||||
case MENU.EvoStoneEffect:
|
||||
return yield* this.evoStoneEffect(use);
|
||||
case MENU.BicycleEffect: {
|
||||
const script = this.host.bicycleScript(this.state);
|
||||
const script = this.host.bicycleScript(this.state, this.usingSelect);
|
||||
if (script === undefined) return false;
|
||||
this.deferred = script;
|
||||
this.deferredItem = item;
|
||||
@@ -730,7 +863,7 @@ export class StartMenuRun {
|
||||
}
|
||||
}
|
||||
yield* this.notice(`Using ${this.data.itemName(item)} ${NOT_PORTED}`);
|
||||
return false;
|
||||
return "unported";
|
||||
}
|
||||
|
||||
/** UseItem_SelectMon: the party menu for an item. The party index, or
|
||||
|
||||
Reference in new issue
Block a user