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>
This commit is contained in:
1 parent
9ac111f262
commit
451464f7c6
38 files changed
+5600
-490
No files matched your search
@@ -0,0 +1,166 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user