/**
* 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;
}