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>
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
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");
|
|
}
|
|
|