Enable Route 32 and its Pokémon Center, the Ruins of Alph with the four chambers, item and word rooms, inner chamber and research center, Union Cave, Route 33, Azalea Town with Kurt's house, the kiln and Bugsy's gym, the Slowpoke Well, Ilex Forest and Route 34. Port what their scripts need: saved WRAM bytes by label (readmem, writemem, loadmem: wFarfetchdPosition, wKurtApricornQuantity), VAR_UNOWNCOUNT and VAR_KURT_APRICORNS, SelectApricornForKurt and verbosegiveitemvar, Unown letters (GetUnownLetter, per-letter pictures, CheckUnownLetter's redraw, UpdateUnownDex, wFirstUnownSeen), the chamber puzzles (_UnownPuzzle), DisplayUnownWords, HoOhChamber and OmanyteChamber. Minigames and specials that draw a whole screen in tiles now share a tile screen effect with ROM graphics, palettes and sprites, answered with buttons; the standard font and textbox frame load for them. The playthrough continues through Kurt's Slowpoke Well, Bugsy, the rival and the Farfetch'd to the Route 34 gate; its helpers move to tests/playthrough-driver.ts. New tests solve the Kabuto puzzle through the world, catch Unown and make Kurt's balls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
6.0 KiB
TypeScript
141 lines
6.0 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_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: 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();
|
|
});
|
|
});
|