Checks / web (push) Canceled after 0s
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>
102 lines
3.7 KiB
TypeScript
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);
|
|
});
|
|
},
|
|
);
|