Checks / web (push) Canceled after 0s
Port forced movement (turn delays, bumps, ice, currents, pits), dynamic return warps, elevators, Surf, Cut, Strength and cycling rules, working map blocks with changeblock/changemapblocks, and the remaining NPC movement types and movement commands. Add tileset animation from the ROM's command tables, grass sprite priority and special map palettes; room decorations with ownership, the bedroom PC and save migration; and Key Items access to the Bicycle. Fourteen original-engine traces compare positions, facing, actors, event flags and scripted poses. Tile animation matches original VRAM for four tilesets, and LoadMapPals matches 192 palette cases. skyfall_top now keeps the $60 sprite offset its step function leaves behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
167 lines
5.2 KiB
TypeScript
167 lines
5.2 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("");
|
|
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<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);
|
|
});
|
|
});
|