diff options
author | elioat <elioat@tilde.institute> | 2025-02-16 21:53:39 -0500 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2025-02-16 21:53:39 -0500 |
commit | 7cf6da0c7ad218833ac4565659dfc2f36909db73 (patch) | |
tree | 69fd032c0cd9d620fc305c84acb910af06a6517c /html | |
parent | 9c113e31d93f7c0c6f611b14997dcaec1a9076c0 (diff) | |
download | tour-7cf6da0c7ad218833ac4565659dfc2f36909db73.tar.gz |
*
Diffstat (limited to 'html')
-rw-r--r-- | html/tower/js/game.js | 88 | ||||
-rw-r--r-- | html/tower/js/mechanics.js | 259 | ||||
-rw-r--r-- | html/tower/js/path.js | 83 | ||||
-rw-r--r-- | html/tower/js/renderer.js | 56 | ||||
-rw-r--r-- | html/tower/js/uiHandlers.js | 48 |
5 files changed, 411 insertions, 123 deletions
diff --git a/html/tower/js/game.js b/html/tower/js/game.js index 1099c46..0e79e4e 100644 --- a/html/tower/js/game.js +++ b/html/tower/js/game.js @@ -113,53 +113,103 @@ function spawnEnemies(timestamp) { } /** - * Renders all game elements to the canvas + * Renders all game elements to the canvas using a layered approach. + * This function demonstrates several key game development patterns: * - * Key concepts: - * - Layer-based rendering - * - Canvas drawing order (background to foreground) - * - Separation of rendering and game logic + * 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() { - // Clear and reset canvas state at the start + // 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 initial state + // Save the initial clean state + // This is part of the state stack pattern used in canvas rendering ctx.save(); - // Render game elements - renderGrid(ctx, gameState.grid); - renderParticles(ctx, gameState.particles); - renderProjectiles(ctx, gameState.projectiles); - renderTowers(ctx, gameState.towers); - renderEnemies(ctx, gameState.enemies); + // 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 + // Restore to clean state before UI rendering + // This ensures UI rendering isn't affected by game world rendering ctx.restore(); ctx.save(); - // Render UI with clean state + // Render UI elements last so they appear on top + // UI is rendered with its own clean state to prevent interference renderUI(ctx, gameState); - // Final restore + // Final state restoration + // Ensures clean state for next frame ctx.restore(); } -// Start the game +/** + * 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; - enemiesRemaining = Math.floor(Math.random() * 26) + 5; // 5-30 enemies + // 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; - // Disable tower palette + // Visual feedback for disabled state document.querySelectorAll('.tower-option').forEach(option => { option.draggable = false; option.style.cursor = 'not-allowed'; diff --git a/html/tower/js/mechanics.js b/html/tower/js/mechanics.js index 13b3ce8..2430ca0 100644 --- a/html/tower/js/mechanics.js +++ b/html/tower/js/mechanics.js @@ -1,23 +1,44 @@ -// Combat mechanics +/** + * Combat Mechanics Module + * + * This module handles all combat-related game mechanics including: + * 1. Enemy movement and behavior + * 2. Tower attacks and targeting + * 3. Projectile and particle systems + * 4. Status effects and special abilities + */ + +/** + * Updates all enemy states including movement, health, and status effects + * Implements core game loop mechanics for enemy behavior + * + * Key features: + * - Path following + * - Health management + * - Status effect processing + * - Collision detection + * + * @returns {void} + */ function updateEnemies() { const cellSize = canvas.width / 20; gameState.enemies = gameState.enemies.filter(enemy => { - // Add progress property if it doesn't exist + // Initialize progress tracking for new enemies if (typeof enemy.progress === 'undefined') { enemy.progress = 0; } - // Update progress + // Update movement progress enemy.progress += enemy.speed * 0.001; - // Check if enemy has completed the path + // Handle path completion if (enemy.progress >= 1) { gameState.enemiesEscaped++; - return false; // Remove from array + return false; } - // Check for collisions with projectiles + // Projectile collision detection const hitByProjectile = gameState.projectiles.some(projectile => { const distance = Math.hypot( enemy.position.x - projectile.startPos.x, @@ -28,15 +49,15 @@ function updateEnemies() { if (hitByProjectile) { gameState.awardEnemyDestroyed(); - return false; // Remove from array + return false; } - // Update enemy position based on progress + // Update position based on path progress const pathPosition = getPathPosition(enemy.progress, gameState.path); enemy.position.x = pathPosition.x; enemy.position.y = pathPosition.y; - // Check if slow effect has expired + // Process slow effect expiration if (enemy.slowed && performance.now() > enemy.slowExpiry) { enemy.slowed = false; enemy.slowStacks = 0; @@ -44,7 +65,7 @@ function updateEnemies() { enemy.speed = enemy.originalSpeed; } - // Add slime trail for slowed enemies (more particles for more stacks) + // Visual feedback for slowed status if (enemy.slowed && Math.random() < 0.2 + (enemy.slowStacks * 0.05)) { gameState.particles.push(createSlimeTrail(enemy, cellSize)); } @@ -65,12 +86,20 @@ function updateEnemies() { }); } +/** + * Updates particle effects with time-based animation + * Implements particle system lifecycle management + * + * @param {Array<Object>} particles - Array of particle objects + * @param {number} timestamp - Current game timestamp + * @param {number} deltaTime - Time elapsed since last frame + * @returns {Array<Object>} Updated particles array + */ function updateParticles(particles, timestamp, deltaTime) { return particles.filter(particle => { const age = timestamp - particle.createdAt; if (age > particle.lifetime) return false; - // Only update position for particles with velocity if (particle.velocity) { particle.position.x += particle.velocity.x * deltaTime; particle.position.y += particle.velocity.y * deltaTime; @@ -80,6 +109,14 @@ function updateParticles(particles, timestamp, deltaTime) { }); } +/** + * Creates death effect particles for defeated entities + * Implements visual feedback system + * + * @param {Object} target - The defeated entity + * @param {number} cellSize - Size of grid cell for scaling + * @returns {Array<Object>} Array of particle objects + */ function createDeathParticles(target, cellSize) { const particles = []; const centerX = (target.position.x + 0.5) * cellSize; @@ -91,8 +128,6 @@ function createDeathParticles(target, cellSize) { const randomAngle = baseAngle + (Math.random() - 0.5) * 1.5; const speedMultiplier = 0.7 + Math.random() * 0.6; const startOffset = Math.random() * 5; - const startX = centerX + Math.cos(randomAngle) * startOffset; - const startY = centerY + Math.sin(randomAngle) * startOffset; particles.push(createParticle( { @@ -100,19 +135,33 @@ function createDeathParticles(target, cellSize) { speed: ParticleTypes.DEATH_PARTICLE.speed * speedMultiplier, lifetime: ParticleTypes.DEATH_PARTICLE.lifetime * (0.8 + Math.random() * 0.4) }, - { x: startX, y: startY }, + { + x: centerX + Math.cos(randomAngle) * startOffset, + y: centerY + Math.sin(randomAngle) * startOffset + }, randomAngle )); } return particles; } +/** + * Processes tower attacks and targeting + * Implements combat mechanics and special abilities + * + * @param {Array<Object>} towers - Array of tower objects + * @param {Array<Object>} enemies - Array of enemy objects + * @param {Array<Object>} projectiles - Array of projectile objects + * @param {Array<Object>} particles - Array of particle objects + * @param {number} timestamp - Current game timestamp + * @param {number} cellSize - Size of grid cell for scaling + */ function processTowerAttacks(towers, enemies, projectiles, particles, timestamp, cellSize) { towers.forEach(tower => { if (timestamp - tower.lastAttackTime > 1000 / tower.attackSpeed) { const enemiesInRange = findEnemiesInRange(tower, enemies); - if (enemiesInRange.length > 0) { + if (enemiesInRange.length > 0 && tower.ammo > 0) { const target = enemiesInRange[0]; handleTowerAttack(tower, target, projectiles, particles, timestamp, cellSize); } @@ -152,15 +201,22 @@ function createSlimeTrail(enemy, cellSize) { }; } +/** + * Handles individual tower attack logic including special effects + * Implements tower ability system + * + * @param {Object} tower - Attacking tower + * @param {Object} target - Target enemy + * @param {Array<Object>} projectiles - Projectile array + * @param {Array<Object>} particles - Particle array + * @param {number} timestamp - Current game timestamp + * @param {number} cellSize - Grid cell size + */ function handleTowerAttack(tower, target, projectiles, particles, timestamp, cellSize) { - // Check if tower has ammo - if (tower.ammo <= 0) { - return; - } - // Decrease ammo tower.ammo--; + // Create projectile projectiles.push({ startPos: tower.position, targetPos: target.position, @@ -169,66 +225,26 @@ function handleTowerAttack(tower, target, projectiles, particles, timestamp, cel towerType: tower.type }); + // Process special abilities if (tower.special === 'slow') { - // Initialize slow effect if not present - if (!target.slowStacks) { - target.slowStacks = 0; - } - - // Add another stack of slow (up to a maximum) - const maxStacks = 5; // Maximum 5 stacks - if (target.slowStacks < maxStacks) { - target.slowStacks++; - // Each stack slows by an additional 10% (multiplicative) - const newSlowAmount = 1 - Math.pow(0.9, target.slowStacks); // 10%, 19%, 27%, 34%, 41% - - // Only adjust speed if this is a stronger slow - if (!target.slowed || newSlowAmount > target.currentSlowAmount) { - const originalSpeed = target.originalSpeed || target.speed; - target.originalSpeed = originalSpeed; // Store original speed if not stored - target.speed = originalSpeed * (1 - newSlowAmount); - target.currentSlowAmount = newSlowAmount; - target.slowed = true; - } - - // Create slime particles for visual feedback - for (let i = 0; i < 4 + target.slowStacks; i++) { // More particles for more stacks - particles.push(createSlimeTrail(target, cellSize)); - } - } - - // Refresh slow duration - target.slowExpiry = timestamp + 2000; // 2 second duration + handleSlowEffect(target, tower, timestamp, particles, cellSize); } else if (tower.special === 'aoe') { - // Find all enemies in AOE radius - const enemiesInAOE = gameState.enemies.filter(enemy => { - const dx = enemy.position.x - target.position.x; - const dy = enemy.position.y - target.position.y; - return Math.sqrt(dx * dx + dy * dy) <= tower.aoeRadius; - }); - - // Create AOE explosion effect - particles.push(createAOEExplosion(target.position, cellSize)); - - // Damage all enemies in range - enemiesInAOE.forEach(enemy => { - enemy.currentHealth -= tower.damage; - if (enemy.currentHealth <= 0) { - particles.push(...createDeathParticles(enemy, cellSize)); - } - }); - } else { - // Normal damage for regular towers - target.currentHealth -= tower.damage; + handleAOEEffect(target, tower, enemies, particles, cellSize); } tower.lastAttackTime = timestamp; - - if (target.currentHealth <= 0) { - particles.push(...createDeathParticles(target, cellSize)); - } } +/** + * Processes enemy attack behaviors and targeting + * Implements enemy combat AI + * + * @param {Array<Object>} enemies - Array of enemy objects + * @param {Array<Object>} towers - Array of tower objects + * @param {Array<Object>} particles - Particle effect array + * @param {number} timestamp - Current game timestamp + * @param {number} cellSize - Grid cell size + */ function processEnemyAttacks(enemies, towers, particles, timestamp, cellSize) { enemies.forEach(enemy => { if (!EnemyTypes[enemy.type].isRanged) return; @@ -244,6 +260,14 @@ function processEnemyAttacks(enemies, towers, particles, timestamp, cellSize) { }); } +/** + * Finds towers within enemy attack range + * Implements targeting system for enemies + * + * @param {Object} enemy - Enemy doing the targeting + * @param {Array<Object>} towers - Array of potential tower targets + * @returns {Array<Object>} Array of towers in range + */ function findTowersInRange(enemy, towers) { return towers.filter(tower => { const dx = tower.position.x - enemy.position.x; @@ -252,13 +276,22 @@ function findTowersInRange(enemy, towers) { }); } +/** + * Handles enemy attack execution and effects + * Implements enemy combat mechanics + * + * @param {Object} enemy - Attacking enemy + * @param {Object} tower - Target tower + * @param {Array<Object>} particles - Particle array for effects + * @param {number} timestamp - Current game timestamp + * @param {number} cellSize - Grid cell size + */ function handleEnemyAttack(enemy, tower, particles, timestamp, cellSize) { - // Create enemy projectile - const projectileColor = '#8e44ad80'; // Semi-transparent purple + // Create projectile effect particles.push(createParticle( { ...ParticleTypes.PROJECTILE, - color: projectileColor, + color: '#8e44ad80', // Semi-transparent purple lifetime: 500 }, { @@ -271,13 +304,85 @@ function handleEnemyAttack(enemy, tower, particles, timestamp, cellSize) { ) )); + // Apply damage and update tower state tower.currentHealth -= enemy.damage; enemy.lastAttackTime = timestamp; - // Reduce tower's damage as it takes damage + // Dynamic damage reduction based on tower health tower.damage = TowerTypes[tower.type].damage * (tower.currentHealth / tower.maxHealth); } +/** + * Handles slow effect application and stacking + * Implements status effect system + * + * @param {Object} target - Enemy to apply slow to + * @param {Object} tower - Tower applying the effect + * @param {number} timestamp - Current game timestamp + * @param {Array<Object>} particles - Particle array for effects + * @param {number} cellSize - Grid cell size + */ +function handleSlowEffect(target, tower, timestamp, particles, cellSize) { + // Initialize slow effect tracking + if (!target.slowStacks) { + target.slowStacks = 0; + } + + const maxStacks = 5; // Maximum 5 stacks + if (target.slowStacks < maxStacks) { + target.slowStacks++; + // Each stack slows by an additional 10% (multiplicative) + const newSlowAmount = 1 - Math.pow(0.9, target.slowStacks); + + // Only update if new slow is stronger + if (!target.slowed || newSlowAmount > target.currentSlowAmount) { + const originalSpeed = target.originalSpeed || target.speed; + target.originalSpeed = originalSpeed; + target.speed = originalSpeed * (1 - newSlowAmount); + target.currentSlowAmount = newSlowAmount; + target.slowed = true; + } + + // Visual feedback particles + for (let i = 0; i < 4 + target.slowStacks; i++) { + particles.push(createSlimeTrail(target, cellSize)); + } + } + + // Refresh effect duration + target.slowExpiry = timestamp + 2000; // 2 second duration +} + +/** + * Handles AOE (Area of Effect) damage and visual effects + * Implements area damage system + * + * @param {Object} target - Primary target + * @param {Object} tower - Tower dealing AOE damage + * @param {Array<Object>} enemies - All enemies for AOE calculation + * @param {Array<Object>} particles - Particle array for effects + * @param {number} cellSize - Grid cell size + */ +function handleAOEEffect(target, tower, enemies, particles, cellSize) { + // Find all enemies in AOE radius + const enemiesInAOE = enemies.filter(enemy => { + const dx = enemy.position.x - target.position.x; + const dy = enemy.position.y - target.position.y; + return Math.sqrt(dx * dx + dy * dy) <= tower.aoeRadius; + }); + + // Create explosion effect + particles.push(createAOEExplosion(target.position, cellSize)); + + // Apply AOE damage + enemiesInAOE.forEach(enemy => { + enemy.currentHealth -= tower.damage; + if (enemy.currentHealth <= 0) { + particles.push(...createDeathParticles(enemy, cellSize)); + } + }); +} + // Update createEnemy to track original speed function createEnemy(startPosition) { const enemy = { diff --git a/html/tower/js/path.js b/html/tower/js/path.js index 46e4bfb..91193e2 100644 --- a/html/tower/js/path.js +++ b/html/tower/js/path.js @@ -1,18 +1,49 @@ -// Path generation using a modified depth-first search algorithm +/** + * Path Generation Module + * + * This module demonstrates several advanced game development concepts: + * 1. Procedural Content Generation (PCG) + * 2. Pathfinding algorithms + * 3. Constraint-based generation + */ + +/** + * Generates a valid path through the game grid using a modified depth-first search. + * This algorithm ensures: + * - Path always moves from left to right + * - No diagonal movements + * - No path segments touch each other (except at turns) + * - Path is always completable + * + * @param {Array<Array<string>>} grid - 2D array representing the game grid + * @returns {Promise<Array<{x: number, y: number}>>} Promise resolving to array of path coordinates + * + * Implementation uses: + * - Backtracking algorithm pattern + * - Constraint satisfaction + * - Random selection for variety + */ function generatePath(grid) { const width = grid[0].length; const height = grid.length; - // Pick random start point on left edge + // Initialize with random start point on left edge const startY = Math.floor(Math.random() * height); let currentPos = { x: 0, y: startY }; - // Initialize path with start position const path = [currentPos]; grid[startY][0] = 'path'; + /** + * Determines valid moves from current position based on game rules + * Uses constraint checking to ensure path validity + * + * @param {Object} pos - Current position {x, y} + * @returns {Array<{x: number, y: number}>} Array of valid next positions + */ function getValidMoves(pos) { const moves = []; + // Prioritize right movement for path progression const directions = [ { x: 1, y: 0 }, // right { x: 0, y: -1 }, // up @@ -23,12 +54,12 @@ function generatePath(grid) { const newX = pos.x + dir.x; const newY = pos.y + dir.y; - // Check bounds + // Enforce boundary constraints if (newX < 0 || newX >= width || newY < 0 || newY >= height) { continue; } - // Check if cell is empty and not adjacent to path (except previous cell) + // Check path isolation constraint if (grid[newY][newX] === 'empty' && !hasAdjacentPath(newX, newY, grid)) { moves.push({ x: newX, y: newY }); } @@ -37,6 +68,15 @@ function generatePath(grid) { return moves; } + /** + * Checks if a position has adjacent path tiles (excluding previous path tile) + * Implements path isolation constraint + * + * @param {number} x - X coordinate to check + * @param {number} y - Y coordinate to check + * @param {Array<Array<string>>} grid - Current grid state + * @returns {boolean} True if position has adjacent path tiles + */ function hasAdjacentPath(x, y, grid) { const adjacentCells = [ { x: x, y: y - 1 }, // up @@ -54,14 +94,14 @@ function generatePath(grid) { }); } - // Generate path until we reach the right edge + // Main path generation loop with backtracking while (currentPos.x < width - 1) { const moves = getValidMoves(currentPos); if (moves.length === 0) { - // If no valid moves, backtrack + // Backtrack when no valid moves exist if (path.length <= 1) { - // Start over if we can't backtrack + // Restart if backtracking fails return generatePath(grid); } @@ -72,7 +112,7 @@ function generatePath(grid) { continue; } - // Choose random valid move + // Random selection for path variety const nextMove = moves[Math.floor(Math.random() * moves.length)]; currentPos = nextMove; path.push(currentPos); @@ -82,11 +122,24 @@ function generatePath(grid) { return Promise.resolve(path); } +/** + * Calculates a position along the path based on a progress value + * Implements smooth entity movement along path segments + * + * @param {number} progress - Progress along path (0-1) + * @param {Array<{x: number, y: number}>} path - Array of path coordinates + * @returns {{x: number, y: number}} Interpolated position along path + * + * Uses: + * - Linear interpolation (lerp) + * - Path segment traversal + * - Normalized progress tracking + */ function getPathPosition(progress, path) { - // Ensure progress is between 0 and 1 + // Normalize progress to valid range progress = Math.max(0, Math.min(1, progress)); - // Get the total path length + // Calculate total path length for normalization let totalLength = 0; for (let i = 1; i < path.length; i++) { const dx = path[i].x - path[i-1].x; @@ -94,10 +147,10 @@ function getPathPosition(progress, path) { totalLength += Math.sqrt(dx * dx + dy * dy); } - // Find target distance along path + // Convert progress to distance along path const targetDistance = progress * totalLength; - // Find the segment where the target position lies + // Find appropriate path segment let currentDistance = 0; for (let i = 1; i < path.length; i++) { const dx = path[i].x - path[i-1].x; @@ -105,7 +158,7 @@ function getPathPosition(progress, path) { const segmentLength = Math.sqrt(dx * dx + dy * dy); if (currentDistance + segmentLength >= targetDistance) { - // Calculate position within this segment + // Linear interpolation within segment const segmentProgress = (targetDistance - currentDistance) / segmentLength; return { x: path[i-1].x + dx * segmentProgress, @@ -116,6 +169,6 @@ function getPathPosition(progress, path) { currentDistance += segmentLength; } - // If we somehow exceed the path length, return the last point + // Fallback to end of path return { ...path[path.length - 1] }; } \ No newline at end of file diff --git a/html/tower/js/renderer.js b/html/tower/js/renderer.js index cf52ebc..f37b5f3 100644 --- a/html/tower/js/renderer.js +++ b/html/tower/js/renderer.js @@ -1,7 +1,25 @@ +/** + * Rendering Module + * + * This module handles all game rendering operations using HTML5 Canvas. + * Demonstrates key game development patterns: + * 1. Layer-based rendering + * 2. Particle systems + * 3. Visual state feedback + * 4. Canvas state management + */ + +/** + * Renders the game grid with path and hover previews + * Implements visual feedback for player actions + * + * @param {CanvasRenderingContext2D} ctx - Canvas rendering context + * @param {Array<Array<string>>} grid - Game grid state + */ function renderGrid(ctx, grid) { const cellSize = canvas.width / 20; - // Draw grid lines + // Draw grid lines for visual reference ctx.strokeStyle = '#ccc'; ctx.lineWidth = 1; @@ -19,7 +37,7 @@ function renderGrid(ctx, grid) { ctx.stroke(); } - // Draw cells + // Render grid cells with path highlighting grid.forEach((row, y) => { row.forEach((cell, x) => { if (cell === 'path') { @@ -29,12 +47,13 @@ function renderGrid(ctx, grid) { }); }); - // Draw hover preview + // Render tower placement preview if (gameState.phase === GamePhase.PLACEMENT && draggedTowerType && hoverCell) { const tower = TowerTypes[draggedTowerType]; const canPlace = grid[hoverCell.y][hoverCell.x] === 'empty' && gameState.playerCurrency >= tower.cost; + // Visual feedback for placement validity ctx.fillStyle = canPlace ? tower.color + '80' : 'rgba(255, 0, 0, 0.3)'; ctx.fillRect( hoverCell.x * cellSize, @@ -43,7 +62,7 @@ function renderGrid(ctx, grid) { cellSize ); - // Draw range preview + // Range indicator preview ctx.beginPath(); ctx.arc( (hoverCell.x + 0.5) * cellSize, @@ -57,19 +76,27 @@ function renderGrid(ctx, grid) { } } +/** + * Renders all enemies with health indicators and effects + * Implements visual state representation + * + * @param {CanvasRenderingContext2D} ctx - Canvas rendering context + * @param {Array<Object>} enemies - Array of enemy objects + */ function renderEnemies(ctx, enemies) { const cellSize = canvas.width / 20; enemies.forEach(enemy => { + // Health-based opacity for visual feedback const healthPercent = enemy.currentHealth / enemy.maxHealth; const opacity = 0.3 + (healthPercent * 0.7); - // Use enemy type color + // Dynamic color based on enemy state ctx.fillStyle = `${EnemyTypes[enemy.type].color}${Math.floor(opacity * 255).toString(16).padStart(2, '0')}`; ctx.strokeStyle = 'rgba(0, 0, 0, 0.8)'; ctx.lineWidth = 2; - // Draw enemy body + // Enemy body ctx.beginPath(); ctx.arc( (enemy.position.x + 0.5) * cellSize, @@ -81,7 +108,7 @@ function renderEnemies(ctx, enemies) { ctx.fill(); ctx.stroke(); - // Draw range indicator for ranged enemies + // Range indicator for special enemy types if (EnemyTypes[enemy.type].isRanged) { ctx.beginPath(); ctx.arc( @@ -97,6 +124,13 @@ function renderEnemies(ctx, enemies) { }); } +/** + * Renders game UI elements with clean state management + * Implements heads-up display (HUD) pattern + * + * @param {CanvasRenderingContext2D} ctx - Canvas rendering context + * @param {Object} gameState - Current game state + */ function renderUI(ctx, gameState) { const padding = 20; const lineHeight = 30; @@ -110,17 +144,17 @@ function renderUI(ctx, gameState) { // Reset any transformations ctx.setTransform(1, 0, 0, 1, 0, 0); - // Draw semi-transparent background + // Semi-transparent background for readability ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.fillRect(0, 0, width, height + padding); - // Set text properties + // Text rendering setup ctx.fillStyle = 'black'; ctx.font = '20px Arial'; ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; // Add this to ensure consistent vertical alignment + ctx.textBaseline = 'top'; - // Draw UI elements + // Game state information ctx.fillText(`Level: ${gameState.level}`, padding, startY); ctx.fillText(`Currency: $${gameState.currency}`, padding, startY + lineHeight); ctx.fillText(`Phase: ${gameState.phase}`, padding, startY + lineHeight * 2); diff --git a/html/tower/js/uiHandlers.js b/html/tower/js/uiHandlers.js index 6e0789e..f171358 100644 --- a/html/tower/js/uiHandlers.js +++ b/html/tower/js/uiHandlers.js @@ -1,18 +1,49 @@ +/** + * UI Handlers Module + * + * This module manages user interactions and UI state. + * Implements: + * 1. Drag and Drop system + * 2. Event handling + * 3. UI state management + * 4. Input validation + */ + +/** + * Initializes drag and drop functionality for tower placement + * Implements HTML5 Drag and Drop API + * + * @param {HTMLCanvasElement} canvas - Game canvas element + * @param {Object} gameState - Current game state + * @returns {Object} Drag handlers and state information + */ function initializeDragAndDrop(canvas, gameState) { let draggedTowerType = null; let hoverCell = null; const dragHandlers = { + /** + * Handles start of tower drag operation + * Sets up drag data and visual feedback + */ onDragStart: (e) => { draggedTowerType = e.target.dataset.towerType; e.dataTransfer.setData('text/plain', ''); }, + /** + * Handles end of drag operation + * Cleans up drag state + */ onDragEnd: () => { draggedTowerType = null; hoverCell = null; }, + /** + * Handles drag over canvas + * Updates hover position and preview + */ onDragOver: (e) => { e.preventDefault(); const rect = canvas.getBoundingClientRect(); @@ -22,6 +53,10 @@ function initializeDragAndDrop(canvas, gameState) { hoverCell = (x >= 0 && x < 20 && y >= 0 && y < 20) ? { x, y } : null; }, + /** + * Handles tower placement on drop + * Validates placement and updates game state + */ onDrop: (e) => { e.preventDefault(); if (!draggedTowerType || !hoverCell) return; @@ -32,9 +67,20 @@ function initializeDragAndDrop(canvas, gameState) { } }; - return { dragHandlers, getHoverInfo: () => ({ draggedTowerType, hoverCell }) }; + return { + dragHandlers, + getHoverInfo: () => ({ draggedTowerType, hoverCell }) + }; } +/** + * Places a tower in the game grid + * Implements tower placement validation and state updates + * + * @param {Object} gameState - Current game state + * @param {string} towerType - Type of tower to place + * @param {Object} position - Grid position for placement + */ function placeTower(gameState, towerType, position) { const tower = TowerTypes[towerType]; if ( |