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
+361
-33
No files matched your search
+15
-5
@@ -302,8 +302,9 @@ when fewer than `NUM_MONMENU_ITEMS` are listed; the strings come from
|
||||
`MonMenuOptionStrings` and move names. `SwitchPartyMons`/`_SwitchPartyMons`
|
||||
swap two party entries. `MoveScreenLoop` swaps two moves and their PP; the
|
||||
original's flag-dependent write into `wBattleMon` after a swap outside
|
||||
battle is not modelled (the next battle reloads that structure), and left
|
||||
and right do not move to another Pokémon's moves. `GiveTakePartyMonItem`,
|
||||
battle is not modelled (the next battle reloads that structure). Its left
|
||||
and right (`.cycle_left`, `.cycle_right`) arrive as "left" and "right"
|
||||
answers of a `menu` effect with `sides`. `GiveTakePartyMonItem`,
|
||||
`TryGiveItemToPartymon` (including the swap and the full-pack refund) and
|
||||
`TakePartyItem` use `GiveItemToPokemon`/`ReceiveItemFromPokemon` on the
|
||||
chosen pack slot. Giving MAIL needs `ComposeMailMessage` and is refused
|
||||
@@ -348,6 +349,13 @@ while the menu is open. A Chromium/WebKit test opens the menu with Start,
|
||||
switches Pokémon, changes pockets and tosses a Potion. These are not
|
||||
emulator comparisons.
|
||||
|
||||
`Pack_InterpretJoypad`'s Select runs `SwitchItemsInBag` on the bag
|
||||
(`.try_combining_stacks`, `.combine_stacks`, `.above` and `.below`) through
|
||||
a `select:` answer of the `pack` effect, which shows the moving entry
|
||||
(`wSwitchItem`) and `AskItemMoveText`; the original's CANCEL row is the
|
||||
pack's BACK. The cursor now stays on the last row chosen while the pack is
|
||||
open.
|
||||
|
||||
`DoRepelStep` runs in the world where `CountStep` calls it, after
|
||||
`CheckSpecialPhoneCall`; the poison, happiness, egg, Day-Care and bike
|
||||
steps of `CountStep` are not ported yet. `RepelWoreOffScript` starts with
|
||||
@@ -385,10 +393,12 @@ the keys (a start-menu run with the unopened maps' positions) and the
|
||||
Squirtbottle (the interpreter facing a test Sudowoodo on Route 36),
|
||||
`Fish` against the Shore tables, a bite, no bite and no fish in New Bark
|
||||
Town with the poses and the bubble, the surf and water refusals, and the
|
||||
Magikarp DV filters. `tests/reference-parity.spec.ts` compares
|
||||
Magikarp DV filters. `tests/start-menu.test.ts` moves and merges items
|
||||
with Select (keys included, and none in GIVE's pack) and steps through the
|
||||
party on the MOVE screen past an Egg. `tests/reference-parity.spec.ts` compares
|
||||
`magikarpLength` with `CalcMagikarpLength` in the reference emulator over
|
||||
1,509 DV and ID pairs. A Chromium/WebKit test hooks a Magikarp with the
|
||||
Old Rod; the four fishing directions were checked in close-up
|
||||
1,509 DV and ID pairs. Chromium/WebKit tests hook a Magikarp with the
|
||||
Old Rod and move a Potion below a Rare Candy with the keyboard's Select; the four fishing directions were checked in close-up
|
||||
screenshots, not against the original.
|
||||
|
||||
## Evolution
|
||||
|
||||
+3
-1
@@ -169,7 +169,7 @@ The numbered sections below keep their full scope. Each one opens with a checkli
|
||||
- PACK: the Items, Balls, Key and TM pockets (left and right change pockets; there are no TMs or HMs yet), and each item's submenu chosen by its permissions and field menu (`USE`, `GIVE`, `TOSS`, `SEL`, `QUIT`). TOSS asks how many and confirms. USE follows `UseItem`: Oak's warning without a field use, "You don't have a POKéMON!" for party items without one, and `_DoItemEffect` by the item's `ItemEffects` address: Potions and the other `HealingHPAmounts` items, status healers, Full Restore, Revive and Max Revive, the bitter Energypowder, Energy Root, Heal Powder and Revival Herb, Ethers, Elixers, the MysteryBerry and PP Up. The Bicycle closes the menu and runs `Script_GetOnBike` or `Script_GetOffBike` outdoors; elsewhere Oak objects, because `wFieldMoveSucceeded` shares `wItemEffectSucceeded`'s byte.
|
||||
- PP Up keeps a quirk: `wUsePPUp` shares `wTempPP`'s byte, so `ComputeMaxPP` adds one step (a fifth of the base PP, at most 7) to the current PP as well as the maximum. Sketch and moves with three PP Ups are "maxed out".
|
||||
- The menu runs on a script draft, like the PC. Changes commit when it closes; a save made while it is open keeps the state from before it. The start menu cursor, the party menu cursor and the last pocket stay in memory between openings and are cleared after a battle, as `CleanUpBattleRAM` does. The party cursor reaches the next battle's `ChangeHappiness`.
|
||||
- Not ported: MAIL, switching to another party member's moves with left and right, and item reordering with Select (each gives a notice and changes nothing; the Escape Rope runs since the Fly section below, Repels and the other items of the last section since then). Rare Candy, evolution stones and vitamins run since the evolution section below; field moves, SEL and Select since the section after it.
|
||||
- Not ported: MAIL (it gives a notice and changes nothing). The Escape Rope runs since the Fly section below; Repels and the other field items, the rods, item reordering with Select and left and right on the MOVE screen since the last section. Rare Candy, evolution stones and vitamins run since the evolution section below; field moves, SEL and Select since the section after it.
|
||||
|
||||
## Implemented: evolution
|
||||
|
||||
@@ -249,6 +249,8 @@ The numbered sections below keep their full scope. Each one opens with a checkli
|
||||
- The Itemfinder answers "Yes! ITEMFINDER indicates there's an item nearby." when a hidden item not found yet lies on the original's screen around the player (`CheckForHiddenItems`: four squares left and up, five right, four down), and "Nope!" otherwise. Route 30's hidden Potion is the only one on an open map.
|
||||
- The Card Key and the Basement Key run their door scripts only facing the Radio Tower 3F slot or the Goldenrod Underground door (Oak objects elsewhere). The Squirtbottle sprinkles water, and on Route 36 starts the Sudowoodo battle when facing it (`WateredWeirdTreeScript`). None of these maps is open yet.
|
||||
- The Old, Good and Super Rods fish facing water, never while surfing (Oak objects otherwise; the facing tile may hold an object, as in the original). The map header's fish group decides: none gives "Not even a nibble!" at once; otherwise `Fish` bites below 50% + 1 and picks the rod's `FishGroups` entry, with `TimeFishGroups` for the morning and day or the night. The player casts with the fishing frames (`LoadFishingGFX`'s body and rod tiles), jiggles four times on a bite, shows "!", and after "Oh! A bite!" puts the rod away before a `BATTLETYPE_FISH` battle: "The hooked MAGIKARP attacked!". Qwilfish and Remoraid swarms come from trainer calls, which are not ported. New Bark Town, Route 30 and Cherrygrove City have fish.
|
||||
- In the start menu's pack, Select marks an item and asks "Where should this be moved to?"; A or Select on another row of the same pocket moves it there (the rows between shift), merging it into a stack of the same item unless either holds 99 (what exceeds 99 stays behind), and B ends the move without leaving the pack. Left and right keep the pocket meanwhile. The packs that GIVE and the PC open ignore Select, as `DepositSellPack` does.
|
||||
- On the MOVE screen, left and right show the previous or next party member's moves, skipping Eggs (`MoveScreenLoop`'s `.cycle_left` and `.cycle_right`, which come back to the same Pokémon past either end), with the cursor back on the first move; not while a move is being moved. The party menu's cursor follows.
|
||||
- Wild Magikarp now battle: `LoadEnemyMon` draws their DVs again as the original does, through `CalcMagikarpLength` (compared with the original routine) and its filters, whose millimetre limits only work as a floor of four feet that about 39% of draws skip (39 percent + 1), nowhere in map group 9 or on maps numbered 6 (the Lake of Rage check). Unown are still refused.
|
||||
- Save format 3 gained `repel`, `coins` and `blueCardBalance`, migrated by absence to 0.
|
||||
|
||||
|
||||
@@ -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). */
|
||||
|
||||
+15
-5
@@ -327,7 +327,11 @@ function showMart(world: GameWorld, mart: MartChoice | undefined) {
|
||||
get("mart-money").hidden = !shop;
|
||||
get("mart-money").textContent = `MONEY ¥${world.liveState.money}`;
|
||||
get("mart-prompt").textContent =
|
||||
mart.kind === "martlist" || mart.kind === "pack" ? "" : mart.prompt;
|
||||
mart.kind === "martlist"
|
||||
? ""
|
||||
: mart.kind === "pack"
|
||||
? (mart.prompt ?? "").replace(/\n/g, " ")
|
||||
: mart.prompt;
|
||||
const key = JSON.stringify(mart);
|
||||
if (key === martMenuKey) return;
|
||||
martMenuKey = key;
|
||||
@@ -368,6 +372,9 @@ function showMart(world: GameWorld, mart: MartChoice | undefined) {
|
||||
}
|
||||
if (mart.kind === "pack") {
|
||||
// Pocket tabs (left and right on the keyboard), then the pocket's items.
|
||||
// While an item moves with Select, the pocket stays and the moving
|
||||
// entry is marked; choosing another entry places it there.
|
||||
const switching = mart.switching;
|
||||
const pocketTabs = mart.pockets.map((pocket, index) => {
|
||||
const tab = button(pocket.name, () => {
|
||||
const world = runtime.world;
|
||||
@@ -378,19 +385,22 @@ function showMart(world: GameWorld, mart: MartChoice | undefined) {
|
||||
showWorld(world);
|
||||
});
|
||||
tab.setAttribute("aria-pressed", String(mart.pocket === index));
|
||||
tab.disabled = switching !== undefined && mart.pocket !== index;
|
||||
return tab;
|
||||
});
|
||||
const entries = mart.pockets[mart.pocket].entries;
|
||||
const buttons = entries.map((entry, index) =>
|
||||
button(
|
||||
entry.name,
|
||||
const buttons = entries.map((entry, index) => {
|
||||
const entryButton = button(
|
||||
switching === index ? `▷${entry.name}` : entry.name,
|
||||
() => runtime.choosePackEntry(mart.pocket, index),
|
||||
{
|
||||
detail: entry.owned === undefined ? undefined : `×${entry.owned}`,
|
||||
selected: mart.selected === index,
|
||||
},
|
||||
),
|
||||
);
|
||||
if (switching === index) entryButton.setAttribute("aria-current", "true");
|
||||
return entryButton;
|
||||
});
|
||||
if (!entries.length) {
|
||||
const empty = button("Empty", () => {}, { wide: true });
|
||||
empty.disabled = true;
|
||||
|
||||
@@ -508,6 +508,37 @@ test("Headbutt shakes a tree and a sleeping Hoothoot falls out", async ({
|
||||
await expect(screen).toHaveAttribute("data-map", "24:3");
|
||||
});
|
||||
|
||||
test("Select moves an item within the pack", async ({ page }) => {
|
||||
await start(page, "candy");
|
||||
const choices = page.locator("#dialogue-choice"),
|
||||
pack = page.locator("#mart");
|
||||
await page.keyboard.press("Enter");
|
||||
await choices.getByRole("option", { name: /^PACK/ }).click();
|
||||
const entries = pack.locator("#mart-menu button");
|
||||
await expect(entries.first()).toContainText("POTION");
|
||||
// Select on the highlighted Potions, down to the Rare Candy, Select.
|
||||
await page.keyboard.press("ShiftLeft");
|
||||
await expect(page.locator("#mart-prompt")).toHaveText(
|
||||
"Where should this be moved to?",
|
||||
);
|
||||
await expect(entries.first()).toContainText("▷POTION");
|
||||
await page.keyboard.press("ArrowDown");
|
||||
await page.keyboard.press("ShiftLeft");
|
||||
await expect(page.locator("#mart-prompt")).toHaveText("");
|
||||
await expect(entries.nth(0)).toContainText("RARE CANDY");
|
||||
await expect(entries.nth(1)).toContainText("POTION");
|
||||
// B leaves the pack, then the start menu, which commits the change.
|
||||
await page.keyboard.press("KeyZ");
|
||||
await expect(pack).toBeHidden();
|
||||
await page.keyboard.press("KeyZ");
|
||||
await page.clock.runFor(500);
|
||||
await expect(page.locator("#native-dialogue")).toBeHidden();
|
||||
const { state } = await backup(page);
|
||||
expect(state.bag.items.map((slot: { item: number }) => slot.item)).toEqual([
|
||||
0x20, 18,
|
||||
]);
|
||||
});
|
||||
|
||||
test("the Old Rod hooks a Magikarp from New Bark Town's shore", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ const NEW_BARK_TOWN = "24:4",
|
||||
PIKACHU = 25,
|
||||
RAICHU = 26;
|
||||
const BICYCLE = 7,
|
||||
COIN_CASE = 0x36,
|
||||
ANTIDOTE = 9,
|
||||
FULL_RESTORE = 0x0e,
|
||||
POTION = 0x12,
|
||||
@@ -313,6 +314,142 @@ describe.skipIf(!existsSync(romPath))("the start menu", () => {
|
||||
expect(world.state.party[0].pp).toEqual([20, 30, 30, 0]);
|
||||
});
|
||||
|
||||
it("shows the other party members' moves with left and right, skipping Eggs", () => {
|
||||
const world = fixture();
|
||||
const party = world.state.party;
|
||||
party.push({ ...structuredClone(party[1]), species: 0xfd }); // EGG
|
||||
open(world);
|
||||
pick(world, "POKéMON");
|
||||
pick(world, "CYNDAQUIL");
|
||||
expect(pick(world, "MOVE")).toEqual(["CYNDAQUIL :L8"]);
|
||||
const heading = (direction: "left" | "right") => {
|
||||
world.moveCursor(direction);
|
||||
return until(world)[0];
|
||||
};
|
||||
expect(heading("right")).toBe("PIDGEY :L4");
|
||||
expect(menuItems(world)).toEqual(["TACKLE"]);
|
||||
// Past the last Pokémon that is not an Egg, .cycle_right comes back.
|
||||
expect(heading("right")).toBe("PIDGEY :L4");
|
||||
expect(heading("left")).toBe("CYNDAQUIL :L8");
|
||||
expect(heading("left")).toBe("CYNDAQUIL :L8");
|
||||
// Not while a move is being moved.
|
||||
pick(world, "LEER");
|
||||
world.moveCursor("right");
|
||||
expect(world.dialogue?.pages[0]).toBe("Where?");
|
||||
world.advanceDialogue();
|
||||
until(world);
|
||||
expect(heading("right")).toBe("PIDGEY :L4");
|
||||
// B returns to the party menu, on the Pokémon shown last.
|
||||
world.advanceDialogue();
|
||||
until(world);
|
||||
const menu = world.dialogue?.choice;
|
||||
expect(menu?.kind === "menu" && menu.items[menu.selected]).toBe("PIDGEY");
|
||||
close(world);
|
||||
});
|
||||
|
||||
it("moves items within a pocket with Select, merging stacks of the same item", () => {
|
||||
const world = fixture();
|
||||
world.state.bag.items = [
|
||||
{ item: POTION, quantity: 5 },
|
||||
{ item: ANTIDOTE, quantity: 1 },
|
||||
{ item: ETHER, quantity: 1 },
|
||||
{ item: POTION, quantity: 98 },
|
||||
];
|
||||
world.state.bag.keyItems = [BICYCLE, COIN_CASE];
|
||||
open(world);
|
||||
pick(world, "PACK");
|
||||
const pack = () => {
|
||||
const choice = world.dialogue?.choice;
|
||||
if (choice?.kind !== "pack") throw new Error(`Expected the pack, got ${choice?.kind}`);
|
||||
return choice;
|
||||
};
|
||||
const items = () => world.state.bag.items.map((slot) => [slot.item, slot.quantity]);
|
||||
// Select marks the Potions and asks where; the pocket stays put.
|
||||
world.useSelect();
|
||||
until(world);
|
||||
expect([pack().switching, pack().prompt]).toEqual([
|
||||
0,
|
||||
"Where should this\nbe moved to?",
|
||||
]);
|
||||
world.moveCursor("right");
|
||||
expect(pack().pocket).toBe(0);
|
||||
// A (or Select) on another row moves them there.
|
||||
world.choosePackEntry(0, 2);
|
||||
until(world);
|
||||
expect(pack().switching).toBeUndefined();
|
||||
expect(pack().selected).toBe(2);
|
||||
// Nothing commits until the menu closes.
|
||||
expect(world.save().bag.items[0].item).toBe(POTION);
|
||||
world.useSelect();
|
||||
until(world);
|
||||
world.choosePackEntry(0, 3, true);
|
||||
until(world);
|
||||
// 5 + 98: the destination fills to 99 and 4 stay behind.
|
||||
world.useSelect();
|
||||
until(world);
|
||||
world.advanceDialogue(); // B ends the move, still in the pack
|
||||
until(world);
|
||||
expect([pack().kind, pack().switching]).toEqual(["pack", undefined]);
|
||||
world.choosePackEntry(0, 1, true);
|
||||
until(world);
|
||||
world.choosePackEntry(0, 1, true); // onto itself: nothing moves
|
||||
until(world);
|
||||
world.moveCursor("right");
|
||||
world.moveCursor("right");
|
||||
world.choosePackEntry(2, 1, true);
|
||||
until(world);
|
||||
world.choosePackEntry(2, 0, true);
|
||||
until(world);
|
||||
close(world);
|
||||
expect(items()).toEqual([
|
||||
[ANTIDOTE, 1],
|
||||
[ETHER, 1],
|
||||
[POTION, 4],
|
||||
[POTION, 99],
|
||||
]);
|
||||
expect(world.state.bag.keyItems).toEqual([COIN_CASE, BICYCLE]);
|
||||
|
||||
// Two stacks that fit merge into one; a full stack never merges.
|
||||
world.state.bag.items = [
|
||||
{ item: POTION, quantity: 2 },
|
||||
{ item: POTION, quantity: 3 },
|
||||
{ item: ETHER, quantity: 99 },
|
||||
{ item: ETHER, quantity: 1 },
|
||||
];
|
||||
open(world);
|
||||
pick(world, "PACK");
|
||||
world.choosePackEntry(0, 0, true);
|
||||
until(world);
|
||||
world.choosePackEntry(0, 1, true);
|
||||
until(world);
|
||||
world.choosePackEntry(0, 2, true);
|
||||
until(world);
|
||||
world.choosePackEntry(0, 1, true);
|
||||
until(world);
|
||||
close(world);
|
||||
expect(items()).toEqual([
|
||||
[POTION, 5],
|
||||
[ETHER, 1],
|
||||
[ETHER, 99],
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not move items in the pack that GIVE opens", () => {
|
||||
const world = fixture();
|
||||
open(world);
|
||||
pick(world, "POKéMON");
|
||||
pick(world, "CYNDAQUIL");
|
||||
pick(world, "ITEM");
|
||||
pick(world, "GIVE");
|
||||
world.useSelect();
|
||||
until(world);
|
||||
const choice = world.dialogue?.choice;
|
||||
expect(choice?.kind).toBe("pack");
|
||||
expect(choice?.kind === "pack" && choice.switching).toBeUndefined();
|
||||
close(world);
|
||||
expect(world.state.bag.items.map((slot) => slot.item)).toEqual([POTION, ANTIDOTE]);
|
||||
});
|
||||
|
||||
it("gives, swaps and takes held items", () => {
|
||||
const world = fixture();
|
||||
world.state.bag.items.push({ item: LEFTOVERS, quantity: 1 });
|
||||
|
||||
Reference in new issue
Block a user