import { describe, expect, it } from "vitest"; import { existsSync, readFileSync } 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 { settle } from "./world-driver"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; describe.skipIf(!existsSync(romPath))("Azalea Town with the local ROM", () => { const bytes = existsSync(romPath) ? new Uint8Array(readFileSync(romPath)) : new Uint8Array(); const KURTS_HOUSE = "8:4"; const EVENT_CLEARED_SLOWPOKE_WELL = 0x2b, EVENT_KURT_GAVE_YOU_LURE_BALL = 0x35, EVENT_GAVE_KURT_RED_APRICORN = 0x258, ENGINE_KURT_MAKING_BALLS = 80; const RED_APRICORN = 0x55, BLU_APRICORN = 0x56, LEVEL_BALL = 0x9f; const world = (day: number) => { let seed = 9; const game = new GameWorld( new CrystalAssets(bytes), () => new Date(`2026-09-${day}T12:00:00`), () => { seed = (seed * 1103515245 + 12345) >>> 0; return (seed >>> 16) & 255; }, ); game.state.party.push( generatePartyMon(game.assets.pokemon, { species: 155, level: 20, item: 0, otId: game.playerId, ot: "CHRIS", dvs: 0x8888, caughtData: 0, }), ); return game; }; /** Answer menus and quantities in order, collecting the pages shown. */ const answer = (game: GameWorld, answers: number[]) => { const pages: string[] = []; for (let tick = 0; tick < 5000; tick++) { const dialogue = game.dialogue; if (dialogue) { const page = dialogue.pages[dialogue.index]; if (page && pages.at(-1) !== page) pages.push(page.replace(/\n/g, " ")); if (dialogue.index === dialogue.pages.length - 1 && dialogue.choice) { const choice = dialogue.choice; if (choice.kind === "martquantity") { pages.push(choice.prompt.replace(/\n/g, " ")); const wanted = answers.shift()!; while (choice.selected < wanted) game.moveCursor("up"); game.choose(choice.selected); } else game.choose(answers.shift() ?? 0); } else game.advanceDialogue(); continue; } if (!game.busy) break; game.tick(); } expect(game.notice, game.diagnostic).toBe(""); return pages.join(" "); }; it("makes Level Balls from Red Apricorns by the next day", () => { const game = world(22); game.state.events.add(EVENT_CLEARED_SLOWPOKE_WELL); game.state.events.add(EVENT_KURT_GAVE_YOU_LURE_BALL); game.state.bag.items = [ { item: BLU_APRICORN, quantity: 1 }, { item: RED_APRICORN, quantity: 4 }, ]; game.restore({ ...game.save(), map: KURTS_HOUSE, x: 3, y: 3, direction: "up" }); settle(game); game.interact(); // The list follows ApricornBalls: RED before BLU. Three of four. let pages = answer(game, [0, 3]); expect(pages).toContain("Which APRICORN"); expect(pages).toContain("How many"); expect(game.state.events.has(EVENT_GAVE_KURT_RED_APRICORN)).toBe(true); expect(game.state.flags.has(ENGINE_KURT_MAKING_BALLS)).toBe(true); expect(game.state.memory.wKurtApricornQuantity).toBe(3); expect(game.state.bag.items).toEqual([ { item: BLU_APRICORN, quantity: 1 }, { item: RED_APRICORN, quantity: 1 }, ]); // The next day, ENGINE_KURT_MAKING_BALLS is cleared and the balls are // ready. const next = world(23); next.restore(JSON.parse(JSON.stringify(game.save()))); settle(next); expect(next.state.flags.has(ENGINE_KURT_MAKING_BALLS)).toBe(false); // EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1 clears when the house is entered // again (ResetMapBufferEventFlags). next.warpTo(KURTS_HOUSE, 3, 3); settle(next); next.move("up"); // Kurt: this only turns the player next.interact(); pages = answer(next, [1]); expect(next.state.bag.balls).toContainEqual({ item: LEVEL_BALL, quantity: 3 }); expect(next.state.events.has(EVENT_GAVE_KURT_RED_APRICORN)).toBe(false); }); it("returns to the apricorn list when the quantity is cancelled", () => { const game = world(22); game.state.events.add(EVENT_CLEARED_SLOWPOKE_WELL); game.state.events.add(EVENT_KURT_GAVE_YOU_LURE_BALL); game.state.bag.items = [{ item: RED_APRICORN, quantity: 2 }]; game.restore({ ...game.save(), map: KURTS_HOUSE, x: 3, y: 3, direction: "up" }); settle(game); game.interact(); // RED, cancel the quantity, then CANCEL the list. for (let tick = 0; tick < 200 && game.dialogue?.choice?.kind !== "menu"; tick++) if (game.dialogue && !game.dialogue.choice) game.advanceDialogue(); else game.tick(); game.choose(0); expect(game.dialogue?.choice?.kind).toBe("martquantity"); game.choose(-1); for (let tick = 0; tick < 200 && game.dialogue?.choice?.kind !== "menu"; tick++) if (game.dialogue && !game.dialogue.choice) game.advanceDialogue(); else game.tick(); expect(game.dialogue?.choice?.kind).toBe("menu"); game.choose(1); const pages = answer(game, []); expect(pages).toContain("letdown"); expect(game.state.bag.items).toEqual([{ item: RED_APRICORN, quantity: 2 }]); }); });