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>
144 lines
6.2 KiB
TypeScript
144 lines
6.2 KiB
TypeScript
import { beforeAll, 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 { flypoints, landmarkPosition, regionAtlas } from "../src/game/world/atlas";
|
|
import { defaultTextContext } from "../src/game/scripts/text";
|
|
import { OW } from "../src/game/world/constants";
|
|
|
|
const romPath =
|
|
process.env.CRYSTAL_ROM_PATH ??
|
|
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
|
|
const pret = ".reference/pokecrystal";
|
|
const NEW_BARK_TOWN = "24:4",
|
|
LANDMARK_NEW_BARK_TOWN = 1,
|
|
LANDMARK_ROUTE_29 = 2,
|
|
LANDMARK_CHERRYGROVE_CITY = 3,
|
|
LANDMARK_VIOLET_CITY = 6,
|
|
LANDMARK_AZALEA_TOWN = 12,
|
|
LANDMARK_BLACKTHORN_CITY = 0x29,
|
|
LANDMARK_SILVER_CAVE = 0x2e,
|
|
LANDMARK_PALLET_TOWN = 47;
|
|
|
|
describe.skipIf(!existsSync(romPath) || !existsSync(`${pret}/data/maps/landmarks.asm`))(
|
|
"the atlas data against the pinned PRET source",
|
|
() => {
|
|
let assets!: CrystalAssets;
|
|
beforeAll(() => {
|
|
assets = new CrystalAssets(new Uint8Array(readFileSync(romPath)));
|
|
});
|
|
const read = (path: string) => readFileSync(`${pret}/${path}`, "utf8");
|
|
// const_def counts from 0, or from the value given.
|
|
const constants = (path: string, prefix: string) => {
|
|
const values = new Map<string, number>();
|
|
let next = 0;
|
|
for (const [, start, name] of read(path).matchAll(/^\s+(?:const_def(?: (\d+))?|const (\w+))/gm)) {
|
|
if (name === undefined) next = Number(start ?? 0);
|
|
else values.set(name, next++);
|
|
}
|
|
return new Map([...values].filter(([name]) => name.startsWith(prefix)));
|
|
};
|
|
|
|
it("places every landmark where data/maps/landmarks.asm does", () => {
|
|
const rows = [...read("data/maps/landmarks.asm").matchAll(/^\s+landmark\s+(-?\d+),\s*(-?\d+),/gm)];
|
|
expect(rows).toHaveLength(OW.LANDMARK_FAST_SHIP + 1);
|
|
rows.forEach(([, x, y], landmark) =>
|
|
expect(landmarkPosition(assets.reader, landmark), String(landmark)).toEqual({
|
|
x: Number(x),
|
|
y: Number(y),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("reads the flypoints of data/maps/flypoints.asm", () => {
|
|
const landmarks = constants("constants/landmark_constants.asm", "LANDMARK_");
|
|
const spawns = constants("constants/map_data_constants.asm", "SPAWN_");
|
|
const rows = [...read("data/maps/flypoints.asm").matchAll(/^\s+db (LANDMARK_\w+),\s*(SPAWN_\w+)/gm)].map(
|
|
([, landmark, spawn]) => ({ landmark: landmarks.get(landmark), spawn: spawns.get(spawn) }),
|
|
);
|
|
expect(rows).toHaveLength(OW.NUM_FLYPOINTS);
|
|
expect(flypoints(assets.reader)).toEqual(rows);
|
|
});
|
|
},
|
|
);
|
|
|
|
describe.skipIf(!existsSync(romPath))("the region atlas", () => {
|
|
let assets!: CrystalAssets;
|
|
beforeAll(() => {
|
|
assets = new CrystalAssets(new Uint8Array(readFileSync(romPath)));
|
|
});
|
|
function fixture(visited: number[]) {
|
|
const world = new GameWorld(assets, () => new Date("2026-09-25T12:00:00"), () => 0x42);
|
|
world.state.party = [
|
|
generatePartyMon(assets.pokemon, {
|
|
species: 155, level: 8, item: 0, otId: world.playerId,
|
|
ot: "GOLD", dvs: 0x9abc, caughtData: 0,
|
|
}),
|
|
];
|
|
world.restore({ ...world.save(), map: NEW_BARK_TOWN, x: 13, y: 7, direction: "down", objects: [] });
|
|
for (const spawn of visited) world.state.flags.add(assets.spawnEngineFlag(spawn)!);
|
|
return world;
|
|
}
|
|
const spawnOf = (landmark: number) =>
|
|
flypoints(assets.reader).find((point) => point.landmark === landmark)!.spawn;
|
|
|
|
it("shows Johto with the player, the visited flypoints and the open areas", () => {
|
|
const world = fixture([spawnOf(LANDMARK_NEW_BARK_TOWN), spawnOf(LANDMARK_CHERRYGROVE_CITY)]);
|
|
const atlas = world.atlas();
|
|
expect(atlas).toMatchObject({ region: "johto", regions: ["johto"], player: LANDMARK_NEW_BARK_TOWN });
|
|
expect(atlas.landmarks.map((landmark) => landmark.landmark)).toEqual(
|
|
Array.from({ length: OW.JOHTO_LANDMARK_LAST }, (_, i) => i + 1),
|
|
);
|
|
const at = (landmark: number) => atlas.landmarks[landmark - 1];
|
|
expect(at(LANDMARK_NEW_BARK_TOWN)).toMatchObject({
|
|
name: "NEW BARK TOWN",
|
|
open: true,
|
|
flypoint: { visited: true },
|
|
});
|
|
expect(at(LANDMARK_CHERRYGROVE_CITY).flypoint?.visited).toBe(true);
|
|
expect(at(LANDMARK_VIOLET_CITY)).toMatchObject({ open: true, flypoint: { visited: false } });
|
|
expect(at(LANDMARK_ROUTE_29)).toMatchObject({ open: true });
|
|
expect(at(LANDMARK_ROUTE_29).flypoint).toBeUndefined();
|
|
expect(at(LANDMARK_AZALEA_TOWN)).toMatchObject({ open: true, flypoint: { visited: false } });
|
|
expect(at(LANDMARK_BLACKTHORN_CITY)).toMatchObject({ open: true, flypoint: { visited: false } });
|
|
// Mt. Silver's cave belongs to Kanto's postgame (board #33).
|
|
expect(at(LANDMARK_SILVER_CAVE)).toMatchObject({ open: false, flypoint: { visited: false } });
|
|
// Kanto stays closed, so asking for it shows Johto.
|
|
expect(world.atlas("kanto").region).toBe("johto");
|
|
});
|
|
|
|
it("opens Kanto once Indigo Plateau is visited, without the player", () => {
|
|
const world = fixture([OW.SPAWN_INDIGO]);
|
|
expect(world.atlas().regions).toEqual(["johto", "kanto"]);
|
|
const kanto = world.atlas("kanto");
|
|
expect(kanto.region).toBe("kanto");
|
|
expect(kanto.player).toBeUndefined();
|
|
expect(kanto.landmarks[0]).toMatchObject({ landmark: LANDMARK_PALLET_TOWN, open: false });
|
|
expect(kanto.landmarks).toHaveLength(OW.KANTO_LANDMARK_LAST - OW.KANTO_LANDMARK + 1);
|
|
});
|
|
|
|
it("starts on Kanto when the player stands there", () => {
|
|
const atlas = regionAtlas({
|
|
reader: assets.reader,
|
|
text: () => defaultTextContext("GOLD"),
|
|
landmark: LANDMARK_PALLET_TOWN,
|
|
visited: () => false,
|
|
open: new Set(),
|
|
});
|
|
expect(atlas).toMatchObject({ region: "kanto", regions: ["johto", "kanto"], player: LANDMARK_PALLET_TOWN });
|
|
});
|
|
|
|
it("leaves the Fast Ship's landmark outside both regions", () => {
|
|
const ship = regionAtlas({
|
|
reader: assets.reader,
|
|
text: () => defaultTextContext("GOLD"),
|
|
landmark: OW.LANDMARK_FAST_SHIP,
|
|
visited: () => false,
|
|
open: new Set(),
|
|
});
|
|
expect(ship.region).toBe("johto");
|
|
expect(ship.player).toBeUndefined();
|
|
});
|
|
});
|