import { describe, expect, it } from "vitest"; import { existsSync, readFileSync } from "node:fs"; import { CrystalAssets } from "../src/game/rom/assets"; import { TrainerData } from "../src/game/rom/trainers"; import { Battle } from "../src/game/battle/battle"; import { C } from "../src/game/battle/constants"; import { battleOptions, battleSteps, partyMon, playBattle } from "./battle-driver"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; describe.skipIf(!existsSync(romPath))("Battle Tower battles (wInBattleTowerBattle)", () => { const assets = existsSync(romPath) ? new CrystalAssets(new Uint8Array(readFileSync(romPath))) : undefined; const trainers = assets && new TrainerData(assets.reader); const TYPHLOSION = 157, RATTATA = 19; /** A Battle Tower trainer as the world builds one: Falkner's AI, no * items or prize, and the texts BattleTowerText chose. */ const tower = (party = [partyMon(assets!, RATTATA, 5)]) => ({ name: "HANSON", className: "FISHER", party, aiFlags: trainers!.attributes(1).aiMoveFlags, switchFlags: trainers!.attributes(1).aiItemSwitchFlags, items: [0, 0] as [number, number], baseReward: 0, reward: 0, }); it("gives no experience or prize, and refuses items", () => { const party = [partyMon(assets!, TYPHLOSION, 50)]; const exp = party[0].exp; const options = battleOptions(assets!, trainers!, party, { wild: { species: RATTATA, level: 5 } }); const battle = new Battle({ ...options, wild: undefined, trainer: tower(), battleTower: true, bag: { items: [{ item: C.POTION, quantity: 1 }], balls: [], keyItems: [], tmsHms: [] } as never, }); const steps = battleSteps(battle); expect(steps.prompt?.kind).toBe("action"); steps.answer({ action: "item", item: C.POTION, slot: 0 }); expect(steps.texts.join(" ")).toContain("Items can't be used here."); expect(steps.prompt?.kind).toBe("action"); for (let turn = 0; turn < 20 && !steps.done; turn++) steps.answer(steps.prompt?.kind === "action" ? { action: "fight", move: 0 } : 1); expect(steps.done).toBe(true); expect(party[0].exp).toBe(exp); expect(steps.texts.join(" ")).not.toContain("EXP. Points"); expect(steps.texts.join(" ")).not.toContain("for winning"); }); it("lets a traded Pokémon over the badge level obey", () => { const traded = { ...partyMon(assets!, TYPHLOSION, 80), otId: 4321 }; const options = battleOptions(assets!, trainers!, [traded], { wild: { species: RATTATA, level: 5 } }); const run = (battleTower: boolean) => playBattle(new Battle({ ...options, wild: undefined, trainer: tower(), battleTower, johtoBadges: 0 })); // Without badges an 80th-level traded Pokémon disobeys sometimes; in // the Battle Tower it never does. const disobeys = /ignored orders|loafing|won't obey|turned away|began to nap/; expect(run(false).texts.join(" ")).toMatch(disobeys); expect(run(true).texts.join(" ")).not.toMatch(disobeys); }); it("ends a loss with the trainer's text and no whiteout prompt", () => { const party = [partyMon(assets!, RATTATA, 2)]; const options = battleOptions(assets!, trainers!, party, { wild: { species: RATTATA, level: 5 } }); const winText = assets!.reader.near( assets!.reader.near(0x11f332 + 2, assets!.reader.bank(0x11f332)), assets!.reader.bank(0x11f332), ); const result = playBattle( new Battle({ ...options, wild: undefined, trainer: { ...tower([partyMon(assets!, TYPHLOSION, 60)]), lossText: winText }, battleTower: true, }), ); expect(result.result & 0x3f).toBe(C.LOSE); expect(result.texts.join(" ")).not.toContain("whited out"); }); });