about summary refs log tree commit diff stats
path: root/html/tower/js/game.js
blob: 4d8ed39a3fa9d476678adf05e88e0c390bc3a567 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
// 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';
    });
}