Files
Crystal/tests/human-test-saves.test.ts
T
Sébastien QUEROLandClaude Opus 5.5 f259163da9 Port Headbutt and Rock Smash encounters
Facing a Headbutt tree asks to HEADBUTT it, and HEADBUTT from the party
menu skips the question. ShakeHeadbuttTree shakes the tree as a sprite over
grass, then TreeMonEncounter scores the facing tile against the trainer ID
and draws from the map's common or rare tree table. A Pokémon that falls
fights as BATTLETYPE_TREE: "fell out of the tree!", and the time of day's
AsleepTreeMons species start asleep.

Smashable rocks run SmashRockScript, and ROCK SMASH from the party menu
acts on the facing rock; after the rock breaks, RockMonEncounter gives the
map's rock set a 40% chance of a wild battle.

Closes #26

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

77 lines
2.9 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 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);
});
},
);