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,90 @@
|
||||
import { emptyDecorations } from "../src/game/world/decorations";
|
||||
import { CrystalAssets } from "../src/game/rom/assets";
|
||||
import { GameWorld } from "../src/game/world/world";
|
||||
import type { GameMap } from "../src/game/world/types";
|
||||
|
||||
/** Synthetic maps and script bytes: these tests run without a private ROM. */
|
||||
export class MovementAssets extends CrystalAssets {
|
||||
readonly fixtures = new Map<string, GameMap>();
|
||||
readonly tiles = new Map<string, number>();
|
||||
readonly bytes: Uint8Array;
|
||||
constructor() {
|
||||
const bytes = new Uint8Array(0xa0000);
|
||||
bytes[0] = 0x50; // Empty weekday name for synthetic script hosts.
|
||||
super(bytes);
|
||||
this.bytes = bytes;
|
||||
for (const id of ["24:7", "24:6"]) {
|
||||
this.fixtures.set(id, {
|
||||
id,
|
||||
group: 24,
|
||||
number: Number(id.split(":")[1]),
|
||||
name: id,
|
||||
location: id,
|
||||
landmark: 1,
|
||||
width: 8,
|
||||
height: 8,
|
||||
border: 0,
|
||||
tileset: 1,
|
||||
environment: 1,
|
||||
paletteMode: 1,
|
||||
blocks: new Uint8Array(16),
|
||||
warps: [],
|
||||
backgroundEvents: [],
|
||||
objects: [],
|
||||
connections: [],
|
||||
sceneScripts: [],
|
||||
callbacks: [],
|
||||
coordinateEvents: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
override map(id: string) {
|
||||
return this.fixtures.get(id)!;
|
||||
}
|
||||
override isPorted(id: string) {
|
||||
return this.fixtures.has(id);
|
||||
}
|
||||
override sceneOwner() {
|
||||
return undefined;
|
||||
}
|
||||
override collision(map: GameMap, x: number, y: number) {
|
||||
if (x < 0 || y < 0 || x >= map.width || y >= map.height) return 7;
|
||||
return this.tiles.get(`${map.id}:${x},${y}`) ?? 0;
|
||||
}
|
||||
/** CollisionPermissionTable in brief: walls, then the water rows
|
||||
* ($2x, $3x, buoys $cx) apart from ice and the wall-like buoy tiles. */
|
||||
override permission(collision: number) {
|
||||
if (collision === 7 || collision === 0x27 || collision === 0x2f) return 15;
|
||||
if (collision === 0x23 || collision === 0x2b) return 0;
|
||||
return [0x20, 0x30, 0xc0].includes(collision & 0xf0) ? 1 : 0;
|
||||
}
|
||||
override grassCollisions() {
|
||||
return [];
|
||||
}
|
||||
override sprite(id: number) {
|
||||
return id ? { graphics: new Uint8Array(0), palette: 0, walking: false } : undefined;
|
||||
}
|
||||
tile(x: number, y: number, collision: number, map = "24:7") {
|
||||
this.tiles.set(`${map}:${x},${y}`, collision);
|
||||
}
|
||||
}
|
||||
|
||||
export function fixture() {
|
||||
const assets = new MovementAssets();
|
||||
const world = new GameWorld(
|
||||
assets,
|
||||
() => new Date("2026-09-22T12:00:00"),
|
||||
() => 0,
|
||||
);
|
||||
world.state.decorations = emptyDecorations();
|
||||
return { assets, world };
|
||||
}
|
||||
|
||||
export function connectWarp(assets: MovementAssets, x = 3, y = 4) {
|
||||
assets.map("24:7").warps.push({ x, y, targetMap: "24:6", targetWarp: 1 });
|
||||
assets
|
||||
.map("24:6")
|
||||
.warps.push({ x: 2, y: 2, targetMap: "24:7", targetWarp: 1 });
|
||||
assets.tile(2, 2, 0x70, "24:6");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user