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 { martFixtureWorld, talkToClerk } from "./mart-fixture"; import { settle, withoutPlayTime } from "./world-driver"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; const fixture = () => martFixtureWorld(new CrystalAssets(new Uint8Array(readFileSync(romPath)))); // constants/item_constants.asm const POKE_BALL = 5, BICYCLE = 7, POTION = 18; describe.skipIf(!existsSync(romPath))("Cherrygrove Mart with the local ROM", () => { it("buys Potions at the ROM price and adds them to the bag", () => { const world = fixture(); const money = world.state.money; talkToClerk(world); // BUY -> Potion (list index 0) -> quantity 3 -> confirm YES -> back -> QUIT const shown = settle(world, [0, 0, 3, 1, -1, 2]); expect(world.notice, world.diagnostic).toBe(""); expect(shown.join("\n")).toContain("Here you are"); expect(world.state.money).toBe(money - 300 * 3); expect(world.state.bag.items).toEqual([{ item: POTION, quantity: 3 }]); }); it("refuses to buy without enough money, changing nothing", () => { const world = fixture(); world.state.money = 100; // less than one Potion (300) talkToClerk(world); const committed = world.save(); const shown = settle(world, [0, 0, 1, 1, -1, 2]); expect(world.notice, world.diagnostic).toBe(""); expect(shown.join("\n")).toMatch(/enough money/i); expect(withoutPlayTime(world.save())).toEqual(withoutPlayTime(committed)); }); it("refuses to buy into a full pocket, changing nothing", () => { const world = fixture(); const data = world.assets.pokemon; const fillers: number[] = []; for (let id = 1; fillers.length < 20; id++) { if (id === POTION) continue; try { if (data.itemPocket(id) === "item") fillers.push(id); } catch { // Not every id names an item; skip gaps. } } world.state.bag.items = fillers.map((item) => ({ item, quantity: 1 })); talkToClerk(world); const committed = world.save(); const shown = settle(world, [0, 0, 1, 1, -1, 2]); expect(world.notice, world.diagnostic).toBe(""); expect(shown.join("\n")).toMatch(/can't carry/i); expect(withoutPlayTime(world.save())).toEqual(withoutPlayTime(committed)); }); it("sells items at half the ROM price", () => { const world = fixture(); world.state.bag.items = [{ item: POTION, quantity: 5 }]; talkToClerk(world); const money = world.state.money; // SELL -> Potion (index 0) -> quantity 2 -> confirm YES -> back -> QUIT const shown = settle(world, [1, 0, 2, 1, -1, 2]); expect(world.notice, world.diagnostic).toBe(""); expect(shown.join("\n")).toContain("Got ¥300"); expect(world.state.money).toBe(money + 300); // 300/2 * 2 expect(world.state.bag.items).toEqual([{ item: POTION, quantity: 3 }]); }); it("caps money at C.MAX_MONEY when selling", () => { const world = fixture(); world.state.bag.items = [{ item: POTION, quantity: 1 }]; world.state.money = C.MAX_MONEY - 10; talkToClerk(world); const shown = settle(world, [1, 0, 1, 1, -1, 2]); expect(world.notice, world.diagnostic).toBe(""); expect(shown.join("\n")).not.toBe(""); expect(world.state.money).toBe(C.MAX_MONEY); expect(world.state.bag.items).toEqual([]); }); it("excludes key items from the sell list", () => { const world = fixture(); world.state.bag.items = [{ item: POTION, quantity: 1 }]; world.state.bag.keyItems = [BICYCLE]; talkToClerk(world); world.choose(1); // SELL const choice = world.dialogue?.choice; expect(choice?.kind).toBe("martlist"); if (choice?.kind === "martlist") { expect(choice.entries.map((entry) => entry.item)).toEqual([POTION]); } }); it("sells a Poké Ball but refuses to sell key items or unaffordable buys together", () => { // MART_CHERRYGROVE_DEX stocks a Poké Ball once the mystery egg is given; // exercising a ball-pocket item confirms pockets beyond "item" work too. const world = fixture(); world.state.events.add(31); // EVENT_GAVE_MYSTERY_EGG_TO_ELM talkToClerk(world); const money = world.state.money; // BUY -> Poké Ball (list index 0 in the dex mart) -> quantity 1 -> YES -> back -> QUIT settle(world, [0, 0, 1, 1, -1, 2]); expect(world.notice, world.diagnostic).toBe(""); expect(world.state.money).toBe(money - 200); expect(world.state.bag.balls).toEqual([{ item: POKE_BALL, quantity: 1 }]); }); it("quits immediately without changing state", () => { const world = fixture(); const before = world.save(); talkToClerk(world); settle(world, [2]); expect(world.notice, world.diagnostic).toBe(""); expect(withoutPlayTime(world.save())).toEqual(withoutPlayTime(before)); }); it("a mid-interaction save restores the committed state and replays it", () => { const world = fixture(); talkToClerk(world); const committed = world.save(); world.choose(0); // BUY world.choose(0); // Potion -> quantity picker expect(world.dialogue?.choice?.kind).toBe("martquantity"); expect(world.save()).toEqual(committed); world.restore(committed); expect(world.dialogue).toBeUndefined(); expect(world.save()).toEqual(committed); // The world is usable again: a fresh visit still works. talkToClerk(world); expect(world.dialogue?.choice?.kind).toBe("martmenu"); }); });