import { describe, expect, it } from "vitest"; import { existsSync, readFileSync } from "node:fs"; import { CrystalAssets } from "../src/game/rom/assets"; import { GameWorld, migrateSave } from "../src/game/world/world"; import { ENGINE_FLAGS } from "../src/game/rom/profile"; import { settle, findPath, walkTo } from "./world-driver"; import { generatePartyMon } from "../src/game/party/pokemon"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; const BERRY = 0xad; const ROUTE_29 = "24:3"; describe.skipIf(!existsSync(romPath))("Route 29 with the local ROM", () => { /** `now` is mutable per test. */ const fixture = (date = "2026-09-22T12:00:00") => { const assets = new CrystalAssets(new Uint8Array(readFileSync(romPath))); const clock = { now: new Date(date) }; const world = new GameWorld(assets, () => clock.now); // Facing FRUITTREE_ROUTE_29 at (12, 2). world.restore({ ...world.save(), map: ROUTE_29, x: 12, y: 3, direction: "up" }); world.tick(); return { world, clock }; }; const berries = (world: GameWorld) => world.state.bag.items.find((slot) => slot.item === BERRY)?.quantity ?? 0; const pick = (world: GameWorld) => { world.interact(); const pages = settle(world); expect(world.notice, world.diagnostic).toBe(""); return pages.join("\n"); }; it("picks a Berry once per day and regrows after the daily reset", () => { const { world, clock } = fixture(); expect(world.state.dailyReset).not.toBeNull(); expect(pick(world)).toMatch(/BERRY/); expect(berries(world)).toBe(1); expect(world.state.fruitTrees).toEqual(new Set([1])); expect(world.state.flags.has(ENGINE_FLAGS.allFruitTrees)).toBe(true); // Same day, even across a save and reload: nothing to pick. const saved = world.save(); expect(saved).toMatchObject({ fruitTrees: [1] }); world.restore(saved); expect(world.save()).toEqual(saved); pick(world); expect(berries(world)).toBe(1); // The first idle tick of the next day clears the daily flag. clock.now = new Date("2026-09-23T00:01:00"); world.tick(); expect(world.state.flags.has(ENGINE_FLAGS.allFruitTrees)).toBe(false); expect(pick(world)).toMatch(/BERRY/); expect(berries(world)).toBe(2); }); it("leaves the tree unpicked when the item pocket is full", () => { const { world } = fixture(); world.state.bag.items = Array.from({ length: 20 }, (_, index) => ({ item: index + 1, quantity: 1, })); const before = world.state.bag.items.map((slot) => ({ ...slot })); pick(world); expect(world.state.bag.items).toEqual(before); expect(world.state.fruitTrees.size).toBe(0); }); it("migrates version-3 saves without daily state and rejects invalid trees", () => { const { world } = fixture(); const { fruitTrees: _, dailyReset: __, ...legacy } = world.save(); expect(migrateSave(legacy, () => 0)).toMatchObject({ fruitTrees: [], dailyReset: null, }); expect(() => world.restore({ ...world.save(), fruitTrees: [0] })).toThrow( "Invalid native save", ); expect(() => world.restore({ ...world.save(), fruitTrees: [1, 1] })).toThrow( "Invalid native save", ); }); it("walks from New Bark Town across Route 29 into Cherrygrove and a house", () => { const assets = new CrystalAssets(new Uint8Array(readFileSync(romPath))); // Random byte 255 never passes an encounter-rate check. const world = new GameWorld(assets, () => new Date("2026-09-22T12:00:00"), () => 255); world.state.party.push( generatePartyMon(assets.pokemon, { species: 155, level: 5, item: 0, otId: world.playerId, ot: "CHRIS", dvs: 0x823b, caughtData: 0, }), ); // After the starter, Elm sets SCENE_NEWBARKTOWN_NOOP: the teacher lets you go. world.restore({ ...world.save(), map: "24:4", x: 5, y: 8, scenes: { "24:4": 1 } }); const crossWest = (next: string) => { const edge = Array.from({ length: world.map.height }, (_, y) => y).find((y) => { try { findPath(world, 0, y); return true; } catch { return false; } })!; walkTo(world, 0, edge); expect(world.move("left"), world.notice).toBe(true); settle(world); expect(world.mapId).toBe(next); expect(world.notice, world.diagnostic).toBe(""); }; crossWest(ROUTE_29); // Route29TuscanyCallback hides Tuscany without the Zephyr Badge, even on // a Tuesday (2026-09-22). expect(world.objects().some((object) => object.id === 8)).toBe(false); crossWest("26:3"); walkTo(world, 17, 8); expect(world.move("up")).toBe(true); settle(world); expect(world.mapId).toBe("26:6"); expect(world.notice, world.diagnostic).toBe(""); const saved = world.save(); world.restore(saved); expect(world.save()).toEqual(saved); }); });