Source: game.js

// generate updated docs
// jsdoc js -d docs

/**
 * Main game entry point
 * Initializes the game state and starts the game loop
 * 
 * @module game
 */

/** Canvas elements for rendering the game */
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

/** Game timing variables */
let lastTimestamp = 0;
const ENEMY_SPAWN_INTERVAL = 1000; // 1 second between enemy spawns
let lastEnemySpawn = 0;
let enemiesRemaining = 0;

/** Drag and drop state tracking */
let draggedTowerType = null;
let hoverCell = null;

/**
 * Main game loop using requestAnimationFrame
 * This is the heart of the game, running approximately 60 times per second
 * 
 * @param {number} timestamp - Current time in milliseconds, provided by requestAnimationFrame
 * 
 * Key concepts:
 * - RequestAnimationFrame for smooth animation
 * - Delta time for consistent motion regardless of frame rate
 * - Game state management
 */
function gameLoop(timestamp) {
    const deltaTime = timestamp - lastTimestamp;
    lastTimestamp = timestamp;
    
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    if (!gameState.isGameOver) {
        if (gameState.phase === GamePhase.COMBAT) {
            handleCombatPhase(timestamp, deltaTime);
            
            // Check for level completion
            if (gameState.checkLevelComplete()) {
                handleLevelComplete();
            }
        }
    }
    
    renderGame();
    requestAnimationFrame(gameLoop);
}

/**
 * Handles all combat phase updates including enemy movement, attacks, and collisions
 * 
 * @param {number} timestamp - Current game time in milliseconds
 * @param {number} deltaTime - Time elapsed since last frame
 * 
 * Key concepts:
 * - Game state updates
 * - Entity management (enemies, towers, projectiles)
 * - Particle effects
 * - Combat mechanics
 */
function handleCombatPhase(timestamp, deltaTime) {
    spawnEnemies(timestamp);
    updateEnemies();
    // Update particle effects with time-based animation
    gameState.particles = updateParticles(gameState.particles, timestamp, deltaTime);
    // Remove expired projectiles
    gameState.projectiles = gameState.projectiles.filter(p => timestamp - p.createdAt < p.lifetime);
    
    const cellSize = canvas.width / 20;
    
    // Process combat interactions
    processTowerAttacks(
        gameState.towers,
        gameState.enemies,
        gameState.projectiles,
        gameState.particles,
        timestamp,
        cellSize
    );
    
    processEnemyAttacks(
        gameState.enemies,
        gameState.towers,
        gameState.particles,
        timestamp,
        cellSize
    );
    
    // Remove defeated enemies and destroyed towers
    // Uses array filter with a callback that has side effects (awarding currency)
    gameState.enemies = gameState.enemies.filter(enemy => {
        if (enemy.currentHealth <= 0) {
            gameState.awardEnemyDestroyed();
            return false;
        }
        return true;
    });
    gameState.towers = gameState.towers.filter(tower => tower.currentHealth > 0);
}

/**
 * Spawns new enemies at regular intervals during combat
 * 
 * @param {number} timestamp - Current game time in milliseconds
 * 
 * Key concepts:
 * - Time-based game events
 * - Enemy creation and management
 * - Game balance through spawn timing
 */
function spawnEnemies(timestamp) {
    if (enemiesRemaining > 0 && timestamp - lastEnemySpawn > ENEMY_SPAWN_INTERVAL) {
        gameState.enemies.push(createEnemy({ x: 0, y: gameState.path[0].y }));
        lastEnemySpawn = timestamp;
        enemiesRemaining--;
    }
}

/**
 * Renders all game elements to the canvas using a layered approach.
 * This function demonstrates several key game development patterns:
 * 
 * 1. Canvas State Management:
 *    - Uses save()/restore() to isolate rendering contexts
 *    - Resets transform matrix to prevent state leaks
 *    - Maintains clean state between rendering phases
 * 
 * 2. Layered Rendering Pattern:
 *    - Renders in specific order (background → entities → UI)
 *    - Each layer builds on top of previous layers
 *    - Separates rendering concerns for easier maintenance
 * 
 * 3. Separation of Concerns:
 *    - Each render function handles one specific type of game element
 *    - UI rendering is isolated from game element rendering
 *    - Clear boundaries between different rendering responsibilities
 * 
 * The rendering order is important:
 * 1. Grid (background)
 * 2. Particles (effects under entities)
 * 3. Projectiles (dynamic game elements)
 * 4. Towers (static game entities)
 * 5. Enemies (moving game entities)
 * 6. UI (top layer)
 */
function renderGame() {
    // Reset the canvas transform matrix to identity
    // This prevents any previous transformations from affecting new renders
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    
    // Clear the entire canvas to prevent ghosting
    // This is crucial for animation smoothness
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    // Save the initial clean state
    // This is part of the state stack pattern used in canvas rendering
    ctx.save();
    
    // Render game world elements in specific order
    // This creates the layered effect common in 2D games
    renderGrid(ctx, gameState.grid);          // Background layer
    renderParticles(ctx, gameState.particles); // Effect layer
    renderProjectiles(ctx, gameState.projectiles); // Dynamic elements
    renderTowers(ctx, gameState.towers);       // Static entities
    renderEnemies(ctx, gameState.enemies);     // Moving entities
    
    // Restore to clean state before UI rendering
    // This ensures UI rendering isn't affected by game world rendering
    ctx.restore();
    ctx.save();
    
    // Render UI elements last so they appear on top
    // UI is rendered with its own clean state to prevent interference
    renderUI(ctx, gameState);
    
    // Final state restoration
    // Ensures clean state for next frame
    ctx.restore();
}

/**
 * Initializes the game by:
 * 1. Generating the path for enemies to follow
 * 2. Setting up initial enemy count
 * 3. Binding event listeners
 * 4. Starting the game loop
 * 
 * Uses Promise-based path generation to handle async initialization
 */
generatePath(gameState.grid).then(path => {
    gameState.path = path;
    // Random enemy count between 5-30 for variety
    enemiesRemaining = Math.floor(Math.random() * 26) + 5;
    initializeEventListeners();
    // Start the game loop using requestAnimationFrame for smooth animation
    requestAnimationFrame(gameLoop);
});

/**
 * Transitions the game from placement to combat phase.
 * Demonstrates state machine pattern commonly used in games.
 * 
 * Side effects:
 * - Updates game phase
 * - Disables UI elements
 * - Updates visual feedback
 */
function startCombat() {
    if (gameState.phase === GamePhase.PLACEMENT && gameState.towers.length > 0) {
        // State transition
        gameState.phase = GamePhase.COMBAT;
        
        // UI updates
        document.getElementById('startCombat').disabled = true;
        
        // Visual feedback for disabled state
        document.querySelectorAll('.tower-option').forEach(option => {
            option.draggable = false;
            option.style.cursor = 'not-allowed';
            option.style.opacity = '0.5';
        });
    }
}

/**
 * Sets up all event listeners for user interaction
 * 
 * Key concepts:
 * - Event-driven programming
 * - HTML5 Drag and Drop API
 * - DOM manipulation
 * - Method decoration (towers.push)
 */
function initializeEventListeners() {
    // Add this at the beginning of the function
    populateTowerPalette();
    
    // Set up tower palette drag events
    document.querySelectorAll('.tower-option').forEach(option => {
        option.addEventListener('dragstart', (e) => {
            draggedTowerType = e.target.dataset.towerType;
            // Required for Firefox - must set data for drag operation
            e.dataTransfer.setData('text/plain', '');
        });
        
        option.addEventListener('dragend', () => {
            draggedTowerType = null;
            hoverCell = null;
        });
    });

    // Set up canvas drag and drop handling
    canvas.addEventListener('dragover', (e) => {
        e.preventDefault(); // Required for drop to work
        const rect = canvas.getBoundingClientRect();
        // Convert mouse coordinates to grid coordinates
        const x = Math.floor((e.clientX - rect.left) / (canvas.width / 20));
        const y = Math.floor((e.clientY - rect.top) / (canvas.height / 20));
        
        // Validate grid boundaries
        if (x >= 0 && x < 20 && y >= 0 && y < 20) {
            hoverCell = { x, y };
        } else {
            hoverCell = null;
        }
    });

    canvas.addEventListener('dragleave', () => {
        hoverCell = null;
    });

    // Handle tower placement on drop
    canvas.addEventListener('drop', (e) => {
        e.preventDefault();
        if (!draggedTowerType || !hoverCell) return;

        const tower = TowerTypes[draggedTowerType];
        // Validate placement and currency
        if (
            gameState.grid[hoverCell.y][hoverCell.x] === 'empty' &&
            gameState.currency >= tower.cost
        ) {
            gameState.grid[hoverCell.y][hoverCell.x] = 'tower';
            gameState.towers.push(createTower(draggedTowerType, { ...hoverCell }));
            gameState.currency -= tower.cost;
        }
        
        // Reset drag state
        draggedTowerType = null;
        hoverCell = null;
    });

    // Combat phase transition
    document.getElementById('startCombat').addEventListener('click', startCombat);
    
    // Dynamic button state management
    const updateStartButton = () => {
        const button = document.getElementById('startCombat');
        button.disabled = gameState.towers.length === 0;
    };
    
    // Decorator pattern: Enhance towers.push to update UI
    const originalPush = gameState.towers.push;
    gameState.towers.push = function(...args) {
        const result = originalPush.apply(this, args);
        updateStartButton();
        return result;
    };
    
    updateStartButton();
}

/**
 * Handles the transition between levels
 * Shows completion message and sets up next level
 */
function handleLevelComplete() {
    // Pause the game briefly
    gameState.phase = GamePhase.TRANSITION;
    
    // Calculate ammo bonus
    let ammoBonus = 0;
    gameState.towers.forEach(tower => {
        ammoBonus += tower.ammo * 0.25;
    });
    ammoBonus = Math.floor(ammoBonus);
    
    // Show level complete message with modal
    const message = `
        Level ${gameState.level} Complete!
        
        Stats:
        - Enemies Destroyed: ${gameState.enemiesDestroyed}
        - Enemies Escaped: ${gameState.enemiesEscaped}
        
        Bonuses:
        - Current Money: $${gameState.currency}
        - Remaining Ammo Bonus: +$${ammoBonus}
        
        Total After Bonuses: $${gameState.currency + ammoBonus + 10}
        
        Ready for Level ${gameState.level + 1}?
    `;
    
    // Use setTimeout to allow the final frame to render
    setTimeout(() => {
        if (confirm(message)) {
            startNextLevel();
        }
    }, 100);
}

/**
 * Sets up the next level
 * Increases difficulty and resets the game state while preserving currency
 */
function startNextLevel() {
    gameState.advanceToNextLevel();
    
    // Generate new path
    generatePath(gameState.grid).then(path => {
        gameState.path = path;
        
        // Exponential enemy scaling
        const baseEnemies = 5;
        const scalingFactor = 1.5;  // Each level increases by 50%
        enemiesRemaining = Math.floor(baseEnemies * Math.pow(scalingFactor, gameState.level - 1));
        
        // Re-enable tower palette
        document.querySelectorAll('.tower-option').forEach(option => {
            option.draggable = true;
            option.style.cursor = 'grab';
            option.style.opacity = '1';
        });
        
        // Reset start button
        const startButton = document.getElementById('startCombat');
        startButton.disabled = false;
        startButton.textContent = `Start Level ${gameState.level}`;
    });
}

// Update the renderUI function to show current level
function renderUI(ctx, gameState) {
    ctx.fillStyle = 'black';
    ctx.font = '20px Arial';
    ctx.fillText(`Level: ${gameState.level}`, 10, 30);
    ctx.fillText(`Currency: $${gameState.currency}`, 10, 60);
    ctx.fillText(`Phase: ${gameState.phase}`, 10, 90);
    ctx.fillText(`Destroyed: ${gameState.enemiesDestroyed}`, 10, 120);
    ctx.fillText(`Escaped: ${gameState.enemiesEscaped}`, 10, 150);
}

/**
 * Dynamically populates the tower palette based on TowerTypes
 */
function populateTowerPalette() {
    const palette = document.querySelector('.tower-palette');
    // Clear existing tower options
    palette.innerHTML = '';
    
    // Create tower options dynamically
    Object.entries(TowerTypes).forEach(([type, tower]) => {
        const towerOption = document.createElement('div');
        towerOption.className = 'tower-option';
        towerOption.draggable = true;
        towerOption.dataset.towerType = type;
        
        towerOption.innerHTML = `
            <div class="tower-preview" style="background: ${tower.color};"></div>
            <div class="tower-info">
                <div class="tower-name">${tower.name}</div>
                <div class="tower-cost">Cost: $${tower.cost}</div>
                <div class="tower-ammo">Ammo: ${tower.maxAmmo}</div>
            </div>
        `;
        
        palette.appendChild(towerOption);
    });
    
    // Add start combat button
    const startButton = document.createElement('button');
    startButton.id = 'startCombat';
    startButton.className = 'start-button';
    startButton.textContent = 'Start Run';
    palette.appendChild(startButton);
}

/**
 * Handles game over state and prompts for restart
 */
function handleGameOver() {
    gameState.phase = GamePhase.TRANSITION;
    gameState.isGameOver = true;
    
    const message = `
        Game Over!
        
        Final Stats:
        Level Reached: ${gameState.level}
        Enemies Destroyed: ${gameState.enemiesDestroyed}
        Enemies Escaped: ${gameState.enemiesEscaped}
        
        Would you like to restart from Level 1?
    `;
    
    setTimeout(() => {
        if (confirm(message)) {
            restartGame();
        }
    }, 100);
}

/**
 * Restarts the game from level 1 with fresh state
 */
function restartGame() {
    gameState.resetGame();
    
    // Generate new path
    generatePath(gameState.grid).then(path => {
        gameState.path = path;
        
        // Reset enemy count to level 1
        enemiesRemaining = 5;
        
        // Re-enable tower palette
        document.querySelectorAll('.tower-option').forEach(option => {
            option.draggable = true;
            option.style.cursor = 'grab';
            option.style.opacity = '1';
        });
        
        // Reset start button
        const startButton = document.getElementById('startCombat');
        startButton.disabled = false;
        startButton.textContent = 'Start Level 1';
    });
}