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 { DecorationData, initialDecorations, } from "../src/game/world/decorations"; import { EVENTS } from "../src/game/rom/profile"; import { settle } from "./world-driver"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; function fixture() { const assets = new CrystalAssets(new Uint8Array(readFileSync(romPath))); const world = new GameWorld( assets, () => new Date("2026-09-22T12:00:00"), () => 0, ); return { assets, world, data: new DecorationData(assets.reader, assets.pokemon), }; } function pc(world: GameWorld) { const save = world.save(); world.restore({ ...save, map: "24:7", x: 2, y: 2, direction: "up", objects: [], }); world.interact(); expect(world.notice, world.diagnostic).toBe(""); expect(world.dialogue?.choice?.kind).toBe("menu"); } function choose(world: GameWorld, name: string) { const choice = world.dialogue?.choice; if (choice?.kind !== "menu") throw new Error("Expected decoration menu"); const index = choice.items.indexOf(name); expect(index, name).toBeGreaterThanOrEqual(0); world.choose(index); } function acknowledge(world: GameWorld) { for (let i = 0; world.dialogue && !world.dialogue.choice && i < 20; i++) world.advanceDialogue(); } describe.skipIf(!existsSync(romPath))("room decorations", () => { it("starts with the original bed and Town Map and migrates old saves", () => { const { world, assets } = fixture(); expect(world.state.decorations).toEqual(initialDecorations()); expect(assets.block(world.map, 0, 4)).toBe(0x1b); expect(assets.block(world.map, 6, 0)).toBe(0x1f); expect(world.state.events.has(EVENTS.playersRoomPoster)).toBe(true); const old = world.save() as Partial>; delete old.decorations; world.restore(old); expect(world.state.decorations).toEqual(initialDecorations()); }); it("PC puts away and restores a bed; mid-menu saves retain committed choices", () => { const { world, assets } = fixture(); pc(world); choose(world, "BED"); choose(world, "PUT IT AWAY"); expect(world.liveState.decorations.bed).toBe(0); expect(world.save().decorations.bed).toBe(2); acknowledge(world); choose(world, "EXIT"); settle(world); expect(world.state.decorations.bed).toBe(0); expect(assets.block(world.map, 0, 4)).toBe( assets.block(assets.map("24:7"), 0, 4), ); world.restore(world.save()); pc(world); choose(world, "BED"); choose(world, "FEATHERY BED"); acknowledge(world); choose(world, "EXIT"); settle(world); expect(assets.block(world.map, 0, 4)).toBe(0x1b); }); it("owned ornaments move between sides without duplication and reload as sprites", () => { const { world, data } = fixture(); world.state.events.add(data.attribute(30).event); pc(world); choose(world, "ORNAMENT"); choose(world, "PIKACHU DOLL"); choose(world, "RIGHT SIDE"); acknowledge(world); choose(world, "ORNAMENT"); choose(world, "PIKACHU DOLL"); choose(world, "LEFT SIDE"); acknowledge(world); choose(world, "EXIT"); settle(world); expect([ world.state.decorations.left, world.state.decorations.right, ]).toEqual([30, 0]); expect(world.objects().some((object) => object.id === 3)).toBe(true); expect(world.objects().some((object) => object.id === 4)).toBe(false); world.restore(world.save()); expect(world.state.variables[1]).toBe(data.attribute(30).sprite); }); it("validates ownership and categories before changing the current game", () => { const { world } = fixture(); const before = world.save(); for (const decorations of [ null, { ...before.decorations, bed: 30 }, { ...before.decorations, bed: 3 }, { ...before.decorations, poster: 999 }, ]) { expect(() => world.restore({ ...before, decorations })).toThrow( /Invalid native save/, ); expect(world.save()).toEqual(before); } }); it("sets every tile category and sprite category from the owned ROM attributes", () => { const { world, assets, data } = fixture(); for (let id = 1; id <= 52; id++) { const a = data.attribute(id); if (a.action & 1) world.state.events.add(a.event); } world.state.decorations = { bed: 5, carpet: 10, plant: 14, poster: 19, console: 24, bigDoll: 28, left: 51, right: 52, }; world.warpTo("24:7", 2, 2); expect( [ [0, 4], [7, 4], [6, 0], [0, 0], [0, 2], [2, 2], [4, 2], ].map(([x, y]) => assets.block(world.map, x, y)), ).toEqual([0x1e, 0x22, 0x25, 0x11, 0x12, 0x13, 0x12]); expect([0, 1, 2, 3].map((i) => world.state.variables[i])).toEqual( [24, 51, 52, 28].map((id) => data.attribute(id).sprite), ); expect(world.objects()).toHaveLength(4); world.restore(world.save()); expect(world.objects()).toHaveLength(4); }); });