import { describe, expect, it } from "vitest"; import { existsSync, readFileSync } from "node:fs"; import { TrainerData } from "../src/game/rom/trainers"; import { RomReader } from "../src/game/rom/reader"; import { CrystalAssets } from "../src/game/rom/assets"; import { NUM_TRAINER_CLASSES } from "../src/game/rom/profile"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; describe.skipIf(!existsSync(romPath))("ROM trainer decoder", () => { const bytes = existsSync(romPath) ? new Uint8Array(readFileSync(romPath)) : new Uint8Array(); const data = new TrainerData(new RomReader(bytes)); it("reads Joey and Mikey, including class DVs and actual payout", () => { const joey = data.trainer(22, 1), mikey = data.trainer(22, 2); expect(joey).toMatchObject({ name: "JOEY", className: "YOUNGSTER", dvs: 0x9888, reward: 64, partyType: 0, }); expect(joey.party).toEqual([{ species: 19, level: 4, item: 0 }]); expect(mikey.party.map((mon) => [mon.species, mon.level])).toEqual([ [16, 2], [19, 4], ]); expect(data.createParty(joey)[0]).toMatchObject({ moves: [33, 39, 0, 0], dvs: 0x9888, statExp: [0, 0, 0, 0, 0], }); }); it("reads Falkner's explicit moves and the rival starter variants", () => { const falkner = data.trainer(1, 1); expect(falkner).toMatchObject({ name: "FALKNER", partyType: 1, dvs: 0x9a77, reward: 900, }); const party = data.createParty(falkner, { ot: "CHRIS", otId: 123 }); expect(party.map((mon) => mon.moves)).toEqual([ [33, 189, 0, 0], [33, 189, 16, 0], ]); expect(party[0].pp.slice(0, 2)).toEqual([35, 10]); expect( party.every((mon) => mon.hp === mon.stats[0] && mon.otId === 123), ).toBe(true); expect([1, 2, 3].map((id) => data.trainer(9, id).party[0].species)).toEqual( [152, 155, 158], ); expect(data.trainer(9, 1).dvs).toBe(0xdddd); }); it("reads held items with and without explicit moves", () => { expect(data.trainer(59, 1).partyType).toBe(2); const gaven = data.trainer(27, 7); expect(gaven.partyType).toBe(3); expect(gaven.party[1].item).toBe(82); expect(data.createParty(gaven)[1].moves).toEqual(gaven.party[1].moves); }); it.skipIf(!existsSync(".reference/pokecrystal/data/trainers/parties.asm"))( "sweeps every party listed in the pinned source", () => { const source = readFileSync( ".reference/pokecrystal/data/trainers/parties.asm", "utf8", ); const groups = source.split(/^\w+Group:\s*$/m).slice(1); expect(groups).toHaveLength(NUM_TRAINER_CLASSES); const formats = new Set(); let total = 0; groups.forEach((group, index) => { const count = [...group.matchAll(/db -1 ; end/g)].length; for (let id = 1; id <= count; id++) { const trainer = data.trainer(index + 1, id); formats.add(trainer.partyType); total++; if (index + 1 === 12 && id === 2) { expect(() => data.createParty(trainer)).toThrow(/Mystery Gift/); continue; } const party = data.createParty(trainer); expect(party.length).toBeGreaterThan(0); expect( party.every((mon) => mon.hp > 0 && mon.hp === mon.stats[0]), ).toBe(true); } expect(() => data.trainer(index + 1, count + 1)).toThrow(); }); expect(formats).toEqual(new Set([0, 1, 2, 3])); expect(total).toBeGreaterThan(500); }, ); it("reads map trainer headers and vision range", () => { const assets = new CrystalAssets(bytes); const route = assets.map("26:1"); const trainers = route.objects.filter((object) => object.kind === 2); expect(trainers.length).toBeGreaterThan(0); for (const object of trainers) { expect(object.sightRange).toBeGreaterThan(0); const header = data.header(object.script); expect(header.seenText).toBeGreaterThan(0); expect(header.beatenText).toBeGreaterThan(0); expect( data.trainer(header.classId, header.id).party.length, ).toBeGreaterThan(0); } }); it("rejects invalid class and trainer IDs", () => { for (const classId of [0, -1, 68, 1.5]) expect(() => data.trainer(classId, 1)).toThrow(); for (const id of [0, -1, 256, 1.5, 2]) expect(() => data.trainer(1, id)).toThrow(); }); });