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
@@ -1,7 +1,9 @@
|
||||
import { OW } from "../world/constants";
|
||||
import { CrystalAssets } from "../rom/assets";
|
||||
import { GameWorld } from "../world/world";
|
||||
import type { Direction } from "../world/types";
|
||||
import { drawTile } from "./pixels";
|
||||
import { TilesetAnimator } from "./tileset-animation";
|
||||
import {
|
||||
BASE_VIEWPORT,
|
||||
renderViewportForBox,
|
||||
@@ -14,6 +16,11 @@ export class WorldRenderer {
|
||||
private context: CanvasRenderingContext2D;
|
||||
private image: ImageData;
|
||||
private canvas: HTMLCanvasElement;
|
||||
/** Background color indices of the current frame, for OAM priority. */
|
||||
private indices = new Uint8Array(0);
|
||||
/** The current tileset's animation, advanced two video frames per tick. */
|
||||
private animator?: TilesetAnimator;
|
||||
private animated = { key: "", ticks: 0 };
|
||||
constructor(
|
||||
private assets: CrystalAssets,
|
||||
canvas: HTMLCanvasElement,
|
||||
@@ -59,8 +66,19 @@ export class WorldRenderer {
|
||||
}
|
||||
render(world: GameWorld) {
|
||||
const map = world.map,
|
||||
tileset = this.assets.tileset(map),
|
||||
palettes = this.assets.palettes(map, world.timeOfDay);
|
||||
tileset = this.assets.tileset(map);
|
||||
const key = `${map.tileset}:${map.group}`;
|
||||
if (this.animated.key !== key) {
|
||||
this.animator = new TilesetAnimator(this.assets, map, tileset.graphics);
|
||||
this.animated = { key, ticks: world.ticks };
|
||||
}
|
||||
const animator = this.animator!;
|
||||
// _AnimateTileset runs in VBlank: two video frames per overworld tick.
|
||||
animator.advance(Math.min(240, (world.ticks - this.animated.ticks) * 2), world.timeOfDay === 3, world.liveState.flags.has(OW.ENGINE_FOREST_IS_RESTLESS));
|
||||
this.animated.ticks = world.ticks;
|
||||
const palettes = animator.palettes(this.assets.palettes(map, world.timeOfDay));
|
||||
if (this.indices.length !== this.width * this.height)
|
||||
this.indices = new Uint8Array(this.width * this.height);
|
||||
const poses = world.poses();
|
||||
const player = poses.find((pose) => pose.id === 0) ?? {
|
||||
x: world.x * 16,
|
||||
@@ -69,8 +87,9 @@ export class WorldRenderer {
|
||||
// Keep the camera player-centered as the original engine does. Beyond the
|
||||
// edges, connected maps are drawn like the original connection strips;
|
||||
// elsewhere the map's own border block fills the view.
|
||||
// An earthquake shakes the view vertically (StepFunction_ScreenShake).
|
||||
const cameraX = Math.round(player.x - (this.width / 2 - 8)),
|
||||
cameraY = Math.round(player.y - (this.height / 2 - 8));
|
||||
cameraY = Math.round(player.y - (this.height / 2 - 8)) + world.screenShake;
|
||||
this.image.data.fill(255);
|
||||
for (
|
||||
let tileY = Math.floor(cameraY / 8);
|
||||
@@ -97,11 +116,12 @@ export class WorldRenderer {
|
||||
this.image.data,
|
||||
this.width,
|
||||
this.height,
|
||||
tileset.graphics,
|
||||
animator.graphics,
|
||||
tile,
|
||||
tileX * 8 - cameraX,
|
||||
tileY * 8 - cameraY,
|
||||
palettes[attribute & 7],
|
||||
{ indices: this.indices },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -136,6 +156,8 @@ export class WorldRenderer {
|
||||
pose.frame,
|
||||
time,
|
||||
pose.palette,
|
||||
pose.facing,
|
||||
pose.inGrass,
|
||||
);
|
||||
const emote =
|
||||
world.emote && poses.find((pose) => pose.id === world.emote!.object);
|
||||
@@ -165,6 +187,8 @@ export class WorldRenderer {
|
||||
frame: number,
|
||||
time: number,
|
||||
paletteIndex?: number,
|
||||
facing?: number,
|
||||
inGrass = false,
|
||||
) {
|
||||
const sprite = this.assets.sprite(id);
|
||||
if (!sprite) return;
|
||||
@@ -172,10 +196,11 @@ export class WorldRenderer {
|
||||
paletteIndex ?? sprite.palette,
|
||||
time,
|
||||
);
|
||||
for (const piece of this.assets.facing(
|
||||
direction,
|
||||
sprite.walking ? frame : 0,
|
||||
)) {
|
||||
const pieces =
|
||||
facing !== undefined
|
||||
? this.assets.facingTiles(facing)
|
||||
: this.assets.facing(direction, sprite.walking ? frame : 0);
|
||||
for (const piece of pieces) {
|
||||
const tile = piece.tile >= 0x80 ? piece.tile - 0x80 + 12 : piece.tile;
|
||||
drawTile(
|
||||
this.image.data,
|
||||
@@ -190,6 +215,10 @@ export class WorldRenderer {
|
||||
transparent: true,
|
||||
flipX: Boolean(piece.attributes & 0x20),
|
||||
flipY: Boolean(piece.attributes & 0x40),
|
||||
// RELATIVE_ATTRIBUTES pieces (the lower half) take OAM_PRIO from
|
||||
// IN_GRASS_F and hide behind the grass's colored pixels.
|
||||
indices: this.indices,
|
||||
behind: inGrass && Boolean(piece.attributes & 0x02),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user