refactor: migrate world rendering from programmatic sprites to PNG assets
Replace pixel-art drawing with pre-rendered PNG map backgrounds and character/NPC sprite images from pokemon-js reference. Maps now use coordinate-based wall arrays instead of tile grids. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
// worldRenderer.js - Renders the tile-based game world on canvas
|
||||
import { drawPlayer, drawTile, drawNPC, drawInteractionPrompt, drawDialogBox, TILE_SIZE, SCALE } from './sprites.js';
|
||||
// worldRenderer.js - Renders PNG-based game world on canvas
|
||||
import {
|
||||
drawMapImage, drawPlayer, drawNPC, drawInteractionPrompt,
|
||||
drawDialogBox, preloadAssets, TILE_PX, SCALE
|
||||
} from './sprites.js';
|
||||
import { worldState } from './worldState.js';
|
||||
import { getMap, getTile, getInteraction, getNPC } from './maps.js';
|
||||
import { getMap, getInteraction, getNPC } from './maps.js';
|
||||
import { updateMovement } from './worldInput.js';
|
||||
|
||||
let canvas = null;
|
||||
@@ -9,14 +12,11 @@ let ctx = null;
|
||||
let animFrameId = null;
|
||||
let lastTime = 0;
|
||||
|
||||
const TILE_PX = TILE_SIZE * SCALE; // 48px per tile on screen
|
||||
|
||||
export function initWorldRenderer() {
|
||||
canvas = document.getElementById('canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
return true;
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
@@ -25,36 +25,30 @@ function resizeCanvas() {
|
||||
canvas.height = canvas.offsetHeight || window.innerHeight;
|
||||
}
|
||||
|
||||
/** Convert tile coords → screen pixel coords (camera-relative) */
|
||||
export function worldToScreen(tileX, tileY) {
|
||||
// ==================== Camera ====================
|
||||
|
||||
/** Get the pixel offset to draw the map so the player is centered */
|
||||
function getCameraOffset() {
|
||||
const p = worldState.player;
|
||||
const cx = canvas.width / 2;
|
||||
const cy = canvas.height / 2;
|
||||
// Player world position with interpolation
|
||||
const pwx = (p.x + p.px) * TILE_PX;
|
||||
const pwy = (p.y + p.py) * TILE_PX;
|
||||
const playerWorldX = (p.x + p.px) * TILE_PX;
|
||||
const playerWorldY = (p.y + p.py) * TILE_PX;
|
||||
return {
|
||||
x: cx + (tileX * TILE_PX - pwx),
|
||||
y: cy + (tileY * TILE_PX - pwy)
|
||||
x: canvas.width / 2 - playerWorldX - TILE_PX / 2,
|
||||
y: canvas.height / 2 - playerWorldY - TILE_PX / 2
|
||||
};
|
||||
}
|
||||
|
||||
/** Visible tile range for culling */
|
||||
function getVisibleBounds() {
|
||||
const p = worldState.player;
|
||||
const halfW = canvas.width / 2;
|
||||
const halfH = canvas.height / 2;
|
||||
const pwx = (p.x + p.px) * TILE_PX;
|
||||
const pwy = (p.y + p.py) * TILE_PX;
|
||||
/** Convert tile position to screen position */
|
||||
function tileToScreen(tileX, tileY) {
|
||||
const cam = getCameraOffset();
|
||||
return {
|
||||
minX: Math.floor((pwx - halfW) / TILE_PX) - 1,
|
||||
minY: Math.floor((pwy - halfH) / TILE_PX) - 1,
|
||||
maxX: Math.ceil((pwx + halfW) / TILE_PX) + 1,
|
||||
maxY: Math.ceil((pwy + halfH) / TILE_PX) + 1
|
||||
x: tileX * TILE_PX + cam.x,
|
||||
y: tileY * TILE_PX + cam.y
|
||||
};
|
||||
}
|
||||
|
||||
/** Tile the player is facing */
|
||||
// ==================== Facing tile ====================
|
||||
|
||||
function getFacingTile() {
|
||||
const p = worldState.player;
|
||||
let x = p.x, y = p.y;
|
||||
@@ -65,12 +59,13 @@ function getFacingTile() {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
/** Main render frame */
|
||||
// ==================== Main render ====================
|
||||
|
||||
export function renderWorld(timestamp) {
|
||||
const dt = (timestamp - lastTime) / 1000;
|
||||
lastTime = timestamp;
|
||||
|
||||
// Update movement interpolation
|
||||
// Update movement
|
||||
updateMovement(dt);
|
||||
|
||||
// Resize check
|
||||
@@ -85,34 +80,34 @@ export function renderWorld(timestamp) {
|
||||
const map = getMap(worldState.currentMap);
|
||||
if (!map) return;
|
||||
|
||||
const bounds = getVisibleBounds();
|
||||
const cam = getCameraOffset();
|
||||
|
||||
// === Layer 1: Tiles ===
|
||||
for (let ty = bounds.minY; ty <= bounds.maxY; ty++) {
|
||||
for (let tx = bounds.minX; tx <= bounds.maxX; tx++) {
|
||||
const tileType = getTile(worldState.currentMap, tx, ty);
|
||||
if (tileType === null) continue;
|
||||
const pos = worldToScreen(tx, ty);
|
||||
drawTile(ctx, pos.x, pos.y, tileType);
|
||||
}
|
||||
}
|
||||
// === Layer 1: Map background (PNG) ===
|
||||
drawMapImage(ctx, map.image, cam.x, cam.y);
|
||||
|
||||
// === Layer 2: NPCs ===
|
||||
if (map.npcs) {
|
||||
for (const npc of map.npcs) {
|
||||
const pos = worldToScreen(npc.x, npc.y);
|
||||
drawNPC(ctx, pos.x, pos.y, npc.type, 0);
|
||||
const pos = tileToScreen(npc.x, npc.y);
|
||||
drawNPC(ctx, pos.x, pos.y, npc.facing || 'down');
|
||||
}
|
||||
}
|
||||
|
||||
// === Layer 3: Player ===
|
||||
const pp = worldToScreen(worldState.player.x + worldState.player.px,
|
||||
worldState.player.y + worldState.player.py);
|
||||
// Adjust: worldToScreen already offsets from player, so player is always at center
|
||||
const pcx = canvas.width / 2 - TILE_PX / 2;
|
||||
const pcy = canvas.height / 2 - TILE_PX / 2;
|
||||
const frame = worldState.player.moving ? (Math.floor(Date.now() / 120) % 2) : 0;
|
||||
drawPlayer(ctx, pcx, pcy, worldState.player.direction, frame);
|
||||
// Character sprite is 32x32 native (2 tiles tall)
|
||||
// Position so bottom half aligns with player tile, top half overlaps above
|
||||
const playerScreen = tileToScreen(
|
||||
worldState.player.x + worldState.player.px,
|
||||
worldState.player.y + worldState.player.py
|
||||
);
|
||||
// Offset upward by 1 tile since character is 2 tiles tall
|
||||
const playerDrawX = playerScreen.x;
|
||||
const playerDrawY = playerScreen.y - TILE_PX;
|
||||
|
||||
const walkFrame = worldState.player.moving
|
||||
? (Math.floor(Date.now() / 150) % 2) + 1 // alternates 1, 2
|
||||
: 0;
|
||||
drawPlayer(ctx, playerDrawX, playerDrawY, worldState.player.direction, walkFrame);
|
||||
|
||||
// === Layer 4: Interaction prompt ===
|
||||
if (!worldState.dialog && !worldState.player.moving) {
|
||||
@@ -120,12 +115,12 @@ export function renderWorld(timestamp) {
|
||||
const inter = getInteraction(worldState.currentMap, ft.x, ft.y);
|
||||
const npc = getNPC(worldState.currentMap, ft.x, ft.y);
|
||||
if (inter || npc) {
|
||||
const pos = worldToScreen(ft.x, ft.y);
|
||||
const pos = tileToScreen(ft.x, ft.y);
|
||||
drawInteractionPrompt(ctx, pos.x, pos.y);
|
||||
}
|
||||
}
|
||||
|
||||
// === Layer 5: Dialog box ===
|
||||
// === Layer 5: Dialog ===
|
||||
if (worldState.dialog) {
|
||||
const line = worldState.dialog.lines[worldState.dialog.currentLine] || '';
|
||||
const speaker = worldState.dialog.speakerName || '';
|
||||
@@ -133,11 +128,10 @@ export function renderWorld(timestamp) {
|
||||
}
|
||||
|
||||
// === HUD ===
|
||||
drawHUD();
|
||||
drawHUD(map);
|
||||
}
|
||||
|
||||
function drawHUD() {
|
||||
const map = getMap(worldState.currentMap);
|
||||
function drawHUD(map) {
|
||||
const mapName = map ? map.name : worldState.currentMap;
|
||||
|
||||
// Background bar
|
||||
@@ -164,7 +158,12 @@ function drawHUD() {
|
||||
ctx.fillText('WASD: Move | E: Interact | TAB: Workshop', canvas.width / 2, 16);
|
||||
}
|
||||
|
||||
export function startWorldLoop() {
|
||||
// ==================== Loop control ====================
|
||||
|
||||
export async function startWorldLoop() {
|
||||
// Ensure assets are loaded before starting
|
||||
await preloadAssets();
|
||||
|
||||
lastTime = performance.now();
|
||||
function loop(ts) {
|
||||
renderWorld(ts);
|
||||
|
||||
Reference in New Issue
Block a user