diff options
Diffstat (limited to 'html/tower/js')
-rw-r--r-- | html/tower/js/game.js | 487 | ||||
-rw-r--r-- | html/tower/js/gameState.js | 288 | ||||
-rw-r--r-- | html/tower/js/mechanics.js | 430 | ||||
-rw-r--r-- | html/tower/js/path.js | 176 | ||||
-rw-r--r-- | html/tower/js/renderer.js | 381 | ||||
-rw-r--r-- | html/tower/js/uiHandlers.js | 96 |
6 files changed, 1858 insertions, 0 deletions
diff --git a/html/tower/js/game.js b/html/tower/js/game.js new file mode 100644 index 0000000..4d8ed39 --- /dev/null +++ b/html/tower/js/game.js @@ -0,0 +1,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'; + }); +} \ No newline at end of file diff --git a/html/tower/js/gameState.js b/html/tower/js/gameState.js new file mode 100644 index 0000000..ac7a968 --- /dev/null +++ b/html/tower/js/gameState.js @@ -0,0 +1,288 @@ +/** + * Game State Module + * + * This module defines the game state and game phases + * + * @module gameState + */ + + +/** + * Game phases + * + * @enum {string} + * @readonly + */ +const GamePhase = { + PLACEMENT: 'place', + COMBAT: 'run' +}; + +/** + * Tower types + * + * @enum {string} + * @readonly + */ +const TowerTypes = { + BASIC: { + name: 'Basic', + cost: 5, + range: 3, + damage: 1, + attackSpeed: 1, + color: '#3498db', + maxAmmo: 75 + }, + RAPID: { + name: 'Fast', + cost: 10, + range: 2, + damage: 1, + attackSpeed: 3, + color: '#16a085', + maxAmmo: 50 + }, + SNIPER: { + name: 'Distance', + cost: 20, + range: 6, + damage: 2, + attackSpeed: 0.5, + color: '#8e44ad', + maxAmmo: 50 + }, + GOOP: { + name: 'Goop', + cost: 20, + range: 3, + damage: 0, + attackSpeed: 1, + color: '#27ae60', + special: 'slow', + slowAmount: 0.75, + maxAmmo: 25 + }, + AOE: { + name: 'AOE', + cost: 25, + range: 2, + damage: 3, + attackSpeed: 0.25, + color: '#d35400', + special: 'aoe', + aoeRadius: 2, + maxAmmo: 25 + } +}; + +/** + * Particle types + * + * @enum {string} + * @readonly + */ +const ParticleTypes = { + DEATH_PARTICLE: { + lifetime: 1000, // milliseconds + speed: 0.1, + colors: ['#e74c3c', '#c0392b', '#d35400', '#e67e22'] + }, + PROJECTILE: { + lifetime: 300, + speed: 0.3, + color: '#ecf0f1' + }, + AOE_EXPLOSION: { + lifetime: 500, + initialRadius: 10, + finalRadius: 60, + color: '#d35400', + ringWidth: 3 + }, + SLIME_TRAIL: { + lifetime: 800, + color: '#27ae60', // Same as Goop tower + size: 12, + fadeStart: 0.2 // When the fade should begin (percentage of lifetime) + } +}; + +/** + * Enemy types + * + * @enum {string} + * @readonly + */ +const EnemyTypes = { + BASIC: { + color: '#c0392b', + baseHealth: { min: 2, max: 6 }, + speed: { min: 1, max: 1.5 }, + damage: 0, + isRanged: false + }, + RANGED: { + color: '#2c3e50', + baseHealth: { min: 1, max: 4 }, + speed: { min: 0.7, max: 1.2 }, + damage: 0.3, + attackRange: 3, + attackSpeed: 1, // attacks per second + isRanged: true + } +}; + +/** + * Creates a tower + * + * @param {string} type - Tower type + * @param {Object} position - Position of the tower + */ +function createTower(type, position) { + const towerType = TowerTypes[type]; + return { + ...towerType, + type, + position, + lastAttackTime: 0, + currentHealth: 10, + maxHealth: 10, + ammo: towerType.maxAmmo // Initialize ammo + }; +} + +/** + * Creates an enemy + * + * @param {Object} startPosition - Starting position of the enemy + */ +function createEnemy(startPosition) { + // 20% chance for ranged enemy + const type = Math.random() < 0.2 ? 'RANGED' : 'BASIC'; + const enemyType = EnemyTypes[type]; + + // Scale health ranges with level + const levelScaling = 1 + (gameState.level - 1) * 0.25; // increase health by 25% per level + const minHealth = Math.floor(enemyType.baseHealth.min * levelScaling); + const maxHealth = Math.floor(enemyType.baseHealth.max * levelScaling); + + const health = Math.floor(Math.random() * + (maxHealth - minHealth + 1)) + minHealth; + + return { + position: { ...startPosition }, + currentHealth: health, + maxHealth: health, + speed: enemyType.speed.min + Math.random() * (enemyType.speed.max - enemyType.speed.min), + pathIndex: 0, + type, + lastAttackTime: 0, + damage: enemyType.damage + }; +} + +/** + * Creates a particle + * + * @param {string} type - Particle type + * @param {Object} position - Position of the particle + */ +function createParticle(type, position, angle) { + return { + position: { ...position }, + velocity: { + x: Math.cos(angle) * type.speed, + y: Math.sin(angle) * type.speed + }, + color: Array.isArray(type.colors) + ? type.colors[Math.floor(Math.random() * type.colors.length)] + : type.color, + createdAt: performance.now(), + lifetime: type.lifetime, + size: 3 + Math.random() * 2 + }; +} + + +/** + * Game state + * + * @type {Object} + */ +const gameState = { + grid: Array(20).fill().map(() => Array(20).fill('empty')), + path: [], + towers: [], + enemies: [], + currency: 100, + phase: GamePhase.PLACEMENT, + isGameOver: false, + particles: [], + projectiles: [], + enemiesDestroyed: 0, + enemiesEscaped: 0, + level: 1, + + /** + * Resets the game state + */ + resetGame() { + this.grid = Array(20).fill().map(() => Array(20).fill('empty')); + this.path = []; + this.towers = []; + this.enemies = []; + this.currency = 100; + this.phase = GamePhase.PLACEMENT; + this.isGameOver = false; + this.particles = []; + this.projectiles = []; + this.enemiesDestroyed = 0; + this.enemiesEscaped = 0; + this.level = 1; + }, + + + /** + * Awards the enemy destroyed + */ + awardEnemyDestroyed() { + this.enemiesDestroyed++; + // Random reward between 1 and 3 + const reward = Math.floor(Math.random() * 3) + 1; + this.currency += reward; + }, + + + /** + * Checks if the level is complete + * + * @returns {boolean} + */ + checkLevelComplete() { + return this.enemies.length === 0 && + enemiesRemaining === 0 && + this.phase === GamePhase.COMBAT; + }, + + + /** + * Advances to the next level + */ + advanceToNextLevel() { + + let ammoBonus = 0; + this.towers.forEach(tower => { + ammoBonus += tower.ammo * 0.25; + }); + this.currency += Math.floor(ammoBonus); // Round down to nearest whole number + + this.level++; + this.phase = GamePhase.PLACEMENT; + this.towers = []; + this.enemies = []; + this.projectiles = []; + this.particles = []; + this.grid = Array(20).fill().map(() => Array(20).fill('empty')); + } +}; \ No newline at end of file diff --git a/html/tower/js/mechanics.js b/html/tower/js/mechanics.js new file mode 100644 index 0000000..bc73fff --- /dev/null +++ b/html/tower/js/mechanics.js @@ -0,0 +1,430 @@ +/** + * 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 + * + * @module mechanics + */ + +/** + * 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 => { + // Initialize progress tracking for new enemies + if (typeof enemy.progress === 'undefined') { + enemy.progress = 0; + } + + // Update movement progress + enemy.progress += enemy.speed * 0.001; + + // Handle path completion + if (enemy.progress >= 1) { + gameState.enemiesEscaped++; + // Deduct currency when enemy escapes + gameState.currency = Math.max(0, gameState.currency - 10); + // Check for game over + if (gameState.currency <= 0) { + handleGameOver(); + } + return false; + } + + // Projectile collision detection and damage application + const hitByProjectile = gameState.projectiles.some(projectile => { + // Calculate current projectile position based on lifetime + const age = performance.now() - projectile.createdAt; + const progress = Math.min(age / projectile.lifetime, 1); + + const currentX = projectile.startPos.x + (projectile.targetPos.x - projectile.startPos.x) * progress; + const currentY = projectile.startPos.y + (projectile.targetPos.y - projectile.startPos.y) * progress; + + const distance = Math.hypot( + enemy.position.x - currentX, + enemy.position.y - currentY + ); + + if (distance < 0.5) { + // Apply damage when projectile hits + enemy.currentHealth -= projectile.damage; + return true; + } + return false; + }); + + if (enemy.currentHealth <= 0) { + gameState.awardEnemyDestroyed(); + // Create death particles + gameState.particles.push(...createDeathParticles(enemy, cellSize)); + return false; + } + + // Update position based on path progress + const pathPosition = getPathPosition(enemy.progress, gameState.path); + enemy.position.x = pathPosition.x; + enemy.position.y = pathPosition.y; + + // Process slow effect expiration + if (enemy.slowed && performance.now() > enemy.slowExpiry) { + enemy.slowed = false; + enemy.slowStacks = 0; + enemy.currentSlowAmount = 0; + enemy.speed = enemy.originalSpeed; + } + + // Visual feedback for slowed status + if (enemy.slowed && Math.random() < 0.2 + (enemy.slowStacks * 0.05)) { + gameState.particles.push(createSlimeTrail(enemy, cellSize)); + } + + return true; + }); + + // Remove projectiles that hit enemies + gameState.projectiles = gameState.projectiles.filter(projectile => { + const age = performance.now() - projectile.createdAt; + if (age >= projectile.lifetime) return false; + + const progress = age / projectile.lifetime; + const currentX = projectile.startPos.x + (projectile.targetPos.x - projectile.startPos.x) * progress; + const currentY = projectile.startPos.y + (projectile.targetPos.y - projectile.startPos.y) * progress; + + const hitEnemy = gameState.enemies.some(enemy => { + const distance = Math.hypot( + enemy.position.x - currentX, + enemy.position.y - currentY + ); + return distance < 0.5; + }); + + return !hitEnemy; + }); +} + +/** + * 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; + + if (particle.velocity) { + particle.position.x += particle.velocity.x * deltaTime; + particle.position.y += particle.velocity.y * deltaTime; + } + + return true; + }); +} + +/** + * 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; + const centerY = (target.position.y + 0.5) * cellSize; + + const particleCount = 8 + Math.floor(Math.random() * 8); + for (let i = 0; i < particleCount; i++) { + const baseAngle = (Math.PI * 2 * i) / particleCount; + const randomAngle = baseAngle + (Math.random() - 0.5) * 1.5; + const speedMultiplier = 0.7 + Math.random() * 0.6; + const startOffset = Math.random() * 5; + + particles.push(createParticle( + { + ...ParticleTypes.DEATH_PARTICLE, + speed: ParticleTypes.DEATH_PARTICLE.speed * speedMultiplier, + lifetime: ParticleTypes.DEATH_PARTICLE.lifetime * (0.8 + Math.random() * 0.4) + }, + { + 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 && tower.ammo > 0) { + const target = enemiesInRange[0]; + handleTowerAttack(tower, target, projectiles, particles, timestamp, cellSize); + } + } + }); +} + +function findEnemiesInRange(tower, enemies) { + return enemies.filter(enemy => { + const dx = enemy.position.x - tower.position.x; + const dy = enemy.position.y - tower.position.y; + return Math.sqrt(dx * dx + dy * dy) <= tower.range; + }); +} + +function createAOEExplosion(position, cellSize) { + return { + position: { + x: (position.x + 0.5) * cellSize, + y: (position.y + 0.5) * cellSize + }, + createdAt: performance.now(), + type: 'AOE_EXPLOSION', + ...ParticleTypes.AOE_EXPLOSION + }; +} + +function createSlimeTrail(enemy, cellSize) { + return { + position: { + x: (enemy.position.x + 0.5) * cellSize, + y: (enemy.position.y + 0.5) * cellSize + }, + createdAt: performance.now(), + type: 'SLIME_TRAIL', + ...ParticleTypes.SLIME_TRAIL + }; +} + +/** + * 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) { + // Only attack if we have ammo + if (tower.ammo <= 0) return; + + // Decrease ammo (already checked it's > 0) + tower.ammo--; + + // Create projectile + projectiles.push({ + startPos: { ...tower.position }, + targetPos: { ...target.position }, + createdAt: timestamp, + lifetime: 300, + towerType: tower.type, + damage: tower.damage + }); + + // Process special abilities + if (tower.special === 'slow') { + handleSlowEffect(target, tower, timestamp, particles, cellSize); + } else if (tower.special === 'aoe') { + handleAOEEffect(target, tower, gameState.enemies, particles, cellSize); + } + + tower.lastAttackTime = timestamp; +} + +/** + * 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; + + if (timestamp - enemy.lastAttackTime > 1000 / EnemyTypes[enemy.type].attackSpeed) { + const towersInRange = findTowersInRange(enemy, towers); + + if (towersInRange.length > 0) { + const target = towersInRange[0]; + handleEnemyAttack(enemy, target, 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; + const dy = tower.position.y - enemy.position.y; + return Math.sqrt(dx * dx + dy * dy) <= EnemyTypes[enemy.type].attackRange; + }); +} + +/** + * 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 projectile effect + particles.push(createParticle( + { + ...ParticleTypes.PROJECTILE, + color: '#8e44ad80', // Semi-transparent purple + lifetime: 500 + }, + { + x: (enemy.position.x + 0.5) * cellSize, + y: (enemy.position.y + 0.5) * cellSize + }, + Math.atan2( + tower.position.y - enemy.position.y, + tower.position.x - enemy.position.x + ) + )); + + // Apply damage and update tower state + tower.currentHealth -= enemy.damage; + enemy.lastAttackTime = timestamp; + + // 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 = { + // ... existing enemy properties ... + slowStacks: 0, + currentSlowAmount: 0 + }; + enemy.originalSpeed = enemy.speed; // Store original speed + return enemy; +} \ No newline at end of file diff --git a/html/tower/js/path.js b/html/tower/js/path.js new file mode 100644 index 0000000..8b840ce --- /dev/null +++ b/html/tower/js/path.js @@ -0,0 +1,176 @@ +/** + * Path Generation Module + * + * This module demonstrates several advanced game development concepts: + * 1. Procedural Content Generation (PCG) + * 2. Pathfinding algorithms + * 3. Constraint-based generation + * + * @module path + */ + +/** + * 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; + + // Initialize with random start point on left edge + const startY = Math.floor(Math.random() * height); + let currentPos = { x: 0, y: startY }; + + 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 + { x: 0, y: 1 } // down + ]; + + for (const dir of directions) { + const newX = pos.x + dir.x; + const newY = pos.y + dir.y; + + // Enforce boundary constraints + if (newX < 0 || newX >= width || newY < 0 || newY >= height) { + continue; + } + + // Check path isolation constraint + if (grid[newY][newX] === 'empty' && !hasAdjacentPath(newX, newY, grid)) { + moves.push({ x: newX, y: newY }); + } + } + + 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 + { x: x, y: y + 1 }, // down + { x: x - 1, y: y }, // left + { x: x + 1, y: y }, // right + ]; + + return adjacentCells.some(cell => { + if (cell.x < 0 || cell.x >= width || cell.y < 0 || cell.y >= height) { + return false; + } + return grid[cell.y][cell.x] === 'path' && + !path.some(p => p.x === cell.x && p.y === cell.y); + }); + } + + // Main path generation loop with backtracking + while (currentPos.x < width - 1) { + const moves = getValidMoves(currentPos); + + if (moves.length === 0) { + // Backtrack when no valid moves exist + if (path.length <= 1) { + // Restart if backtracking fails + return generatePath(grid); + } + + path.pop(); + const lastPos = path[path.length - 1]; + grid[currentPos.y][currentPos.x] = 'empty'; + currentPos = lastPos; + continue; + } + + // Random selection for path variety + const nextMove = moves[Math.floor(Math.random() * moves.length)]; + currentPos = nextMove; + path.push(currentPos); + grid[currentPos.y][currentPos.x] = 'path'; + } + + 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) { + // Normalize progress to valid range + progress = Math.max(0, Math.min(1, progress)); + + // 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; + const dy = path[i].y - path[i-1].y; + totalLength += Math.sqrt(dx * dx + dy * dy); + } + + // Convert progress to distance along path + const targetDistance = progress * totalLength; + + // Find appropriate path segment + let currentDistance = 0; + for (let i = 1; i < path.length; i++) { + const dx = path[i].x - path[i-1].x; + const dy = path[i].y - path[i-1].y; + const segmentLength = Math.sqrt(dx * dx + dy * dy); + + if (currentDistance + segmentLength >= targetDistance) { + // Linear interpolation within segment + const segmentProgress = (targetDistance - currentDistance) / segmentLength; + return { + x: path[i-1].x + dx * segmentProgress, + y: path[i-1].y + dy * segmentProgress + }; + } + + currentDistance += segmentLength; + } + + // 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 new file mode 100644 index 0000000..fce4b88 --- /dev/null +++ b/html/tower/js/renderer.js @@ -0,0 +1,381 @@ +/** + * 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 + * + * @module renderer + */ + +/** + * 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 for visual reference + ctx.strokeStyle = '#ccc'; + ctx.lineWidth = 1; + + for (let i = 0; i <= 20; i++) { + // Vertical lines + ctx.beginPath(); + ctx.moveTo(i * cellSize, 0); + ctx.lineTo(i * cellSize, canvas.height); + ctx.stroke(); + + // Horizontal lines + ctx.beginPath(); + ctx.moveTo(0, i * cellSize); + ctx.lineTo(canvas.width, i * cellSize); + ctx.stroke(); + } + + // Render grid cells with path highlighting + grid.forEach((row, y) => { + row.forEach((cell, x) => { + if (cell === 'path') { + ctx.fillStyle = '#f4a460'; + ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); + } + }); + }); + + // 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, + hoverCell.y * cellSize, + cellSize, + cellSize + ); + + // Range indicator preview + ctx.beginPath(); + ctx.arc( + (hoverCell.x + 0.5) * cellSize, + (hoverCell.y + 0.5) * cellSize, + tower.range * cellSize, + 0, + Math.PI * 2 + ); + ctx.strokeStyle = canPlace ? tower.color + '40' : 'rgba(255, 0, 0, 0.2)'; + ctx.stroke(); + } +} + +/** + * 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); + + // Dynamic color based on enemy state + const color = EnemyTypes[enemy.type].color; + const hexOpacity = Math.floor(opacity * 255).toString(16).padStart(2, '0'); + + // Draw enemy body with solid black border + ctx.beginPath(); + ctx.arc( + (enemy.position.x + 0.5) * cellSize, + (enemy.position.y + 0.5) * cellSize, + cellSize / 3, + 0, + Math.PI * 2 + ); + + // Fill with dynamic opacity + ctx.fillStyle = `${color}${hexOpacity}`; + ctx.fill(); + + // Add solid black border + ctx.strokeStyle = 'black'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Range indicator for special enemy types + if (EnemyTypes[enemy.type].isRanged) { + ctx.beginPath(); + ctx.arc( + (enemy.position.x + 0.5) * cellSize, + (enemy.position.y + 0.5) * cellSize, + EnemyTypes[enemy.type].attackRange * cellSize, + 0, + Math.PI * 2 + ); + ctx.strokeStyle = `${EnemyTypes[enemy.type].color}40`; + ctx.stroke(); + } + }); +} + +/** + * 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; + const startY = padding; + const width = 200; + const height = lineHeight * 5; + + // Save the current canvas state + ctx.save(); + + // Reset any transformations + ctx.setTransform(1, 0, 0, 1, 0, 0); + + // Semi-transparent background for readability + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.fillRect(0, 0, width, height + padding); + + // Text rendering setup + ctx.fillStyle = 'black'; + ctx.font = '20px Arial'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + // 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); + ctx.fillText(`Destroyed: ${gameState.enemiesDestroyed}`, padding, startY + lineHeight * 3); + ctx.fillText(`Escaped: ${gameState.enemiesEscaped}`, padding, startY + lineHeight * 4); + + // Restore the canvas state + ctx.restore(); +} + +function renderTowers(ctx, towers) { + const cellSize = canvas.width / 20; + + towers.forEach(tower => { + const healthPercent = tower.currentHealth / tower.maxHealth; + + // Draw tower body + ctx.fillStyle = tower.color + Math.floor(healthPercent * 255).toString(16).padStart(2, '0'); + ctx.fillRect( + tower.position.x * cellSize + cellSize * 0.1, + tower.position.y * cellSize + cellSize * 0.1, + cellSize * 0.8, + cellSize * 0.8 + ); + + // Draw ammo count + ctx.fillStyle = 'white'; + ctx.font = '12px Arial'; + ctx.textAlign = 'center'; + ctx.fillText( + tower.ammo, + (tower.position.x + 0.5) * cellSize, + (tower.position.y + 0.7) * cellSize + ); + + // Draw range indicator + if (gameState.phase === GamePhase.PLACEMENT) { + ctx.beginPath(); + ctx.arc( + (tower.position.x + 0.5) * cellSize, + (tower.position.y + 0.5) * cellSize, + tower.range * cellSize, + 0, + Math.PI * 2 + ); + ctx.strokeStyle = tower.color + '40'; + ctx.stroke(); + } + }); +} + +// Add new render function for particles +function renderParticles(ctx, particles) { + particles.forEach(particle => { + const age = performance.now() - particle.createdAt; + const lifePercent = age / particle.lifetime; + + if (lifePercent <= 1) { + if (particle.type === 'SLIME_TRAIL') { + // Calculate opacity based on lifetime and fade start + let opacity = 1; + if (lifePercent > particle.fadeStart) { + opacity = 1 - ((lifePercent - particle.fadeStart) / (1 - particle.fadeStart)); + } + opacity *= 0.3; // Make it translucent + + ctx.globalAlpha = opacity; + ctx.fillStyle = particle.color; + + // Draw a circular slime splat + ctx.beginPath(); + ctx.arc( + particle.position.x, + particle.position.y, + particle.size * (1 - lifePercent * 0.3), // Slightly shrink over time + 0, + Math.PI * 2 + ); + ctx.fill(); + + // Add some variation to the splat + for (let i = 0; i < 3; i++) { + const angle = (Math.PI * 2 * i) / 3; + const distance = particle.size * 0.4; + ctx.beginPath(); + ctx.arc( + particle.position.x + Math.cos(angle) * distance, + particle.position.y + Math.sin(angle) * distance, + particle.size * 0.4 * (1 - lifePercent * 0.3), + 0, + Math.PI * 2 + ); + ctx.fill(); + } + } else if (particle.type === 'AOE_EXPLOSION') { + // Draw expanding circle + const radius = particle.initialRadius + + (particle.finalRadius - particle.initialRadius) * lifePercent; + + // Draw multiple rings for better effect + const numRings = 3; + for (let i = 0; i < numRings; i++) { + const ringRadius = radius * (1 - (i * 0.2)); + const ringAlpha = (1 - lifePercent) * (1 - (i * 0.3)); + + ctx.beginPath(); + ctx.arc( + particle.position.x, + particle.position.y, + ringRadius, + 0, + Math.PI * 2 + ); + ctx.strokeStyle = particle.color; + ctx.lineWidth = particle.ringWidth * (1 - (i * 0.2)); + ctx.globalAlpha = ringAlpha; + ctx.stroke(); + } + + // Draw affected area + ctx.beginPath(); + ctx.arc( + particle.position.x, + particle.position.y, + radius, + 0, + Math.PI * 2 + ); + ctx.fillStyle = particle.color + '20'; // Very transparent fill + ctx.fill(); + } else { + // Original particle rendering + ctx.fillStyle = particle.color; + ctx.beginPath(); + ctx.arc( + particle.position.x, + particle.position.y, + particle.size * (1 - lifePercent), + 0, + Math.PI * 2 + ); + ctx.fill(); + } + } + }); + ctx.globalAlpha = 1; +} + +// Add new render function for projectiles +function renderProjectiles(ctx, projectiles) { + const cellSize = canvas.width / 20; + + projectiles.forEach(projectile => { + const age = performance.now() - projectile.createdAt; + const progress = age / projectile.lifetime; + + if (progress <= 1) { + // Draw projectile trail + ctx.beginPath(); + ctx.moveTo( + projectile.startPos.x * cellSize + cellSize / 2, + projectile.startPos.y * cellSize + cellSize / 2 + ); + + const currentX = projectile.startPos.x + (projectile.targetPos.x - projectile.startPos.x) * progress; + const currentY = projectile.startPos.y + (projectile.targetPos.y - projectile.startPos.y) * progress; + + ctx.lineTo( + currentX * cellSize + cellSize / 2, + currentY * cellSize + cellSize / 2 + ); + + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Draw projectile head + ctx.beginPath(); + ctx.arc( + currentX * cellSize + cellSize / 2, + currentY * cellSize + cellSize / 2, + 4, + 0, + Math.PI * 2 + ); + ctx.fillStyle = '#fff'; + ctx.fill(); + } + }); +} + +// Update level complete message in game.js +function handleLevelComplete() { + gameState.phase = GamePhase.TRANSITION; + + // Calculate ammo bonus + let ammoBonus = 0; + gameState.towers.forEach(tower => { + ammoBonus += tower.ammo * 2; + }); + + const message = ` + Level ${gameState.level} Complete! + Current Money: $${gameState.currency} + Ammo Bonus: +$${ammoBonus} + Level Bonus: +$10 + + Ready for Level ${gameState.level + 1}? + `; + + setTimeout(() => { + if (confirm(message)) { + startNextLevel(); + } + }, 100); +} \ No newline at end of file diff --git a/html/tower/js/uiHandlers.js b/html/tower/js/uiHandlers.js new file mode 100644 index 0000000..00651ca --- /dev/null +++ b/html/tower/js/uiHandlers.js @@ -0,0 +1,96 @@ +/** + * 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 + * + * @module uiHandlers + */ + +/** + * 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(); + const x = Math.floor((e.clientX - rect.left) / (canvas.width / 20)); + const y = Math.floor((e.clientY - rect.top) / (canvas.height / 20)); + + 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; + + placeTower(gameState, draggedTowerType, hoverCell); + draggedTowerType = null; + hoverCell = null; + } + }; + + 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 ( + gameState.grid[position.y][position.x] === 'empty' && + gameState.currency >= tower.cost + ) { + gameState.grid[position.y][position.x] = 'tower'; + gameState.towers.push(createTower(towerType, { ...position })); + gameState.currency -= tower.cost; + } +} \ No newline at end of file |