// worldRenderer.js - Renders the tile-based game world on canvas import { drawPlayer, drawTile, drawNPC, drawInteractionPrompt, drawDialogBox, TILE_SIZE, SCALE } from './sprites.js'; import { worldState } from './worldState.js'; import { getMap, getTile, getInteraction, getNPC } from './maps.js'; import { updateMovement } from './worldInput.js'; let canvas = null; 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() { if (!canvas) return; canvas.width = canvas.offsetWidth || window.innerWidth; canvas.height = canvas.offsetHeight || window.innerHeight; } /** Convert tile coords → screen pixel coords (camera-relative) */ export function worldToScreen(tileX, tileY) { 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; return { x: cx + (tileX * TILE_PX - pwx), y: cy + (tileY * TILE_PX - pwy) }; } /** 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; 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 }; } /** Tile the player is facing */ function getFacingTile() { const p = worldState.player; let x = p.x, y = p.y; if (p.direction === 'up') y--; else if (p.direction === 'down') y++; else if (p.direction === 'left') x--; else if (p.direction === 'right') x++; return { x, y }; } /** Main render frame */ export function renderWorld(timestamp) { const dt = (timestamp - lastTime) / 1000; lastTime = timestamp; // Update movement interpolation updateMovement(dt); // Resize check if (canvas.width !== canvas.offsetWidth || canvas.height !== canvas.offsetHeight) { resizeCanvas(); } // Clear ctx.fillStyle = '#0a0a0f'; ctx.fillRect(0, 0, canvas.width, canvas.height); const map = getMap(worldState.currentMap); if (!map) return; const bounds = getVisibleBounds(); // === 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 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); } } // === 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); // === Layer 4: Interaction prompt === if (!worldState.dialog && !worldState.player.moving) { const ft = getFacingTile(); 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); drawInteractionPrompt(ctx, pos.x, pos.y); } } // === Layer 5: Dialog box === if (worldState.dialog) { const line = worldState.dialog.lines[worldState.dialog.currentLine] || ''; const speaker = worldState.dialog.speakerName || ''; drawDialogBox(ctx, canvas.width, canvas.height, line, speaker); } // === HUD === drawHUD(); } function drawHUD() { const map = getMap(worldState.currentMap); const mapName = map ? map.name : worldState.currentMap; // Background bar ctx.fillStyle = 'rgba(10, 10, 15, 0.75)'; ctx.fillRect(0, 0, canvas.width, 32); ctx.font = 'bold 13px "Segoe UI", system-ui, sans-serif'; ctx.textBaseline = 'middle'; // Map name ctx.fillStyle = '#00e599'; ctx.textAlign = 'left'; ctx.fillText(`📍 ${mapName}`, 12, 16); // Inventory ctx.fillStyle = '#ff44aa'; ctx.textAlign = 'right'; ctx.fillText(`🔧 Components: ${worldState.inventory.length}`, canvas.width - 12, 16); // Controls hint ctx.fillStyle = '#555'; ctx.textAlign = 'center'; ctx.font = '11px "Segoe UI", system-ui, sans-serif'; ctx.fillText('WASD: Move | E: Interact | TAB: Workshop', canvas.width / 2, 16); } export function startWorldLoop() { lastTime = performance.now(); function loop(ts) { renderWorld(ts); animFrameId = requestAnimationFrame(loop); } animFrameId = requestAnimationFrame(loop); } export function stopWorldLoop() { if (animFrameId !== null) { cancelAnimationFrame(animFrameId); animFrameId = null; } }