Port the start menu, the party menu and the pack outside battle
Checks / web (push) Canceled after 0s

Start opens the original start menu. POKéMON shows the party with the
Pokémon submenu (stats, switching, move order, held items); PACK shows the
four pockets with the field item effects for healing, PP and the Bicycle,
plus giving and tossing. The menu runs on a script draft, and its cursors
persist until a battle clears them. Select keeps the Key Items shortcut.

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 16:10:22 +02:00
1 parent 60e06ae678
commit 1af2beacdc
18 files changed
+1918 -49

No files matched your search

+59 -4
View File
@@ -43,11 +43,11 @@ async function backup(page: Page) {
state: JSON.parse(Buffer.from(record.state, "base64").toString("utf8")),
};
}
test("Start opens Key Items, freezes movement, and uses the Bicycle with keyboard and touch", async ({
test("Select opens Key Items, freezes movement, and uses the Bicycle with keyboard and touch", async ({
page,
}) => {
await start(page);
await page.keyboard.press("Enter");
await page.keyboard.press("ShiftLeft");
await expect(page.locator("#field-menu")).toBeVisible();
await page.keyboard.down("ArrowDown");
await page.clock.runFor(1000);
@@ -92,10 +92,10 @@ test("B cancels the pack and an indoor Bicycle attempt leaves the player walking
page,
}) => {
await start(page, true);
await page.keyboard.press("Enter");
await page.keyboard.press("ShiftLeft");
await page.keyboard.press("KeyZ");
await expect(page.locator("#field-menu")).toBeHidden();
await page.keyboard.press("Enter");
await page.keyboard.press("ShiftLeft");
await page
.locator("#field-items")
.getByRole("button", { name: "BICYCLE", exact: true })
@@ -184,3 +184,58 @@ test("the Pokémon Center PC deposits a Pokémon and an item", async ({
expect(state.pcItems).toEqual([{ item: 18, quantity: 2 }]);
expect(state.bag.items).toEqual([{ item: 18, quantity: 3 }]);
});
test("Start opens the start menu: SWITCH reorders the party and the pack tosses an item", async ({
page,
}) => {
await start(page, "center");
const text = page.locator("#dialogue-text"),
choices = page.locator("#dialogue-choice"),
pack = page.locator("#mart");
await page.keyboard.press("Enter");
await expect(choices.getByRole("option", { name: /^POKéMON/ })).toBeVisible();
await expect(choices.getByRole("option", { name: /^PACK/ })).toBeVisible();
// Movement is frozen while the menu is open.
await page.keyboard.down("ArrowLeft");
await page.clock.runFor(500);
await page.keyboard.up("ArrowLeft");
await expect(page.locator("#game-screen")).toHaveAttribute("data-x", "9");
await choices.getByRole("option", { name: /^POKéMON/ }).click();
await expect(text).toHaveText("Choose a POKéMON.");
await choices.getByRole("option", { name: /^CYNDAQUIL/ }).click();
await choices.getByRole("option", { name: "SWITCH", exact: true }).click();
await expect(text).toHaveText("Move to where?");
await choices.getByRole("option", { name: /^PIDGEY/ }).click();
await expect(choices.getByRole("option").first()).toHaveAccessibleName(
/^PIDGEY/,
);
// B leaves the party menu for the start menu.
await page.keyboard.press("KeyZ");
await choices.getByRole("option", { name: /^PACK/ }).click();
await expect(pack).toHaveAttribute("aria-label", "Pack");
await expect(
pack.getByRole("button", { name: "ITEM POCKET", exact: true }),
).toHaveAttribute("aria-pressed", "true");
// Right twice: the Balls pocket, then Key Items.
await page.keyboard.press("ArrowRight");
await page.keyboard.press("ArrowRight");
await expect(pack.getByRole("button", { name: "BICYCLE" })).toBeVisible();
await pack.getByRole("button", { name: "ITEM POCKET", exact: true }).click();
await pack.getByRole("button", { name: /POTION/ }).click();
await choices.getByRole("option", { name: "TOSS", exact: true }).click();
await expect(page.locator("#mart-prompt")).toContainText("Throw away how");
await pack.getByRole("button", { name: "TOSS", exact: true }).click();
await expect(text).toContainText("Throw away 1 POTION(S)?");
await choices.getByRole("button", { name: /YES/ }).click();
await expect(text).toContainText("Threw away");
await page.keyboard.press("KeyX");
await pack.getByRole("button", { name: "BACK", exact: true }).click();
await choices.getByRole("option", { name: /^EXIT/ }).click();
await page.clock.runFor(500);
await expect(page.locator("#game-notice")).toHaveText("");
const { state } = await backup(page);
expect(state.party.map((mon: { species: number }) => mon.species)).toEqual([
16, 155,
]);
expect(state.bag.items).toEqual([{ item: 18, quantity: 4 }]);
});
+557
View File
@@ -0,0 +1,557 @@
import { describe, expect, it } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { CrystalAssets } from "../src/game/rom/assets";
import { GameWorld } from "../src/game/world/world";
import { generatePartyMon, maxPP } from "../src/game/party/pokemon";
import { ENGINE_FLAGS } from "../src/game/rom/profile";
import { settle } from "./world-driver";
const romPath =
process.env.CRYSTAL_ROM_PATH ??
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
const NEW_BARK_TOWN = "24:4",
PLAYERS_HOUSE = "24:6",
CYNDAQUIL = 155,
PIDGEY = 16;
const BICYCLE = 7,
ANTIDOTE = 9,
FULL_RESTORE = 0x0e,
POTION = 0x12,
RARE_CANDY = 0x20,
REVIVE = 0x27,
PP_UP = 0x3e,
ETHER = 0x3f,
ELIXER = 0x41,
ENERGYPOWDER = 0x79,
LEFTOVERS = 0x92,
FLOWER_MAIL = 0x9e,
BERRY = 0xad;
const CUT = 0x0f,
TACKLE = 0x21,
LEER = 0x2b,
SMOKESCREEN = 0x6c;
const POISONED = 0x08;
describe.skipIf(!existsSync(romPath))("the start menu", () => {
const assets = () => new CrystalAssets(new Uint8Array(readFileSync(romPath)));
/** Standing in New Bark Town (outdoors) with two Pokémon and a pack. */
function fixture(map = NEW_BARK_TOWN, x = 13, y = 7) {
const world = new GameWorld(
assets(),
() => new Date("2026-09-25T12:00:00"),
() => 0x42,
);
const data = world.assets.pokemon;
const mon = (species: number, level: number) =>
generatePartyMon(data, {
species,
level,
item: 0,
otId: world.playerId,
ot: "CHRIS",
dvs: 0x9abc,
caughtData: 0,
});
world.state.party = [mon(CYNDAQUIL, 8), mon(PIDGEY, 4)];
world.state.bag.items = [
{ item: POTION, quantity: 5 },
{ item: ANTIDOTE, quantity: 1 },
];
world.state.bag.keyItems = [BICYCLE];
world.state.scenes[NEW_BARK_TOWN] = 1;
world.restore({
...world.save(),
map,
x,
y,
direction: "down",
objects: [],
});
return world;
}
/** Acknowledge pages up to the next choice; returns the pages seen. */
function until(world: GameWorld) {
const pages: string[] = [];
for (let tick = 0; tick < 500; tick++) {
const dialogue = world.dialogue;
if (!dialogue) {
if (!world.busy) return pages;
world.tick();
continue;
}
const page = dialogue.pages[dialogue.index].replace(/\n/g, " ");
if (page && pages.at(-1) !== page) pages.push(page);
const last = dialogue.index === dialogue.pages.length - 1;
if (last && dialogue.choice) return pages;
if (last && dialogue.holdTicks !== undefined) world.tick();
else world.advanceDialogue();
}
throw new Error("No choice appeared.");
}
function menuItems(world: GameWorld) {
const choice = world.dialogue?.choice;
return choice?.kind === "menu" ? choice.items : [];
}
/** Choose the menu item matching `label` (the whole label, or a regex). */
function pick(world: GameWorld, label: string | RegExp) {
const items = menuItems(world);
const index = items.findIndex((item) =>
typeof label === "string" ? item === label : label.test(item),
);
expect(index, `${label} in ${items.join(", ")}`).toBeGreaterThanOrEqual(0);
world.choose(index);
return until(world);
}
/** Choose the pack entry for `item` in its pocket. */
function pickItem(world: GameWorld, item: number) {
const choice = world.dialogue?.choice;
if (choice?.kind !== "pack") throw new Error(`Expected the pack, got ${choice?.kind}`);
for (const [pocket, { entries }] of choice.pockets.entries()) {
const index = entries.findIndex((entry) => entry.item === item);
if (index >= 0) {
world.choosePackEntry(pocket, index);
return until(world);
}
}
throw new Error(`Item ${item} is not in the pack.`);
}
function open(world: GameWorld) {
expect(world.openStartMenu()).toBe(true);
return until(world);
}
/** B until every menu has closed. */
function close(world: GameWorld) {
for (let i = 0; i < 20 && world.busy; i++) {
if (world.dialogue?.choice) world.advanceDialogue();
until(world);
}
settle(world);
expect(world.diagnostic).toBe("");
}
it("lists the original items for the player's progress", () => {
const world = fixture();
open(world);
expect(menuItems(world)).toEqual([
"POKéMON",
"PACK",
"CHRIS",
"SAVE",
"OPTION",
"EXIT",
]);
const choice = world.dialogue?.choice;
expect(choice?.kind === "menu" && choice.details?.[1]).toBe("Contains items");
pick(world, "EXIT");
expect(world.busy).toBe(false);
world.state.flags.add(ENGINE_FLAGS.pokedex);
world.state.flags.add(ENGINE_FLAGS.pokegear);
open(world);
expect(menuItems(world)).toEqual([
"POKéDEX",
"POKéMON",
"PACK",
"POKéGEAR",
"CHRIS",
"SAVE",
"OPTION",
"EXIT",
]);
// The cursor stays on the last choice.
pick(world, "POKéGEAR");
expect(world.notice).toBe("POKéGEAR is not ported yet.");
close(world);
open(world);
const reopened = world.dialogue?.choice;
expect(reopened?.kind === "menu" && reopened.items[reopened.selected]).toBe(
"POKéGEAR",
);
close(world);
});
it("opens only while the player stands still", () => {
const world = fixture();
world.tick("left");
expect(world.activity).toBe("walking");
expect(world.openStartMenu()).toBe(false);
});
it("shows the Pokémon submenu with field moves first", () => {
const world = fixture();
world.state.party[1].moves = [TACKLE, CUT, 0, 0];
open(world);
expect(pick(world, "POKéMON")).toEqual(["Choose a POKéMON."]);
expect(menuItems(world)).toEqual(["CYNDAQUIL", "PIDGEY"]);
pick(world, "CYNDAQUIL");
expect(menuItems(world)).toEqual(["STATS", "SWITCH", "MOVE", "ITEM", "CANCEL"]);
pick(world, "CANCEL");
pick(world, "PIDGEY");
expect(menuItems(world)).toEqual(["CUT", "STATS", "SWITCH", "MOVE", "ITEM", "CANCEL"]);
pick(world, "CUT");
expect(world.notice).toBe("Using CUT from the menu is not ported yet.");
// STATS shows the stats screen, and B returns to the party.
pick(world, "PIDGEY");
world.choose(menuItems(world).indexOf("STATS"));
expect(world.statsScreen?.species).toBe(PIDGEY);
world.advanceDialogue();
expect(until(world)).toEqual(["Choose a POKéMON."]);
expect(world.statsScreen).toBeUndefined();
close(world);
});
it("switches two Pokémon, and B cancels the move", () => {
const world = fixture();
open(world);
pick(world, "POKéMON");
pick(world, "CYNDAQUIL");
expect(pick(world, "SWITCH")).toEqual(["Move to where?"]);
expect(menuItems(world)).toEqual(["▷CYNDAQUIL", "PIDGEY"]);
world.advanceDialogue();
until(world);
expect(menuItems(world)).toEqual(["CYNDAQUIL", "PIDGEY"]);
pick(world, "CYNDAQUIL");
pick(world, "SWITCH");
pick(world, "PIDGEY");
expect(menuItems(world)).toEqual(["PIDGEY", "CYNDAQUIL"]);
// Nothing commits until the menu closes.
expect(world.save().party?.[0].species).toBe(CYNDAQUIL);
close(world);
expect(world.state.party.map((mon) => mon.species)).toEqual([PIDGEY, CYNDAQUIL]);
// wPartyMenuCursor stays on the row chosen last, in the next menu too.
open(world);
pick(world, "POKéMON");
const party = world.dialogue?.choice;
expect(party?.kind === "menu" && party.selected).toBe(1);
close(world);
});
it("reorders moves with their PP", () => {
const world = fixture();
const mon = world.state.party[0];
expect(mon.moves).toEqual([TACKLE, LEER, SMOKESCREEN, 0]);
mon.pp[0] = 30;
open(world);
pick(world, "POKéMON");
pick(world, "CYNDAQUIL");
expect(pick(world, "MOVE")).toEqual(["CYNDAQUIL :L8"]);
expect(menuItems(world)).toEqual(["TACKLE", "LEER", "SMOKESCREEN"]);
expect(pick(world, "TACKLE")).toEqual(["Where?"]);
// B while choosing where cancels only the move being moved.
world.advanceDialogue();
until(world);
pick(world, "TACKLE");
pick(world, "SMOKESCREEN");
expect(menuItems(world)).toEqual(["SMOKESCREEN", "LEER", "TACKLE"]);
close(world);
expect(world.state.party[0].moves).toEqual([SMOKESCREEN, LEER, TACKLE, 0]);
expect(world.state.party[0].pp).toEqual([20, 30, 30, 0]);
});
it("gives, swaps and takes held items", () => {
const world = fixture();
world.state.bag.items.push({ item: LEFTOVERS, quantity: 1 });
open(world);
pick(world, "POKéMON");
pick(world, "CYNDAQUIL");
pick(world, "ITEM");
expect(menuItems(world)).toEqual(["GIVE", "TAKE"]);
expect(pick(world, "TAKE")).toEqual([
"CYNDAQUIL isn't holding anything.",
"Choose a POKéMON.",
]);
pick(world, "CYNDAQUIL");
pick(world, "ITEM");
pick(world, "GIVE");
// Key items cannot be held; the pack stays open.
expect(pickItem(world, BICYCLE)).toEqual(["This item can't be held."]);
expect(pickItem(world, POTION)).toEqual([
"Made CYNDAQUIL hold POTION.",
"Choose a POKéMON.",
]);
pick(world, "CYNDAQUIL");
pick(world, "ITEM");
pick(world, "GIVE");
expect(pickItem(world, LEFTOVERS)).toEqual([
"CYNDAQUIL is already holding",
"POTION. Switch items?",
]);
world.choose(1);
expect(until(world)).toEqual([
"Took CYNDAQUIL's POTION and",
"made it hold LEFTOVERS.",
"Choose a POKéMON.",
]);
close(world);
expect(world.state.party[0].item).toBe(LEFTOVERS);
expect(world.state.bag.items).toEqual([
{ item: POTION, quantity: 5 },
{ item: ANTIDOTE, quantity: 1 },
]);
open(world);
pick(world, "POKéMON");
pick(world, "CYNDAQUIL");
pick(world, "ITEM");
expect(pick(world, "TAKE")[0]).toBe("Took LEFTOVERS from CYNDAQUIL.");
close(world);
expect(world.state.party[0].item).toBe(0);
expect(world.state.bag.items.at(-1)).toEqual({ item: LEFTOVERS, quantity: 1 });
});
it("keeps a held item when the pack is full", () => {
const world = fixture();
world.state.party[0].item = LEFTOVERS;
world.state.bag.items = Array.from({ length: 20 }, (_, i) => ({
item: [POTION, ANTIDOTE, BERRY][i % 3],
quantity: 99,
}));
open(world);
pick(world, "POKéMON");
pick(world, "CYNDAQUIL");
pick(world, "ITEM");
expect(pick(world, "TAKE")[0]).toBe("Item storage space full.");
close(world);
expect(world.state.party[0].item).toBe(LEFTOVERS);
});
it("refuses to compose MAIL without changing anything", () => {
const world = fixture();
world.state.bag.items.push({ item: FLOWER_MAIL, quantity: 1 });
open(world);
pick(world, "PACK");
pickItem(world, FLOWER_MAIL);
expect(menuItems(world)).toEqual(["GIVE", "TOSS", "QUIT"]);
expect(pick(world, "GIVE")).toEqual(["To which POKéMON?"]);
pick(world, "CYNDAQUIL");
expect(world.notice).toBe("Writing MAIL is not ported yet.");
close(world);
expect(world.state.party[0].item).toBe(0);
expect(world.state.bag.items.at(-1)).toEqual({ item: FLOWER_MAIL, quantity: 1 });
});
it("uses healing items from the pack on the party", () => {
const world = fixture();
const [cyndaquil, pidgey] = world.state.party;
cyndaquil.hp = 5;
pidgey.status = POISONED;
world.state.bag.items.push(
{ item: REVIVE, quantity: 1 },
{ item: FULL_RESTORE, quantity: 1 },
);
open(world);
pick(world, "PACK");
// The pack opens on the Items pocket, with the ROM's pocket names.
const pack = world.dialogue?.choice;
expect(
pack?.kind === "pack" && [pack.pocket, pack.pockets.map((pocket) => pocket.name)],
).toEqual([0, ["ITEM POCKET", "BALL POCKET", "KEY POCKET", "TM POCKET"]]);
pickItem(world, POTION);
expect(menuItems(world)).toEqual(["USE", "GIVE", "TOSS", "QUIT"]);
expect(pick(world, "USE")).toEqual(["Use on which POKéMON?"]);
// Full HP: no effect, the Potion stays and the pack is shown again.
expect(pick(world, "PIDGEY")).toEqual(["It won't have any effect."]);
expect(world.dialogue?.choice?.kind).toBe("pack");
pickItem(world, POTION);
pick(world, "USE");
expect(pick(world, "CYNDAQUIL")).toEqual(["CYNDAQUIL recovered 20HP!"]);
pickItem(world, ANTIDOTE);
pick(world, "USE");
expect(pick(world, "PIDGEY")).toEqual(["PIDGEY's cured of poison."]);
// A Revive on a Pokémon that has not fainted has no effect.
pickItem(world, REVIVE);
pick(world, "USE");
expect(pick(world, "PIDGEY")).toEqual(["It won't have any effect."]);
close(world);
const [healed, cured] = world.state.party;
expect(healed.hp).toBe(25);
expect(cured.status).toBe(0);
expect(world.state.bag.items).toEqual([
{ item: POTION, quantity: 4 },
{ item: REVIVE, quantity: 1 },
{ item: FULL_RESTORE, quantity: 1 },
]);
world.state.party[1].hp = 0;
world.state.party[0].hp = 1;
world.state.party[0].status = POISONED;
open(world);
pick(world, "PACK");
pickItem(world, REVIVE);
pick(world, "USE");
expect(pick(world, "PIDGEY")).toEqual(["PIDGEY is revitalized."]);
pickItem(world, FULL_RESTORE);
pick(world, "USE");
const max = world.state.party[0].stats[0];
expect(pick(world, "CYNDAQUIL")).toEqual([`CYNDAQUIL recovered ${max - 1}HP!`]);
close(world);
expect(world.state.party[1].hp).toBe(world.state.party[1].stats[0] >> 1);
expect(world.state.party[0].hp).toBe(max);
expect(world.state.party[0].status).toBe(0);
expect(world.state.bag.items).toEqual([{ item: POTION, quantity: 4 }]);
});
it("makes bitter herbs cost happiness", () => {
const world = fixture();
world.state.party[0].hp = 1;
world.state.bag.items.push({ item: ENERGYPOWDER, quantity: 1 });
const happiness = world.state.party[0].happiness;
open(world);
pick(world, "PACK");
pickItem(world, ENERGYPOWDER);
pick(world, "USE");
expect(pick(world, "CYNDAQUIL")).toEqual([
"CYNDAQUIL recovered 24HP!",
"It looks bitter…",
]);
close(world);
expect(world.state.party[0].happiness).toBe(happiness - 5);
});
it("restores and raises PP", () => {
const world = fixture();
world.state.party[0].pp = [10, 30, 3, 0];
world.state.bag.items.push(
{ item: ETHER, quantity: 1 },
{ item: ELIXER, quantity: 1 },
{ item: PP_UP, quantity: 2 },
);
open(world);
pick(world, "PACK");
pickItem(world, ETHER);
pick(world, "USE");
expect(pick(world, "CYNDAQUIL")).toEqual(["Restore the PP of which move?"]);
expect(menuItems(world)).toEqual(["TACKLE", "LEER", "SMOKESCREEN"]);
// B returns to the party menu.
world.advanceDialogue();
expect(until(world)).toEqual(["Use on which POKéMON?"]);
pick(world, "CYNDAQUIL");
// Full PP: no effect.
expect(pick(world, "LEER")).toEqual(["It won't have any effect."]);
pickItem(world, ETHER);
pick(world, "USE");
pick(world, "CYNDAQUIL");
expect(pick(world, "TACKLE")).toEqual(["PP was restored."]);
pickItem(world, ELIXER);
pick(world, "USE");
expect(pick(world, "CYNDAQUIL")).toEqual(["PP was restored."]);
pickItem(world, PP_UP);
pick(world, "USE");
expect(pick(world, "CYNDAQUIL")).toEqual(["Raise the PP of which move?"]);
expect(pick(world, "TACKLE")).toEqual(["TACKLE's PP increased."]);
close(world);
// Tackle: 10, +10 (Ether), +10 (Elixer), then a PP Up adds a fifth of
// the base PP (7 of 35) to both the current PP and the maximum.
const pp = world.state.party[0].pp;
expect(pp).toEqual([0x40 | 37, 30, 13, 0]);
expect(maxPP(world.assets.pokemon, TACKLE, 1) & 63).toBe(42);
const items = world.state.bag.items;
expect(items.find((slot) => slot.item === PP_UP)?.quantity).toBe(1);
expect(items.some((slot) => slot.item === ETHER || slot.item === ELIXER)).toBe(false);
});
it("refuses a PP Up on a move that has had three", () => {
const world = fixture();
world.state.party[0].pp[1] = 0xc0 | 30;
world.state.bag.items.push({ item: PP_UP, quantity: 1 });
open(world);
pick(world, "PACK");
pickItem(world, PP_UP);
pick(world, "USE");
pick(world, "CYNDAQUIL");
// PPIsMaxedOutText, then the move list again.
expect(pick(world, "LEER")).toEqual([
"LEER's PP is maxed out.",
"Raise the PP of which move?",
]);
close(world);
expect(world.state.party[0].pp[1]).toBe(0xc0 | 30);
expect(world.state.bag.items.at(-1)).toEqual({ item: PP_UP, quantity: 1 });
});
it("tosses items after confirming the quantity", () => {
const world = fixture();
open(world);
pick(world, "PACK");
pickItem(world, POTION);
pick(world, "TOSS");
const choice = world.dialogue?.choice;
expect(choice?.kind === "martquantity" && [choice.prompt, choice.max]).toEqual([
"Throw away how\nmany?",
5,
]);
world.choose(3);
expect(until(world)).toEqual(["Throw away 3 POTION(S)?"]);
world.choose(0);
until(world);
pickItem(world, POTION);
pick(world, "TOSS");
world.choose(3);
until(world);
world.choose(1);
expect(until(world)).toEqual(["Threw away POTION(S)."]);
close(world);
expect(world.state.bag.items[0]).toEqual({ item: POTION, quantity: 2 });
});
it("gets on the Bicycle from the Key pocket, or gives Oak's warning indoors", () => {
const world = fixture();
open(world);
pick(world, "PACK");
pickItem(world, BICYCLE);
expect(menuItems(world)).toEqual(["USE", "SEL", "QUIT"]);
world.choose(0);
expect(settle(world).join(" ")).toContain("got on the\nBICYCLE.");
expect(world.notice, world.diagnostic).toBe("");
expect(world.state.playerState).toBe(1);
const indoors = fixture(PLAYERS_HOUSE, 7, 4);
open(indoors);
pick(indoors, "PACK");
pickItem(indoors, BICYCLE);
expect(pick(indoors, "USE")).toEqual([
"OAK: CHRIS! This isn't the time to use that!",
]);
close(indoors);
expect(indoors.state.playerState).toBe(0);
});
it("offers no USE without a field menu, and a notice for unported effects", () => {
const world = fixture();
world.state.bag.items.push(
{ item: BERRY, quantity: 1 },
{ item: RARE_CANDY, quantity: 1 },
);
world.state.bag.balls = [{ item: 5, quantity: 3 }];
open(world);
pick(world, "PACK");
// Poké Balls have no field menu: GIVE and TOSS only.
pickItem(world, 5);
expect(menuItems(world)).toEqual(["GIVE", "TOSS", "QUIT"]);
pick(world, "QUIT");
pickItem(world, RARE_CANDY);
pick(world, "USE");
expect(world.notice).toBe("Using RARE CANDY is not ported yet.");
close(world);
expect(world.state.bag.items.find((slot) => slot.item === RARE_CANDY)).toBeTruthy();
});
it("saves the state from before the menu while it is open", () => {
const world = fixture();
world.state.party[0].hp = 5;
open(world);
pick(world, "PACK");
pickItem(world, POTION);
pick(world, "USE");
pick(world, "CYNDAQUIL");
const saved = world.save();
expect(saved.party?.[0].hp).toBe(5);
expect(saved.bag?.items[0].quantity).toBe(5);
const reloaded = fixture();
reloaded.restore(saved);
expect(reloaded.busy).toBe(false);
expect(reloaded.state.party[0].hp).toBe(5);
close(world);
expect(world.state.party[0].hp).toBe(25);
});
});