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 { Playthrough } from "./playthrough-driver"; import { findPath } from "./world-driver"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; describe.skipIf(!existsSync(romPath))("the Radio Tower takeover with the local ROM", () => { const assets = existsSync(romPath) ? new CrystalAssets(new Uint8Array(readFileSync(romPath))) : undefined; const SWITCH_ROOM = "3:54", WAREHOUSE = "3:56", RADIO_TOWER_5F = "3:21", GOLDENROD = "11:2"; const TYPHLOSION = 157, SCENE_GOLDENRODUNDERGROUNDSWITCHROOMENTRANCES_NOOP = 1, SCENE_RADIOTOWER5F_ROCKET_BOSS = 1; const EVENT_SWITCH_1 = 0x2d3, EVENT_DOOR_1_OPEN = 0x2d7, EVENT_RADIO_TOWER_ROCKET_TAKEOVER = 0x6ce, EVENT_CLEARED_RADIO_TOWER = 0x21, EVENT_GOT_CLEAR_BELL = 0x78, ENGINE_ROCKETS_IN_RADIO_TOWER = 0x13; /** RADIOTOWER5F_DIRECTOR, the first object (no event flag). */ const DIRECTOR = 2; /** A playthrough driver on a fresh world with one Pokémon at `level`. */ const driver = (level: number) => { const p = new Playthrough(assets!); p.world = new GameWorld(assets!, p.now, p.random); p.world.state.party = [ generatePartyMon(assets!.pokemon, { species: TYPHLOSION, level, item: 0, otId: p.world.playerId, ot: "KAWA", dvs: 0xffff, caughtData: 0, }), ]; return p; }; /** The switches, from switch 1 on the right, and the doors' events. */ const SWITCHES = [ [16, 1], [10, 1], [2, 1], ] as const; const flags = (world: GameWorld) => Array.from({ length: 15 }, (_, i) => +world.state.events.has(EVENT_SWITCH_1 + i)).join(""); it("keeps the switches and doors through a reload until the warehouse resets them", () => { const p = driver(70); const { world } = p; world.state.scenes[SWITCH_ROOM] = SCENE_GOLDENRODUNDERGROUNDSWITCHROOMENTRANCES_NOOP; world.warpTo(SWITCH_ROOM, 23, 3); p.play(); // Every door is shut, so the warehouse (22, 10) is out of reach. expect(() => findPath(world, 22, 10)).toThrow(); for (const [x, y] of [SWITCHES[2], SWITCHES[1], SWITCHES[0]]) { p.walk(x, y + 1); world.move("up"); world.interact(); p.play([1]); } // Positions 3, 5 and 6 in turn: doors 3, 5, 6, 8, 9 and 11 stay open. expect(world.save().memory).toMatchObject({ wUndergroundSwitchPositions: 6 }); expect(flags(world)).toBe("111000101101101"); expect(findPath(world, 22, 10).length).toBeGreaterThan(0); // The tiles callback reopens the doors after a reload. p.reload(); expect(flags(p.world)).toBe("111000101101101"); expect(findPath(p.world, 22, 10).length).toBeGreaterThan(0); p.walk(22, 10); expect(p.world.mapId).toBe(WAREHOUSE); // GoldenrodUndergroundWarehouseResetSwitchesCallback. expect(flags(p.world)).toBe("0".repeat(15)); expect(p.world.save().memory).toMatchObject({ wUndergroundSwitchPositions: 0 }); expect(p.world.state.events.has(EVENT_DOOR_1_OPEN)).toBe(false); }); it("whites out cleanly when the rival in the switch room wins", () => { const p = driver(2); const { world } = p; world.state.money = 3000; world.warpTo(SWITCH_ROOM, 23, 3); p.play(); // UndergroundRivalScene1: the battle runs inside an scall, and the loss // jumps to Script_Whiteout, whose endall drops the caller. world.restore({ ...world.save(), x: 18, y: 4, direction: "right" }); world.move("right"); p.play(); expect(world.notice, world.diagnostic).toBe(""); expect(p.log.join(" ")).toContain("whited\nout!"); expect(world.mapId).not.toBe(SWITCH_ROOM); expect(world.state.money).toBe(1500); expect(world.state.party[0].hp).toBe(world.state.party[0].stats[0]); }); it("keeps the director gone after a save once he walks out on 5F", () => { const p = driver(80); const { world } = p; world.state.flags.add(ENGINE_ROCKETS_IN_RADIO_TOWER); world.state.scenes[RADIO_TOWER_5F] = SCENE_RADIOTOWER5F_ROCKET_BOSS; world.warpTo(RADIO_TOWER_5F, 15, 5); p.play(); p.world.move("right"); p.play(); p.world.move("right"); p.play(); expect(world.state.events.has(EVENT_CLEARED_RADIO_TOWER)).toBe(true); expect(world.state.events.has(EVENT_GOT_CLEAR_BELL)).toBe(true); expect(world.state.events.has(EVENT_RADIO_TOWER_ROCKET_TAKEOVER)).toBe(true); // He has no event flag: disappear only removes his object struct, // which Continue keeps as saved. expect(world.objects().some((object) => object.id === DIRECTOR)).toBe(false); expect(world.save().objects).toContainEqual( expect.objectContaining({ id: DIRECTOR, hidden: true }), ); p.reload(); expect(p.world.objects().some((object) => object.id === DIRECTOR)).toBe(false); // The stairs he walked out by are free. expect(findPath(p.world, 12, 0).length).toBeGreaterThan(0); // A later visit rebuilds the objects from the map: he is back. p.travel(GOLDENROD, ["3:17", "3:18", "3:19", "3:20", "3:21"]); p.world.warpTo(RADIO_TOWER_5F, 0, 1); p.play(); expect(p.world.objects().some((object) => object.id === DIRECTOR)).toBe(true); }); });