'use strict'; // === MAIN RENDERER === // Draws the game world: floor, walls, torches, doors, etc. function renderFloor(theme) { for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { if (gameState.layout[y][x] === TILE_TYPES.FLOOR) { const shade = ((x + y) % 2 === 0) ? (theme.floorColor || COLORS.floor) : (theme.floorDarkColor || COLORS.floorDark); ctx.fillStyle = shade; ctx.fillRect(x * TILE, y * TILE, TILE, TILE); } } } } function renderWalls(theme) { const wc = getWallColors(theme); for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { if (gameState.layout[y][x] === TILE_TYPES.WALL) { const px = x * TILE; const py = y * TILE; ctx.fillStyle = wc.wall; ctx.fillRect(px, py, TILE, TILE); ctx.fillStyle = wc.top; ctx.fillRect(px, py, TILE, 3); ctx.fillStyle = wc.front; ctx.fillRect(px, py + 3, TILE, TILE - 6); ctx.fillStyle = wc.bottom; ctx.fillRect(px, py + TILE - 3, TILE, 3); ctx.strokeStyle = theme.wallBorderColor || '#404040'; ctx.lineWidth = 0.5; ctx.strokeRect(px + 0.5, py + 0.5, TILE - 1, TILE - 1); } } } } function renderTorches(theme) { for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { if (gameState.layout[y][x] === TILE_TYPES.TORCH) { const cx = x * TILE + TILE / 2; const cy = y * TILE + TILE / 2; // Glow ctx.fillStyle = theme.torchGlow || COLORS.torchGlow; ctx.beginPath(); ctx.arc(cx, cy, TILE * 1.2, 0, Math.PI * 2); ctx.fill(); // Floor tile under torch ctx.fillStyle = theme.floorColor || COLORS.floor; ctx.fillRect(x * TILE, y * TILE, TILE, TILE); // Stick ctx.fillStyle = theme.torchStickColor || COLORS.torchStick; ctx.fillRect(cx - 2, cy + 2, 4, TILE * 0.4); // Flame ctx.fillStyle = theme.torchFlameColor || COLORS.torchFlame; ctx.beginPath(); ctx.ellipse(cx, cy - 4, 4, 6, 0, 0, Math.PI * 2); ctx.fill(); // Flame core ctx.fillStyle = theme.torchCoreColor || '#FFFF00'; ctx.beginPath(); ctx.ellipse(cx, cy - 3, 2, 3, 0, 0, Math.PI * 2); ctx.fill(); } } } } function renderDoors(theme) { for (const door of gameState.doors) { const px = door.x * TILE; const py = door.y * TILE; ctx.fillStyle = COLORS.doorFrame; ctx.fillRect(px - 1, py - 1, TILE + 2, TILE + 2); if (door.opened) { ctx.fillStyle = (theme && theme.floorColor) || COLORS.floor; ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4); } else { ctx.fillStyle = COLORS.door; ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4); ctx.strokeStyle = '#8B7355'; ctx.lineWidth = 1; ctx.strokeRect(px + 2, py + 2, TILE - 4, TILE - 4); ctx.fillStyle = COLORS.doorKnob; ctx.beginPath(); const knobX = door.dir === 3 ? px + TILE - 7 : px + 7; ctx.arc(knobX, py + TILE / 2, 3, 0, Math.PI * 2); ctx.fill(); } } } function renderSoftwalls() { for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { if (gameState.layout[y][x] === TILE_TYPES.SOFTWALL) { const px = x * TILE; const py = y * TILE; ctx.fillStyle = COLORS.softwall; ctx.fillRect(px, py, TILE, TILE); ctx.fillStyle = COLORS.softwallDark; ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4); // Crack pattern ctx.strokeStyle = '#3A2818'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(px + 4, py + 4); ctx.lineTo(px + TILE / 2, py + TILE / 2); ctx.lineTo(px + TILE - 4, py + 6); ctx.stroke(); } } } } function renderLava() { for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { if (gameState.layout[y][x] === TILE_TYPES.LAVA) { const px = x * TILE; const py = y * TILE; const t = Date.now() * 0.003; ctx.fillStyle = COLORS.lava; ctx.fillRect(px, py, TILE, TILE); ctx.fillStyle = COLORS.lavaLight; const offset = Math.sin(t + x * 0.5 + y * 0.3) * 3; ctx.fillRect(px + 4 + offset, py + 6, TILE - 8, TILE - 10); // Bubbles ctx.fillStyle = '#FF6347'; ctx.beginPath(); const bx = px + TILE / 2 + Math.sin(t * 1.5 + x) * 4; const by = py + TILE / 2 + Math.cos(t * 1.2 + y) * 3; ctx.arc(bx, by, 2, 0, Math.PI * 2); ctx.fill(); } } } } function renderWater() { for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { if (gameState.layout[y][x] === TILE_TYPES.WATER) { const px = x * TILE; const py = y * TILE; const t = Date.now() * 0.002; ctx.fillStyle = COLORS.water; ctx.fillRect(px, py, TILE, TILE); ctx.fillStyle = COLORS.waterLight; const offset = Math.sin(t + x * 0.8 + y * 0.4) * 2; ctx.fillRect(px + 3 + offset, py + 8, TILE - 6, 3); ctx.fillRect(px + 6 - offset, py + 16, TILE - 10, 2); } } } } function renderTraps() { for (const trap of gameState.traps) { if (trap.triggered) continue; const px = trap.x * TILE; const py = trap.y * TILE; // Hidden trap - draw subtle indicator ctx.fillStyle = (trap.color || COLORS.trap) + '40'; ctx.fillRect(px + 4, py + 4, TILE - 8, TILE - 8); // Spike pattern ctx.fillStyle = trap.color || COLORS.trap; for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { ctx.beginPath(); const sx = px + 6 + i * 9; const sy = py + TILE - 6 - j * 9; ctx.moveTo(sx, sy - 4); ctx.lineTo(sx - 2, sy); ctx.lineTo(sx + 2, sy); ctx.closePath(); ctx.fill(); } } } } function renderLevers() { for (const lever of gameState.levers) { if (lever.activated) continue; const px = lever.x * TILE; const py = lever.y * TILE; // Base ctx.fillStyle = '#666'; ctx.fillRect(px + TILE / 2 - 2, py + TILE / 2 - 2, 4, 4); // Handle ctx.fillStyle = COLORS.lever; ctx.fillRect(px + TILE / 2 - 1, py + 4, 3, TILE / 2 - 4); // Knob ctx.fillStyle = '#DAA520'; ctx.beginPath(); ctx.arc(px + TILE / 2, py + 4, 3, 0, Math.PI * 2); ctx.fill(); } } function renderSwitches() { for (const sw of gameState.switches) { const px = sw.x * TILE; const py = sw.y * TILE; if (sw.activated) { ctx.fillStyle = COLORS.switchFloor; ctx.fillRect(px + 4, py + 4, TILE - 8, TILE - 8); ctx.fillStyle = '#00FF00'; ctx.beginPath(); ctx.arc(px + TILE / 2, py + TILE / 2, 3, 0, Math.PI * 2); ctx.fill(); } else { ctx.fillStyle = COLORS.switchFloor; ctx.fillRect(px + 4, py + 4, TILE - 8, TILE - 8); ctx.fillStyle = '#FFD700'; ctx.beginPath(); ctx.arc(px + TILE / 2, py + TILE / 2, 3, 0, Math.PI * 2); ctx.fill(); } } } function renderTargets() { for (const target of gameState.targets) { const cx = target.x * TILE + TILE / 2; const cy = target.y * TILE + TILE / 2; const occupied = gameState.rocks.some(r => r.x === target.x && r.y === target.y); ctx.fillStyle = COLORS.targetGlow; ctx.beginPath(); ctx.arc(cx, cy, TILE * 0.7, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = occupied ? '#00FF00' : COLORS.target; ctx.beginPath(); ctx.arc(cx, cy, TILE * 0.3, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = occupied ? '#00AA00' : '#CC9900'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(cx, cy, TILE * 0.3, 0, Math.PI * 2); ctx.stroke(); } } function renderRocks() { for (const rock of gameState.rocks) { const px = rock.x * TILE; const py = rock.y * TILE; const inset = 3; ctx.fillStyle = COLORS.rock; ctx.beginPath(); ctx.roundRect(px + inset, py + inset, TILE - inset * 2, TILE - inset * 2, 4); ctx.fill(); ctx.fillStyle = COLORS.rockLight; ctx.beginPath(); ctx.roundRect(px + inset + 1, py + inset + 1, (TILE - inset * 2) * 0.5, (TILE - inset * 2) * 0.4, 2); ctx.fill(); ctx.fillStyle = COLORS.rockDark; ctx.beginPath(); ctx.roundRect(px + inset + (TILE - inset * 2) * 0.4, py + inset + (TILE - inset * 2) * 0.5, (TILE - inset * 2) * 0.4, (TILE - inset * 2) * 0.4, 2); ctx.fill(); ctx.strokeStyle = '#555555'; ctx.lineWidth = 1; ctx.beginPath(); ctx.roundRect(px + inset, py + inset, TILE - inset * 2, TILE - inset * 2, 4); ctx.stroke(); } } function renderChests() { for (const chest of gameState.chests) { const px = chest.x * TILE; const py = chest.y * TILE; if (chest.opened) { ctx.fillStyle = COLORS.chestOpen; ctx.fillRect(px + 2, py + 10, TILE - 4, TILE - 12); ctx.fillStyle = COLORS.chestGold; ctx.fillRect(px + 4, py + 12, TILE - 8, TILE - 14); ctx.fillStyle = COLORS.chestClosed; ctx.fillRect(px + 2, py + 2, TILE - 4, 6); ctx.fillStyle = COLORS.chestGold; ctx.fillRect(px + 4, py + 3, TILE - 8, 4); } else { ctx.fillStyle = COLORS.chestClosed; ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4); ctx.fillStyle = COLORS.chestGold; ctx.fillRect(px + 4, py + 4, TILE - 8, TILE - 8); ctx.fillStyle = '#FFD700'; ctx.beginPath(); ctx.arc(px + TILE / 2, py + TILE / 2, 3, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#B8860B'; ctx.lineWidth = 1; ctx.strokeRect(px + 2, py + 2, TILE - 4, TILE - 4); } } } function renderItems() { for (const item of gameState.floorItems) { if (item.collected) continue; const px = item.x * TILE; const py = item.y * TILE; const def = ITEMS.find(i => i.id === item.id); if (!def) continue; const bob = Math.sin(Date.now() * 0.005 + item.x + item.y) * 2; if (item.id === 'heart_piece') { drawHeart(ctx, px + TILE / 2, py + TILE / 2 + bob, 10, true); } else if (item.id.startsWith('potion')) { ctx.fillStyle = def.color || '#FF0000'; ctx.fillRect(px + 10, py + 8 + bob, 12, 16); ctx.fillStyle = '#FFFFFF'; ctx.fillRect(px + 12, py + 6 + bob, 8, 4); } else if (item.id === 'arrows') { ctx.fillStyle = '#8B4513'; ctx.fillRect(px + 12, py + 4 + bob, 2, 24); ctx.fillStyle = '#808080'; ctx.fillRect(px + 10, py + 4 + bob, 6, 4); } else if (item.id === 'bombs') { ctx.fillStyle = '#333'; ctx.beginPath(); ctx.arc(px + TILE / 2, py + TILE / 2 + bob, 6, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#FF0000'; ctx.fillRect(px + TILE / 2 - 1, py + 2 + bob, 2, 4); } else if (item.id === 'key') { ctx.fillStyle = '#FFD700'; ctx.beginPath(); ctx.arc(px + 10, py + 10 + bob, 4, 0, Math.PI * 2); ctx.fill(); ctx.fillRect(px + 14, py + 9 + bob, 10, 3); ctx.fillRect(px + 20, py + 9 + bob, 3, 5); } else { ctx.fillStyle = def.color || '#FFD700'; ctx.beginPath(); ctx.arc(px + TILE / 2, py + TILE / 2 + bob, 5, 0, Math.PI * 2); ctx.fill(); } } } // Draw exit door function renderExit() { if (!gameState.exit) return; const px = gameState.exit.x * TILE; const py = gameState.exit.y * TILE; ctx.fillStyle = COLORS.doorFrame; ctx.fillRect(px - 1, py - 1, TILE + 2, TILE + 2); if (gameState.exitOpen) { ctx.fillStyle = '#1A1A1A'; ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4); } else { ctx.fillStyle = COLORS.door; ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4); ctx.strokeStyle = '#8B7355'; ctx.lineWidth = 1; ctx.strokeRect(px + 2, py + 2, TILE - 4, TILE - 4); ctx.fillStyle = COLORS.doorKnob; ctx.beginPath(); const knobX = gameState.exit.dir === 3 ? px + TILE - 7 : px + 7; ctx.arc(knobX, py + TILE / 2, 3, 0, Math.PI * 2); ctx.fill(); } ctx.fillStyle = '#FFFFFF'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center'; const arrowX = gameState.exit.dir === 3 ? px + TILE + 10 : px - 10; ctx.fillText( gameState.exit.dir === 3 ? '\u2192' : gameState.exit.dir === 2 ? '\u2190' : gameState.exit.dir === 0 ? '\u2193' : '\u2191', arrowX, py + TILE / 2 + 4 ); ctx.textAlign = 'left'; } // Draw player character function renderPlayer() { const p = gameState.player; const px = p.x * TILE; const py = p.y * TILE; const isHit = p.hitTimer > 0; const isInvincible = p.invincibleTimer > 0; ctx.save(); if (isHit) { ctx.globalAlpha = 0.5 + Math.sin(Date.now() * 0.03) * 0.5; } if (isInvincible) { ctx.globalAlpha = Math.sin(Date.now() * 0.01) > 0 ? 1 : 0.3; } const bodyColor = isHit ? '#FF6666' : COLORS.playerBody; // Body ctx.fillStyle = bodyColor; ctx.fillRect(px + 6, py + 14, TILE - 12, TILE - 16); // Tunic detail ctx.fillStyle = COLORS.playerTunic; ctx.fillRect(px + 8, py + 16, TILE - 16, TILE - 20); // Head ctx.fillStyle = COLORS.playerHead; ctx.beginPath(); ctx.arc(px + TILE / 2, py + 10, 7, 0, Math.PI * 2); ctx.fill(); // Hair ctx.fillStyle = COLORS.playerHair; ctx.beginPath(); ctx.arc(px + TILE / 2, py + 8, 7, Math.PI, Math.PI * 2); ctx.fill(); // Eyes ctx.fillStyle = COLORS.playerEyes; const eyeOffX = DIRS[p.dir].x * 2; const eyeOffY = DIRS[p.dir].y * 2; ctx.fillRect(px + TILE / 2 - 3 + eyeOffX, py + 9 + eyeOffY, 2, 2); ctx.fillRect(px + TILE / 2 + 1 + eyeOffX, py + 9 + eyeOffY, 2, 2); // Active weapon indicator if (p.activeWeapon) { drawWeaponHeld(p.activeWeapon, px, py, p.dir); } else if (p.weapons.length > 0) { drawWeaponHeld(p.weapons[0], px, py, p.dir); } ctx.restore(); } // Draw attack animation function renderAttackAnim() { const p = gameState.player; if (p.attackAnim <= 0 || !p.activeWeapon) return; const px = p.x * TILE; const py = p.y * TILE; const dir = DIRS[p.dir]; ctx.save(); ctx.globalAlpha = p.attackAnim / (p.activeWeapon.animDuration || ATTACK_ANIM); const ax = px + TILE / 2 + dir.x * TILE * 0.6; const ay = py + TILE / 2 + dir.y * TILE * 0.6; // Weapon-specific attack visual if (p.activeWeapon.type === 'melee') { ctx.fillStyle = p.activeWeapon.attackColor || COLORS.swordBlade; if (dir.x !== 0) { ctx.fillRect(ax - 3, ay - TILE * 0.4, 6, TILE * 0.8); } else { ctx.fillRect(ax - TILE * 0.4, ay - 3, TILE * 0.8, 6); } } // Slash effect ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = 2; ctx.globalAlpha = p.attackAnim / (p.activeWeapon.animDuration || ATTACK_ANIM) * 0.7; if (dir.x !== 0) { ctx.beginPath(); ctx.moveTo(ax, ay - TILE * 0.5); ctx.lineTo(ax + dir.x * 8, ay); ctx.lineTo(ax, ay + TILE * 0.5); ctx.stroke(); } else { ctx.beginPath(); ctx.moveTo(ax - TILE * 0.5, ay); ctx.lineTo(ax, ay + dir.y * 8); ctx.lineTo(ax + TILE * 0.5, ay); ctx.stroke(); } ctx.restore(); } // Draw messages function renderMessages() { if (gameState.messageTimer <= 0) return; ctx.save(); ctx.globalAlpha = Math.min(1, gameState.messageTimer / 300); ctx.fillStyle = 'rgba(0,0,0,0.7)'; const textW = ctx.measureText(gameState.message).width; const boxW = textW + 20; const boxX = (CANVAS_W - boxW) / 2; const boxY = CANVAS_H / 2 - 30; ctx.fillRect(boxX, boxY, boxW, 24); ctx.strokeStyle = '#FFD700'; ctx.lineWidth = 1; ctx.strokeRect(boxX, boxY, boxW, 24); ctx.fillStyle = COLORS.msgText; ctx.font = '14px monospace'; ctx.textAlign = 'center'; ctx.fillText(gameState.message, CANVAS_W / 2, boxY + 17); ctx.textAlign = 'left'; ctx.restore(); } if (typeof module !== 'undefined' && module.exports) { module.exports = { renderFloor, renderWalls, renderTorches, renderDoors, renderSoftwalls, renderLava, renderWater, renderTraps, renderLevers, renderSwitches, renderTargets, renderRocks, renderChests, renderItems, renderExit, renderPlayer, renderAttackAnim, renderMessages }; }