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 { 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", () => { const 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(); 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", () => { const 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(); }); });