Open Tin Tower and Whirl Islands: Suicune, Ho-Oh and Lugia (#32)
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>
This commit is contained in:
1 parent
9c3579937c
commit
3fbc6798eb
10 files changed
+369
-21
No files matched your search
@@ -0,0 +1,144 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { CrystalAssets } from "../src/game/rom/assets";
|
||||
import { GameWorld } from "../src/game/world/world";
|
||||
import { generatePartyMon } from "../src/game/party/pokemon";
|
||||
import { toBoxMon } from "../src/game/party/storage";
|
||||
import { Playthrough } from "./playthrough-driver";
|
||||
|
||||
const romPath =
|
||||
process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
|
||||
|
||||
describe.skipIf(!existsSync(romPath))("Tin Tower and Whirl Islands with the local ROM", () => {
|
||||
const assets = existsSync(romPath)
|
||||
? new CrystalAssets(new Uint8Array(readFileSync(romPath)))
|
||||
: undefined;
|
||||
|
||||
const TIN_TOWER = ["3:4", "3:5", "3:6", "3:7", "3:8", "3:9", "3:10", "3:11", "3:12"],
|
||||
TIN_TOWER_ROOF = "15:12",
|
||||
ROUTE_41 = "22:2",
|
||||
WHIRL_ISLANDS = ["3:66", "3:67", "3:68", "3:69", "3:70", "3:71", "3:72"],
|
||||
LUGIA_CHAMBER = "3:73";
|
||||
const TYPHLOSION = 157,
|
||||
FERALIGATR = 160,
|
||||
RAIKOU = 243,
|
||||
ENTEI = 244,
|
||||
SUICUNE = 245,
|
||||
SURF = 57,
|
||||
WHIRLPOOL = 250,
|
||||
WATERFALL = 127,
|
||||
RAINBOW_WING = 0xb2,
|
||||
SILVER_WING = 0x47;
|
||||
const EVENT_FOUGHT_SUICUNE = 0x335,
|
||||
EVENT_BEAT_ELITE_FOUR = 0x44,
|
||||
EVENT_GOT_RAINBOW_WING = 0x336,
|
||||
EVENT_FOUGHT_HO_OH = 0x317,
|
||||
EVENT_FOUGHT_LUGIA = 0x318,
|
||||
SCENE_TINTOWER1F_NOOP = 1;
|
||||
/** TINTOWER1F_RAIKOU and TINTOWER1F_ENTEI (objects 2 and 3). */
|
||||
const RAIKOU_OBJECT = 3,
|
||||
ENTEI_OBJECT = 4;
|
||||
|
||||
const mon = (world: GameWorld, species: number, { level = 100, ot = "KAWA", otId = world.playerId } = {}) =>
|
||||
generatePartyMon(assets!.pokemon, {
|
||||
species,
|
||||
level,
|
||||
item: 0,
|
||||
otId,
|
||||
ot,
|
||||
dvs: 0xffff,
|
||||
caughtData: 0,
|
||||
});
|
||||
/** A playthrough driver on a fresh world named KAWA with all badges. */
|
||||
const driver = () => {
|
||||
const p = new Playthrough(assets!);
|
||||
p.world = new GameWorld(assets!, p.now, p.random);
|
||||
// Tin Tower's floors are joined by ledges.
|
||||
p.ledges = true;
|
||||
for (let badge = 0x1b; badge <= 0x22; badge++) p.world.state.flags.add(badge);
|
||||
return p;
|
||||
};
|
||||
|
||||
it("hides the beasts the player owns, by ID and the OT's first five letters (MonCheck)", () => {
|
||||
const p = driver();
|
||||
const { world } = p;
|
||||
world.name = "KRISTAL";
|
||||
const name = world.name;
|
||||
world.state.party = [mon(world, TYPHLOSION, { ot: name })];
|
||||
const beasts = () => {
|
||||
world.warpTo(TIN_TOWER[0], 9, 14);
|
||||
world.state.scenes[TIN_TOWER[0]] = SCENE_TINTOWER1F_NOOP;
|
||||
p.play();
|
||||
return [RAIKOU_OBJECT, ENTEI_OBJECT].map((id) =>
|
||||
world.objects().some((object) => object.id === id),
|
||||
);
|
||||
};
|
||||
expect(beasts()).toEqual([true, true]);
|
||||
// A Raikou in a box with the player's ID and OT counts.
|
||||
world.state.storage.boxes[3].push(toBoxMon(mon(world, RAIKOU, { ot: name })));
|
||||
expect(beasts()).toEqual([false, true]);
|
||||
// CheckOwnMon compares only five letters of the OT.
|
||||
world.state.party.push(mon(world, ENTEI, { ot: "KRISTOF" }));
|
||||
expect(beasts()).toEqual([false, false]);
|
||||
// Another trainer's ID does not.
|
||||
world.state.party.pop();
|
||||
world.state.party.push(mon(world, ENTEI, { ot: name, otId: world.playerId ^ 1 }));
|
||||
expect(beasts()).toEqual([false, true]);
|
||||
// Without any party Pokémon, CheckOwnMonAnywhere finds nothing.
|
||||
world.state.party = [];
|
||||
expect(beasts()).toEqual([true, true]);
|
||||
});
|
||||
|
||||
it("gives the Rainbow Wing after the League with all three beasts, and Ho-Oh waits on the roof", () => {
|
||||
const p = driver();
|
||||
const { world } = p;
|
||||
const name = world.name;
|
||||
world.state.party = [
|
||||
mon(world, TYPHLOSION, { ot: name }),
|
||||
mon(world, RAIKOU, { ot: name }),
|
||||
mon(world, ENTEI, { ot: name }),
|
||||
mon(world, SUICUNE, { ot: name }),
|
||||
];
|
||||
world.state.events.add(EVENT_FOUGHT_SUICUNE);
|
||||
world.state.events.add(EVENT_BEAT_ELITE_FOUR);
|
||||
world.state.scenes[TIN_TOWER[0]] = SCENE_TINTOWER1F_NOOP;
|
||||
world.warpTo(TIN_TOWER[0], 9, 14);
|
||||
p.play();
|
||||
// BeastsCheck: the second trio appears; the one at (9, 1) has the wing.
|
||||
p.talkTo(world.map.objects[8].id);
|
||||
p.play();
|
||||
expect(world.state.events.has(EVENT_GOT_RAINBOW_WING)).toBe(true);
|
||||
expect(world.state.bag.keyItems).toContain(RAINBOW_WING);
|
||||
// The stairs it reveals lead up through the floors to the roof.
|
||||
p.travel(TIN_TOWER_ROOF, TIN_TOWER.slice(1));
|
||||
expect(p.world.mapId).toBe(TIN_TOWER_ROOF);
|
||||
p.talkTo(p.world.map.objects[0].id);
|
||||
p.play();
|
||||
expect(p.world.notice, p.world.diagnostic).toBe("");
|
||||
expect(p.world.state.events.has(EVENT_FOUGHT_HO_OH)).toBe(true);
|
||||
expect(p.log.join(" ")).toContain("HO-OH");
|
||||
});
|
||||
|
||||
it("leads through Whirl Islands to Lugia with the Silver Wing", () => {
|
||||
const p = driver();
|
||||
const { world } = p;
|
||||
const feraligatr = mon(world, FERALIGATR);
|
||||
feraligatr.moves = [SURF, WHIRLPOOL, WATERFALL, 0];
|
||||
feraligatr.pp = [15, 15, 15, 0];
|
||||
world.state.party = [mon(world, TYPHLOSION), feraligatr];
|
||||
world.state.bag.keyItems.push(SILVER_WING);
|
||||
// On the island in front of the north-west entrance.
|
||||
world.warpTo(ROUTE_41, 12, 18);
|
||||
p.play();
|
||||
p.travel(LUGIA_CHAMBER, [ROUTE_41, ...WHIRL_ISLANDS], { water: true });
|
||||
expect(p.world.mapId).toBe(LUGIA_CHAMBER);
|
||||
// Lugia waits in the pool at (9, 5).
|
||||
p.sail(9, 6);
|
||||
if (p.world.save().direction !== "up") p.world.move("up");
|
||||
p.world.interact();
|
||||
p.play();
|
||||
expect(p.world.notice, p.world.diagnostic).toBe("");
|
||||
expect(p.world.state.events.has(EVENT_FOUGHT_LUGIA)).toBe(true);
|
||||
expect(p.log.join(" ")).toContain("LUGIA");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user