Tin Tower's floors and roof and the Whirl Islands are open. The Clear Bell gets the player past the sages to Suicune in Tin Tower; after the League, the Rainbow Wing leads up to Ho-Oh, and the Silver Wing to Lugia. New specials: MonCheck and BeastsCheck (CheckOwnMonAnywhere, with its five-letter OT comparison and no Day-Care search). The playthrough driver plans ledge jumps for the later stages, waterfall descents, warp panels and Surf between warps; the run gains two stages, Routes 45 and 46 back to Ecruteak and Tin Tower's Suicune. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1491 lines
57 KiB
TypeScript
1491 lines
57 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";
|
|
import { findPath } from "./world-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",
|
|
FLOWER_SHOP = "11:8",
|
|
RADIO_TOWER_1F = "3:17";
|
|
const ROUTE_35 = "10:2",
|
|
ROUTE_35_NATIONAL_PARK_GATE = "10:15",
|
|
NATIONAL_PARK = "3:15",
|
|
ROUTE_36_NATIONAL_PARK_GATE = "10:17",
|
|
ROUTE_35_GATE = "10:14",
|
|
ROUTE_36 = "10:3",
|
|
ROUTE_37 = "10:4",
|
|
ECRUTEAK = "4:9",
|
|
ECRUTEAK_CENTER = "4:3",
|
|
ECRUTEAK_MART = "4:6",
|
|
ECRUTEAK_GYM = "4:7",
|
|
DANCE_THEATER = "4:5",
|
|
BURNED_TOWER_1F = "3:13",
|
|
BURNED_TOWER_B1F = "3:14";
|
|
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;
|
|
const SQUIRTBOTTLE = 0xaf;
|
|
const EVENT_MET_FLORIA = 0xb9,
|
|
EVENT_GOT_SQUIRTBOTTLE = 0x5c,
|
|
EVENT_FOUGHT_SUDOWOODO = 0x2a,
|
|
EVENT_GOT_HM03_SURF = 0x12,
|
|
EVENT_RIVAL_BURNED_TOWER = 0x6c5,
|
|
EVENT_HOLE_IN_BURNED_TOWER = 0x332;
|
|
const EVENT_RELEASED_THE_BEASTS = 0x7b,
|
|
EVENT_BURNED_TOWER_MORTY = 0x764,
|
|
EVENT_ECRUTEAK_GYM_GRAMPS = 0x7a8,
|
|
EVENT_ECRUTEAK_CITY_GRAMPS = 0x7a9,
|
|
EVENT_BEAT_SAGE_JEFFREY = 0x415,
|
|
EVENT_BEAT_SAGE_PING = 0x416,
|
|
EVENT_BEAT_MEDIUM_MARTHA = 0x58b,
|
|
EVENT_BEAT_MEDIUM_GRACE = 0x58c,
|
|
EVENT_BEAT_MORTY = 0x4c0,
|
|
EVENT_GOT_TM30_SHADOW_BALL = 0xc,
|
|
ENGINE_FOGBADGE = 30,
|
|
RAIKOU = 0xf3,
|
|
ENTEI = 0xf4,
|
|
ROUTE_42 = "2:5";
|
|
const ROUTE_38_GATE = "1:9",
|
|
ROUTE_38 = "1:12",
|
|
ROUTE_39 = "1:13",
|
|
OLIVINE = "1:14",
|
|
OLIVINE_CENTER = "1:1",
|
|
OLIVINE_GYM = "1:2",
|
|
OLIVINE_CAFE = "1:7",
|
|
GOOD_ROD_HOUSE = "1:6",
|
|
OLIVINE_MART = "1:8",
|
|
LIGHTHOUSE = ["3:42", "3:43", "3:44", "3:45", "3:46", "3:47"],
|
|
ROUTE_40 = "22:1",
|
|
ROUTE_41 = "22:2",
|
|
CIANWOOD = "22:3",
|
|
CIANWOOD_GYM = "22:5",
|
|
CIANWOOD_CENTER = "22:6",
|
|
CIANWOOD_PHARMACY = "22:7";
|
|
const GOOD_ROD = 0x3b,
|
|
SECRETPOTION = 0x43,
|
|
HM02 = 0xf4,
|
|
HM03 = 0xf5,
|
|
HM04 = 0xf6,
|
|
KRABBY = 98,
|
|
SURF = 0x39,
|
|
STRENGTH = 0x46,
|
|
EVENT_GOT_HM04_STRENGTH = 0x13,
|
|
EVENT_GOT_GOOD_ROD = 0x18,
|
|
EVENT_RIVAL_OLIVINE_CITY = 0x6c3,
|
|
EVENT_JASMINE_EXPLAINED_AMPHYS_SICKNESS = 0x37,
|
|
EVENT_FOUGHT_EUSINE = 0x333,
|
|
EVENT_GOT_SECRETPOTION_FROM_PHARMACY = 0x23,
|
|
EVENT_BEAT_CHUCK = 0x4c2,
|
|
EVENT_GOT_TM01_DYNAMICPUNCH = 0xa,
|
|
EVENT_GOT_HM02_FLY = 0x11,
|
|
EVENT_JASMINE_RETURNED_TO_GYM = 0x20,
|
|
EVENT_BEAT_JASMINE = 0x4c1,
|
|
EVENT_GOT_TM23_IRON_TAIL = 0xd,
|
|
ENGINE_MINERALBADGE = 31,
|
|
ENGINE_STORMBADGE = 32;
|
|
const ROUTE_42_GATE = "2:4",
|
|
MAHOGANY = "2:7",
|
|
MAHOGANY_CENTER = "2:3",
|
|
MAHOGANY_GYM = "2:2",
|
|
MAHOGANY_SHOP = "3:48",
|
|
ROCKET_HIDEOUT = ["3:49", "3:50", "3:51"],
|
|
ROUTE_43_MAHOGANY_GATE = "9:3",
|
|
ROUTE_43_GATE = "9:4",
|
|
ROUTE_43 = "9:5",
|
|
LAKE_OF_RAGE = "9:6";
|
|
const RED_SCALE = 0x42,
|
|
EVENT_BEAT_POKEMANIAC_SHANE = 0x4e7,
|
|
EVENT_LAKE_OF_RAGE_RED_GYARADOS = 0x751,
|
|
EVENT_DECIDED_TO_HELP_LANCE = 0x60,
|
|
EVENT_UNCOVERED_STAIRCASE_IN_MAHOGANY_MART = 0x2e2,
|
|
EVENT_LANCE_HEALED_YOU_IN_TEAM_ROCKET_BASE = 0x4c,
|
|
EVENT_LEARNED_SLOWPOKETAIL = 0x301,
|
|
EVENT_LEARNED_RATICATE_TAIL = 0x302,
|
|
EVENT_LEARNED_HAIL_GIOVANNI = 0x2ff,
|
|
EVENT_RIVAL_TEAM_ROCKET_BASE = 0x6c0,
|
|
EVENT_BEAT_ROCKET_EXECUTIVEM_4 = 0x574,
|
|
EVENT_BEAT_ROCKET_EXECUTIVEF_2 = 0x570,
|
|
EVENT_CLEARED_ROCKET_HIDEOUT = 0x22,
|
|
EVENT_GOT_HM06_WHIRLPOOL = 0x15,
|
|
EVENT_BEAT_PRYCE = 0x4c3,
|
|
EVENT_GOT_TM16_ICY_WIND = 0xe,
|
|
EVENT_RADIO_TOWER_ROCKET_TAKEOVER = 0x6ce,
|
|
ENGINE_GLACIERBADGE = 0x21,
|
|
ENGINE_ROCKETS_IN_RADIO_TOWER = 0x13;
|
|
const RADIO_TOWER = ["3:17", "3:18", "3:19", "3:20", "3:21"],
|
|
UNDERGROUND = "3:53",
|
|
SWITCH_ROOM = "3:54",
|
|
DEPT_STORE_B1F = "3:55",
|
|
WAREHOUSE = "3:56",
|
|
DEPT_STORE_1F = "11:11",
|
|
ELEVATOR = "11:17",
|
|
ROUTE_44 = "2:6",
|
|
ICE_PATH = ["3:61", "3:62", "3:63", "3:64", "3:65"],
|
|
BLACKTHORN = "5:10",
|
|
BLACKTHORN_CENTER = "5:6",
|
|
BLACKTHORN_MART = "5:5",
|
|
BLACKTHORN_GYM = ["5:1", "5:2"],
|
|
ROUTE_45 = "5:8",
|
|
DRAGONS_DEN = ["3:80", "3:81"],
|
|
DRAGON_SHRINE = "3:82";
|
|
const BASEMENT_KEY = 0x85,
|
|
CARD_KEY = 0x7f,
|
|
CLEAR_BELL = 0x46,
|
|
HM06 = 0xf8,
|
|
DRATINI = 147,
|
|
EXTREMESPEED = 245,
|
|
EVENT_BEAT_ROCKET_EXECUTIVEM_3 = 0x573,
|
|
EVENT_USED_BASEMENT_KEY = 0x49,
|
|
EVENT_RIVAL_GOLDENROD_UNDERGROUND = 0x6c1,
|
|
EVENT_RECEIVED_CARD_KEY = 0x4a,
|
|
EVENT_USED_THE_CARD_KEY_IN_THE_RADIO_TOWER = 0x25,
|
|
EVENT_CLEARED_RADIO_TOWER = 0x21,
|
|
EVENT_GOT_CLEAR_BELL = 0x78,
|
|
EVENT_BOULDER_IN_ICE_PATH_2A = 0x70e,
|
|
EVENT_BEAT_CLAIR = 0x4c4,
|
|
EVENT_GOT_TM24_DRAGONBREATH = 0xf,
|
|
EVENT_GOT_DRATINI = 0xbd,
|
|
EVENT_ANSWERED_DRAGON_MASTER_QUIZ_WRONG = 0xc1,
|
|
ENGINE_RISINGBADGE = 0x22;
|
|
const ROUTE_46 = "5:9",
|
|
ROUTE_29_ROUTE_46_GATE = "24:13",
|
|
TIN_TOWER_ENTRANCE = "4:1",
|
|
WISE_TRIOS_ROOM = "4:2",
|
|
TIN_TOWER_1F = "3:4",
|
|
EVENT_RANG_CLEAR_BELL_2 = 0x767,
|
|
EVENT_FOUGHT_SUICUNE = 0x335;
|
|
|
|
/** 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,
|
|
travel,
|
|
sail,
|
|
toChoice,
|
|
choose,
|
|
choosePack,
|
|
talkTo,
|
|
buy,
|
|
} = 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");
|
|
};
|
|
|
|
/** Route 35 and Route 36 meet only through the National Park and
|
|
* its two gates (Route 36's Sudowoodo still blocks the way from
|
|
* Violet City). */
|
|
const toRoute36 = () => {
|
|
stairs(ROUTE_35_GATE);
|
|
stairs(ROUTE_35);
|
|
stairs(ROUTE_35_NATIONAL_PARK_GATE);
|
|
stairs(NATIONAL_PARK);
|
|
stairs(ROUTE_36_NATIONAL_PARK_GATE);
|
|
stairs(ROUTE_36);
|
|
};
|
|
const toGoldenrod = () => {
|
|
stairs(ROUTE_36_NATIONAL_PARK_GATE);
|
|
stairs(NATIONAL_PARK);
|
|
stairs(ROUTE_35_NATIONAL_PARK_GATE);
|
|
stairs(ROUTE_35);
|
|
stairs(ROUTE_35_GATE);
|
|
stairs(GOLDENROD);
|
|
};
|
|
|
|
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();
|
|
writeTestSave("route-34");
|
|
}, 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();
|
|
writeTestSave("goldenrod-city");
|
|
}, 120_000);
|
|
|
|
it("meets Floria on Route 36 and gets the SquirtBottle from the flower shop", () => {
|
|
traceFailures();
|
|
toRoute36();
|
|
// ROUTE36_FLORIA is the eighth object in Route36_MapEvents.
|
|
talkTo(p.world.map.objects[7].id);
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_MET_FLORIA)).toBe(true);
|
|
toGoldenrod();
|
|
enter(FLOWER_SHOP, "up");
|
|
// Floria walked here from Route 36; talk to her first
|
|
// (EVENT_TALKED_TO_FLORIA_AT_FLOWER_SHOP), then the Teacher gives
|
|
// the SquirtBottle since the PLAINBADGE is already held.
|
|
talkTo(p.world.map.objects[1].id);
|
|
play();
|
|
talkTo(p.world.map.objects[0].id);
|
|
play([1]);
|
|
// The Squirtbottle is a key item (ItemAttributes' pocket byte).
|
|
expect(p.world.state.bag.keyItems).toContain(SQUIRTBOTTLE);
|
|
expect(p.world.state.events.has(EVENT_GOT_SQUIRTBOTTLE)).toBe(true);
|
|
exit(GOLDENROD);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("waters the weird tree on Route 36 and battles Sudowoodo, then crosses Route 37 to Ecruteak City", () => {
|
|
traceFailures();
|
|
toRoute36();
|
|
const tree = p.world.map.objects[2].id; // ROUTE36_WEIRD_TREE
|
|
talkTo(tree);
|
|
play([1]);
|
|
expect(p.world.state.events.has(EVENT_FOUGHT_SUDOWOODO)).toBe(true);
|
|
cross("up", ROUTE_37);
|
|
cross("up", ECRUTEAK);
|
|
expect(p.world.mapId).toBe(ECRUTEAK);
|
|
heal(ECRUTEAK_CENTER, ECRUTEAK);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("beats all five Kimono Girls for HM03 Surf from the Dance Theater's gentleman", () => {
|
|
traceFailures();
|
|
enter(DANCE_THEATER, "up");
|
|
// The five Kimono Girls are objects 2 through 6 (file order); the
|
|
// gentleman who gives Surf is object 7. An already-beaten Kimono
|
|
// Girl's trainer header just shows her after-text, so healing
|
|
// between fights (five single-Pokémon trainers in a row otherwise
|
|
// wear the lead down) is safe to repeat.
|
|
for (let id = 2; id <= 6; id++) {
|
|
if (lead().hp < lead().stats[0]) {
|
|
exit(ECRUTEAK);
|
|
heal(ECRUTEAK_CENTER, ECRUTEAK);
|
|
enter(DANCE_THEATER, "up");
|
|
}
|
|
talkTo(id);
|
|
play();
|
|
}
|
|
talkTo(7);
|
|
play([1]);
|
|
expect(p.world.state.bag.tmsHms[52]).toBe(1); // HM03 (50 TMs + HM03 - 1)
|
|
expect(p.world.state.events.has(EVENT_GOT_HM03_SURF)).toBe(true);
|
|
exit(ECRUTEAK);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("meets Eusine and the rival at the Burned Tower, falling to the basement", () => {
|
|
traceFailures();
|
|
// Stock up on Super Potions first: the rival's Croconaw hits the
|
|
// Fire-type lead for super-effective damage on top of Haunter's
|
|
// Curse, so a careful player restocks before a hard fight rather
|
|
// than going in on whatever is left from Route 36.
|
|
enter(ECRUTEAK_MART, "up");
|
|
const money = p.world.state.money;
|
|
buy(p.world.map.objects[0].id, C.SUPER_POTION, 10);
|
|
expect(p.world.state.money).toBe(money - 700 * 10);
|
|
expect(p.world.state.bag.items).toContainEqual({ item: C.SUPER_POTION, quantity: 10 });
|
|
exit(ECRUTEAK);
|
|
enter(BURNED_TOWER_1F, "up");
|
|
// BurnedTower1FMeetEusineScene runs automatically on the first idle
|
|
// tick (sdefer), as Ecruteak's Pokémon Center's Bill scene does.
|
|
expect(p.log.join(" ")).toContain("EUSINE: My name's");
|
|
// The only way to any tile by the rival crosses the coordinate event
|
|
// at (11, 9); walking there triggers BurnedTowerRivalBattleScript,
|
|
// which ends with the player falling to the basement, so the usual
|
|
// walk() helper (which expects to stay on this map unless stepping
|
|
// onto a listed warp) does not fit here.
|
|
// A loss whites out (healed) to Ecruteak's spawn point outside the
|
|
// Pokémon Center with the scene still set, so the player tries again.
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
for (const direction of findPath(p.world, 11, 9)) {
|
|
if (!p.world.move(direction)) break;
|
|
play();
|
|
if (p.world.mapId !== BURNED_TOWER_1F) break;
|
|
}
|
|
if (p.world.mapId === BURNED_TOWER_B1F) break;
|
|
expect(p.world.mapId, "whited out to Ecruteak").toBe(ECRUTEAK);
|
|
enter(BURNED_TOWER_1F, "up");
|
|
}
|
|
expect(p.world.state.events.has(EVENT_RIVAL_BURNED_TOWER)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_HOLE_IN_BURNED_TOWER)).toBe(true);
|
|
expect(p.world.mapId).toBe(BURNED_TOWER_B1F);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("releases the beasts, says goodbye to Eusine and climbs back to Ecruteak", () => {
|
|
traceFailures();
|
|
// The coordinate event at (10, 6) runs ReleaseTheBeasts: the three
|
|
// beasts flee one by one through InitRoamMons, which starts Raikou
|
|
// and Entei roaming (Suicune stays put until Cianwood).
|
|
walk(10, 6);
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_RELEASED_THE_BEASTS)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_BURNED_TOWER_MORTY)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_ECRUTEAK_GYM_GRAMPS)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_ECRUTEAK_CITY_GRAMPS)).toBe(false);
|
|
expect(p.world.state.roamMons[0]).toMatchObject({ species: RAIKOU, mapId: ROUTE_42 });
|
|
expect(p.world.state.roamMons[1]).toMatchObject({ species: ENTEI, mapId: ROUTE_37 });
|
|
// Eusine appears once the beasts are gone (BurnedTowerB1F_EUSINE's
|
|
// own flag, set by `appear` rather than a scene variable) and says
|
|
// goodbye before leaving through the tower's own exit sound.
|
|
talkTo(p.world.map.objects[8].id);
|
|
play();
|
|
expect(p.log.join(" ")).toContain("I'm all choked up!");
|
|
// Multiple small stairs lead back to 1F now that the ladder at (6,
|
|
// 14) is in place (BurnedTowerB1FLadderCallback); any of them works.
|
|
stairs(BURNED_TOWER_1F);
|
|
exit(ECRUTEAK);
|
|
reload();
|
|
}, 120_000);
|
|
|
|
it("beats Ecruteak Gym's trainers and Morty for the FOGBADGE and TM30", () => {
|
|
traceFailures();
|
|
// Morty's own Gengar leans on Hypnosis and Dream Eater; a careful
|
|
// player also stocks Awakenings, not just Super Potions, before a
|
|
// Ghost-type gym.
|
|
enter(ECRUTEAK_MART, "up");
|
|
buy(p.world.map.objects[0].id, C.AWAKENING, 5);
|
|
expect(p.world.state.bag.items).toContainEqual({ item: C.AWAKENING, quantity: 5 });
|
|
exit(ECRUTEAK);
|
|
// EcruteakGymForcedToLeaveScene (the default scene) no longer plays:
|
|
// ReleaseTheBeasts already set SCENE_ECRUTEAKGYM_NOOP for this map.
|
|
enter(ECRUTEAK_GYM, "up");
|
|
// The trainers stand past Morty's own invisible-floor maze; findPath
|
|
// already treats every unlisted pit tile as impassable (the same
|
|
// warps the player would fall through), so it finds the real floor
|
|
// on its own, the way a player who knows the safe path would.
|
|
const [morty, jeffrey, ping, martha, grace] = p.world.map.objects.map(
|
|
(object) => object.id,
|
|
);
|
|
// Ghost types shrug off Normal moves entirely ("No normal-type
|
|
// attack can harm them!", straight from Sage Ping's own text), so
|
|
// EMBER is the lead's only way to damage any of them; a cautious
|
|
// player heals between fights (restoring its PP, not just HP) so
|
|
// a long multi-Pokémon trainer never finds EMBER run dry.
|
|
for (const trainer of [jeffrey, ping, martha, grace]) {
|
|
exit(ECRUTEAK);
|
|
heal(ECRUTEAK_CENTER, ECRUTEAK);
|
|
enter(ECRUTEAK_GYM, "up");
|
|
talkTo(trainer);
|
|
play();
|
|
}
|
|
expect(p.world.state.events.has(EVENT_BEAT_SAGE_JEFFREY)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_BEAT_SAGE_PING)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_BEAT_MEDIUM_MARTHA)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_BEAT_MEDIUM_GRACE)).toBe(true);
|
|
exit(ECRUTEAK);
|
|
heal(ECRUTEAK_CENTER, ECRUTEAK);
|
|
enter(ECRUTEAK_GYM, "up");
|
|
talkTo(morty);
|
|
play([1]);
|
|
expect(p.world.state.events.has(EVENT_BEAT_MORTY), p.log.join("|")).toBe(true);
|
|
expect(p.world.state.flags.has(ENGINE_FOGBADGE)).toBe(true);
|
|
expect(p.log.join(" ")).toContain("FOGBADGE");
|
|
// MortyText_FogBadgeSpeech and the TM30 Shadow Ball gift follow in
|
|
// the same conversation (EVENT_GOT_TM30_SHADOW_BALL was not yet set).
|
|
expect(p.world.state.events.has(EVENT_GOT_TM30_SHADOW_BALL)).toBe(true);
|
|
expect(p.world.state.bag.tmsHms[29]).toBe(1); // TM30
|
|
expect(p.log.join(" ")).toContain("SHADOW BALL");
|
|
exit(ECRUTEAK);
|
|
reload();
|
|
writeTestSave("ecruteak-city");
|
|
}, 240_000);
|
|
|
|
it("crosses Routes 38 and 39 to Olivine City, where the rival is waiting", () => {
|
|
traceFailures();
|
|
// Ecruteak's west gate and the gate's own west side are edge warps.
|
|
exit(ROUTE_38_GATE, "left");
|
|
exit(ROUTE_38, "left");
|
|
cross("left", ROUTE_39);
|
|
cross("down", OLIVINE);
|
|
// OlivineCityRivalSceneTop/Bottom: the way from Route 39 to the
|
|
// Pokémon Center crosses one of the two coordinate events.
|
|
heal(OLIVINE_CENTER, OLIVINE);
|
|
expect(p.log.join(" ")).toContain("GYM LEADER isn't\nhere.");
|
|
expect(p.world.state.events.has(EVENT_RIVAL_OLIVINE_CITY)).toBe(true);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("gets HM04 and the Good Rod, fishes up a Krabby and teaches it Surf", () => {
|
|
traceFailures();
|
|
enter(OLIVINE_CAFE, "up");
|
|
talkTo(p.world.map.objects[0].id); // OLIVINECAFE_SAILOR1
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_GOT_HM04_STRENGTH)).toBe(true);
|
|
exit(OLIVINE);
|
|
enter(GOOD_ROD_HOUSE, "up");
|
|
talkTo(p.world.map.objects[0].id);
|
|
play([1]);
|
|
expect(p.world.state.events.has(EVENT_GOT_GOOD_ROD)).toBe(true);
|
|
expect(p.world.state.bag.keyItems).toContain(GOOD_ROD);
|
|
exit(OLIVINE);
|
|
// Olivine's Mart has Great Balls, not Poké Balls.
|
|
enter(OLIVINE_MART, "up");
|
|
buy(p.world.map.objects[0].id, C.GREAT_BALL, 5);
|
|
exit(OLIVINE);
|
|
// Typhlosion cannot learn Surf: fish a Krabby (FISHGROUP_SHORE's
|
|
// Good Rod table) from the nearest shore tile facing open water.
|
|
const STEP = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] } as const;
|
|
const map = p.world.map;
|
|
const shore = (Object.keys(STEP) as (keyof typeof STEP)[])
|
|
.flatMap((face) =>
|
|
Array.from({ length: map.width * map.height }, (_, i) => {
|
|
const x = i % map.width,
|
|
y = Math.floor(i / map.width);
|
|
const [dx, dy] = STEP[face];
|
|
if (p.world.assets.collision(map, x + dx, y + dy) !== 0x29) return [];
|
|
try {
|
|
return [{ x, y, face, length: findPath(p.world, x, y).length }];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}).flat(),
|
|
)
|
|
.sort((a, b) => a.length - b.length)[0];
|
|
walk(shore.x, shore.y);
|
|
p.wanted = KRABBY;
|
|
for (let cast = 0; cast < 40; cast++) {
|
|
if (p.world.state.party.some((mon) => mon.species === KRABBY)) break;
|
|
p.world.move(shore.face);
|
|
expect(p.world.openStartMenu()).toBe(true);
|
|
choose("PACK");
|
|
choosePack(GOOD_ROD);
|
|
choose("USE");
|
|
play();
|
|
}
|
|
p.wanted = 0;
|
|
const krabby = p.world.state.party.findIndex((mon) => mon.species === KRABBY);
|
|
expect(krabby).toBeGreaterThan(0);
|
|
p.teach(HM03, krabby);
|
|
// Strength replaces Tackle (the first move) on the lead.
|
|
p.teach(HM04, 0);
|
|
expect(p.world.state.party[krabby].moves).toContain(SURF);
|
|
expect(lead().moves).toContain(STRENGTH);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("climbs the Lighthouse to Jasmine and her sick Ampharos", () => {
|
|
traceFailures();
|
|
enter(LIGHTHOUSE[0], "up");
|
|
// Two stairways and the outside ledges lead up; Jasmine stands by
|
|
// Amphy in 6F's far corner.
|
|
expect(route(LIGHTHOUSE[5], LIGHTHOUSE, 12, { x: 8, y: 9 })).toBe(true);
|
|
talkTo(p.world.map.objects[0].id); // OLIVINELIGHTHOUSE6F_JASMINE
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_JASMINE_EXPLAINED_AMPHYS_SICKNESS)).toBe(true);
|
|
expect(p.log.join(" ")).toContain("wonderful PHARMACY\nin CIANWOOD");
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("drops back down the Lighthouse and surfs Routes 40 and 41 to Cianwood City", () => {
|
|
traceFailures();
|
|
// The holes by the windows drop to the floor below; some land in
|
|
// corners cut off from the next hole, so search the floors.
|
|
travel(OLIVINE, LIGHTHOUSE);
|
|
heal(OLIVINE_CENTER, OLIVINE);
|
|
cross("left", ROUTE_40);
|
|
cross("down", ROUTE_41, { water: true });
|
|
cross("left", CIANWOOD, { water: true });
|
|
expect(p.world.mapId).toBe(CIANWOOD);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("meets Suicune and Eusine, gets the SecretPotion and beats Chuck for the STORMBADGE and HM02", () => {
|
|
traceFailures();
|
|
sail(23, 44); // ashore by the Pokémon Center
|
|
heal(CIANWOOD_CENTER, CIANWOOD);
|
|
// ReleaseTheBeasts armed SCENE_CIANWOODCITY_SUICUNE_AND_EUSINE; its
|
|
// coordinate event is on the way north.
|
|
walk(11, 16);
|
|
expect(p.world.state.events.has(EVENT_FOUGHT_EUSINE)).toBe(true);
|
|
expect(p.log.join(" ")).toContain("SUICUNE running on\nthe waves.");
|
|
heal(CIANWOOD_CENTER, CIANWOOD);
|
|
enter(CIANWOOD_PHARMACY, "up");
|
|
talkTo(p.world.map.objects[0].id);
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_GOT_SECRETPOTION_FROM_PHARMACY)).toBe(true);
|
|
expect(p.world.state.bag.keyItems).toContain(SECRETPOTION);
|
|
// Afterwards the pharmacist runs MARTTYPE_PHARMACY.
|
|
buy(p.world.map.objects[0].id, C.SUPER_POTION, 10);
|
|
exit(CIANWOOD);
|
|
// The four black belts stand on the way to the boulders.
|
|
enter(CIANWOOD_GYM, "up");
|
|
walk(3, 8);
|
|
exit(CIANWOOD);
|
|
// Chuck's Poliwrath can win with critical hits; a loss whites out to
|
|
// Cianwood's Pokémon Center and the boulders go back in place.
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
if (p.world.state.events.has(EVENT_BEAT_CHUCK)) break;
|
|
heal(CIANWOOD_CENTER, CIANWOOD);
|
|
enter(CIANWOOD_GYM, "up");
|
|
walk(3, 8);
|
|
// AskStrengthScript from the left boulder, then push it up, the
|
|
// right one up and the middle one left into the gap.
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
play([1]);
|
|
p.world.move("up");
|
|
play();
|
|
walk(5, 8);
|
|
p.world.move("up");
|
|
play();
|
|
walk(5, 7);
|
|
p.world.move("left");
|
|
play();
|
|
walk(4, 2);
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
play([1]);
|
|
}
|
|
expect(p.world.state.events.has(EVENT_BEAT_CHUCK)).toBe(true);
|
|
expect(p.world.state.flags.has(ENGINE_STORMBADGE)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_GOT_TM01_DYNAMICPUNCH)).toBe(true);
|
|
expect(p.world.state.bag.tmsHms[0]).toBe(1); // TM01
|
|
exit(CIANWOOD);
|
|
talkTo(p.world.map.objects[9].id); // CIANWOODCITY_POKEFAN_F
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_GOT_HM02_FLY)).toBe(true);
|
|
expect(p.world.state.bag.tmsHms[51]).toBe(1); // HM02
|
|
reload();
|
|
writeTestSave("cianwood-city");
|
|
}, 240_000);
|
|
|
|
it("brings the SecretPotion to Amphy and beats Jasmine for the MINERALBADGE and TM23", () => {
|
|
traceFailures();
|
|
// Nobody in the party learns Fly; surf back.
|
|
cross("right", ROUTE_41, { water: true });
|
|
cross("up", ROUTE_40, { water: true });
|
|
cross("right", OLIVINE, { water: true });
|
|
heal(OLIVINE_CENTER, OLIVINE);
|
|
enter(LIGHTHOUSE[0], "up");
|
|
expect(route(LIGHTHOUSE[5], LIGHTHOUSE, 12, { x: 8, y: 9 })).toBe(true);
|
|
talkTo(p.world.map.objects[0].id);
|
|
play([1]);
|
|
expect(p.world.state.events.has(EVENT_JASMINE_RETURNED_TO_GYM)).toBe(true);
|
|
expect(p.world.state.bag.keyItems).not.toContain(SECRETPOTION);
|
|
travel(OLIVINE, LIGHTHOUSE);
|
|
heal(OLIVINE_CENTER, OLIVINE);
|
|
enter(OLIVINE_GYM, "up");
|
|
talkTo(p.world.map.objects[0].id);
|
|
play([1]);
|
|
expect(p.world.state.events.has(EVENT_BEAT_JASMINE)).toBe(true);
|
|
expect(p.world.state.flags.has(ENGINE_MINERALBADGE)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_GOT_TM23_IRON_TAIL)).toBe(true);
|
|
expect(p.world.state.bag.tmsHms[22]).toBe(1); // TM23
|
|
exit(OLIVINE);
|
|
reload();
|
|
writeTestSave("olivine-city");
|
|
}, 240_000);
|
|
it("crosses Route 42 past Mt. Mortar to Mahogany Town", () => {
|
|
traceFailures();
|
|
cross("up", ROUTE_39);
|
|
cross("right", ROUTE_38);
|
|
stairs(ROUTE_38_GATE);
|
|
stairs(ECRUTEAK);
|
|
stairs(ROUTE_42_GATE);
|
|
stairs(ROUTE_42);
|
|
// Route 42's pond splits it around Mt. Mortar; Krabby surfs across.
|
|
cross("right", MAHOGANY, { water: true });
|
|
expect(p.world.state.events.has(EVENT_BEAT_POKEMANIAC_SHANE)).toBe(true);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("pays the Rockets' toll on Route 43 and stops the Red Gyarados for Lance", () => {
|
|
traceFailures();
|
|
stairs(ROUTE_43_MAHOGANY_GATE);
|
|
stairs(ROUTE_43);
|
|
// Route43GateRocketShakedownScene: ¥1000 to go through.
|
|
const money = p.world.state.money;
|
|
stairs(ROUTE_43_GATE);
|
|
expect(p.log.join(" ")).toContain("The toll is ¥1000");
|
|
expect(p.world.state.money).toBe(money - 1000);
|
|
stairs(ROUTE_43);
|
|
cross("up", LAKE_OF_RAGE);
|
|
// The Gyarados waits in the middle of the lake (18, 22).
|
|
sail(18, 23);
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_LAKE_OF_RAGE_RED_GYARADOS)).toBe(true);
|
|
expect(p.world.state.bag.keyItems).toContain(RED_SCALE);
|
|
// Lance appears on the shore (21, 28).
|
|
sail(21, 29);
|
|
talkTo(p.world.map.objects[0].id);
|
|
play([1]);
|
|
expect(p.world.state.events.has(EVENT_DECIDED_TO_HELP_LANCE)).toBe(true);
|
|
expect(p.log.join(" ")).toContain("from MAHOGANY is\nthe cause.");
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("follows Lance into the Rocket Hideout and learns both passwords", () => {
|
|
traceFailures();
|
|
cross("down", ROUTE_43);
|
|
// Route 43 sets the gate's shakedown scene again on every visit.
|
|
stairs(ROUTE_43_GATE);
|
|
stairs(ROUTE_43);
|
|
stairs(ROUTE_43_MAHOGANY_GATE);
|
|
stairs(MAHOGANY);
|
|
heal(MAHOGANY_CENTER, MAHOGANY);
|
|
// MahoganyMart1FLanceUncoversStaircaseScript: Dragonite's Hyper Beam.
|
|
enter(MAHOGANY_SHOP, "up");
|
|
expect(p.world.state.events.has(EVENT_UNCOVERED_STAIRCASE_IN_MAHOGANY_MART)).toBe(true);
|
|
// B1F's cameras and exploding traps lie on the way down.
|
|
stairs(ROCKET_HIDEOUT[0]);
|
|
stairs(ROCKET_HIDEOUT[1]);
|
|
walk(5, 14); // LanceHealsScript1
|
|
expect(p.world.state.events.has(EVENT_LANCE_HEALED_YOU_IN_TEAM_ROCKET_BASE)).toBe(true);
|
|
// B3F: the grunts tell their passwords when talked to again.
|
|
stairs(ROCKET_HIDEOUT[2]);
|
|
expect(p.log.join(" ")).toContain("two passwords to");
|
|
const [, , , slowpokeTail, raticateTail] = p.world.map.objects.map((object) => object.id);
|
|
for (const grunt of [slowpokeTail, slowpokeTail, raticateTail, raticateTail]) {
|
|
talkTo(grunt);
|
|
play();
|
|
}
|
|
expect(p.world.state.events.has(EVENT_LEARNED_SLOWPOKETAIL)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_LEARNED_RATICATE_TAIL)).toBe(true);
|
|
// The boss's door is reached from B2F's other stairs; the rival
|
|
// waits on the way (RocketBaseRival).
|
|
expect(route(ROCKET_HIDEOUT[2], ROCKET_HIDEOUT, 6, { x: 10, y: 10 })).toBe(true);
|
|
walk(10, 10);
|
|
expect(p.world.state.events.has(EVENT_RIVAL_TEAM_ROCKET_BASE)).toBe(true);
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
play();
|
|
walk(10, 8); // RocketBaseBossLeft
|
|
expect(p.world.state.events.has(EVENT_BEAT_ROCKET_EXECUTIVEM_4)).toBe(true);
|
|
talkTo(p.world.map.objects[2].id); // the Murkrow
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_LEARNED_HAIL_GIOVANNI)).toBe(true);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("beats the executive with Lance and the Electrodes for HM06", () => {
|
|
traceFailures();
|
|
expect(route(ROCKET_HIDEOUT[1], ROCKET_HIDEOUT, 6, { x: 14, y: 13 })).toBe(true);
|
|
walk(14, 13);
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
play();
|
|
// RocketBaseBossFRight's coordinate event is two steps in.
|
|
p.world.move("up");
|
|
play();
|
|
p.world.move("up");
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_BEAT_ROCKET_EXECUTIVEF_2)).toBe(true);
|
|
// Lance takes the right-hand Electrodes; the player the left three.
|
|
for (let round = 0; round < 6; round++) {
|
|
const left = p.world.map.objects
|
|
.slice(4, 7)
|
|
.map((object) => object.id)
|
|
.filter((id) => p.world.objects().some((object) => object.id === id));
|
|
if (!left.length) break;
|
|
talkTo(left[0]);
|
|
play();
|
|
}
|
|
expect(p.world.state.events.has(EVENT_CLEARED_ROCKET_HIDEOUT)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_GOT_HM06_WHIRLPOOL)).toBe(true);
|
|
expect(p.world.state.bag.tmsHms[55]).toBe(1); // HM06
|
|
travel(MAHOGANY_SHOP, ROCKET_HIDEOUT);
|
|
exit(MAHOGANY);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("slides across Pryce's ice for the GLACIERBADGE, and the Rockets take the Radio Tower", () => {
|
|
traceFailures();
|
|
heal(MAHOGANY_CENTER, MAHOGANY);
|
|
enter(MAHOGANY_GYM, "up");
|
|
talkTo(p.world.map.objects[0].id);
|
|
play([1]);
|
|
expect(p.world.state.events.has(EVENT_BEAT_PRYCE)).toBe(true);
|
|
expect(p.world.state.flags.has(ENGINE_GLACIERBADGE)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_GOT_TM16_ICY_WIND)).toBe(true);
|
|
expect(p.world.state.bag.tmsHms[15]).toBe(1); // TM16
|
|
// The seventh badge runs RadioTowerRocketsScript.
|
|
expect(p.world.state.flags.has(ENGINE_ROCKETS_IN_RADIO_TOWER)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_RADIO_TOWER_ROCKET_TAKEOVER)).toBe(false);
|
|
exit(MAHOGANY);
|
|
reload();
|
|
writeTestSave("mahogany-town");
|
|
}, 240_000);
|
|
/** Mahogany to Goldenrod the long way round: Route 42, Ecruteak,
|
|
* Routes 37 and 36, the National Park and Route 35. */
|
|
const mahoganyToGoldenrod = () => {
|
|
cross("left", ROUTE_42, { water: true });
|
|
sail(2, 8); // Route 42's pond, then ashore by the gate
|
|
stairs(ROUTE_42_GATE);
|
|
stairs(ECRUTEAK);
|
|
cross("down", ROUTE_37);
|
|
cross("down", ROUTE_36);
|
|
stairs(ROUTE_36_NATIONAL_PARK_GATE);
|
|
stairs(NATIONAL_PARK);
|
|
stairs(ROUTE_35_NATIONAL_PARK_GATE);
|
|
stairs(ROUTE_35);
|
|
stairs(ROUTE_35_GATE);
|
|
stairs(GOLDENROD);
|
|
};
|
|
|
|
it("walks back to Goldenrod and climbs the Rocket-held Radio Tower to the fake director", () => {
|
|
traceFailures();
|
|
mahoganyToGoldenrod();
|
|
heal(GOLDENROD_CENTER, GOLDENROD);
|
|
enter(RADIO_TOWER[0], "up");
|
|
for (const floor of RADIO_TOWER.slice(1)) stairs(floor);
|
|
walk(0, 3); // FakeDirectorScript
|
|
expect(p.world.state.events.has(EVENT_BEAT_ROCKET_EXECUTIVEM_3)).toBe(true);
|
|
expect(p.world.state.bag.keyItems).toContain(BASEMENT_KEY);
|
|
expect(p.log.join(" ")).toContain("UNDERGROUND\nWAREHOUSE.");
|
|
travel(GOLDENROD, RADIO_TOWER);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("opens the basement door, works the Underground switches and frees the director", () => {
|
|
traceFailures();
|
|
// Fresh PP before the rival waiting in the switch room.
|
|
heal(GOLDENROD_CENTER, GOLDENROD);
|
|
stairs(SWITCH_ROOM);
|
|
stairs(UNDERGROUND);
|
|
walk(18, 7);
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_USED_BASEMENT_KEY)).toBe(true);
|
|
// The door leads to the Underground's other corner.
|
|
p.world.move("up");
|
|
play();
|
|
stairs(SWITCH_ROOM);
|
|
// From all off, switches 3, 2 and 1 leave doors 3, 5, 6, 8, 9 and 11
|
|
// open: a way from the switches down to the warehouse.
|
|
for (const [x, y] of [[2, 1], [10, 1], [16, 1]]) {
|
|
walk(x, y + 1);
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
play([1]);
|
|
}
|
|
expect(p.world.save().memory).toMatchObject({ wUndergroundSwitchPositions: 6 });
|
|
expect(p.world.state.events.has(EVENT_RIVAL_GOLDENROD_UNDERGROUND)).toBe(true);
|
|
stairs(WAREHOUSE);
|
|
// The warehouse resets the switches when entered.
|
|
expect(p.world.save().memory).toMatchObject({ wUndergroundSwitchPositions: 0 });
|
|
talkTo(p.world.map.objects[3].id);
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_RECEIVED_CARD_KEY)).toBe(true);
|
|
expect(p.world.state.bag.keyItems).toContain(CARD_KEY);
|
|
// Out through the department store's basement and elevator.
|
|
stairs(DEPT_STORE_B1F);
|
|
stairs(ELEVATOR);
|
|
walk(3, 1);
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
play([1]); // 1F
|
|
walk(2, 3);
|
|
p.world.move("down");
|
|
play();
|
|
expect(p.world.mapId).toBe(DEPT_STORE_1F);
|
|
exit(GOLDENROD);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("opens the 3F shutters with the Card Key and beats the executive for the Clear Bell", () => {
|
|
traceFailures();
|
|
heal(GOLDENROD_CENTER, GOLDENROD);
|
|
enter(RADIO_TOWER[0], "up");
|
|
stairs(RADIO_TOWER[1]);
|
|
stairs(RADIO_TOWER[2]);
|
|
walk(14, 3); // CardKeySlotScript faces up
|
|
p.world.move("up");
|
|
p.world.interact();
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_USED_THE_CARD_KEY_IN_THE_RADIO_TOWER)).toBe(true);
|
|
walk(17, 0); // the stairs behind the shutters
|
|
expect(p.world.mapId).toBe(RADIO_TOWER[3]);
|
|
stairs(RADIO_TOWER[4]);
|
|
walk(16, 5); // RadioTower5FRocketBossScript
|
|
expect(p.world.state.events.has(EVENT_CLEARED_RADIO_TOWER)).toBe(true);
|
|
expect(p.world.state.flags.has(ENGINE_ROCKETS_IN_RADIO_TOWER)).toBe(false);
|
|
expect(p.world.state.events.has(EVENT_GOT_CLEAR_BELL)).toBe(true);
|
|
expect(p.world.state.bag.keyItems).toContain(CLEAR_BELL);
|
|
// The director walks out and disappears; a save keeps him gone.
|
|
reload();
|
|
travel(RADIO_TOWER[0], RADIO_TOWER.slice(1));
|
|
exit(GOLDENROD);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("crosses Route 44 and Ice Path's boulders to Blackthorn City", () => {
|
|
traceFailures();
|
|
toRoute36();
|
|
cross("up", ROUTE_37);
|
|
cross("up", ECRUTEAK);
|
|
stairs(ROUTE_42_GATE);
|
|
stairs(ROUTE_42);
|
|
cross("right", MAHOGANY, { water: true });
|
|
heal(MAHOGANY_CENTER, MAHOGANY);
|
|
cross("right", ROUTE_44);
|
|
stairs(ICE_PATH[0]);
|
|
stairs(ICE_PATH[1]);
|
|
// B1F: drop boulder 2 down its hole, then fall through another; on
|
|
// B2F's ice it stops a slide on the way to B3F.
|
|
p.boulders([{ id: 3, x: 4, y: 7 }], [2]);
|
|
expect(p.world.state.events.has(EVENT_BOULDER_IN_ICE_PATH_2A)).toBe(false);
|
|
walk(5, 12);
|
|
expect(p.world.mapId).toBe(ICE_PATH[2]);
|
|
walk(9, 11);
|
|
expect(p.world.mapId).toBe(ICE_PATH[4]);
|
|
travel(BLACKTHORN, [ICE_PATH[0], ICE_PATH[1], ICE_PATH[3], ICE_PATH[4]]);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("trains on Route 45, drops the gym's boulders and beats Clair", () => {
|
|
traceFailures();
|
|
// Clair's Kingdra outclasses a level 53 lead; a player trains first.
|
|
cross("down", ROUTE_45);
|
|
train(62, () => {
|
|
cross("up", BLACKTHORN);
|
|
heal(BLACKTHORN_CENTER, BLACKTHORN);
|
|
cross("down", ROUTE_45);
|
|
});
|
|
cross("up", BLACKTHORN);
|
|
enter(BLACKTHORN_MART, "up");
|
|
buy(p.world.map.objects[0].id, C.MAX_POTION, 4);
|
|
exit(BLACKTHORN);
|
|
heal(BLACKTHORN_CENTER, BLACKTHORN);
|
|
enter(BLACKTHORN_GYM[0], "up");
|
|
stairs(BLACKTHORN_GYM[1]);
|
|
// Boulders 1 and 3 fall to 1F and bridge the way to Clair.
|
|
p.boulders(
|
|
[
|
|
{ id: 4, x: 8, y: 3 },
|
|
{ id: 6, x: 8, y: 7 },
|
|
],
|
|
[5, 7, 8, 9],
|
|
);
|
|
expect(route(BLACKTHORN_GYM[0], BLACKTHORN_GYM, 4, { x: 5, y: 4 })).toBe(true);
|
|
talkTo(p.world.map.objects[0].id);
|
|
play([1]);
|
|
expect(p.world.state.events.has(EVENT_BEAT_CLAIR)).toBe(true);
|
|
expect(p.world.state.flags.has(ENGINE_RISINGBADGE)).toBe(false);
|
|
expect(p.log.join(" ")).toContain("DRAGON'S DEN");
|
|
travel(BLACKTHORN, BLACKTHORN_GYM);
|
|
reload();
|
|
}, 360_000);
|
|
|
|
it("passes the Dragon Shrine's test for the RISINGBADGE, TM24 and Dratini", () => {
|
|
traceFailures();
|
|
heal(BLACKTHORN_CENTER, BLACKTHORN);
|
|
// Krabby learns Whirlpool in place of Leer for Dragon's Den.
|
|
p.teach(HM06, 4, 1);
|
|
expect(p.world.state.party[4].moves).toContain(250);
|
|
const den = p.world.map.warps.find((warp) => warp.targetMap === DRAGONS_DEN[0])!;
|
|
sail(den.x, den.y + 1);
|
|
p.world.move("up");
|
|
play();
|
|
walk(3, 4); // the ladder down to 1F's lower part
|
|
p.world.move("up");
|
|
play();
|
|
stairs(DRAGONS_DEN[1]);
|
|
sail(19, 31, { whirlpools: true });
|
|
walk(19, 30);
|
|
// DragonShrineTakeTestScript: five questions, all answered right.
|
|
p.world.move("up");
|
|
play([0, 0, 1, 0, 1]);
|
|
expect(p.world.mapId).toBe(DRAGON_SHRINE);
|
|
expect(p.world.state.flags.has(ENGINE_RISINGBADGE)).toBe(true);
|
|
expect(p.world.state.events.has(EVENT_ANSWERED_DRAGON_MASTER_QUIZ_WRONG)).toBe(false);
|
|
// Clair waits by the door with TM24 (DragonsDenB1F_ClairScene).
|
|
stairs(DRAGONS_DEN[1]);
|
|
expect(p.world.state.events.has(EVENT_GOT_TM24_DRAGONBREATH)).toBe(true);
|
|
expect(p.world.state.bag.tmsHms[23]).toBe(1); // TM24
|
|
// Back inside, the elder gives Dratini with Extremespeed.
|
|
p.world.move("up");
|
|
play();
|
|
talkTo(p.world.map.objects[0].id);
|
|
play();
|
|
expect(p.world.state.events.has(EVENT_GOT_DRATINI)).toBe(true);
|
|
const dratini = p.world.state.party.at(-1)!;
|
|
expect(dratini).toMatchObject({ species: DRATINI, level: 15 });
|
|
expect(dratini.moves).toEqual([35, 86, 239, EXTREMESPEED]);
|
|
expect(dratini.pp).toEqual([20, 20, 20, 5]);
|
|
// Out: the whirlpool is back after the map reload.
|
|
stairs(DRAGONS_DEN[1]);
|
|
sail(20, 4, { whirlpools: true });
|
|
p.world.move("up");
|
|
play();
|
|
walk(5, 14);
|
|
p.world.move("up");
|
|
play();
|
|
stairs(BLACKTHORN);
|
|
sail(17, 14);
|
|
reload();
|
|
writeTestSave("blackthorn-city");
|
|
}, 240_000);
|
|
it("takes Routes 45 and 46 down to Route 29 and walks back to Ecruteak", () => {
|
|
traceFailures();
|
|
// From here on the driver plans ledge jumps (Route 45, Tin Tower).
|
|
p.ledges = true;
|
|
heal(BLACKTHORN_CENTER, BLACKTHORN);
|
|
cross("down", ROUTE_45);
|
|
// Route 45's ledges only lead down; Route 46 joins it on the west.
|
|
cross("left", ROUTE_46);
|
|
stairs(ROUTE_29_ROUTE_46_GATE);
|
|
stairs(ROUTE_29);
|
|
cross("left", CHERRYGROVE);
|
|
cross("up", ROUTE_30);
|
|
cross("up", ROUTE_31);
|
|
stairs(ROUTE_31_GATE);
|
|
stairs(VIOLET);
|
|
cross("left", ROUTE_36);
|
|
cross("up", ROUTE_37);
|
|
cross("up", ECRUTEAK);
|
|
reload();
|
|
}, 240_000);
|
|
|
|
it("rings the Clear Bell for the sages and faces Suicune in Tin Tower", () => {
|
|
traceFailures();
|
|
heal(ECRUTEAK_CENTER, ECRUTEAK);
|
|
enter(TIN_TOWER_ENTRANCE, "up");
|
|
// EcruteakTinTowerEntranceSageBlocks: a sage steps in the way; told
|
|
// about the Clear Bell, he lets the player through.
|
|
walk(5, 8);
|
|
p.world.move("up");
|
|
play();
|
|
if (p.world.save().direction !== "up") p.world.move("up");
|
|
p.world.interact();
|
|
play();
|
|
expect(p.log.join(" ")).toContain("The sound of that\nCLEAR BELL!");
|
|
expect(p.world.state.events.has(EVENT_RANG_CLEAR_BELL_2)).toBe(true);
|
|
walk(5, 4); // the stairs at (5, 3) lead to the building's far side
|
|
p.world.move("up");
|
|
play();
|
|
stairs(WISE_TRIOS_ROOM);
|
|
stairs(ECRUTEAK);
|
|
// TinTower1FSuicuneBattleScript: Raikou and Entei run; Suicune stays.
|
|
stairs(TIN_TOWER_1F);
|
|
expect(p.world.state.events.has(EVENT_FOUGHT_SUICUNE)).toBe(true);
|
|
expect(p.log.join(" ")).toContain("SUICUNE");
|
|
expect(p.log.join(" ")).toContain("rainbow-");
|
|
reload();
|
|
writeTestSave("tin-tower");
|
|
}, 240_000);
|
|
},
|
|
);
|