Source: renderer.js

/**
 * Rendering Module
 * 
 * This module handles all game rendering operations using HTML5 Canvas.
 * Demonstrates key game development patterns:
 * 1. Layer-based rendering
 * 2. Particle systems
 * 3. Visual state feedback
 * 4. Canvas state management
 * 
 * @module renderer
 */

/**
 * Renders the game grid with path and hover previews
 * Implements visual feedback for player actions
 * 
 * @param {CanvasRenderingContext2D} ctx - Canvas rendering context
 * @param {Array<Array<string>>} grid - Game grid state
 */
function renderGrid(ctx, grid) {
    const cellSize = canvas.width / 20;
    
    // Draw grid lines for visual reference
    ctx.strokeStyle = '#ccc';
    ctx.lineWidth = 1;
    
    for (let i = 0; i <= 20; i++) {
        // Vertical lines
        ctx.beginPath();
        ctx.moveTo(i * cellSize, 0);
        ctx.lineTo(i * cellSize, canvas.height);
        ctx.stroke();
        
        // Horizontal lines
        ctx.beginPath();
        ctx.moveTo(0, i * cellSize);
        ctx.lineTo(canvas.width, i * cellSize);
        ctx.stroke();
    }
    
    // Render grid cells with path highlighting
    grid.forEach((row, y) => {
        row.forEach((cell, x) => {
            if (cell === 'path') {
                ctx.fillStyle = '#f4a460';
                ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
            }
        });
    });

    // Render tower placement preview
    if (gameState.phase === GamePhase.PLACEMENT && draggedTowerType && hoverCell) {
        const tower = TowerTypes[draggedTowerType];
        const canPlace = grid[hoverCell.y][hoverCell.x] === 'empty' &&
                        gameState.playerCurrency >= tower.cost;
        
        // Visual feedback for placement validity
        ctx.fillStyle = canPlace ? tower.color + '80' : 'rgba(255, 0, 0, 0.3)';
        ctx.fillRect(
            hoverCell.x * cellSize,
            hoverCell.y * cellSize,
            cellSize,
            cellSize
        );
        
        // Range indicator preview
        ctx.beginPath();
        ctx.arc(
            (hoverCell.x + 0.5) * cellSize,
            (hoverCell.y + 0.5) * cellSize,
            tower.range * cellSize,
            0,
            Math.PI * 2
        );
        ctx.strokeStyle = canPlace ? tower.color + '40' : 'rgba(255, 0, 0, 0.2)';
        ctx.stroke();
    }
}

/**
 * Renders all enemies with health indicators and effects
 * Implements visual state representation
 * 
 * @param {CanvasRenderingContext2D} ctx - Canvas rendering context
 * @param {Array<Object>} enemies - Array of enemy objects
 */
function renderEnemies(ctx, enemies) {
    const cellSize = canvas.width / 20;
    
    enemies.forEach(enemy => {
        // Health-based opacity for visual feedback
        const healthPercent = enemy.currentHealth / enemy.maxHealth;
        const opacity = 0.3 + (healthPercent * 0.7);
        
        // Dynamic color based on enemy state
        const color = EnemyTypes[enemy.type].color;
        const hexOpacity = Math.floor(opacity * 255).toString(16).padStart(2, '0');
        
        // Draw enemy body with solid black border
        ctx.beginPath();
        ctx.arc(
            (enemy.position.x + 0.5) * cellSize,
            (enemy.position.y + 0.5) * cellSize,
            cellSize / 3,
            0,
            Math.PI * 2
        );
        
        // Fill with dynamic opacity
        ctx.fillStyle = `${color}${hexOpacity}`;
        ctx.fill();
        
        // Add solid black border
        ctx.strokeStyle = 'black';
        ctx.lineWidth = 2;
        ctx.stroke();
        
        // Range indicator for special enemy types
        if (EnemyTypes[enemy.type].isRanged) {
            ctx.beginPath();
            ctx.arc(
                (enemy.position.x + 0.5) * cellSize,
                (enemy.position.y + 0.5) * cellSize,
                EnemyTypes[enemy.type].attackRange * cellSize,
                0,
                Math.PI * 2
            );
            ctx.strokeStyle = `${EnemyTypes[enemy.type].color}40`;
            ctx.stroke();
        }
    });
}

/**
 * Renders game UI elements with clean state management
 * Implements heads-up display (HUD) pattern
 * 
 * @param {CanvasRenderingContext2D} ctx - Canvas rendering context
 * @param {Object} gameState - Current game state
 */
function renderUI(ctx, gameState) {
    const padding = 20;
    const lineHeight = 30;
    const startY = padding;
    const width = 200;
    const height = lineHeight * 5;
    
    // Save the current canvas state
    ctx.save();
    
    // Reset any transformations
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    
    // Semi-transparent background for readability
    ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
    ctx.fillRect(0, 0, width, height + padding);
    
    // Text rendering setup
    ctx.fillStyle = 'black';
    ctx.font = '20px Arial';
    ctx.textAlign = 'left';
    ctx.textBaseline = 'top';
    
    // Game state information
    ctx.fillText(`Level: ${gameState.level}`, padding, startY);
    ctx.fillText(`Currency: $${gameState.currency}`, padding, startY + lineHeight);
    ctx.fillText(`Phase: ${gameState.phase}`, padding, startY + lineHeight * 2);
    ctx.fillText(`Destroyed: ${gameState.enemiesDestroyed}`, padding, startY + lineHeight * 3);
    ctx.fillText(`Escaped: ${gameState.enemiesEscaped}`, padding, startY + lineHeight * 4);
    
    // Restore the canvas state
    ctx.restore();
}

function renderTowers(ctx, towers) {
    const cellSize = canvas.width / 20;
    
    towers.forEach(tower => {
        const healthPercent = tower.currentHealth / tower.maxHealth;
        
        // Draw tower body
        ctx.fillStyle = tower.color + Math.floor(healthPercent * 255).toString(16).padStart(2, '0');
        ctx.fillRect(
            tower.position.x * cellSize + cellSize * 0.1,
            tower.position.y * cellSize + cellSize * 0.1,
            cellSize * 0.8,
            cellSize * 0.8
        );
        
        // Draw ammo count
        ctx.fillStyle = 'white';
        ctx.font = '12px Arial';
        ctx.textAlign = 'center';
        ctx.fillText(
            tower.ammo,
            (tower.position.x + 0.5) * cellSize,
            (tower.position.y + 0.7) * cellSize
        );
        
        // Draw range indicator
        if (gameState.phase === GamePhase.PLACEMENT) {
            ctx.beginPath();
            ctx.arc(
                (tower.position.x + 0.5) * cellSize,
                (tower.position.y + 0.5) * cellSize,
                tower.range * cellSize,
                0,
                Math.PI * 2
            );
            ctx.strokeStyle = tower.color + '40';
            ctx.stroke();
        }
    });
}

// Add new render function for particles
function renderParticles(ctx, particles) {
    particles.forEach(particle => {
        const age = performance.now() - particle.createdAt;
        const lifePercent = age / particle.lifetime;
        
        if (lifePercent <= 1) {
            if (particle.type === 'SLIME_TRAIL') {
                // Calculate opacity based on lifetime and fade start
                let opacity = 1;
                if (lifePercent > particle.fadeStart) {
                    opacity = 1 - ((lifePercent - particle.fadeStart) / (1 - particle.fadeStart));
                }
                opacity *= 0.3; // Make it translucent
                
                ctx.globalAlpha = opacity;
                ctx.fillStyle = particle.color;
                
                // Draw a circular slime splat
                ctx.beginPath();
                ctx.arc(
                    particle.position.x,
                    particle.position.y,
                    particle.size * (1 - lifePercent * 0.3), // Slightly shrink over time
                    0,
                    Math.PI * 2
                );
                ctx.fill();
                
                // Add some variation to the splat
                for (let i = 0; i < 3; i++) {
                    const angle = (Math.PI * 2 * i) / 3;
                    const distance = particle.size * 0.4;
                    ctx.beginPath();
                    ctx.arc(
                        particle.position.x + Math.cos(angle) * distance,
                        particle.position.y + Math.sin(angle) * distance,
                        particle.size * 0.4 * (1 - lifePercent * 0.3),
                        0,
                        Math.PI * 2
                    );
                    ctx.fill();
                }
            } else if (particle.type === 'AOE_EXPLOSION') {
                // Draw expanding circle
                const radius = particle.initialRadius + 
                    (particle.finalRadius - particle.initialRadius) * lifePercent;
                
                // Draw multiple rings for better effect
                const numRings = 3;
                for (let i = 0; i < numRings; i++) {
                    const ringRadius = radius * (1 - (i * 0.2));
                    const ringAlpha = (1 - lifePercent) * (1 - (i * 0.3));
                    
                    ctx.beginPath();
                    ctx.arc(
                        particle.position.x,
                        particle.position.y,
                        ringRadius,
                        0,
                        Math.PI * 2
                    );
                    ctx.strokeStyle = particle.color;
                    ctx.lineWidth = particle.ringWidth * (1 - (i * 0.2));
                    ctx.globalAlpha = ringAlpha;
                    ctx.stroke();
                }
                
                // Draw affected area
                ctx.beginPath();
                ctx.arc(
                    particle.position.x,
                    particle.position.y,
                    radius,
                    0,
                    Math.PI * 2
                );
                ctx.fillStyle = particle.color + '20'; // Very transparent fill
                ctx.fill();
            } else {
                // Original particle rendering
                ctx.fillStyle = particle.color;
                ctx.beginPath();
                ctx.arc(
                    particle.position.x,
                    particle.position.y,
                    particle.size * (1 - lifePercent),
                    0,
                    Math.PI * 2
                );
                ctx.fill();
            }
        }
    });
    ctx.globalAlpha = 1;
}

// Add new render function for projectiles
function renderProjectiles(ctx, projectiles) {
    const cellSize = canvas.width / 20;
    
    projectiles.forEach(projectile => {
        const age = performance.now() - projectile.createdAt;
        const progress = age / projectile.lifetime;
        
        if (progress <= 1) {
            // Draw projectile trail
            ctx.beginPath();
            ctx.moveTo(
                projectile.startPos.x * cellSize + cellSize / 2,
                projectile.startPos.y * cellSize + cellSize / 2
            );
            
            const currentX = projectile.startPos.x + (projectile.targetPos.x - projectile.startPos.x) * progress;
            const currentY = projectile.startPos.y + (projectile.targetPos.y - projectile.startPos.y) * progress;
            
            ctx.lineTo(
                currentX * cellSize + cellSize / 2,
                currentY * cellSize + cellSize / 2
            );
            
            ctx.strokeStyle = '#fff';
            ctx.lineWidth = 2;
            ctx.stroke();
            
            // Draw projectile head
            ctx.beginPath();
            ctx.arc(
                currentX * cellSize + cellSize / 2,
                currentY * cellSize + cellSize / 2,
                4,
                0,
                Math.PI * 2
            );
            ctx.fillStyle = '#fff';
            ctx.fill();
        }
    });
}

// Update level complete message in game.js
function handleLevelComplete() {
    gameState.phase = GamePhase.TRANSITION;
    
    // Calculate ammo bonus
    let ammoBonus = 0;
    gameState.towers.forEach(tower => {
        ammoBonus += tower.ammo * 2;
    });
    
    const message = `
        Level ${gameState.level} Complete!
        Current Money: $${gameState.currency}
        Ammo Bonus: +$${ammoBonus}
        Level Bonus: +$10
        
        Ready for Level ${gameState.level + 1}?
    `;
    
    setTimeout(() => {
        if (confirm(message)) {
            startNextLevel();
        }
    }, 100);
}