Files
Crystal/tests/human-test-saves.test.ts
T
Sébastien QUEROLandClaude Opus 5.5 802397b668
Checks / web (push) Canceled after 0s
Record the field items, fishing and MAIL commits in the session handoff
The handoff lists the four commits for the remaining field items, the
gaps found on the way and the manual test cards, whose backup the
manual-test save writer now produces.

[no-card]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 01:12:26 +02:00

102 lines
3.7 KiB
TypeScript

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 field-items: New Bark Town's shore with rods, MAIL and a Repel", () => {
const a = assets();
// Cyndaquil facing the sea with the Old Rod.
const world = foundationFixture(a, "fishing");
world.state.party.push(
generatePartyMon(a.pokemon, {
species: 16, // PIDGEY
level: 4,
item: 0,
otId: world.playerId,
ot: "CHRIS",
dvs: 0x9abc,
caughtData: 0,
}),
);
world.state.bag.items = [
{ item: 0x12, quantity: 5 }, // POTION
{ item: 0x9e, quantity: 2 }, // FLOWER_MAIL
{ item: 0x14, quantity: 1 }, // REPEL
];
world.state.bag.keyItems.push(0x3b, 0x3d); // GOOD_ROD, SUPER_ROD
expect(world.mapId).toBe("24:4");
write("field-items", 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);
});
},
);