Implement native opening scenes, battle foundation and overworld movement
Checks / web (push) Canceled after 0s
Checks / web (push) Canceled after 0s
This commit is contained in:
1 parent
8b80d05ca2
commit
c08d250cf6
39 files changed
+10727
-1524
No files matched your search
+100
-40
@@ -2,16 +2,26 @@ import { CrystalAssets } from "../rom/assets";
|
||||
import { GameWorld } from "../world/world";
|
||||
import type { Direction } from "../world/types";
|
||||
import { drawTile } from "./pixels";
|
||||
import {
|
||||
BASE_VIEWPORT,
|
||||
renderViewportForBox,
|
||||
type RenderViewport,
|
||||
} from "./viewport";
|
||||
|
||||
export class WorldRenderer {
|
||||
readonly width = 160;
|
||||
readonly height = 144;
|
||||
width: number = BASE_VIEWPORT.width;
|
||||
height: number = BASE_VIEWPORT.height;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private image: ImageData;
|
||||
private canvas: HTMLCanvasElement;
|
||||
constructor(
|
||||
private assets: CrystalAssets,
|
||||
canvas: HTMLCanvasElement,
|
||||
) {
|
||||
this.canvas = canvas;
|
||||
const viewport = this.viewportFromCanvas();
|
||||
this.width = viewport.width;
|
||||
this.height = viewport.height;
|
||||
canvas.width = this.width;
|
||||
canvas.height = this.height;
|
||||
const context = canvas.getContext("2d", { alpha: false });
|
||||
@@ -19,19 +29,48 @@ export class WorldRenderer {
|
||||
this.context = context;
|
||||
this.image = context.createImageData(this.width, this.height);
|
||||
}
|
||||
render(
|
||||
world: GameWorld,
|
||||
playerX = world.x * 16,
|
||||
playerY = world.y * 16,
|
||||
frame = 0,
|
||||
) {
|
||||
resize(cssWidth: number, cssHeight: number) {
|
||||
// ResizeObserver reports zero-sized boxes while an ancestor is hidden.
|
||||
// Retaining the last usable viewport avoids destroying the current frame.
|
||||
if (
|
||||
!Number.isFinite(cssWidth) ||
|
||||
!Number.isFinite(cssHeight) ||
|
||||
cssWidth <= 0 ||
|
||||
cssHeight <= 0
|
||||
)
|
||||
return false;
|
||||
const viewport = renderViewportForBox(cssWidth, cssHeight);
|
||||
if (viewport.width === this.width && viewport.height === this.height)
|
||||
return false;
|
||||
this.width = viewport.width;
|
||||
this.height = viewport.height;
|
||||
this.canvas.width = this.width;
|
||||
this.canvas.height = this.height;
|
||||
this.image = this.context.createImageData(this.width, this.height);
|
||||
return true;
|
||||
}
|
||||
private viewportFromCanvas(): RenderViewport {
|
||||
try {
|
||||
const box = this.canvas.getBoundingClientRect();
|
||||
return renderViewportForBox(box.width, box.height);
|
||||
} catch {
|
||||
return { ...BASE_VIEWPORT, pixelScale: 3 };
|
||||
}
|
||||
}
|
||||
render(world: GameWorld) {
|
||||
const map = world.map,
|
||||
tileset = this.assets.tileset(map),
|
||||
palettes = this.assets.palettes(map, world.timeOfDay);
|
||||
// Keep the camera player-centered as the original engine does. Out-of-map
|
||||
// pixels use the map's own border block, including small room padding.
|
||||
const cameraX = Math.round(playerX - 72),
|
||||
cameraY = Math.round(playerY - 64);
|
||||
const poses = world.poses();
|
||||
const player = poses.find((pose) => pose.id === 0) ?? {
|
||||
x: world.x * 16,
|
||||
y: world.y * 16,
|
||||
};
|
||||
// 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.
|
||||
const cameraX = Math.round(player.x - (this.width / 2 - 8)),
|
||||
cameraY = Math.round(player.y - (this.height / 2 - 8));
|
||||
this.image.data.fill(255);
|
||||
for (
|
||||
let tileY = Math.floor(cameraY / 8);
|
||||
@@ -43,7 +82,7 @@ export class WorldRenderer {
|
||||
tileX < Math.ceil((cameraX + this.width) / 8);
|
||||
tileX++
|
||||
) {
|
||||
const block = this.assets.block(
|
||||
const block = this.assets.visibleBlock(
|
||||
map,
|
||||
Math.floor(tileX / 2),
|
||||
Math.floor(tileY / 2),
|
||||
@@ -66,35 +105,56 @@ export class WorldRenderer {
|
||||
);
|
||||
}
|
||||
}
|
||||
const actors = world
|
||||
.objects()
|
||||
.map((object) => ({
|
||||
x: object.x * 16,
|
||||
y: object.y * 16,
|
||||
sprite: object.sprite,
|
||||
direction: world.objectDirection(object),
|
||||
palette: object.palette ? object.palette - 1 : undefined,
|
||||
frame: 0,
|
||||
}));
|
||||
actors.push({
|
||||
x: playerX,
|
||||
y: playerY,
|
||||
sprite: world.avatar === "kris" ? 0x60 : 1,
|
||||
direction: world.direction,
|
||||
palette: undefined,
|
||||
frame,
|
||||
});
|
||||
actors.sort((a, b) => a.y - b.y);
|
||||
for (const actor of actors)
|
||||
const time = world.timeOfDay;
|
||||
// Temporary effect objects use PAL_OW_EMOTE.
|
||||
const effectPalette = this.assets.objectPalette(5, time);
|
||||
for (const pose of poses) {
|
||||
if (pose.shadow === undefined) continue;
|
||||
const shadow = this.assets.emote(8);
|
||||
for (const [x, flipX] of [
|
||||
[0, false],
|
||||
[8, true],
|
||||
] as const)
|
||||
drawTile(
|
||||
this.image.data,
|
||||
this.width,
|
||||
this.height,
|
||||
shadow,
|
||||
0,
|
||||
pose.x - cameraX + x,
|
||||
pose.y - cameraY - 4 + pose.shadow,
|
||||
effectPalette,
|
||||
{ transparent: true, flipX },
|
||||
);
|
||||
}
|
||||
for (const pose of [...poses].sort((a, b) => a.y - b.y))
|
||||
this.actor(
|
||||
actor.sprite,
|
||||
Math.round(actor.x - cameraX),
|
||||
Math.round(actor.y - cameraY - 4),
|
||||
actor.direction,
|
||||
actor.frame,
|
||||
world.timeOfDay,
|
||||
actor.palette,
|
||||
pose.sprite,
|
||||
pose.x - cameraX,
|
||||
pose.y - cameraY - 4 + (pose.lift ?? 0),
|
||||
pose.direction,
|
||||
pose.frame,
|
||||
time,
|
||||
pose.palette,
|
||||
);
|
||||
const emote =
|
||||
world.emote && poses.find((pose) => pose.id === world.emote!.object);
|
||||
if (emote) {
|
||||
// FacingEmote: a 2x2 bubble 16 pixels above the object.
|
||||
const graphics = this.assets.emote(world.emote!.emote);
|
||||
for (const piece of this.assets.facingTiles(0x14))
|
||||
drawTile(
|
||||
this.image.data,
|
||||
this.width,
|
||||
this.height,
|
||||
graphics,
|
||||
piece.tile - 0xf8,
|
||||
emote.x - cameraX + piece.x,
|
||||
emote.y - cameraY - 4 - 16 + piece.y,
|
||||
effectPalette,
|
||||
{ transparent: true },
|
||||
);
|
||||
}
|
||||
this.context.putImageData(this.image, 0, 0);
|
||||
}
|
||||
private actor(
|
||||
|
||||
Reference in new issue
Block a user