The Rocket-held Radio Tower, the Underground's switch room and warehouse, Ice Path, Blackthorn City and its gym, Routes 45 and 46, Dark Cave's Blackthorn entrance, Dragon's Den and the Dragon Shrine are open. The story plays through the Basement Key, the switches and the director, the Clear Bell, Ice Path's falling boulder, Clair's gym and the shrine's test for the RISINGBADGE, TM24 and the elder's Dratini. New specials: GiveDratini and MoveDeletion. A save keeps an object that a script made disappear (or appear against its flag) as Continue does, so the Radio Tower director no longer blocks the 5F stairs after a reload. A whiteout from a battle started in an scall no longer refuses. The playthrough driver pushes boulders into their holes (planned breadth first), clears whirlpools, chooses the move a TM replaces and heals with the strongest potion; the run gains six stages to the Rising Badge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1436 lines
55 KiB
TypeScript
1436 lines
55 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;
|
|
|
|
/** 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);
|
|
},
|
|
);
|