/** * 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} particles - Array of particle objects * @param {number} timestamp - Current game timestamp * @param {number} deltaTime - Time elapsed since last frame * @returns {Array} 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} 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} towers - Array of tower objects * @param {Array} enemies - Array of enemy objects * @param {Array} projectiles - Array of projectile objects * @param {Array} 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} projectiles - Projectile array * @param {Array} 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} enemies - Array of enemy objects * @param {Array} towers - Array of tower objects * @param {Array} 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} towers - Array of potential tower targets * @returns {Array} 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} 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} 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} enemies - All enemies for AOE calculation * @param {Array} 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; }