// Rendering System const Renderer = { canvas: null, ctx: null, camera: { x: 0, y: 0, zoom: 1 }, // Initialize renderer init() { this.canvas = document.getElementById('game-canvas'); this.ctx = this.canvas.getContext('2d'); this.resize(); window.addEventListener('resize', () => this.resize()); }, // Resize canvas to container resize() { const container = this.canvas.parentElement; this.canvas.width = container.clientWidth; this.canvas.height = container.clientHeight; }, // Center camera on position centerCamera(x, y) { this.camera.x = x * CONFIG.TILE_SIZE * this.camera.zoom - this.canvas.width / 2; this.camera.y = y * CONFIG.TILE_SIZE * this.camera.zoom - this.canvas.height / 2; }, // Main render function render(currentTool, rotation, mouseWorld) { this.clear(); this.drawTerrain(); this.drawBuildings(); this.drawTowers(); this.drawEnemies(); this.drawProjectiles(); this.drawMiningProgress(); this.drawPlacementPreview(currentTool, rotation, mouseWorld); this.drawDeletePreview(currentTool, mouseWorld); }, // Clear canvas clear() { this.ctx.fillStyle = '#1a2a1a'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); }, // Draw terrain and resources drawTerrain() { const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom; const startX = Math.floor(this.camera.x / tileScreenSize); const startY = Math.floor(this.camera.y / tileScreenSize); const endX = startX + Math.ceil(this.canvas.width / tileScreenSize) + 1; const endY = startY + Math.ceil(this.canvas.height / tileScreenSize) + 1; for (let y = startY; y < endY; y++) { for (let x = startX; x < endX; x++) { if (!Utils.inBounds(x, y)) continue; const tile = Terrain.getTile(x, y); const screen = Utils.worldToScreen(x, y, this.camera); const size = tileScreenSize; // Grass with variation const grassVariant = ((x * 7 + y * 13) % 3); const grassColors = ['#2d4a2d', '#3a5a3a', '#335533']; this.ctx.fillStyle = grassColors[grassVariant]; this.ctx.fillRect(screen.x, screen.y, size, size); // Grid lines this.ctx.strokeStyle = 'rgba(0,0,0,0.2)'; this.ctx.lineWidth = 1; this.ctx.strokeRect(screen.x, screen.y, size, size); // Draw resources if (tile.resource && tile.amount > 0) { this.drawResource(screen.x, screen.y, size, tile.resource, tile.amount); } } } }, // Draw a resource tile drawResource(screenX, screenY, size, type, amount) { const padding = size * 0.1; const richness = Math.min(1, amount / 2000); const colors = CONFIG.RESOURCE_COLORS[type]; // Gradient fill const grad = this.ctx.createLinearGradient(screenX, screenY, screenX + size, screenY + size); grad.addColorStop(0, colors.light); grad.addColorStop(1, colors.dark); this.ctx.fillStyle = grad; Utils.drawRoundedRect(this.ctx, screenX + padding, screenY + padding, size - padding * 2, size - padding * 2, 4 * this.camera.zoom); this.ctx.fill(); // Richness highlight this.ctx.fillStyle = `rgba(255,255,255,${richness * 0.25})`; Utils.drawRoundedRect(this.ctx, screenX + padding, screenY + padding, size - padding * 2, size - padding * 2, 4 * this.camera.zoom); this.ctx.fill(); // Texture dots this.ctx.fillStyle = 'rgba(255,255,255,0.3)'; for (let i = 0; i < 3; i++) { const ox = screenX + size * 0.3 + (i % 2) * size * 0.4; const oy = screenY + size * 0.3 + Math.floor(i / 2) * size * 0.4; this.ctx.beginPath(); this.ctx.arc(ox, oy, 2 * this.camera.zoom, 0, Math.PI * 2); this.ctx.fill(); } }, // Draw all buildings drawBuildings() { const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom; Buildings.list.forEach(b => { const screen = Utils.worldToScreen(b.x, b.y, this.camera); const bSize = Utils.getBuildingSize(b.type); const w = bSize.w * tileScreenSize; const h = bSize.h * tileScreenSize; const pad = 3 * this.camera.zoom; const style = CONFIG.BUILDING_STYLES[b.type]; // Shadow this.ctx.fillStyle = 'rgba(0,0,0,0.3)'; Utils.drawRoundedRect(this.ctx, screen.x + pad + 3, screen.y + pad + 3, w - pad * 2, h - pad * 2, 6 * this.camera.zoom); this.ctx.fill(); // Main body gradient const grad = this.ctx.createLinearGradient(screen.x, screen.y, screen.x + w, screen.y + h); grad.addColorStop(0, style.light); grad.addColorStop(1, style.dark); this.ctx.fillStyle = grad; Utils.drawRoundedRect(this.ctx, screen.x + pad, screen.y + pad, w - pad * 2, h - pad * 2, 6 * this.camera.zoom); this.ctx.fill(); // Highlight this.ctx.fillStyle = 'rgba(255,255,255,0.15)'; Utils.drawRoundedRect(this.ctx, screen.x + pad, screen.y + pad, w - pad * 2, (h - pad * 2) * 0.4, 6 * this.camera.zoom); this.ctx.fill(); // Border this.ctx.strokeStyle = 'rgba(0,0,0,0.4)'; this.ctx.lineWidth = 2 * this.camera.zoom; Utils.drawRoundedRect(this.ctx, screen.x + pad, screen.y + pad, w - pad * 2, h - pad * 2, 6 * this.camera.zoom); this.ctx.stroke(); // Direction indicator if (['belt', 'inserter', 'miner'].includes(b.type)) { this.drawDirectionArrow(screen.x + w / 2, screen.y + h / 2, w, b.rotation); } // Progress bar if (b.progress > 0 && b.type !== 'belt') { this.drawProgressBar(screen.x, screen.y, w, h, pad, b.progress); } // Belt items if (b.type === 'belt') { const totalItems = Buildings.getTotalInventory(b) + Buildings.getTotalOutput(b); if (totalItems > 0) { this.ctx.fillStyle = 'rgba(0,0,0,0.3)'; this.ctx.beginPath(); this.ctx.arc(screen.x + w / 2 + 1, screen.y + h / 2 + 1, 5 * this.camera.zoom, 0, Math.PI * 2); this.ctx.fill(); this.ctx.fillStyle = '#ffcc44'; this.ctx.beginPath(); this.ctx.arc(screen.x + w / 2, screen.y + h / 2, 5 * this.camera.zoom, 0, Math.PI * 2); this.ctx.fill(); } } // Assembler recipe icon if (b.type === 'assembler' && b.recipe) { this.ctx.fillStyle = 'rgba(0,0,0,0.5)'; this.ctx.font = `bold ${16 * this.camera.zoom}px sans-serif`; this.ctx.textAlign = 'center'; this.ctx.textBaseline = 'middle'; this.ctx.fillText(b.recipe === 'gear' ? '⚙' : '⚡', screen.x + w / 2 + 1, screen.y + h / 2 + 1); this.ctx.fillStyle = '#fff'; this.ctx.fillText(b.recipe === 'gear' ? '⚙' : '⚡', screen.x + w / 2, screen.y + h / 2); } // Chest contents if (b.type === 'chest') { const total = Buildings.getTotalOutput(b); if (total > 0) { this.ctx.fillStyle = '#fff'; this.ctx.font = `bold ${10 * this.camera.zoom}px sans-serif`; this.ctx.textAlign = 'center'; this.ctx.textBaseline = 'middle'; this.ctx.fillText(total > 99 ? '99+' : total, screen.x + w / 2, screen.y + h / 2); } } }); }, // Draw direction arrow drawDirectionArrow(cx, cy, size, rotation) { this.ctx.save(); this.ctx.translate(cx, cy); this.ctx.rotate(rotation * Math.PI / 2); // Shadow this.ctx.fillStyle = 'rgba(0,0,0,0.3)'; this.ctx.beginPath(); this.ctx.moveTo(size * 0.25 + 2, 2); this.ctx.lineTo(-size * 0.05 + 2, -size * 0.12 + 2); this.ctx.lineTo(-size * 0.05 + 2, size * 0.12 + 2); this.ctx.closePath(); this.ctx.fill(); // Arrow this.ctx.fillStyle = '#fff'; this.ctx.beginPath(); this.ctx.moveTo(size * 0.25, 0); this.ctx.lineTo(-size * 0.05, -size * 0.12); this.ctx.lineTo(-size * 0.05, size * 0.12); this.ctx.closePath(); this.ctx.fill(); this.ctx.restore(); }, // Draw progress bar drawProgressBar(x, y, w, h, pad, progress) { const barW = w - pad * 4; const barH = 6 * this.camera.zoom; const barX = x + pad * 2; const barY = y + h - pad * 2 - barH; this.ctx.fillStyle = 'rgba(0,0,0,0.5)'; Utils.drawRoundedRect(this.ctx, barX, barY, barW, barH, 2 * this.camera.zoom); this.ctx.fill(); const progGrad = this.ctx.createLinearGradient(barX, barY, barX, barY + barH); progGrad.addColorStop(0, '#66ff66'); progGrad.addColorStop(1, '#22aa22'); this.ctx.fillStyle = progGrad; Utils.drawRoundedRect(this.ctx, barX + 1, barY + 1, (barW - 2) * progress, barH - 2, 2 * this.camera.zoom); this.ctx.fill(); }, // Draw manual mining progress drawMiningProgress() { if (!Simulation.mining.active) return; const screen = Utils.worldToScreen(Simulation.mining.x, Simulation.mining.y, this.camera); const size = CONFIG.TILE_SIZE * this.camera.zoom; // Mining highlight this.ctx.fillStyle = 'rgba(136, 204, 68, 0.3)'; this.ctx.fillRect(screen.x, screen.y, size, size); // Progress ring this.ctx.strokeStyle = '#88cc44'; this.ctx.lineWidth = 4 * this.camera.zoom; this.ctx.beginPath(); this.ctx.arc( screen.x + size / 2, screen.y + size / 2, size / 3, -Math.PI / 2, -Math.PI / 2 + Simulation.mining.progress * Math.PI * 2 ); this.ctx.stroke(); // Pickaxe icon this.ctx.fillStyle = '#fff'; this.ctx.font = `${14 * this.camera.zoom}px sans-serif`; this.ctx.textAlign = 'center'; this.ctx.textBaseline = 'middle'; this.ctx.fillText('⛏', screen.x + size / 2, screen.y + size / 2); }, // Draw placement preview drawPlacementPreview(currentTool, rotation, mouseWorld) { if (currentTool === 'select' || currentTool === 'delete' || currentTool === 'mine') return; const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom; const screen = Utils.worldToScreen(mouseWorld.x, mouseWorld.y, this.camera); // Check if it's a tower if (Towers.TYPES[currentTool]) { const towerType = Towers.TYPES[currentTool]; const canPlace = Towers.canPlace(currentTool, mouseWorld.x, mouseWorld.y); this.ctx.globalAlpha = 0.6; // Tower preview circle this.ctx.fillStyle = canPlace ? 'rgba(100, 255, 100, 0.3)' : 'rgba(255, 100, 100, 0.3)'; this.ctx.beginPath(); this.ctx.arc(screen.x + tileScreenSize/2, screen.y + tileScreenSize/2, tileScreenSize/2, 0, Math.PI * 2); this.ctx.fill(); this.ctx.strokeStyle = canPlace ? '#66ff66' : '#ff6666'; this.ctx.lineWidth = 2; this.ctx.setLineDash([5, 5]); this.ctx.beginPath(); this.ctx.arc(screen.x + tileScreenSize/2, screen.y + tileScreenSize/2, tileScreenSize/2, 0, Math.PI * 2); this.ctx.stroke(); // Range preview this.ctx.strokeStyle = canPlace ? 'rgba(100, 255, 100, 0.3)' : 'rgba(255, 100, 100, 0.3)'; this.ctx.beginPath(); this.ctx.arc(screen.x + tileScreenSize/2, screen.y + tileScreenSize/2, towerType.range * this.camera.zoom, 0, Math.PI * 2); this.ctx.stroke(); this.ctx.setLineDash([]); this.ctx.globalAlpha = 1; return; } if (!CONFIG.COSTS[currentTool]) return; const previewSize = Utils.getBuildingSize(currentTool); const w = previewSize.w * tileScreenSize; const h = previewSize.h * tileScreenSize; const canPlace = Buildings.canPlace(currentTool, mouseWorld.x, mouseWorld.y) && Resources.canAfford(CONFIG.COSTS[currentTool]); this.ctx.globalAlpha = 0.6; this.ctx.fillStyle = canPlace ? 'rgba(100, 255, 100, 0.3)' : 'rgba(255, 100, 100, 0.3)'; this.ctx.fillRect(screen.x, screen.y, w, h); this.ctx.strokeStyle = canPlace ? '#66ff66' : '#ff6666'; this.ctx.lineWidth = 2; this.ctx.setLineDash([5, 5]); this.ctx.strokeRect(screen.x, screen.y, w, h); this.ctx.setLineDash([]); // Rotation arrow preview if (['belt', 'inserter', 'miner'].includes(currentTool)) { this.ctx.save(); this.ctx.translate(screen.x + w / 2, screen.y + h / 2); this.ctx.rotate(rotation * Math.PI / 2); this.ctx.fillStyle = canPlace ? '#66ff66' : '#ff6666'; this.ctx.beginPath(); this.ctx.moveTo(w * 0.3, 0); this.ctx.lineTo(-w * 0.1, -w * 0.15); this.ctx.lineTo(-w * 0.1, w * 0.15); this.ctx.closePath(); this.ctx.fill(); this.ctx.restore(); } this.ctx.globalAlpha = 1; }, // Draw delete preview drawDeletePreview(currentTool, mouseWorld) { if (currentTool !== 'delete') return; const building = Buildings.getAt(mouseWorld.x, mouseWorld.y); const tower = Towers.getAt(mouseWorld.x, mouseWorld.y); if (building) { const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom; const screen = Utils.worldToScreen(building.x, building.y, this.camera); const bSize = Utils.getBuildingSize(building.type); const w = bSize.w * tileScreenSize; const h = bSize.h * tileScreenSize; this.drawDeleteBox(screen.x, screen.y, w, h); } else if (tower) { const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom; const screen = Utils.worldToScreen(tower.x, tower.y, this.camera); this.drawDeleteBox(screen.x, screen.y, tileScreenSize, tileScreenSize); } }, // Draw delete box helper drawDeleteBox(x, y, w, h) { this.ctx.fillStyle = 'rgba(255, 50, 50, 0.4)'; this.ctx.fillRect(x, y, w, h); this.ctx.strokeStyle = '#ff3333'; this.ctx.lineWidth = 3; this.ctx.strokeRect(x, y, w, h); // X mark this.ctx.strokeStyle = '#ff3333'; this.ctx.lineWidth = 4 * this.camera.zoom; this.ctx.beginPath(); this.ctx.moveTo(x + w * 0.2, y + h * 0.2); this.ctx.lineTo(x + w * 0.8, y + h * 0.8); this.ctx.moveTo(x + w * 0.8, y + h * 0.2); this.ctx.lineTo(x + w * 0.2, y + h * 0.8); this.ctx.stroke(); }, // Draw all towers drawTowers() { const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom; Towers.list.forEach(tower => { const type = Towers.TYPES[tower.type]; const screen = Utils.worldToScreen(tower.x, tower.y, this.camera); const size = tileScreenSize; const pad = 3 * this.camera.zoom; // Shadow this.ctx.fillStyle = 'rgba(0,0,0,0.3)'; this.ctx.beginPath(); this.ctx.arc(screen.x + size/2 + 3, screen.y + size/2 + 3, size/2 - pad, 0, Math.PI * 2); this.ctx.fill(); // Base const grad = this.ctx.createRadialGradient( screen.x + size/2, screen.y + size/2, 0, screen.x + size/2, screen.y + size/2, size/2 ); grad.addColorStop(0, type.color); grad.addColorStop(1, type.colorDark); this.ctx.fillStyle = grad; this.ctx.beginPath(); this.ctx.arc(screen.x + size/2, screen.y + size/2, size/2 - pad, 0, Math.PI * 2); this.ctx.fill(); // Highlight this.ctx.fillStyle = 'rgba(255,255,255,0.2)'; this.ctx.beginPath(); this.ctx.arc(screen.x + size/2, screen.y + size/3, size/4, 0, Math.PI * 2); this.ctx.fill(); // Border this.ctx.strokeStyle = 'rgba(0,0,0,0.5)'; this.ctx.lineWidth = 2 * this.camera.zoom; this.ctx.beginPath(); this.ctx.arc(screen.x + size/2, screen.y + size/2, size/2 - pad, 0, Math.PI * 2); this.ctx.stroke(); // Turret barrel this.ctx.save(); this.ctx.translate(screen.x + size/2, screen.y + size/2); this.ctx.rotate(tower.angle); // Barrel this.ctx.fillStyle = tower.firing ? '#fff' : '#333'; this.ctx.fillRect(0, -4 * this.camera.zoom, size/2.5, 8 * this.camera.zoom); this.ctx.restore(); // Range indicator when selected if (tower.target) { this.ctx.strokeStyle = 'rgba(255,100,100,0.2)'; this.ctx.lineWidth = 1; this.ctx.beginPath(); this.ctx.arc(screen.x + size/2, screen.y + size/2, type.range * this.camera.zoom, 0, Math.PI * 2); this.ctx.stroke(); } // Health bar if (tower.health < tower.maxHealth) { const barW = size - pad * 2; const barH = 4 * this.camera.zoom; const barX = screen.x + pad; const barY = screen.y - barH - 4; this.ctx.fillStyle = 'rgba(0,0,0,0.5)'; this.ctx.fillRect(barX, barY, barW, barH); const healthPct = tower.health / tower.maxHealth; this.ctx.fillStyle = healthPct > 0.5 ? '#66ff66' : healthPct > 0.25 ? '#ffcc00' : '#ff3333'; this.ctx.fillRect(barX, barY, barW * healthPct, barH); } }); }, // Draw all enemies drawEnemies() { Enemies.list.forEach(enemy => { const type = Enemies.TYPES[enemy.type]; const screen = { x: enemy.x * this.camera.zoom - this.camera.x, y: enemy.y * this.camera.zoom - this.camera.y }; const size = CONFIG.TILE_SIZE * this.camera.zoom * type.size; // Shadow this.ctx.fillStyle = 'rgba(0,0,0,0.3)'; this.ctx.beginPath(); this.ctx.ellipse(screen.x + 2, screen.y + 4, size/2, size/3, 0, 0, Math.PI * 2); this.ctx.fill(); // Body this.ctx.save(); this.ctx.translate(screen.x, screen.y); this.ctx.rotate(enemy.angle); // Damage flash const color = enemy.damageFlash > 0 ? '#fff' : type.color; if (enemy.damageFlash > 0) enemy.damageFlash -= 0.016; // Main body const grad = this.ctx.createRadialGradient(0, 0, 0, 0, 0, size/2); grad.addColorStop(0, color); grad.addColorStop(1, type.colorDark); this.ctx.fillStyle = grad; this.ctx.beginPath(); this.ctx.ellipse(0, 0, size/2, size/3, 0, 0, Math.PI * 2); this.ctx.fill(); // Eyes this.ctx.fillStyle = '#ff0000'; this.ctx.beginPath(); this.ctx.arc(size/4, -size/8, size/10, 0, Math.PI * 2); this.ctx.arc(size/4, size/8, size/10, 0, Math.PI * 2); this.ctx.fill(); // Legs animation this.ctx.strokeStyle = type.colorDark; this.ctx.lineWidth = 2 * this.camera.zoom; const legOffset = Math.sin(enemy.animTime) * 5; for (let i = 0; i < 3; i++) { const angle = (i - 1) * 0.5; const offset = i === 1 ? legOffset : -legOffset; // Left leg this.ctx.beginPath(); this.ctx.moveTo(-size/6, -size/4); this.ctx.lineTo(-size/3 + offset, -size/2 - Math.abs(offset)); this.ctx.stroke(); // Right leg this.ctx.beginPath(); this.ctx.moveTo(-size/6, size/4); this.ctx.lineTo(-size/3 - offset, size/2 + Math.abs(offset)); this.ctx.stroke(); } this.ctx.restore(); // Health bar if (enemy.health < enemy.maxHealth) { const barW = size; const barH = 4 * this.camera.zoom; const barX = screen.x - size/2; const barY = screen.y - size/2 - barH - 4; this.ctx.fillStyle = 'rgba(0,0,0,0.5)'; this.ctx.fillRect(barX, barY, barW, barH); const healthPct = enemy.health / enemy.maxHealth; this.ctx.fillStyle = healthPct > 0.5 ? '#66ff66' : healthPct > 0.25 ? '#ffcc00' : '#ff3333'; this.ctx.fillRect(barX, barY, barW * healthPct, barH); } // Burning effect if (enemy.burning && enemy.burning.duration > 0) { this.ctx.fillStyle = `rgba(255, 100, 0, ${0.3 + Math.sin(Date.now() / 100) * 0.2})`; this.ctx.beginPath(); this.ctx.arc(screen.x, screen.y, size/2, 0, Math.PI * 2); this.ctx.fill(); } }); }, // Draw projectiles drawProjectiles() { Towers.projectiles.forEach(proj => { const screen = { x: proj.x * this.camera.zoom - this.camera.x, y: proj.y * this.camera.zoom - this.camera.y }; // Projectile glow this.ctx.fillStyle = proj.color; this.ctx.shadowColor = proj.color; this.ctx.shadowBlur = 10; this.ctx.beginPath(); this.ctx.arc(screen.x, screen.y, 4 * this.camera.zoom, 0, Math.PI * 2); this.ctx.fill(); this.ctx.shadowBlur = 0; // Trail const dx = proj.targetX - proj.x; const dy = proj.targetY - proj.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { const trailX = screen.x - (dx / dist) * 15 * this.camera.zoom; const trailY = screen.y - (dy / dist) * 15 * this.camera.zoom; this.ctx.strokeStyle = proj.color; this.ctx.lineWidth = 2 * this.camera.zoom; this.ctx.globalAlpha = 0.5; this.ctx.beginPath(); this.ctx.moveTo(screen.x, screen.y); this.ctx.lineTo(trailX, trailY); this.ctx.stroke(); this.ctx.globalAlpha = 1; } // Chain lightning effect if (proj.chainTargets && proj.chainTargets.length > 1) { this.ctx.strokeStyle = proj.color; this.ctx.lineWidth = 2 * this.camera.zoom; this.ctx.globalAlpha = 0.8; for (let i = 0; i < proj.chainTargets.length - 1; i++) { const t1 = proj.chainTargets[i]; const t2 = proj.chainTargets[i + 1]; const s1 = { x: t1.x * this.camera.zoom - this.camera.x, y: t1.y * this.camera.zoom - this.camera.y }; const s2 = { x: t2.x * this.camera.zoom - this.camera.x, y: t2.y * this.camera.zoom - this.camera.y }; this.ctx.beginPath(); this.ctx.moveTo(s1.x, s1.y); // Jagged line for lightning effect const midX = (s1.x + s2.x) / 2 + (Math.random() - 0.5) * 20; const midY = (s1.y + s2.y) / 2 + (Math.random() - 0.5) * 20; this.ctx.lineTo(midX, midY); this.ctx.lineTo(s2.x, s2.y); this.ctx.stroke(); } this.ctx.globalAlpha = 1; } }); } };