import { describe, expect, it } from "vitest"; import { existsSync, readFileSync } from "node:fs"; import { CrystalAssets } from "../src/game/rom/assets"; import { C } from "../src/game/battle/constants"; import { trainerFixtureWorld } from "./trainer-fixture"; import type { GameWorld } from "../src/game/world/world"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; const fixture = (tail?: number[], lose = false, winText = false) => trainerFixtureWorld(new CrystalAssets(new Uint8Array(readFileSync(romPath))), tail, lose, winText); function finish(world: GameWorld) { const messages: string[] = []; for (let tick = 0; tick < 10000; tick++) { const view = world.battle; if (view) { if (view.message) messages.push(view.message.text); if (view.prompt?.kind === "action") world.battleAction({ action: "fight", move: 0 }); else if (view.prompt?.kind === "yesno") world.battleChoose(1); else if (view.prompt?.kind === "forget") world.battleChoose(3); else if (view.prompt?.kind === "stats" || view.message?.prompt) world.interact(); else world.tick(); } else if (world.dialogue) world.advanceDialogue(); else if (world.busy) world.tick(); else return messages.join("\n"); } throw new Error(`Trainer battle did not settle: ${world.diagnostic}`); } describe.skipIf(!existsSync(romPath))("scripted trainer world transactions", () => { it("commits Joey's ROM reward, party experience, sighting and postbattle event together", () => { const world = fixture(); const before = world.save(); world.interact(); expect(world.battle).toBeDefined(); expect(world.save()).toEqual(before); const messages = finish(world); expect(world.notice, world.diagnostic).toBe(""); expect(messages).toContain("JOEY"); expect(world.state.money).toBe(before.money + 64); expect(world.state.party[0].exp).toBeGreaterThan(before.party[0].exp); expect(world.state.pokedex.seen.has(19)).toBe(true); expect(world.state.events.has(256)).toBe(true); const saved = world.save(); world.restore(saved); expect(world.save()).toEqual(saved); }); it("reads scripted win text from the script bank's ROM pointer", () => { const world = fixture(undefined, false, true); world.interact(); const messages = finish(world); expect(world.notice, world.diagnostic).toBe(""); expect(messages).toContain("Ack! I lost again!"); expect(messages.indexOf("Ack! I lost again!")).toBeLessThan(messages.lastIndexOf("64")); }); it("caps trainer rewards at the money limit", () => { const world = fixture(); world.state.money = C.MAX_MONEY - 1; world.interact(); finish(world); expect(world.notice, world.diagnostic).toBe(""); expect(world.state.money).toBe(C.MAX_MONEY); }); it("rolls back reward, experience and sightings if the following script is unsupported", () => { const world = fixture([0xff]); const before = world.save(); world.interact(); finish(world); expect(world.notice).toContain("not been ported"); expect(world.save()).toEqual(before); }); it("restores a midbattle save without partially paying the reward", () => { const world = fixture(); const before = world.save(); world.interact(); for (let tick = 0; tick < 1000 && !world.battle?.prompt; tick++) { if (world.battle?.message?.prompt) world.interact(); else world.tick(); } expect(world.save()).toEqual(before); world.restore(before); expect(world.battle).toBeUndefined(); expect(world.save()).toEqual(before); }); it("CANLOSE scripts receive LOSE in wScriptVar without forcing a whiteout", () => { // ifequal LOSE, .lost; invalid; .lost: setevent 256; end const world = fixture([0x91], true); const clerk = world.assets.map("26:4").objects[0]; const start = clerk.script + 12; // after winlosstext/loadtrainer/loadvar/startbattle const target = ((start + 5) % 0x4000) + 0x4000; world.assets.reader.bytes.set([0x06, 1, target & 255, target >> 8, 0xff, 0x33, 0, 1, 0x91], start); const before = world.save(); world.interact(); const messages = finish(world); expect(world.notice, world.diagnostic).toBe(""); expect(world.state.events.has(256)).toBe(true); expect(world.state.money).toBe(before.money); expect(world.save().map).toBe(before.map); expect(messages).not.toMatch(/whited\s+out/i); }); });