Enable Goldenrod City and its buildings, the department store with its elevator and rooftop, the Underground and its rooms, the Radio Tower floors and the Route 35 gate. Port what their scripts need: the herb shop, bargain shop, pharmacy and rooftop sale marts with ROM stock, prices and text; the Name Rater; the haircut brothers' happiness tables; the Lucky Number show over the party and boxes with its saved number and Friday countdown (RTC day timers with the 140-day wrap); coin and money balance boxes; Game Corner prize registration through NewPokedexEntry; gift Pokémon to the box when the party is full, with OT, nickname and MAIL (Kenya's 15-letter first line now stores). Saves gain hallOfFame, timers and luckyNumber, with defaults for older saves, and wUndergroundSwitchPositions joins the saved script bytes. The playthrough continues to the Bicycle, Whitney, the PLAINBADGE and the radio card. SlotMachine and CardFlip are still refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
564 lines
20 KiB
TypeScript
564 lines
20 KiB
TypeScript
import { beforeEach, describe, expect, it, onTestFailed } from "vitest";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { CrystalAssets } from "../src/game/rom/assets";
|
|
import { C } from "../src/game/battle/constants";
|
|
import { Playthrough } from "./playthrough-driver";
|
|
|
|
const romPath =
|
|
process.env.CRYSTAL_ROM_PATH ??
|
|
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
|
|
const NEW_BARK = "24:4",
|
|
ELMS_LAB = "24:5",
|
|
ROUTE_29 = "24:3",
|
|
CHERRYGROVE = "26:3",
|
|
ROUTE_30 = "26:1",
|
|
MR_POKEMONS_HOUSE = "26:10",
|
|
CHERRYGROVE_CENTER = "26:5",
|
|
ROUTE_31 = "26:2",
|
|
ROUTE_31_GATE = "26:11",
|
|
VIOLET = "10:5",
|
|
VIOLET_CENTER = "10:10",
|
|
VIOLET_GYM = "10:7",
|
|
SPROUT_1F = "3:1",
|
|
SPROUT_2F = "3:2",
|
|
SPROUT_3F = "3:3",
|
|
KYLES_HOUSE = "10:11";
|
|
const DARK_CAVE = "3:78",
|
|
EARLS_ACADEMY = "10:8";
|
|
const ROUTE_32 = "10:1",
|
|
ROUTE_32_CENTER = "10:13",
|
|
UNION_CAVE_1F = "3:37",
|
|
ROUTE_33 = "8:6",
|
|
AZALEA = "8:7",
|
|
AZALEA_CENTER = "8:1",
|
|
KURTS_HOUSE = "8:4",
|
|
AZALEA_GYM = "8:5",
|
|
SLOWPOKE_WELL_B1F = "3:40",
|
|
ILEX_GATE = "11:22",
|
|
ILEX_FOREST = "3:52",
|
|
ROUTE_34_GATE = "11:23",
|
|
ROUTE_34 = "11:1",
|
|
GOLDENROD = "11:2",
|
|
GOLDENROD_CENTER = "11:20",
|
|
GOLDENROD_GYM = "11:3",
|
|
BIKE_SHOP = "11:4",
|
|
RADIO_TOWER_1F = "3:17";
|
|
const MIRACLE_SEED = 0x75,
|
|
LURE_BALL = 0xa0,
|
|
HM01 = 0xf3,
|
|
CUT = 15,
|
|
BICYCLE = 7;
|
|
const EVENT_CLEARED_SLOWPOKE_WELL = 0x2b,
|
|
EVENT_BEAT_BUGSY = 0x4be,
|
|
EVENT_RIVAL_AZALEA_TOWN = 0x6bf,
|
|
EVENT_HERDED_FARFETCHD = 0x29,
|
|
ENGINE_HIVEBADGE = 0x1c,
|
|
ENGINE_PLAINBADGE = 0x1d,
|
|
ENGINE_RADIO_CARD = 0,
|
|
EVENT_BEAT_WHITNEY = 0x4bf;
|
|
const PHONE_BUG_CATCHER_WADE = 16;
|
|
const FLASH = 148,
|
|
HM05 = 0xf7;
|
|
const TOGEPI = 175,
|
|
BELLSPROUT = 69,
|
|
ONIX = 95;
|
|
const EVENT_TALKED_TO_MOM_AFTER_MYSTERY_EGG_QUEST = 0x40,
|
|
EVENT_GOT_HM05_FLASH = 0x14,
|
|
EVENT_BEAT_FALKNER = 0x4bd,
|
|
EVENT_GOT_TM31_MUD_SLAP = 0x8,
|
|
EVENT_GOT_TOGEPI_EGG_FROM_ELMS_AIDE = 0x2d,
|
|
ENGINE_ZEPHYRBADGE = 0x1b;
|
|
|
|
/** The acceptance run for PLAN section 2: a new game played from the
|
|
* introduction to the catching tutorial through the world's own inputs,
|
|
* without fixtures or story flags set by hand. Every stage ends with a
|
|
* save serialized to JSON and loaded into a fresh world. The stages share
|
|
* one world and run in order, so run the file rather than a single stage. */
|
|
describe.skipIf(!existsSync(romPath))(
|
|
"a continuous opening playthrough",
|
|
() => {
|
|
const assets = existsSync(romPath)
|
|
? new CrystalAssets(new Uint8Array(readFileSync(romPath)))
|
|
: undefined;
|
|
const p = new Playthrough(assets!);
|
|
const {
|
|
play,
|
|
walk,
|
|
cross,
|
|
enter,
|
|
exit,
|
|
reload,
|
|
lead,
|
|
writeTestSave,
|
|
heal,
|
|
train,
|
|
stairs,
|
|
route,
|
|
toChoice,
|
|
choose,
|
|
choosePack,
|
|
talkTo,
|
|
} = p;
|
|
/** From Route 31: through the gate to Violet's Pokémon Center and back. */
|
|
const restInViolet = () => {
|
|
exit(ROUTE_31_GATE, "left");
|
|
exit(VIOLET, "left");
|
|
heal(VIOLET_CENTER, VIOLET);
|
|
exit(ROUTE_31_GATE, "right");
|
|
exit(ROUTE_31, "right");
|
|
};
|
|
|
|
|
|
beforeEach(() => {
|
|
p.log.length = 0;
|
|
});
|
|
const traceFailures = () => onTestFailed(() => console.log(p.trace()));
|
|
|
|
it("plays the introduction and Mom's scene", () => {
|
|
p.start();
|
|
traceFailures();
|
|
p.world.beginIntro();
|
|
play([0, "", 1, "", 1, 0, "KAWA"]);
|
|
expect(p.world.intro).toBe(false);
|
|
expect(p.world.name).toBe("KAWA");
|
|
reload();
|
|
walk(7, 0);
|
|
expect(p.world.mapId).toBe("24:6");
|
|
walk(9, 3);
|
|
expect(p.world.move("down")).toBe(true);
|
|
play([1, 1, 1, 1, 1]);
|
|
expect(p.log).toContain("KAWA received\nPOKéGEAR.");
|
|
reload();
|
|
});
|
|
|
|
it("meets Elm, takes Cyndaquil and the aide's Potion", () => {
|
|
traceFailures();
|
|
walk(6, 7);
|
|
expect(p.world.move("down")).toBe(true);
|
|
play();
|
|
expect(p.world.mapId).toBe(NEW_BARK);
|
|
enter(ELMS_LAB, "up");
|
|
play([0, 1]);
|
|
walk(6, 4);
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
play([1, 1, ""]);
|
|
expect(p.world.state.party.map((mon) => mon.species)).toEqual([155]);
|
|
walk(4, 7);
|
|
expect(p.world.move("down")).toBe(true);
|
|
play();
|
|
expect(p.world.state.bag.items).toEqual([{ item: 18, quantity: 1 }]);
|
|
reload();
|
|
});
|
|
|
|
it("walks to Mr. Pokémon's house through Route 29, Cherrygrove and Route 30", () => {
|
|
traceFailures();
|
|
exit(NEW_BARK);
|
|
cross("left", ROUTE_29);
|
|
cross("left", CHERRYGROVE);
|
|
reload();
|
|
cross("up", ROUTE_30);
|
|
enter(MR_POKEMONS_HOUSE, "up");
|
|
play();
|
|
expect(p.world.state.bag.keyItems).toEqual([0x45]); // MYSTERY_EGG
|
|
expect(p.world.state.flags.has(11)).toBe(true); // the Pokédex
|
|
reload();
|
|
});
|
|
|
|
it("takes Elm's call outside and meets the rival in Cherrygrove", () => {
|
|
traceFailures();
|
|
exit(ROUTE_30);
|
|
expect(p.log.join("\n")).toContain("It's a disaster!");
|
|
expect(p.world.state.events.has(67)).toBe(true); // ELM_CALLED_ABOUT_STOLEN_POKEMON
|
|
reload();
|
|
cross("down", CHERRYGROVE);
|
|
// The rival's scene blocks the way east to Route 29.
|
|
cross("right", ROUTE_29);
|
|
const rival = p.log.join("\n");
|
|
expect(rival).toContain("RIVAL ?");
|
|
expect(rival).toContain("My name's ???.");
|
|
expect(p.world.state.scenes[CHERRYGROVE]).toBe(0);
|
|
reload();
|
|
});
|
|
|
|
it("names the rival for the officer and returns the Egg to Elm", () => {
|
|
traceFailures();
|
|
cross("right", NEW_BARK);
|
|
enter(ELMS_LAB, "up");
|
|
walk(4, 6);
|
|
expect(p.world.move("up")).toBe(true);
|
|
play(["SILVER"]);
|
|
expect(p.world.state.rivalName).toBe("SILVER");
|
|
reload();
|
|
walk(5, 3);
|
|
p.world.move("up"); // Elm stands at 5,2: this only turns the player.
|
|
p.world.interact();
|
|
play();
|
|
expect(p.world.state.bag.keyItems).toEqual([]);
|
|
walk(4, 8);
|
|
expect(p.world.state.bag.balls).toEqual([
|
|
{ item: C.POKE_BALL, quantity: 5 },
|
|
]);
|
|
reload();
|
|
});
|
|
|
|
it("watches the catching tutorial on Route 29 and reaches Cherrygrove", () => {
|
|
traceFailures();
|
|
exit(NEW_BARK);
|
|
cross("left", ROUTE_29);
|
|
cross("left", CHERRYGROVE);
|
|
const pages = p.log.join("\n").replace(/\n/g, " ");
|
|
expect(pages).toContain("show you how to catch POKéMON?");
|
|
expect(pages).toContain("That's how you do it");
|
|
expect(p.world.state.events.has(66)).toBe(true); // LEARNED_TO_CATCH_POKEMON
|
|
expect(p.world.state.scenes[ROUTE_29]).toBe(0);
|
|
reload();
|
|
const save = p.world.save();
|
|
expect(save.party[0].species).toBe(155);
|
|
expect(save.pokedex.caught).toContain(155);
|
|
expect(save.rivalName).toBe("SILVER");
|
|
expect(save.intro).toBe(false);
|
|
});
|
|
|
|
it("trains and walks Route 30 and Route 31 to Violet City", () => {
|
|
traceFailures();
|
|
const restInCherrygrove = () => {
|
|
cross("left", CHERRYGROVE);
|
|
heal(CHERRYGROVE_CENTER, CHERRYGROVE);
|
|
cross("right", ROUTE_29);
|
|
};
|
|
heal(CHERRYGROVE_CENTER, CHERRYGROVE);
|
|
cross("right", ROUTE_29);
|
|
train(10, restInCherrygrove);
|
|
cross("left", CHERRYGROVE);
|
|
heal(CHERRYGROVE_CENTER, CHERRYGROVE);
|
|
cross("up", ROUTE_30);
|
|
cross("up", ROUTE_31);
|
|
exit(ROUTE_31_GATE, "left");
|
|
// Mom worries when the player has not come home since the Egg; the
|
|
// call rings on a step after Route 31's callback queued it.
|
|
expect(p.world.state.events.has(EVENT_TALKED_TO_MOM_AFTER_MYSTERY_EGG_QUEST), p.log.join("|")).toBe(true);
|
|
exit(VIOLET, "left");
|
|
expect(p.world.state.flags.size).toBeGreaterThan(0);
|
|
reload();
|
|
writeTestSave("violet-city");
|
|
}, 120_000);
|
|
|
|
it("trains on Route 31 and earns Flash from Elder Li in Sprout Tower", () => {
|
|
traceFailures();
|
|
exit(ROUTE_31_GATE, "right");
|
|
exit(ROUTE_31, "right");
|
|
train(14, restInViolet);
|
|
restInViolet();
|
|
exit(ROUTE_31_GATE, "left");
|
|
exit(VIOLET, "left");
|
|
enter(SPROUT_1F, "up");
|
|
expect(route(SPROUT_3F, [SPROUT_1F, SPROUT_2F])).toBe(true);
|
|
walk(10, 3);
|
|
p.world.move("up"); // Elder Li: this only turns the player
|
|
p.world.interact();
|
|
play();
|
|
const pages = p.log.join("\n").replace(/\n/g, " ");
|
|
expect(pages).toContain("FLASH");
|
|
expect(p.world.state.events.has(EVENT_GOT_HM05_FLASH)).toBe(true);
|
|
reload();
|
|
}, 120_000);
|
|
|
|
it("beats Falkner for the ZEPHYRBADGE and TM31, then hears from Elm's aide", () => {
|
|
traceFailures();
|
|
expect(route(SPROUT_1F, [SPROUT_1F, SPROUT_2F, SPROUT_3F], 6, { x: 9, y: 15 })).toBe(true);
|
|
exit(VIOLET);
|
|
heal(VIOLET_CENTER, VIOLET);
|
|
enter(VIOLET_GYM, "up");
|
|
walk(5, 2);
|
|
p.world.move("up"); // Falkner: this only turns the player
|
|
p.world.interact();
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_BEAT_FALKNER), p.log.join("|")).toBe(true);
|
|
expect(p.world.state.flags.has(ENGINE_ZEPHYRBADGE)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_GOT_TM31_MUD_SLAP)).toBe(true);
|
|
expect(p.world.state.bag.tmsHms[30]).toBe(1); // TM31
|
|
reload();
|
|
exit(VIOLET);
|
|
// SPECIALCALL_ASSISTANT rings on a step: the aide waits in the
|
|
// Pokémon Center with the Egg.
|
|
enter(VIOLET_CENTER, "up");
|
|
expect(p.log.join("\n").replace(/\n/g, " ")).toContain("POKéMON CENTER");
|
|
// The aide's object flag (EVENT_ELMS_AIDE_IN_VIOLET_POKEMON_CENTER).
|
|
const aide = p.world.assets.map(VIOLET_CENTER).objects[4];
|
|
expect(p.world.state.events.has(aide.eventFlag)).toBe(false);
|
|
reload();
|
|
}, 120_000);
|
|
|
|
it("takes the Egg from Elm's aide", () => {
|
|
traceFailures();
|
|
walk(4, 4);
|
|
p.world.move("up"); // the aide: this only turns the player
|
|
p.world.interact();
|
|
play([1]);
|
|
expect(p.world.state.events.has(EVENT_GOT_TOGEPI_EGG_FROM_ELMS_AIDE)).toBe(true);
|
|
const egg = p.world.state.party.at(-1)!;
|
|
expect(egg).toMatchObject({
|
|
species: TOGEPI,
|
|
egg: true,
|
|
nickname: "EGG",
|
|
hp: 0,
|
|
happiness: p.world.assets.pokemon.baseData(TOGEPI).eggCycles,
|
|
});
|
|
// An Egg is neither seen nor caught.
|
|
expect(p.world.state.pokedex.caught.has(TOGEPI)).toBe(false);
|
|
reload();
|
|
});
|
|
|
|
it("catches a Bellsprout on Route 31 and trades it to Kyle for Onix", () => {
|
|
traceFailures();
|
|
exit(VIOLET);
|
|
exit(ROUTE_31_GATE, "right");
|
|
exit(ROUTE_31, "right");
|
|
p.wanted = BELLSPROUT;
|
|
try {
|
|
for (let round = 0; round < 400 && !p.world.state.party.some((mon) => mon.species === BELLSPROUT); round++) {
|
|
if (!p.world.state.bag.balls.length) break;
|
|
train(lead().level + 1, restInViolet);
|
|
}
|
|
} finally {
|
|
p.wanted = 0;
|
|
}
|
|
const bellsprout = p.world.state.party.findIndex((mon) => mon.species === BELLSPROUT);
|
|
expect(bellsprout, p.log.slice(-20).join("|")).toBeGreaterThan(0);
|
|
exit(ROUTE_31_GATE, "left");
|
|
exit(VIOLET, "left");
|
|
enter(KYLES_HOUSE, "up");
|
|
const kyle = p.world.objects().find((object) => object.sprite === p.world.assets.map(KYLES_HOUSE).objects[1].sprite && object.id === p.world.assets.map(KYLES_HOUSE).objects[1].id)!;
|
|
walk(kyle.x - 1, kyle.y);
|
|
p.world.move("right");
|
|
p.world.interact();
|
|
play([1, bellsprout]);
|
|
const onix = p.world.state.party.at(-1)!;
|
|
expect(onix).toMatchObject({ species: ONIX, nickname: "ROCKY", ot: "KYLE", otId: 48926, dvs: 0x9666 });
|
|
expect(p.world.state.party.some((mon) => mon.species === BELLSPROUT && !mon.egg)).toBe(false);
|
|
expect(p.world.state.tradeFlags).toBe(1 << 1);
|
|
expect(p.world.state.pokedex.caught.has(ONIX)).toBe(true);
|
|
reload();
|
|
}, 120_000);
|
|
|
|
it("teaches HM05 to a new Bellsprout and lights up Dark Cave with FLASH", () => {
|
|
traceFailures();
|
|
exit(VIOLET);
|
|
exit(ROUTE_31_GATE, "right");
|
|
exit(ROUTE_31, "right");
|
|
p.wanted = BELLSPROUT;
|
|
try {
|
|
for (let round = 0; round < 400 && !p.world.state.party.some((mon) => mon.species === BELLSPROUT); round++)
|
|
train(lead().level + 1, restInViolet);
|
|
} finally {
|
|
p.wanted = 0;
|
|
}
|
|
const slot = p.world.state.party.findIndex((mon) => mon.species === BELLSPROUT);
|
|
expect(slot).toBeGreaterThan(0);
|
|
const nickname = p.world.state.party[slot].nickname;
|
|
// PACK, the TM/HM pocket's HM05, USE, YES, then the Bellsprout.
|
|
expect(p.world.openStartMenu()).toBe(true);
|
|
choose("PACK");
|
|
choosePack(HM05);
|
|
choose("USE");
|
|
expect(toChoice().kind).toBe("yesno");
|
|
p.world.choose(1);
|
|
const party = toChoice();
|
|
expect(party.kind === "menu" && party.details?.[slot]).toBe("ABLE");
|
|
p.world.choose(slot);
|
|
if (p.world.state.party[slot].moves.every(Boolean)) {
|
|
// A full move list asks which move to forget: the first one.
|
|
expect(toChoice().kind).toBe("yesno");
|
|
p.world.choose(1);
|
|
toChoice();
|
|
p.world.choose(0);
|
|
}
|
|
for (let i = 0; i < 10 && p.world.dialogue?.choice?.kind !== "pack"; i++) toChoice();
|
|
expect(p.log.join("\n").replace(/\n/g, " ")).toContain("learned FLASH!");
|
|
// The menus work on a draft that is kept once they close.
|
|
for (let i = 0; i < 10 && p.world.busy; i++) {
|
|
if (p.world.dialogue?.choice) p.world.advanceDialogue();
|
|
else p.world.tick();
|
|
}
|
|
play();
|
|
expect(p.world.state.party[slot].moves).toContain(FLASH);
|
|
expect(p.world.state.bag.tmsHms[50 + 4]).toBe(1); // an HM is kept
|
|
// Dark Cave is dark until FLASH, which the ZEPHYRBADGE allows.
|
|
enter(DARK_CAVE, "up");
|
|
expect(p.world.state.flash).toBe(false);
|
|
expect(p.world.openStartMenu()).toBe(true);
|
|
choose("POKéMON");
|
|
choose(nickname);
|
|
choose("FLASH");
|
|
play();
|
|
expect(p.world.state.flash, p.log.slice(-6).join("|")).toBe(true);
|
|
reload();
|
|
}, 120_000);
|
|
|
|
it("registers Bug Catcher Wade's number and reads Earl's blackboard", () => {
|
|
traceFailures();
|
|
exit(ROUTE_31);
|
|
talkTo(p.world.map.objects[2].id);
|
|
play([1]);
|
|
const pages = p.log.join("\n").replace(/\n/g, " ");
|
|
expect(pages).toContain("BUG CATCHER WADE");
|
|
expect(p.world.state.phone).toContain(PHONE_BUG_CATCHER_WADE);
|
|
reload();
|
|
restInViolet();
|
|
exit(ROUTE_31_GATE, "left");
|
|
exit(VIOLET, "left");
|
|
enter(EARLS_ACADEMY, "up");
|
|
walk(3, 1);
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
choose("PSN");
|
|
toChoice();
|
|
expect(p.log.join("\n").replace(/\n/g, " ")).toContain("steadily loses HP");
|
|
// B leaves the blackboard.
|
|
p.world.choose(-1);
|
|
play();
|
|
expect(p.world.busy).toBe(false);
|
|
reload();
|
|
}, 120_000);
|
|
|
|
it("takes Route 32 and Union Cave to Azalea Town", () => {
|
|
traceFailures();
|
|
exit(VIOLET);
|
|
cross("down", ROUTE_32);
|
|
// The man at Route 32's north end gives the Miracle Seed after the
|
|
// ZEPHYRBADGE and the Egg.
|
|
expect(p.world.state.bag.items.some((slot) => slot.item === MIRACLE_SEED)).toBe(false);
|
|
p.talkTo(p.world.map.objects[7].id);
|
|
play();
|
|
expect(p.world.state.bag.items.some((slot) => slot.item === MIRACLE_SEED)).toBe(true);
|
|
stairs(UNION_CAVE_1F);
|
|
stairs(ROUTE_33);
|
|
cross("left", AZALEA);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("follows Kurt into the Slowpoke Well and drives out Team Rocket", () => {
|
|
traceFailures();
|
|
enter(KURTS_HOUSE, "up");
|
|
talkTo(p.world.map.objects[0].id);
|
|
play();
|
|
expect(p.log.join(" ")).toContain("Old KURT is on his\nway!");
|
|
exit(AZALEA);
|
|
stairs(SLOWPOKE_WELL_B1F);
|
|
// The three grunts, then the one by the boulder, whose defeat ends
|
|
// the scene in Kurt's house.
|
|
for (const index of [0, 2, 3, 1]) {
|
|
const id = p.world.map.objects[index].id;
|
|
if (!p.world.objects().some((object) => object.id === id)) continue;
|
|
talkTo(id);
|
|
play();
|
|
}
|
|
expect(p.world.mapId).toBe(KURTS_HOUSE);
|
|
expect(p.world.state.events.has(EVENT_CLEARED_SLOWPOKE_WELL)).toBe(true);
|
|
talkTo(p.world.map.objects[0].id);
|
|
play();
|
|
expect(p.world.state.bag.balls.some((slot) => slot.item === LURE_BALL)).toBe(true);
|
|
reload();
|
|
}, 120_000);
|
|
|
|
it("beats Bugsy for the HIVEBADGE and the rival on the way to Ilex Forest", () => {
|
|
traceFailures();
|
|
exit(AZALEA);
|
|
heal(AZALEA_CENTER, AZALEA);
|
|
enter(AZALEA_GYM, "up");
|
|
talkTo(p.world.map.objects[0].id);
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_BEAT_BUGSY)).toBe(true);
|
|
expect(p.world.state.flags.has(ENGINE_HIVEBADGE)).toBe(true);
|
|
expect(p.world.state.bag.tmsHms[48]).toBe(1); // TM49 FURY CUTTER
|
|
exit(AZALEA);
|
|
heal(AZALEA_CENTER, AZALEA);
|
|
// The rival waits at the town's west end (SCENE_AZALEATOWN_RIVAL_BATTLE).
|
|
stairs(ILEX_GATE);
|
|
expect(p.world.state.events.has(EVENT_RIVAL_AZALEA_TOWN)).toBe(true);
|
|
reload();
|
|
}, 120_000);
|
|
|
|
it("herds the Farfetch'd back in Ilex Forest and cuts through to Route 34", () => {
|
|
traceFailures();
|
|
stairs(ILEX_FOREST);
|
|
const STEP = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] } as const;
|
|
const bird = p.world.map.objects[0].id;
|
|
// wFarfetchdPosition 1 → 2 from any side, 2 → 8 and 8 → 9 facing
|
|
// down, then 9 → 10 facing left.
|
|
for (const face of ["up", "down", "down", "left"] as const) {
|
|
const at = p.world.objects().find((object) => object.id === bird)!;
|
|
walk(at.x - STEP[face][0], at.y - STEP[face][1]);
|
|
p.world.move(face);
|
|
p.world.interact();
|
|
play();
|
|
}
|
|
expect(p.world.state.memory.wFarfetchdPosition).toBe(10);
|
|
expect(p.world.state.events.has(EVENT_HERDED_FARFETCHD)).toBe(true);
|
|
talkTo(p.world.map.objects[2].id); // the charcoal master
|
|
play();
|
|
expect(p.world.state.bag.tmsHms[50]).toBe(1); // HM01
|
|
reload();
|
|
const bellsprout = p.world.state.party.findIndex((mon) => mon.species === BELLSPROUT);
|
|
p.teach(HM01, bellsprout);
|
|
expect(p.world.state.party[bellsprout].moves).toContain(CUT);
|
|
p.cut(8, 25);
|
|
expect(p.log).toContain("BELLSPROUT used\nCUT!");
|
|
stairs(ROUTE_34_GATE);
|
|
reload();
|
|
}, 120_000);
|
|
|
|
it("reaches Goldenrod City and gets a Bicycle", () => {
|
|
traceFailures();
|
|
stairs(ROUTE_34);
|
|
cross("up", GOLDENROD);
|
|
heal(GOLDENROD_CENTER, GOLDENROD);
|
|
enter(BIKE_SHOP, "up");
|
|
talkTo(p.world.map.objects[0].id); // the clerk, across the counter
|
|
play([1]);
|
|
expect(p.world.state.bag.keyItems).toContain(BICYCLE);
|
|
exit(GOLDENROD);
|
|
reload();
|
|
}, 120_000);
|
|
|
|
it("trains on Route 34 and beats Whitney for the PLAINBADGE", () => {
|
|
traceFailures();
|
|
const restInGoldenrod = () => {
|
|
cross("up", GOLDENROD);
|
|
heal(GOLDENROD_CENTER, GOLDENROD);
|
|
cross("down", ROUTE_34);
|
|
};
|
|
cross("down", ROUTE_34);
|
|
train(30, restInGoldenrod);
|
|
restInGoldenrod();
|
|
cross("up", GOLDENROD);
|
|
enter(GOLDENROD_GYM, "up");
|
|
const whitney = p.world.map.objects[0].id;
|
|
talkTo(whitney);
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_BEAT_WHITNEY)).toBe(true);
|
|
// She cries; Bridget explains on the way out, then the badge.
|
|
exit(GOLDENROD);
|
|
expect(p.log.join(" ")).toContain("WHITNEY cry.");
|
|
enter(GOLDENROD_GYM, "up");
|
|
talkTo(whitney);
|
|
play();
|
|
expect(p.world.state.flags.has(ENGINE_PLAINBADGE)).toBe(true);
|
|
expect(p.world.state.bag.tmsHms[44]).toBe(1); // TM45 ATTRACT
|
|
exit(GOLDENROD);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("answers the Radio Tower quiz for the radio card", () => {
|
|
traceFailures();
|
|
enter(RADIO_TOWER_1F, "up");
|
|
talkTo(p.world.map.objects[5].id);
|
|
play([1, 1, 1, 0, 1, 0]);
|
|
expect(p.world.state.flags.has(ENGINE_RADIO_CARD)).toBe(true);
|
|
exit(GOLDENROD);
|
|
reload();
|
|
}, 120_000);
|
|
},
|
|
);
|