Files
Crystal/tests/menu-driver.ts
T
Sébastien QUEROLandClaude Opus 5.5 4cb4676c7d Port Repels and the other non-fishing field items
Repel, Super Repel and Max Repel count down 100, 200 or 250 steps where
CountStep does, keep weaker wild Pokémon away from the first party member
that has not fainted, and print "REPEL's effect wore off." on the last
step. The Coin Case and the Blue Card show their balances, Sacred Ash
revives the party once one Pokémon has fainted, and the Normal and
Gorgeous Boxes send their trophy home.

The Itemfinder looks for unfound hidden items on the original's screen
around the player. The Card Key, the Basement Key and the Squirtbottle
run the original's checks for their doors and Sudowoodo, whose maps are
not open yet. Saves gain the Repel's steps, the coins and the Blue Card's
points.

Part of #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:32:33 +02:00

80 lines
2.6 KiB
TypeScript

import { expect } from "vitest";
import type { GameWorld } from "../src/game/world/world";
import { settle } from "./world-driver";
/** Acknowledge pages up to the next choice; returns the pages seen. */
export 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.");
}
export 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). */
export 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. */
export 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.`);
}
export function open(world: GameWorld) {
expect(world.openStartMenu()).toBe(true);
return until(world);
}
/** PACK, the item, then USE: the pages shown up to the next choice. */
export function useItem(world: GameWorld, item: number) {
open(world);
pick(world, "PACK");
pickItem(world, item);
return pick(world, "USE");
}
/** B until every menu has closed; returns the pages shown meanwhile. */
export function close(world: GameWorld) {
const pages: string[] = [];
for (let i = 0; i < 20 && world.busy; i++) {
if (world.dialogue?.choice) world.advanceDialogue();
pages.push(...until(world));
}
pages.push(...settle(world));
expect(world.diagnostic).toBe("");
return pages;
}