/** 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.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; // Show level complete message with modal const message = ` Level ${gameState.level} Complete! Current Money: $${gameState.currency} Level Bonus: +$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; // Increase number of enemies for each level const baseEnemies = 5; const enemiesPerLevel = 3; enemiesRemaining = baseEnemies + (gameState.level - 1) * enemiesPerLevel; // 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 = `