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>
138 lines
5.4 KiB
TypeScript
138 lines
5.4 KiB
TypeScript
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);
|
|
});
|
|
});
|