/** 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) { // Calculate time since last frame for consistent motion const deltaTime = timestamp - lastTimestamp; lastTimestamp = timestamp; // Clear the canvas for the next frame ctx.clearRect(0, 0, canvas.width, canvas.height); // Update game state based on current phase if (gameState.phase === GamePhase.COMBAT) { handleCombatPhase(timestamp, deltaTime); } // Render the current game state renderGame(); // Schedule the next frame 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 * * Key concepts: * - Layer-based rendering * - Canvas drawing order (background to foreground) * - Separation of rendering and game logic */ function renderGame() { renderGrid(ctx, gameState.grid); // Background grid renderParticles(ctx, gameState.particles); // Particles (including slime) go under everything renderProjectiles(ctx, gameState.projectiles); renderTowers(ctx, gameState.towers); renderEnemies(ctx, gameState.enemies); // Enemies on top of slime trail renderUI(ctx, gameState); } // Start the game generatePath(gameState.grid).then(path => { gameState.path = path; enemiesRemaining = Math.floor(Math.random() * 26) + 5; // 5-30 enemies initializeEventListeners(); requestAnimationFrame(gameLoop); }); function startCombat() { if (gameState.phase === GamePhase.PLACEMENT && gameState.towers.length > 0) { gameState.phase = GamePhase.COMBAT; document.getElementById('startCombat').disabled = true; // Disable tower palette 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() { // 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(); }