Port field moves from the menu, SEL with Select, and moving Pokémon in Bill's PC
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:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-30 17:22:56 +02:00
1 parent 45275ba83b
commit 3456aa4e11
25 files changed
+1129 -186

No files matched your search

+163
View File
@@ -419,6 +419,169 @@ describe.skipIf(!existsSync(romPath))("field moves with the local ROM", () => {
expect(used).toContain("used\nWHIRLPOOL!");
expect(world.map.blocks).not.toEqual(before);
});
/** Start → POKéMON → the first Pokémon → `move`; returns the pages
* shown until the menu closed or a choice (the party menu) returned. */
const useFromMenu = (world: GameWorld, move: string) => {
expect(world.openStartMenu()).toBe(true);
const choose = (label: string) => {
const choice = world.dialogue?.choice;
if (choice?.kind !== "menu") throw new Error(`No menu for ${label}`);
const index = choice.items.findIndex((item) => item.startsWith(label));
expect(index, `${label} in ${choice.items}`).toBeGreaterThanOrEqual(0);
world.choose(index);
};
choose("POKéMON");
choose("SWIMMY");
choose(move);
const pages: string[] = [];
for (let i = 0; i < 50 && world.dialogue && !world.dialogue.choice; i++) {
pages.push(world.dialogue.pages[world.dialogue.index]);
world.advanceDialogue();
}
return pages;
};
/** B out of every menu, then let any queued script finish. */
const closeMenus = (world: GameWorld) => {
for (let i = 0; i < 10 && world.dialogue?.choice; i++) world.advanceDialogue();
return settle(world).join("\n");
};
it("surfs from the party menu, after the badge and facing checks", () => {
const { assets, world } = fixture([]);
world.state.party = partyWith(assets, [0x0f, 0x39]); // CUT, SURF
world.state.scenes["24:4"] = 1;
world.restore({ ...world.save(), map: "24:4", x: 17, y: 7, direction: "right", objects: [] });
expect(world.dialogue).toBeUndefined();
// CUT and SURF come first in the submenu.
expect(useFromMenu(world, "SURF")).toEqual(["Sorry! A new BADGE\nis required."]);
closeMenus(world);
world.state.flags.add(30); // ENGINE_FOGBADGE
world.restore({ ...world.save(), direction: "up" });
expect(useFromMenu(world, "SURF")).toEqual(["You can't SURF\nhere."]);
closeMenus(world);
world.restore({ ...world.save(), direction: "right" });
const shown = [...useFromMenu(world, "SURF"), closeMenus(world)].join("\n");
expect(world.notice, world.diagnostic).toBe("");
// No "Want to SURF?" question from the menu.
expect(shown).not.toContain("?");
expect(shown).toContain("SWIMMY used\nSURF!");
expect([world.x, world.y, world.state.playerState]).toEqual([18, 7, 4]);
expect(useFromMenu(world, "SURF")).toEqual(["You're already\nSURFING."]);
closeMenus(world);
});
it("cuts from the party menu, or finds nothing to cut", () => {
const { assets, world } = fixture([]);
const spot = find(assets, (map, x, y) =>
[1, 2, 3, 12, 25].includes(map.tileset) &&
assets.collision(map, x, y) === 0x12 &&
assets.permission(assets.collision(map, x, y + 1)) === 0 &&
!map.objects.some((o) => o.x === x && o.y === y + 1) &&
map.callbacks.length === 0,
);
const ported = assets.isPorted.bind(assets);
assets.isPorted = (id) => ported(id) || id === spot.map;
world.state.party = partyWith(assets, [0x0f]);
world.state.flags.add(28); // ENGINE_HIVEBADGE
world.warpTo(spot.map, spot.x, spot.y + 1);
world.restore({ ...world.save(), direction: "down" });
expect(useFromMenu(world, "CUT")).toEqual(["There's nothing to\nCUT here."]);
closeMenus(world);
world.restore({ ...world.save(), direction: "up" });
const before = world.map.blocks.slice();
const shown = [...useFromMenu(world, "CUT"), closeMenus(world)].join("\n");
expect(world.notice, world.diagnostic).toBe("");
expect(shown).toContain("SWIMMY used\nCUT!");
expect(world.map.blocks).not.toEqual(before);
});
it("uses Strength from the party menu", () => {
const { assets, world } = fixture([]);
world.state.party = partyWith(assets, [0x46]);
world.state.flags.add(29); // ENGINE_PLAINBADGE
world.state.scenes["24:4"] = 1;
world.restore({ ...world.save(), map: "24:4", x: 13, y: 7, direction: "down", objects: [] });
const shown = [...useFromMenu(world, "STRENGTH"), closeMenus(world)].join("\n");
expect(world.notice, world.diagnostic).toBe("");
expect(shown).toContain("SWIMMY used\nSTRENGTH!");
expect(world.state.flags.has(24)).toBe(true); // ENGINE_STRENGTH_ACTIVE
});
it("lights a dark cave with Flash, which fails elsewhere", () => {
// Dark Cave's Violet entrance (PALETTE_DARK).
const DARK_CAVE = "3:78";
const { assets, world } = fixture([DARK_CAVE]);
const map = assets.map(DARK_CAVE);
expect(map.paletteMode).toBe(4);
let spot = { map: DARK_CAVE, x: 0, y: 0 };
search: for (let y = 1; y < map.height - 1; y++)
for (let x = 1; x < map.width - 1; x++)
if (
assets.permission(assets.collision(map, x, y)) === 0 &&
!map.objects.some((o) => o.x === x && o.y === y)
) {
spot = { map: DARK_CAVE, x, y };
break search;
}
world.state.party = partyWith(assets, [0x94]); // FLASH
world.state.flags.add(27); // ENGINE_ZEPHYRBADGE
world.warpTo(spot.map, spot.x, spot.y);
expect(world.timeOfDay).toBe(3);
const shown = [...useFromMenu(world, "FLASH"), closeMenus(world)].join("\n");
expect(world.notice, world.diagnostic).toBe("");
expect(shown).toContain("A blinding FLASH\nlights the area!");
expect(world.state.flash).toBe(true);
expect(world.timeOfDay).toBe(2);
expect(useFromMenu(world, "FLASH")).toEqual(["Can't use that\nhere."]);
closeMenus(world);
});
it("recognises a smashable rock for Rock Smash, which is not ported further", () => {
// A rock (SPRITEMOVEDATA_SMASHABLE_ROCK) in Dark Cave at 27, 6.
const spot = { map: "3:78", x: 27, y: 6 };
const { assets, world } = fixture([spot.map]);
expect(
assets.map(spot.map).objects.some(
(o) => o.x === spot.x && o.y === spot.y && o.movement === 0x18,
),
).toBe(true);
world.state.party = partyWith(assets, [0xf9]); // ROCK_SMASH
world.warpTo(spot.map, spot.x, spot.y + 1);
world.restore({ ...world.save(), direction: "down" });
expect(useFromMenu(world, "ROCK SMASH")).toEqual(["Can't use that\nhere."]);
closeMenus(world);
world.restore({ ...world.save(), direction: "up" });
useFromMenu(world, "ROCK SMASH");
expect(world.notice).toBe("Smashing rocks is not ported yet.");
closeMenus(world);
expect(world.diagnostic).toBe("");
});
it("draws a wild Pokémon with Sweet Scent in grass, and nothing elsewhere", () => {
const { assets, world } = fixture([]);
world.state.party = partyWith(assets, [0xe6]); // SWEET_SCENT
world.state.scenes["24:4"] = 1;
world.restore({ ...world.save(), map: "24:4", x: 13, y: 7, direction: "down", objects: [] });
const nothing = [...useFromMenu(world, "SWEET SCENT"), closeMenus(world)].join("\n");
expect(world.notice, world.diagnostic).toBe("");
expect(nothing).toContain("SWIMMY used\nSWEET SCENT!");
expect(nothing).toContain("nothing");
const grass = find(assets, (map, x, y) =>
map.id === "24:3" &&
assets.grassCollisions().includes(assets.collision(map, x, y)) &&
!map.objects.some((o) => o.x === x && o.y === y),
);
world.warpTo(grass.map, grass.x, grass.y);
useFromMenu(world, "SWEET SCENT");
for (let i = 0; i < 10 && world.dialogue?.choice; i++) world.advanceDialogue();
for (let tick = 0; tick < 200 && !world.battle; tick++) {
if (world.dialogue) world.advanceDialogue();
world.tick();
}
expect(world.notice, world.diagnostic).toBe("");
expect(world.battle).toBeDefined();
});
});
describe.skipIf(!existsSync(romPath))("Strength boulders and stone tables with the local ROM", () => {