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
+15
-4
@@ -9,6 +9,7 @@ import { WorldRenderer } from "./rendering/renderer";
|
||||
export class NativeRuntime implements GameRuntime {
|
||||
readonly id = "crystal-native-world-v1";
|
||||
world?: GameWorld;
|
||||
fieldMenuOpen = false;
|
||||
private renderer?: WorldRenderer;
|
||||
private pressed = new Set<Button>();
|
||||
private raf = 0;
|
||||
@@ -26,6 +27,7 @@ export class NativeRuntime implements GameRuntime {
|
||||
) {}
|
||||
async load(bytes: Uint8Array, canvas: HTMLCanvasElement) {
|
||||
this.dispose();
|
||||
this.fieldMenuOpen = false;
|
||||
const assets = new CrystalAssets(bytes);
|
||||
const world = new GameWorld(assets);
|
||||
const player = this.player();
|
||||
@@ -57,20 +59,28 @@ export class NativeRuntime implements GameRuntime {
|
||||
if (this.pressed.has(button) || !this.world) return;
|
||||
this.pressed.add(button);
|
||||
const world = this.world;
|
||||
if (world.battle && this.battleInput?.(button)) {
|
||||
if (this.battleInput?.(button)) {
|
||||
this.publish();
|
||||
return;
|
||||
}
|
||||
if (button === "a") world.interact();
|
||||
if (button === "b") world.advanceDialogue();
|
||||
if (["up", "down", "left", "right"].includes(button) &&
|
||||
(world.dialogue?.choice || world.battle?.prompt?.kind === "yesno"))
|
||||
if (
|
||||
["up", "down", "left", "right"].includes(button) &&
|
||||
(world.dialogue?.choice || world.battle?.prompt?.kind === "yesno")
|
||||
)
|
||||
world.moveCursor(button as Direction);
|
||||
if ((button === "start" || button === "select") && !world.busy)
|
||||
world.notice =
|
||||
"Use the Savegames tab to save, export or import. Party, pack and Pokégear menus are not ported yet.";
|
||||
this.publish();
|
||||
}
|
||||
useBicycle() {
|
||||
this.fieldMenuOpen = false;
|
||||
if (this.world && !this.world.useBicycle())
|
||||
this.world.notice = "You can’t use the BICYCLE here.";
|
||||
this.publish();
|
||||
}
|
||||
/** Answer a choice from a pointer or keyboard control outside the canvas. */
|
||||
choose(answer: number | string) {
|
||||
this.world?.choose(answer);
|
||||
@@ -108,7 +118,7 @@ export class NativeRuntime implements GameRuntime {
|
||||
let ticks = 0;
|
||||
while (this.pending >= 1 / TICKS_PER_SECOND && ticks < 6) {
|
||||
this.pending -= 1 / TICKS_PER_SECOND;
|
||||
world.tick(held);
|
||||
if (!this.fieldMenuOpen) world.tick(held);
|
||||
ticks++;
|
||||
}
|
||||
if (ticks === 6) this.pending = 0;
|
||||
@@ -225,6 +235,7 @@ export class NativeRuntime implements GameRuntime {
|
||||
restoreState(bytes: Uint8Array) {
|
||||
if (!this.world || bytes.length > 64_000)
|
||||
throw new Error("Invalid native save size.");
|
||||
this.fieldMenuOpen = false;
|
||||
this.world.restore(
|
||||
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user