import { describe, expect, it } from "vitest"; import { existsSync, mkdirSync, readFileSync, writeFileSync } 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 { CRYSTAL_ROM } from "../src/rom/validate"; import { encodeSave } from "../src/storage/saves"; import { foundationFixture } from "./foundation-fixture"; // Backups for the manual checks on the project board's "A tester" column, // built from the local ROM and imported through Savegames → Import native // backup. They hold names read from the ROM, so they stay out of the // repository (*.crystal-save.json is ignored). // // MAKE_TEST_SAVES=1 npx vitest run tests/human-test-saves.test.ts const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; const OUT = "test-saves"; /** NativeRuntime.id, which an imported backup must name. */ const CORE_ID = "crystal-native-world-v1"; describe.skipIf(!existsSync(romPath) || !process.env.MAKE_TEST_SAVES)( "saves for manual testing", () => { const assets = () => new CrystalAssets(new Uint8Array(readFileSync(romPath))); function write(name: string, world: GameWorld) { const state = new TextEncoder().encode(JSON.stringify(world.save())); // The backup must load as the app would load it. new GameWorld(assets()).restore(JSON.parse(new TextDecoder().decode(state))); mkdirSync(OUT, { recursive: true }); writeFileSync( `${OUT}/${name}.crystal-save.json`, encodeSave({ version: 1, romHash: CRYSTAL_ROM.sha1, coreId: CORE_ID, savedAt: new Date().toISOString(), state, }), ); } it("writes fly-teleport: New Bark Town with Fly, Teleport and Escape Ropes", () => { const a = assets(); // Pidgeot with Fly, the Storm Badge, New Bark and Cherrygrove visited. const world = foundationFixture(a, "fly"); const abra = generatePartyMon(a.pokemon, { species: 63, // ABRA level: 20, item: 0, otId: world.playerId, ot: "CHRIS", dvs: 0x9abc, caughtData: 0, }); abra.moves = [0x64, 0, 0, 0]; // TELEPORT abra.pp = [20, 0, 0, 0]; world.state.party.push(abra); // Violet City too, whose map is not ported yet. world.state.flags.add(a.spawnEngineFlag(16)!); world.state.lastSpawnMap = "26:3"; // Cherrygrove City world.state.bag.items = [{ item: 0x13, quantity: 3 }]; // ESCAPE ROPE expect(world.mapId).toBe("24:4"); write("fly-teleport", world); }); it("writes headbutt: Route 29 facing a Headbutt tree", () => { // Totodile with Headbutt and Tackle, below the first reachable tree. const world = foundationFixture(assets(), "headbutt"); expect(world.mapId).toBe("24:3"); write("headbutt", world); }); }, );