describe.skipIf still runs the describe body to collect tests, so the ROM and PRET reads at describe level failed the synthetic CI job without a ROM (runs 123–133). The reads now happen in beforeAll, as in the other ROM suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
5.9 KiB
TypeScript
139 lines
5.9 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_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: 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();
|
|
});
|
|
});
|