Complete the overworld foundation
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>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-25 16:47:29 +02:00
1 parent 9ac111f262
commit 451464f7c6
38 files changed
+5600 -490

No files matched your search

+19 -3
View File
@@ -106,7 +106,10 @@ describe("native ROM formats", () => {
it("decodes movement data and refuses unported movement commands", () => {
const reader = new RomReader(
Uint8Array.from([
0x02, 0x0f, 0x08, 0x31, 0x3b, 0x40, 0x46, 12, 0x47, 0x4c, 0x47,
0x02, 0x0f, 0x08, 0x31, 0x3b, 0x40, 0x46, 12, 0x47, 0x5a, 0x47,
// turn_step, turn_in, set_sliding, step_dig 16, skyfall,
// step_shake $54, fish_got_bite, step_bump, step_wait_end 5
0x05, 0x26, 0x39, 0x4f, 16, 0x4e, 0x55, 0x54, 0x51, 0x50, 0x48, 5,
]),
);
expect(readMovement(reader, 0)).toEqual([
@@ -119,6 +122,17 @@ describe("native ROM formats", () => {
{ kind: "sleep", ticks: 12 },
]);
expect(() => readMovement(reader, 9)).toThrow("not ported");
expect(readMovement(reader, 11)).toEqual([
{ kind: "turn-step", direction: "up", ticks: 3 },
{ kind: "step", direction: "left", ticks: 8, animate: true, spin: true },
{ kind: "sliding", sliding: true },
{ kind: "pose", pose: "spin", ticks: 16 },
{ kind: "pose", pose: "skyfall", ticks: 31 },
{ kind: "shake", displacement: 0x54 },
{ kind: "pose", pose: "fish-bite", ticks: 8 },
{ kind: "bump", ticks: 1 },
{ kind: "wait-end", ticks: 5 },
]);
});
it("reads RAM-backed names and string buffers only from native values", () => {
// text_ram wStringBuffer4, " ", text_buffer 0, done
@@ -192,7 +206,7 @@ describe.skipIf(!existsSync(romPath))(
load(),
() => new Date("2026-09-22T12:00:00"),
);
world.restore({ ...world.save(), x: 3, y: 2, direction: "down" });
world.restore({ ...world.save(), x: 3, y: 2, direction: "right" });
const ticks: number[] = [];
for (let tick = 1; tick <= 40 && world.x < 7; tick++) {
const x = world.x;
@@ -415,11 +429,13 @@ describe.skipIf(!existsSync(romPath))(
objects: [],
});
world.tick(direction);
expect([world.x, world.y]).toEqual([x + dx * 2, y + dy * 2]);
// The first half of the jump occupies the tile jumped over.
expect([world.x, world.y]).toEqual([x + dx, y + dy]);
expect(
world.poses().find((pose) => pose.id === 0)?.lift,
).toBeLessThan(0);
settle(world);
expect([world.x, world.y]).toEqual([x + dx * 2, y + dy * 2]);
expect(world.notice).toBe("");
checked.add(collision);
}