// 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';
});
}