Files
Crystal/tests/decorations.test.ts
T
Sébastien QUEROLandClaude Opus 5.5 3bdca0099e
Checks / web (push) Canceled after 0s
Port the PC and boxes
Read TileCollisionStdScripts from the ROM so the Pokémon Center PC runs
PokemonCenterPC: Bill's PC (deposit, withdraw, release, change and name
boxes), the player's item storage (withdraw, deposit, toss, mail box) and
Oak's Pokédex rating. The house PC now opens the full player's PC menu.

A catch with a full party goes to the current box through SendMonIntoBox;
a full box keeps the ball, and filling it queues Bill's call as a memory
script after the battle. Saves gain boxes, box names and PC items,
migrated by absence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:12:26 +02:00

170 lines
5.3 KiB
TypeScript

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("");
// _PlayersHousePC: "<PLAYER> turned on the PC.", then _PlayersPC's menu.
acknowledge(world);
expect(world.dialogue?.choice?.kind).toBe("menu");
choose(world, "DECORATION");
}
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<ReturnType<GameWorld["save"]>>;
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);
});
});