PRET's crystal and crystal_au builds reproduce both retail cartridges, and every label the profile pins keeps its address in all three; check:reference now verifies that when the rebuilds are present. The revision comes from the header. Revision 1.0's Battle Tower draws only the first 21 trainers and picks its texts' gender from the name's sixth character, checked against each cartridge. A native backup imports across the three, which share every map, event and object. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
55 lines
2.8 KiB
TypeScript
55 lines
2.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { CrystalAssets } from "../src/game/rom/assets";
|
|
import { romRevision } from "../src/game/rom/revision";
|
|
import { CRYSTAL_ROMS, validateRom } from "../src/rom/validate";
|
|
import { emptyBattleTowerState, loadOpponent } from "../src/game/scripts/battle-tower";
|
|
import type { SpecialContext } from "../src/game/scripts/special-context";
|
|
|
|
/** The user's 1.1 cartridge and PRET's rebuilds of the other English ones
|
|
* (`make crystal crystal_au` in .reference/pokecrystal), each skipped when
|
|
* absent. */
|
|
const cartridges = [
|
|
{ revision: "1.1", path: process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc", rom: CRYSTAL_ROMS[0] },
|
|
{ revision: "1.0", path: ".reference/pokecrystal/pokecrystal.gbc", rom: CRYSTAL_ROMS[1] },
|
|
{ revision: "au", path: ".reference/pokecrystal/pokecrystal_au.gbc", rom: CRYSTAL_ROMS[2] },
|
|
] as const;
|
|
|
|
for (const { revision, path, rom } of cartridges)
|
|
describe.skipIf(!existsSync(path))(`the ${revision} cartridge`, () => {
|
|
const bytes = existsSync(path) ? new Uint8Array(readFileSync(path)) : new Uint8Array();
|
|
it("is accepted under its name and recognised from its header", async () => {
|
|
// CRYSTAL_ROM_PATH may point the 1.1 entry at another cartridge.
|
|
const assets = new CrystalAssets(bytes);
|
|
const own = cartridges.find((c) => c.revision === romRevision(assets.reader))!;
|
|
expect(await validateRom(new File([bytes], "crystal.gbc"))).toMatchObject({ sha1: own.rom.sha1, name: own.rom.name });
|
|
if (revision !== "1.1" || !process.env.CRYSTAL_ROM_PATH) expect(own.revision).toBe(revision);
|
|
// One changed byte is a modified ROM.
|
|
const changed = bytes.slice();
|
|
changed[0x7fff] ^= 1;
|
|
await expect(validateRom(new File([changed], "crystal.gbc"))).rejects.toThrow("not a supported English");
|
|
});
|
|
|
|
it("draws Battle Tower trainers from all 70, or from the first 21 on 1.0", () => {
|
|
const assets = new CrystalAssets(bytes);
|
|
let seed = 84;
|
|
const context = {
|
|
reader: assets.reader,
|
|
data: assets.pokemon,
|
|
random: () => {
|
|
seed = (seed * 1103515245 + 12345) >>> 0;
|
|
return (seed >>> 16) & 255;
|
|
},
|
|
text: () => ({ player: "KAWA", rival: "SILVER", mom: "MOM", strings: {}, weekday: "SUNDAY" }),
|
|
} as unknown as SpecialContext;
|
|
const drawn = new Set<number>();
|
|
for (let draw = 0; draw < 2000; draw++) {
|
|
const bt = { ...emptyBattleTowerState(), levelGroup: 1 + (draw % 10) };
|
|
drawn.add(loadOpponent(context, bt).trainer);
|
|
}
|
|
const limit = romRevision(assets.reader) === "1.0" ? 21 : 70;
|
|
expect(Math.max(...drawn)).toBe(limit - 1);
|
|
expect(drawn.size).toBe(limit);
|
|
});
|
|
});
|