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"); }); });