/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { // Handle enemy bullets targeting towers if (self.isEnemyBullet) { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Damage the tower instead of an enemy var tower = self.targetEnemy; if (tower.takeDamage) { tower.takeDamage(self.damage); } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } return; } // Original enemy targeting logic if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Special handling for missiles hitting immune, flying, or tank enemies - destroy instantly if (self.type === 'slow' && (self.targetEnemy.isImmune || self.targetEnemy.isFlying || self.targetEnemy.type === 'tank')) { // Instantly destroy immune, flying, or tank enemy regardless of health self.targetEnemy.health = 0; } else if (self.type === 'tank' && (self.targetEnemy.isImmune || self.targetEnemy.isFlying || self.targetEnemy.type === 'tank' || self.targetEnemy.isBoss)) { // Tank weapons instantly destroy immune, flying, tank, and boss enemies self.targetEnemy.health = 0; } else { // Apply damage to target enemy normally self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } } // Create explosion effect for missile hits if (self.type === 'slow') { // Store explosion position for area damage calculation var explosionX = self.targetEnemy.x; var explosionY = self.targetEnemy.y; // Increased explosion radius to destroy nearby enemies (especially fast enemies) var explosionRadius = CELL_SIZE * 2.5; // Increased from smaller radius to 2.5 cells // Apply area damage to all enemies within explosion radius for (var i = 0; i < enemies.length; i++) { var nearbyEnemy = enemies[i]; if (nearbyEnemy !== self.targetEnemy) { var dx = nearbyEnemy.x - explosionX; var dy = nearbyEnemy.y - explosionY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= explosionRadius) { // Instantly destroy fast enemies within explosion radius if (nearbyEnemy.type === 'fast') { nearbyEnemy.health = 0; } else { // Apply significant damage to other enemy types nearbyEnemy.health -= self.damage * 1.5; // 1.5x damage for area effect if (nearbyEnemy.health <= 0) { nearbyEnemy.health = 0; } else { nearbyEnemy.healthBar.width = nearbyEnemy.health / nearbyEnemy.maxHealth * 70; } } // Create individual explosion effect for each affected enemy var nearbyExplosion = new EffectIndicator(nearbyEnemy.x, nearbyEnemy.y, 'splash'); nearbyExplosion.children[0].tint = 0xFF6600; // Orange-red for area damage nearbyExplosion.children[0].width = nearbyExplosion.children[0].height = CELL_SIZE * 1.5; nearbyExplosion.alpha = 0.8; nearbyExplosion.scaleX = 0.2; nearbyExplosion.scaleY = 0.2; game.addChild(nearbyExplosion); // Animate nearby explosion tween(nearbyExplosion, { scaleX: 1.2, scaleY: 1.2, alpha: 0.6 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(nearbyExplosion, { scaleX: 1.8, scaleY: 1.8, alpha: 0 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { nearbyExplosion.destroy(); } }); } }); } } } // Create MASSIVE tank weapon explosion with devastating area damage if (self.type === 'tank') { // Store explosion position for area damage calculation var explosionX = self.targetEnemy.x; var explosionY = self.targetEnemy.y; // MASSIVE explosion radius - much larger than missiles for devastating effect var explosionRadius = CELL_SIZE * 4.0; // 4 cell radius for tank weapons (60% larger than missiles) // Apply devastating area damage to all enemies within explosion radius for (var i = 0; i < enemies.length; i++) { var nearbyEnemy = enemies[i]; if (nearbyEnemy !== self.targetEnemy) { var dx = nearbyEnemy.x - explosionX; var dy = nearbyEnemy.y - explosionY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= explosionRadius) { // Tank weapons cause MASSIVE damage - instantly destroy most enemies if (nearbyEnemy.type === 'fast' || nearbyEnemy.type === 'flying' || nearbyEnemy.isFlying) { // Instantly destroy fast and flying enemies nearbyEnemy.health = 0; } else if (nearbyEnemy.type === 'immune' || nearbyEnemy.isImmune) { // Tank weapons instantly destroy immune enemies nearbyEnemy.health = 0; } else if (nearbyEnemy.type === 'tank') { // Tank weapons instantly destroy tank enemies nearbyEnemy.health = 0; } else if (nearbyEnemy.isBoss) { // Tank weapons instantly destroy boss enemies nearbyEnemy.health = 0; } else { // Massive damage to other enemy types nearbyEnemy.health -= self.damage * 3.0; // 3x damage for devastating effect if (nearbyEnemy.health <= 0) { nearbyEnemy.health = 0; } else { nearbyEnemy.healthBar.width = nearbyEnemy.health / nearbyEnemy.maxHealth * 70; } } // Create individual massive explosion effect for each affected enemy var nearbyExplosion = new EffectIndicator(nearbyEnemy.x, nearbyEnemy.y, 'splash'); nearbyExplosion.children[0].tint = 0x8B4513; // Dark brown for tank explosion nearbyExplosion.children[0].width = nearbyExplosion.children[0].height = CELL_SIZE * 2.0; // Larger than missile explosions nearbyExplosion.alpha = 1.0; nearbyExplosion.scaleX = 0.3; nearbyExplosion.scaleY = 0.3; game.addChild(nearbyExplosion); // Animate massive nearby explosion tween(nearbyExplosion, { scaleX: 1.8, scaleY: 1.8, alpha: 0.8 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(nearbyExplosion, { scaleX: 2.5, scaleY: 2.5, alpha: 0 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { nearbyExplosion.destroy(); } }); } }); } } } // Create ULTRA-MASSIVE tank weapon explosion with enhanced visual effects // Stage 1: Initial massive impact flash - GIGANTIC white flash var impactFlash = new EffectIndicator(explosionX, explosionY, 'splash'); impactFlash.children[0].tint = 0xFFFFFF; // Bright white impact flash impactFlash.children[0].width = impactFlash.children[0].height = CELL_SIZE * 6.0; // ENORMOUS initial flash impactFlash.alpha = 1; impactFlash.scaleX = 0.1; impactFlash.scaleY = 0.1; game.addChild(impactFlash); // Ultra-fast bright flash expansion tween(impactFlash, { scaleX: 8.0, // Much bigger than missile explosions scaleY: 8.0, alpha: 0 }, { duration: 100, easing: tween.easeOut }); // Stage 2: MASSIVE main tank explosion fireball - dark brown/orange core var mainExplosion = new EffectIndicator(explosionX, explosionY, 'splash'); mainExplosion.children[0].tint = 0x8B4513; // Dark brown for tank explosion mainExplosion.children[0].width = mainExplosion.children[0].height = CELL_SIZE * 10.0; // ENORMOUS main explosion mainExplosion.alpha = 1; mainExplosion.scaleX = 0.2; mainExplosion.scaleY = 0.2; game.addChild(mainExplosion); // Dramatic MASSIVE fireball expansion tween(mainExplosion, { scaleX: 6.5, // Bigger than missile explosions scaleY: 6.5, alpha: 0.95 }, { duration: 300, easing: tween.bounceOut, onFinish: function onFinish() { tween(mainExplosion, { scaleX: 10.0, // ENORMOUS final scale scaleY: 10.0, alpha: 0 }, { duration: 800, easing: tween.easeIn, onFinish: function onFinish() { mainExplosion.destroy(); } }); } }); // Stage 3: Create QUADRUPLE the explosion particles for MASSIVE debris effect for (var particleIndex = 0; particleIndex < 32; particleIndex++) { // More particles than missiles var explosionParticle = new EffectIndicator(explosionX, explosionY, 'splash'); // Tank explosion colors - browns and oranges var particleColors = [0x8B4513, 0xA0522D, 0xD2691E, 0xFF8C00, 0xCD853F, 0xDEB887, 0x654321, 0x8B7355]; explosionParticle.children[0].tint = particleColors[particleIndex % particleColors.length]; explosionParticle.children[0].width = explosionParticle.children[0].height = CELL_SIZE * (2.2 + Math.random() * 1.8); // Bigger particles explosionParticle.alpha = 0.95; explosionParticle.scaleX = 0.6 + Math.random() * 0.5; explosionParticle.scaleY = 0.6 + Math.random() * 0.5; // Random direction for particle scatter with ULTRA-LONG distance var particleAngle = particleIndex / 32 * Math.PI * 2 + (Math.random() - 0.5) * 0.8; var particleDistance = CELL_SIZE * (4.5 + Math.random() * 3.5); // Ultra-far particle scatter var targetX = explosionX + Math.cos(particleAngle) * particleDistance; var targetY = explosionY + Math.sin(particleAngle) * particleDistance; game.addChild(explosionParticle); // Animate particles flying outward tween(explosionParticle, { x: targetX, y: targetY, scaleX: 4.0 + Math.random() * 1.5, // Much bigger final scale scaleY: 4.0 + Math.random() * 1.5, alpha: 0.8 }, { duration: 400 + Math.random() * 300, easing: tween.easeOut, onFinish: function onFinish() { tween(explosionParticle, { scaleX: 0.3, scaleY: 0.3, alpha: 0 }, { duration: 500 + Math.random() * 250, easing: tween.easeIn, onFinish: function onFinish() { explosionParticle.destroy(); } }); } }); } // Stage 4: QUINTUPLE shockwave rings for tank weapons var primaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash'); primaryShockwave.children[0].tint = 0xDEB887; // Light brown shockwave primaryShockwave.children[0].alpha = 0.8; primaryShockwave.children[0].width = primaryShockwave.children[0].height = CELL_SIZE * 2.0; primaryShockwave.alpha = 1.0; game.addChild(primaryShockwave); // MASSIVE expanding primary shockwave tween(primaryShockwave, { scaleX: 15.0, // Bigger than missile shockwaves scaleY: 15.0, alpha: 0 }, { duration: 900, easing: tween.easeOut, onFinish: function onFinish() { primaryShockwave.destroy(); } }); // Multiple additional shockwaves for devastating effect for (var shockIndex = 1; shockIndex <= 4; shockIndex++) { var additionalShockwave = new EffectIndicator(explosionX, explosionY, 'splash'); additionalShockwave.children[0].tint = 0x8B7355; // Darker brown shockwave additionalShockwave.children[0].alpha = 0.6 - shockIndex * 0.1; additionalShockwave.children[0].width = additionalShockwave.children[0].height = CELL_SIZE * (2.0 - shockIndex * 0.2); additionalShockwave.alpha = 0.8 - shockIndex * 0.1; game.addChild(additionalShockwave); // Delayed massive shockwaves tween(additionalShockwave, { scaleX: 16.0 + shockIndex * 1.0, scaleY: 16.0 + shockIndex * 1.0, alpha: 0 }, { duration: 1000 + shockIndex * 100, delay: shockIndex * 200, easing: tween.easeOut, onFinish: function onFinish() { additionalShockwave.destroy(); } }); } // Stage 5: MASSIVE lingering smoke cloud var smokeCloud = new EffectIndicator(explosionX, explosionY, 'splash'); smokeCloud.children[0].tint = 0x696969; // Dark gray smoke smokeCloud.children[0].alpha = 0.7; smokeCloud.children[0].width = smokeCloud.children[0].height = CELL_SIZE * 8.0; // Bigger smoke cloud smokeCloud.alpha = 0; smokeCloud.scaleX = 1.2; smokeCloud.scaleY = 1.2; game.addChild(smokeCloud); // Delayed massive smoke appearance tween(smokeCloud, { alpha: 1.0, scaleX: 8.0, scaleY: 8.0 }, { duration: 500, delay: 500, easing: tween.easeOut, onFinish: function onFinish() { // Smoke slowly dissipates over very long time tween(smokeCloud, { alpha: 0, scaleX: 14.0, // ENORMOUS final scale scaleY: 14.0 }, { duration: 2000, // Very long dissipation time easing: tween.easeIn, onFinish: function onFinish() { smokeCloud.destroy(); } }); } }); } // Create ULTRA-MASSIVE spectacular multi-stage missile explosion with ENHANCED particle effects // Stage 1: Initial impact flash - ULTRA bright white flash (GIGANTIC) var impactFlash = new EffectIndicator(explosionX, explosionY, 'splash'); impactFlash.children[0].tint = 0xFFFFFF; // Bright white impact flash impactFlash.children[0].width = impactFlash.children[0].height = CELL_SIZE * 5.0; // GIGANTIC initial flash (increased from 3.5) impactFlash.alpha = 1; impactFlash.scaleX = 0.1; impactFlash.scaleY = 0.1; game.addChild(impactFlash); // Ultra-fast bright flash expansion (GIGANTIC SCALE) tween(impactFlash, { scaleX: 7.0, // MUCH bigger scale (increased from 4.5) scaleY: 7.0, alpha: 0 }, { duration: 120, easing: tween.easeOut }); // Stage 2: ULTRA-MASSIVE main explosion fireball - orange/red core (ENORMOUS) var mainExplosion = new EffectIndicator(explosionX, explosionY, 'splash'); mainExplosion.children[0].tint = 0xFF4500; // Orange-red fireball mainExplosion.children[0].width = mainExplosion.children[0].height = CELL_SIZE * 8.0; // ENORMOUS main explosion (increased from 6.0) mainExplosion.alpha = 1; mainExplosion.scaleX = 0.2; mainExplosion.scaleY = 0.2; game.addChild(mainExplosion); // Dramatic ULTRA-MASSIVE fireball expansion with bounce effect tween(mainExplosion, { scaleX: 5.5, // Much bigger scale (increased from 3.8) scaleY: 5.5, alpha: 0.95 }, { duration: 250, easing: tween.bounceOut, onFinish: function onFinish() { tween(mainExplosion, { scaleX: 8.5, // ENORMOUS final scale (increased from 6.0) scaleY: 8.5, alpha: 0 }, { duration: 700, easing: tween.easeIn, onFinish: function onFinish() { mainExplosion.destroy(); } }); } }); // Stage 3: Create TRIPLE the explosion particles for ULTRA-MASSIVE debris effect (TRIPLED) for (var particleIndex = 0; particleIndex < 24; particleIndex++) { // Increased from 16 to 24 var explosionParticle = new EffectIndicator(explosionX, explosionY, 'splash'); // Vary particle colors for realistic explosion with MORE variety var particleColors = [0xFF6600, 0xFF4500, 0xFF8800, 0xFFAA00, 0xFF2200, 0xFFCC00, 0xFF3300, 0xFFDD00]; explosionParticle.children[0].tint = particleColors[particleIndex % particleColors.length]; explosionParticle.children[0].width = explosionParticle.children[0].height = CELL_SIZE * (1.8 + Math.random() * 1.5); // MUCH bigger particles (increased from 1.2 + 1.0) explosionParticle.alpha = 0.9; explosionParticle.scaleX = 0.5 + Math.random() * 0.4; explosionParticle.scaleY = 0.5 + Math.random() * 0.4; // Random direction for particle scatter with ULTRA-LONG distance var particleAngle = particleIndex / 24 * Math.PI * 2 + (Math.random() - 0.5) * 0.8; var particleDistance = CELL_SIZE * (3.5 + Math.random() * 3.0); // ULTRA-FAR particle scatter (increased from 2.5 + 2.0) var targetX = explosionX + Math.cos(particleAngle) * particleDistance; var targetY = explosionY + Math.sin(particleAngle) * particleDistance; game.addChild(explosionParticle); // Animate particles flying outward with ULTRA-enhanced physics tween(explosionParticle, { x: targetX, y: targetY, scaleX: 3.5 + Math.random() * 1.2, // Much bigger final scale (increased from 2.5 + 0.8) scaleY: 3.5 + Math.random() * 1.2, alpha: 0.7 }, { duration: 350 + Math.random() * 250, // Longer duration easing: tween.easeOut, onFinish: function onFinish() { tween(explosionParticle, { scaleX: 0.2, scaleY: 0.2, alpha: 0 }, { duration: 450 + Math.random() * 200, // Longer fade duration easing: tween.easeIn, onFinish: function onFinish() { explosionParticle.destroy(); } }); } }); } // Stage 4: ULTRA-MASSIVE enhanced shockwave with QUADRUPLE rings var primaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash'); primaryShockwave.children[0].tint = 0xFFDDDD; // Light pink shockwave primaryShockwave.children[0].alpha = 0.7; primaryShockwave.children[0].width = primaryShockwave.children[0].height = CELL_SIZE * 1.5; // Bigger base primaryShockwave.alpha = 0.9; game.addChild(primaryShockwave); // ULTRA-HUGE expanding primary shockwave tween(primaryShockwave, { scaleX: 12.0, // ENORMOUS scale (increased from 8.5) scaleY: 12.0, alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { primaryShockwave.destroy(); } }); // Secondary ULTRA-MASSIVE shockwave var secondaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash'); secondaryShockwave.children[0].tint = 0xFFFFFF; // White secondary wave secondaryShockwave.children[0].alpha = 0.5; secondaryShockwave.children[0].width = secondaryShockwave.children[0].height = CELL_SIZE * 1.0; secondaryShockwave.alpha = 0.8; game.addChild(secondaryShockwave); // Delayed ULTRA-MASSIVE secondary shockwave tween(secondaryShockwave, { scaleX: 13.0, // ENORMOUS scale (increased from 9.0) scaleY: 13.0, alpha: 0 }, { duration: 900, delay: 150, easing: tween.easeOut, onFinish: function onFinish() { secondaryShockwave.destroy(); } }); // Third shockwave for QUADRUPLE-ring ULTRA-MASSIVE effect var tertiaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash'); tertiaryShockwave.children[0].tint = 0xFFAAAA; // Light red tertiary wave tertiaryShockwave.children[0].alpha = 0.3; tertiaryShockwave.children[0].width = tertiaryShockwave.children[0].height = CELL_SIZE * 0.8; tertiaryShockwave.alpha = 0.7; game.addChild(tertiaryShockwave); // Third delayed ULTRA-MASSIVE shockwave tween(tertiaryShockwave, { scaleX: 14.0, // ENORMOUS scale (increased from 10.0) scaleY: 14.0, alpha: 0 }, { duration: 1000, delay: 300, easing: tween.easeOut, onFinish: function onFinish() { tertiaryShockwave.destroy(); } }); // Fourth shockwave for QUADRUPLE-ring effect (NEW!) var quaternaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash'); quaternaryShockwave.children[0].tint = 0xFFCCCC; // Very light pink quaternary wave quaternaryShockwave.children[0].alpha = 0.2; quaternaryShockwave.children[0].width = quaternaryShockwave.children[0].height = CELL_SIZE * 0.6; quaternaryShockwave.alpha = 0.6; game.addChild(quaternaryShockwave); // Fourth delayed ULTRA-MASSIVE shockwave tween(quaternaryShockwave, { scaleX: 15.0, // ULTRA-ENORMOUS scale scaleY: 15.0, alpha: 0 }, { duration: 1100, delay: 450, easing: tween.easeOut, onFinish: function onFinish() { quaternaryShockwave.destroy(); } }); // Stage 5: ULTRA-MASSIVE smoke cloud effect - ENORMOUS and lingers much longer var smokeCloud = new EffectIndicator(explosionX, explosionY, 'splash'); smokeCloud.children[0].tint = 0x666666; // Dark gray smoke smokeCloud.children[0].alpha = 0.6; smokeCloud.children[0].width = smokeCloud.children[0].height = CELL_SIZE * 6.0; // ENORMOUS smoke cloud (increased from 4.0) smokeCloud.alpha = 0; smokeCloud.scaleX = 1.0; smokeCloud.scaleY = 1.0; game.addChild(smokeCloud); // Delayed ULTRA-MASSIVE smoke appearance that grows slowly and lingers tween(smokeCloud, { alpha: 0.9, scaleX: 6.0, // Bigger initial expansion (increased from 4.0) scaleY: 6.0 }, { duration: 450, delay: 400, easing: tween.easeOut, onFinish: function onFinish() { // Smoke slowly dissipates over ULTRA-LONG time tween(smokeCloud, { alpha: 0, scaleX: 10.0, // ENORMOUS final scale (increased from 7.0) scaleY: 10.0 }, { duration: 1500, // ULTRA-LONG dissipation time (increased from 1200) easing: tween.easeIn, onFinish: function onFinish() { smokeCloud.destroy(); } }); } }); } // Apply special effects based on bullet type if (self.type === 'flamethrower') { // Create visual flame effect with enhanced animation var flameEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'flamethrower'); game.addChild(flameEffect); // Apply burning effect to target enemy if (!self.targetEnemy.isImmune) { self.targetEnemy.burning = true; self.targetEnemy.burnDamage = self.damage * 0.3; // 30% of original damage per tick self.targetEnemy.burnDuration = 180; // 3 seconds at 60 FPS } // Flame damage to enemies in a cone behind the target var coneRange = CELL_SIZE * 1.2; var targetAngle = Math.atan2(self.targetEnemy.y - self.y, self.targetEnemy.x - self.x); for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var flameDx = otherEnemy.x - self.targetEnemy.x; var flameDy = otherEnemy.y - self.targetEnemy.y; var flameDistance = Math.sqrt(flameDx * flameDx + flameDy * flameDy); var enemyAngle = Math.atan2(flameDy, flameDx); var angleDiff = Math.abs(enemyAngle - targetAngle); // Normalize angle difference while (angleDiff > Math.PI) angleDiff -= Math.PI * 2; while (angleDiff < -Math.PI) angleDiff += Math.PI * 2; angleDiff = Math.abs(angleDiff); // Check if enemy is within cone (45 degrees = PI/4 radians) if (flameDistance <= coneRange && angleDiff <= Math.PI / 4) { // Apply flame damage (40% of original damage) otherEnemy.health -= self.damage * 0.4; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } // Apply burning effect if (!otherEnemy.isImmune) { otherEnemy.burning = true; otherEnemy.burnDamage = self.damage * 0.2; otherEnemy.burnDuration = 120; // 2 seconds for cone targets } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Apply slow effect // Make slow percentage scale with tower level (default 50%, up to 80% at max level) var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6 var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; // Slow by X% self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { self.targetEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'poison') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual poison effect var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Apply poison effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } self.destroy(); } else { var angle = Math.atan2(dy, dx); // Apply spread angle for machine gun bullets if (self.type === 'trap' && self.spreadAngle !== undefined) { angle += self.spreadAngle; } // Rotate missile asset to face target direction if (self.type === 'slow' && self.children.length > 0) { var missileGraphics = self.children[0]; if (missileGraphics.targetRotation === undefined) { missileGraphics.targetRotation = angle; missileGraphics.rotation = angle; } else { // Only rotate if angle difference is significant if (Math.abs(angle - missileGraphics.targetRotation) > 0.1) { tween.stop(missileGraphics, { rotation: true }); // Calculate shortest rotation path var currentRotation = missileGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } missileGraphics.targetRotation = angle; tween(missileGraphics, { rotation: currentRotation + angleDiff }, { duration: 100, easing: tween.easeOut }); } } } self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); // DebugCell class removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'flamethrower': effectGraphics.tint = 0xFF4500; effectGraphics.width = CELL_SIZE * 1.5; effectGraphics.height = CELL_SIZE * 0.8; // Elongated flame effect break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .02; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; self.collisionRadius = 25; // Base collision detection radius self.lastCollisionCheck = 0; // Performance optimization self.smokeEffectActive = false; // Track if smoke effect is already active self.lastHealthPercentage = 1.0; // Track last health percentage to detect transitions // Adjust collision radius based on enemy type switch (self.type) { case 'tank': self.collisionRadius = 40; // Larger radius for tank enemies break; case 'fast': self.collisionRadius = 20; // Smaller radius for fast enemies break; case 'flying': self.collisionRadius = 30; // Medium radius for flying enemies break; default: self.collisionRadius = 25; // Default radius break; } // Boss enemies have larger collision radius if (self.isBoss) { self.collisionRadius *= 1.5; } // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 1; // Normal speed (half of previous 2x) // Speed boost for wave 5 fast enemies if (self.waveNumber === 5) { self.speed *= 1.2; // 1.2x speed for wave 5 fast enemies } // Speed boost for wave 6 fast enemies if (self.waveNumber === 6) { self.speed *= 1.1; // 1.1x speed for wave 6 fast enemies } self.maxHealth = 1; break; case 'immune': self.isImmune = true; self.maxHealth = 200; // Increase speed for wave 5 immune enemies only if (self.waveNumber === 5) { self.speed *= 3; // 3x speed for wave 5 immune enemies } // Speed boost for wave 2 immune enemies when they reach the second turn self.hasReachedSecondTurn = false; break; case 'flying': self.isFlying = true; self.speed *= 2; // 2x speed for flying enemies self.maxHealth = 80; break; case 'tank': self.speed *= 0.5; // Half speed for tank enemies self.maxHealth = 300; // Very high health for tank enemies break; default: // Default enemy uses fast values self.speed *= 1; // Normal speed (half of previous 2x) self.maxHealth = 1; break; } if (currentWave === totalWaves && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.isBoss) { assetId = 'Boss'; } else if (self.type === 'tank') { assetId = 'Tank'; } else if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Check for 50% health threshold for smoke effect var currentHealthPercentage = self.health / self.maxHealth; if (!self.smokeEffectActive && self.lastHealthPercentage > 0.5 && currentHealthPercentage <= 0.5) { // Check if this enemy type should have smoke effect (Flying, BOSS, Immune, Tank) if (self.isFlying || self.isBoss || self.isImmune || self.type === 'tank') { self.smokeEffectActive = true; // Create animated smoke steam effect var smokeEffect = new Container(); smokeEffect.x = self.x; smokeEffect.y = self.y - enemyGraphics.height / 2 - 10; // Create multiple smoke particles for realistic steam effect for (var smokeIndex = 0; smokeIndex < 5; smokeIndex++) { var smokeParticle = smokeEffect.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); // Set smoke particle properties - scaled up to 2x size smokeParticle.width = (15 + Math.random() * 10) * 2; smokeParticle.height = (20 + Math.random() * 15) * 2; smokeParticle.tint = 0x666666; // Dark gray smoke smokeParticle.alpha = 0.6 + Math.random() * 0.3; // Position particles in a small cluster smokeParticle.x = (Math.random() - 0.5) * 20; smokeParticle.y = smokeIndex * -8; // Animate smoke particles rising and fading tween(smokeParticle, { y: smokeParticle.y - 60 - Math.random() * 40, x: smokeParticle.x + (Math.random() - 0.5) * 30, scaleX: 1.5 + Math.random() * 0.5, scaleY: 1.5 + Math.random() * 0.5, alpha: 0 }, { duration: 2000 + Math.random() * 1000, delay: smokeIndex * 200, easing: tween.easeOut, onFinish: function onFinish() { smokeParticle.destroy(); } }); } game.addChild(smokeEffect); // Create continuous smoke effect that follows the enemy self.continuousSmokeEffect = smokeEffect; // Set up repeating smoke generation self.smokeTimer = 0; self.smokeInterval = 60; // Generate new smoke every 60 frames (1 second) } } // Update continuous smoke effect position if active if (self.smokeEffectActive && self.continuousSmokeEffect && self.continuousSmokeEffect.parent) { self.continuousSmokeEffect.x = self.x; self.continuousSmokeEffect.y = self.y - enemyGraphics.height / 2 - 10; // Generate new smoke particles periodically self.smokeTimer++; if (self.smokeTimer >= self.smokeInterval) { self.smokeTimer = 0; // Create new smoke particle var newSmokeParticle = self.continuousSmokeEffect.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); newSmokeParticle.width = (12 + Math.random() * 8) * 2; newSmokeParticle.height = (16 + Math.random() * 12) * 2; newSmokeParticle.tint = 0x888888; // Lighter gray for continuous smoke newSmokeParticle.alpha = 0.4 + Math.random() * 0.2; newSmokeParticle.x = (Math.random() - 0.5) * 15; newSmokeParticle.y = 0; // Animate new smoke particle tween(newSmokeParticle, { y: -40 - Math.random() * 20, x: newSmokeParticle.x + (Math.random() - 0.5) * 20, scaleX: 1.2 + Math.random() * 0.3, scaleY: 1.2 + Math.random() * 0.3, alpha: 0 }, { duration: 1500 + Math.random() * 500, easing: tween.easeOut, onFinish: function onFinish() { newSmokeParticle.destroy(); } }); } } // Update last health percentage for next frame comparison self.lastHealthPercentage = currentHealthPercentage; // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed, poisoned, or burned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; self.burning = false; self.burnEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle burning effect from flamethrower if (self.burning) { // Visual indication of burning status if (!self.burnEffect) { self.burnEffect = true; } // Apply burn damage every 20 frames (3 times per second) if (LK.ticks % 20 === 0) { self.health -= self.burnDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.burnDuration--; if (self.burnDuration <= 0) { self.burning = false; self.burnEffect = false; // Only reset tint if not slowed or poisoned if (!self.slowed && !self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Enemy overlap avoidance - check every frame for immune and tank enemies for better convoy formation if (self.type === 'immune' || self.type === 'tank' || LK.ticks % 3 === 0) { for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self && otherEnemy.parent) { // Prevent collision between flying and ground enemies (fast, immune, tank) if (self.isFlying && !otherEnemy.isFlying || !self.isFlying && otherEnemy.isFlying) { continue; // Skip collision detection between flying and ground enemies } var dx = otherEnemy.x - self.x; var dy = otherEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); var minDistance = (self.collisionRadius + otherEnemy.collisionRadius) * 1.4; // Increased buffer for better separation if (distance < minDistance && distance > 0) { // Much stronger separation force for immune and tank enemies var overlap = minDistance - distance; var separationForce; if (self.type === 'immune' || self.type === 'tank' || otherEnemy.type === 'immune' || otherEnemy.type === 'tank') { // Extra strong separation for immune/tank enemies to maintain convoy formation separationForce = Math.pow(overlap / minDistance, 1.5) * 4.0; // Stronger force with smoother scaling } else { separationForce = Math.pow(overlap / minDistance, 2) * 2.0; // Original force for other enemies } var separationX = dx / distance * separationForce; var separationY = dy / distance * separationForce; // Apply separation with stronger horizontal bias to maintain lanes self.currentCellX -= separationX; self.currentCellY -= separationY * 0.7; // Reduced Y separation to maintain forward movement // Apply weaker counter-force to maintain formation otherEnemy.currentCellX += separationX * 0.3; otherEnemy.currentCellY += separationY * 0.3; // Keep enemies within reasonable bounds self.currentCellX = Math.max(2, Math.min(22, self.currentCellX)); self.currentCellY = Math.max(-20, Math.min(33, self.currentCellY)); otherEnemy.currentCellX = Math.max(2, Math.min(22, otherEnemy.currentCellX)); otherEnemy.currentCellY = Math.max(-20, Math.min(33, otherEnemy.currentCellY)); } } } } // Fast enemy shooting capability if (self.type === 'fast') { // Initialize shooting properties if not already set if (self.shootingCooldown === undefined) { self.shootingCooldown = 0; self.shootingRange = 12 * CELL_SIZE; // 12 cell shooting range (3x increase) self.shootingDamage = 5; self.bulletSpeed = 3; } // Decrease shooting cooldown if (self.shootingCooldown > 0) { self.shootingCooldown--; } // Try to find and shoot at towers within range if (self.shootingCooldown <= 0 && self.currentCellY >= 4) { var targetTower = null; var closestDistance = self.shootingRange; // Find closest tower within shooting range for (var i = 0; i < towers.length; i++) { var tower = towers[i]; // Skip landmine towers - enemies cannot see them if (tower.id === 'landmine') { continue; } // Allow targeting of all other tower types including trap/shooter towers var dx = tower.x - self.x; var dy = tower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.shootingRange && distance < closestDistance) { closestDistance = distance; targetTower = tower; } } // Shoot at the target tower if found if (targetTower) { var bulletX = self.x; var bulletY = self.y; var enemyBullet = new Bullet(bulletX, bulletY, targetTower, self.shootingDamage, self.bulletSpeed); // Customize enemy bullet appearance if (enemyBullet.children[0]) { enemyBullet.children[0].tint = 0xFF0000; // Red color for enemy bullets enemyBullet.children[0].width = 20; enemyBullet.children[0].height = 20; } // Mark as enemy bullet to handle differently enemyBullet.isEnemyBullet = true; game.addChild(enemyBullet); bullets.push(enemyBullet); // Set cooldown (shoot every 1.5 seconds at 60 FPS) self.shootingCooldown = 90; } } } // Immune enemy shooting capability if (self.type === 'immune') { // Initialize shooting properties if not already set if (self.shootingCooldown === undefined) { self.shootingCooldown = 0; self.shootingRange = 8 * CELL_SIZE; // 8 cell shooting range for immune enemies self.shootingDamage = 8; self.bulletSpeed = 4; } // Decrease shooting cooldown if (self.shootingCooldown > 0) { self.shootingCooldown--; } // Try to find and shoot at towers within range if (self.shootingCooldown <= 0 && self.currentCellY >= 4) { var targetTower = null; var closestDistance = self.shootingRange; // Find closest tower within shooting range for (var i = 0; i < towers.length; i++) { var tower = towers[i]; // Skip landmine towers - enemies cannot see them if (tower.id === 'landmine') { continue; } // Allow targeting of all other tower types including trap/shooter towers var dx = tower.x - self.x; var dy = tower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.shootingRange && distance < closestDistance) { closestDistance = distance; targetTower = tower; } } // Shoot at the target tower if found if (targetTower) { var bulletX = self.x; var bulletY = self.y; var enemyBullet = new Bullet(bulletX, bulletY, targetTower, self.shootingDamage, self.bulletSpeed); // Customize immune enemy bullet appearance if (enemyBullet.children[0]) { enemyBullet.children[0].tint = 0xAA0000; // Dark red color for immune enemy bullets enemyBullet.children[0].width = 25; enemyBullet.children[0].height = 25; } // Mark as enemy bullet to handle differently enemyBullet.isEnemyBullet = true; game.addChild(enemyBullet); bullets.push(enemyBullet); // Set cooldown (shoot every 1 second at 60 FPS) self.shootingCooldown = 60; } } } // Immune enemy trap collision detection if (self.type === 'immune') { // Initialize trap contact tracking if not set if (self.trapContactMap === undefined) { self.trapContactMap = {}; } // Check collision with trap towers every 10 frames for performance if (LK.ticks % 10 === 0) { for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (tower.id === 'trap') { var dx = tower.x - self.x; var dy = tower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if immune enemy is touching the trap (within 80 pixels) if (distance <= 80) { // Only reduce health if this trap hasn't been touched before var trapKey = tower.x + ',' + tower.y; // Unique identifier for this trap if (!self.trapContactMap[trapKey]) { self.trapContactMap[trapKey] = true; // Reduce immune enemy's health by half self.health = Math.floor(self.health / 2); if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; // Create visual effect for trap contact var trapEffect = new EffectIndicator(self.x, self.y, 'splash'); trapEffect.children[0].tint = 0xAAAAAA; // Gray color for trap effect trapEffect.children[0].width = trapEffect.children[0].height = CELL_SIZE * 1.2; game.addChild(trapEffect); } // Break after first trap collision to avoid multiple hits per frame break; } } } } } // Tank enemy shooting capability if (self.type === 'tank') { // Initialize shooting properties if not already set if (self.shootingCooldown === undefined) { self.shootingCooldown = 0; self.shootingRange = 16 * CELL_SIZE; // 2x increased range for better targeting (was 8) self.shootingDamage = 15; // Increased damage for tank enemies self.bulletSpeed = 3; // Increased bullet speed } // Decrease shooting cooldown if (self.shootingCooldown > 0) { self.shootingCooldown--; } // Try to find and shoot at towers within range - tank enemies shoot when positioned if (self.shootingCooldown <= 0 && self.currentCellY >= 4 && self.tankMovementComplete) { var targetTower = null; var closestDistance = self.shootingRange; // Find closest tower within shooting range for (var i = 0; i < towers.length; i++) { var tower = towers[i]; // Skip landmine towers - enemies cannot see them if (tower.id === 'landmine') { continue; } // Allow targeting of all other tower types including trap/shooter towers var dx = tower.x - self.x; var dy = tower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.shootingRange && distance < closestDistance) { closestDistance = distance; targetTower = tower; } } // Shoot at the target tower if found if (targetTower) { var bulletX = self.x; var bulletY = self.y; var enemyBullet = new Bullet(bulletX, bulletY, targetTower, self.shootingDamage, self.bulletSpeed); // Customize tank enemy bullet appearance if (enemyBullet.children[0]) { enemyBullet.children[0].tint = 0x888888; // Gray color for tank enemy bullets enemyBullet.children[0].width = 35; // Larger bullets for tank enemies enemyBullet.children[0].height = 35; } // Mark as enemy bullet to handle differently enemyBullet.isEnemyBullet = true; game.addChild(enemyBullet); bullets.push(enemyBullet); // Set cooldown (shoot every 1.5 seconds at 60 FPS for more frequent shooting) self.shootingCooldown = 90; } } } // Convoy formation for immune and tank enemies - maintain proper spacing and formation if ((self.type === 'immune' || self.type === 'tank') && self.currentCellY >= 4) { // Find nearby same-type enemies to form convoy with var nearbyConvoyEnemies = []; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self && otherEnemy.parent && otherEnemy.type === self.type) { var dx = otherEnemy.x - self.x; var dy = otherEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= CELL_SIZE * 4) { // Within 4 cells for convoy formation nearbyConvoyEnemies.push({ enemy: otherEnemy, distance: distance, dx: dx, dy: dy }); } } } // Apply convoy formation forces if (nearbyConvoyEnemies.length > 0) { for (var j = 0; j < nearbyConvoyEnemies.length; j++) { var convoyData = nearbyConvoyEnemies[j]; var convoyEnemy = convoyData.enemy; var distance = convoyData.distance; var dx = convoyData.dx; var dy = convoyData.dy; var idealDistance = CELL_SIZE * 2.8; // Ideal convoy spacing if (distance < idealDistance) { // Too close - apply separation var separationForce = (idealDistance - distance) / idealDistance * 0.8; var separationX = dx / distance * separationForce; var separationY = dy / distance * separationForce * 0.4; // Less Y separation self.currentCellX -= separationX; self.currentCellY -= separationY; } else if (distance > idealDistance * 1.5) { // Too far - apply attraction to maintain convoy var attractionForce = (distance - idealDistance) / distance * 0.3; var attractionX = dx / distance * attractionForce; var attractionY = dy / distance * attractionForce * 0.2; self.currentCellX += attractionX; self.currentCellY += attractionY; } } } // Keep enemies within valid bounds self.currentCellX = Math.max(3, Math.min(21, self.currentCellX)); self.currentCellY = Math.max(-20, Math.min(33, self.currentCellY)); } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed && self.burning) { // Combine all three effects enemyGraphics.tint = 0x8B4513; // Brown-ish mix } else if (self.poisoned && self.slowed) { // Combine poison (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned && self.burning) { // Combine poison and burn enemyGraphics.tint = 0x8B7355; // Brownish-green mix } else if (self.slowed && self.burning) { // Combine slow and burn enemyGraphics.tint = 0xCC4500; // Orange-red mix } else if (self.burning) { enemyGraphics.tint = 0xFF4500; // Orange for burning } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox) + Math.PI / 2; if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.25) { // Further increased threshold to reduce tween frequency tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 150, // Reduced duration for faster rotation easing: tween.easeOut }); } } } } // Keep health bars positioned below level indicators var healthBarY = CELL_SIZE * 0.7 + 25; if (self.towerHealthBarOutline) { self.towerHealthBarOutline.y = healthBarY; self.towerHealthBarBG.y = healthBarY; self.towerHealthBar.y = healthBarY; } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ // Create simplified straight road path waypoints with 3-cell width var pathWaypoints = [{ x: 12, y: 0 }, // Start point (spawn) { x: 12, y: 8 }, // First horizontal road { x: 2, y: 8 }, // Turn down { x: 2, y: 15 }, // Second horizontal road { x: 22, y: 15 }, // Turn down { x: 22, y: 22 }, // Third horizontal road { x: 12, y: 22 }, // Final segment to goal { x: 12, y: gridHeight - 1 } // End point (goal) ]; // Create path cells array to mark which cells are part of the road var pathCells = []; // Generate path between waypoints for (var w = 0; w < pathWaypoints.length - 1; w++) { var start = pathWaypoints[w]; var end = pathWaypoints[w + 1]; // Create straight line between waypoints var steps = Math.max(Math.abs(end.x - start.x), Math.abs(end.y - start.y)); for (var s = 0; s <= steps; s++) { var t = steps === 0 ? 0 : s / steps; var pathX = Math.round(start.x + (end.x - start.x) * t); var pathY = Math.round(start.y + (end.y - start.y) * t); // Add path width - make road 7 cells wide horizontally, 3 cells wide vertically for (var offsetX = -3; offsetX <= 3; offsetX++) { for (var offsetY = -1; offsetY <= 1; offsetY++) { var cellX = pathX + offsetX; var cellY = pathY + offsetY; if (cellX >= 0 && cellX < gridWidth && cellY >= 0 && cellY < gridHeight) { var key = cellX + ',' + cellY; if (pathCells.indexOf(key) === -1) { pathCells.push(key); } } } } } } for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellKey = i + ',' + j; var isOnPath = pathCells.indexOf(cellKey) !== -1; // Default to wall, then carve out path and other areas var cellType = 1; // Wall by default // Set path cells if (isOnPath) { cellType = 0; // Path } // Set spawn points at the start of the path if (i >= 11 && i <= 13 && j === 0) { cellType = 2; // Spawn self.spawns.push(cell); } // Set goal points at the end of the path if (i >= 11 && i <= 13 && j === gridHeight - 1) { cellType = 3; // Goal self.goals.push(cell); } // Keep borders as walls if (i === 0 || i === gridWidth - 1 || j === 0 || j === gridHeight - 1) { if (cellType !== 2 && cellType !== 3) { // Don't override spawn/goal cellType = 1; // Wall } } cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; // Render road asset for path cells if (isOnPath) { var roadVisual = self.attachAsset('road', { anchorX: 0.5, anchorY: 0.6 }); roadVisual.x = i * CELL_SIZE + CELL_SIZE / 2; roadVisual.y = j * CELL_SIZE + CELL_SIZE / 2; } // Debug cells removed for performance } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 1 && node.left.type != 1) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.right.type != 1) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.right.type != 1) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.left.type != 1) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { // Debug cells removed for performance }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.type == 3) { // BOSS Enemy reached the goal - reduce lives by 100 (100% of 100 total lives) if (enemy.isBoss) { lives = Math.max(0, lives - 100); } else { // Regular enemy reached the goal - reduce lives by 10 (10% of 100 total lives) lives = Math.max(0, lives - 10); } updateUI(); if (lives <= 0) { LK.showGameOver(); } return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Consistent movement speed for all enemy types during initial phase var initialSpeed = enemy.speed; if (enemy.type === 'tank') { initialSpeed = 0.03; // 3x faster than normal tank speed for smoother entry } // Move directly downward enemy.currentCellY += initialSpeed; // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) + 90 degrees right var angle = Math.PI / 2 + Math.PI / 2; if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update enemy's position (reduce calculation frequency) if (LK.ticks % 2 === 0) { enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; } // If enemy has now reached the entry area, update cell coordinates if (enemy.currentCellY >= 4) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Special movement for boss enemies - straight line down the middle if (enemy.isBoss) { // Boss enemies move straight down the middle column (x = 12) enemy.currentCellX = 12; // Always stay in middle column enemy.currentCellY += enemy.speed; // Move straight down } else { // Regular flying enemy logic // Initialize flying enemy direction if not set if (enemy.flyingDirection === undefined) { // Use the global alternating direction flag enemy.flyingDirection = lastDirectionWasRight ? 1 : -1; // 1 for right, -1 for left enemy.startX = enemy.currentCellX; // Remember starting X position enemy.hasChangedDirection = false; // Track if enemy has changed direction yet } // Assign flying enemies to one of 3 vertical lanes if not already assigned if (enemy.flyingLane === undefined) { // Determine lane based on initial direction instead of spawn position if (enemy.flyingDirection === -1) { // Flying left: start from right side lanes enemy.flyingLane = 2; // Right lane - x position 18 enemy.targetX = 18; } else { // Flying right: start from left side lanes enemy.flyingLane = 0; // Left lane - x position 6 enemy.targetX = 6; } } // Move straight down in the assigned lane enemy.currentCellY += enemy.speed; // Implement alternating direction movement for flying enemies if (!enemy.hasChangedDirection && enemy.currentCellY > 15) { // After reaching middle of map, start moving in assigned direction enemy.hasChangedDirection = true; // Set new target X based on direction - enemies move to opposite side if (enemy.flyingDirection > 0) { // Moving right - target right side of map enemy.targetX = 20; } else { // Moving left - target left side of map enemy.targetX = 4; } } // Gradually move to the target X position (lane center or direction target) var xDiff = enemy.targetX - enemy.currentCellX; if (Math.abs(xDiff) > 0.1) { enemy.currentCellX += xDiff * 0.05; // Gradual movement to target } } // Rotate enemy graphic to face downward var angle = Math.PI; // Face downward (180 degrees) if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); // Update visual position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Check if enemy reached the bottom (goal) var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.type == 3) { // BOSS Flying enemy reached the goal - reduce lives by 100 (100% of 100 total lives) if (enemy.isBoss) { lives = Math.max(0, lives - 100); } else { // Flying enemy reached the goal - reduce lives by 10 (10% of 100 total lives) lives = Math.max(0, lives - 10); } updateUI(); if (lives <= 0) { LK.showGameOver(); } return true; } // Check if enemy has moved off the bottom of the screen if (enemy.currentCellY > 35) { // Grid height is 35 (29 + 6) // BOSS Enemy went off-screen - reduce lives by 100 (100% of 100 total lives) if (enemy.isBoss) { lives = Math.max(0, lives - 100); } else { // Enemy went off-screen - reduce lives by 10 (10% of 100 total lives) lives = Math.max(0, lives - 10); } updateUI(); if (lives <= 0) { LK.showGameOver(); } return true; } // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (enemy.isBoss) { // Boss enemies move straight down the middle column regardless of type enemy.currentCellX = 12; // Always stay in middle column enemy.currentCellY += enemy.speed; // Move straight down // Force boss position to center immediately enemy.cellX = 12; enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); // Set rotation to face straight down without animation if (enemy.children[0]) { enemy.children[0].rotation = Math.PI; // Face downward (180 degrees) enemy.children[0].targetRotation = Math.PI; } } else if (enemy.type === 'fast') { // Fast enemies move straight down without pathfinding or turning enemy.currentCellY += enemy.speed; enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); // Set rotation to face straight down without animation if (enemy.children[0]) { enemy.children[0].rotation = Math.PI; // Face downward (180 degrees) enemy.children[0].targetRotation = Math.PI; } } else if (enemy.type === 'immune' && enemy.waveNumber === 2 && !enemy.hasReachedSecondTurn && enemy.currentCellY >= 15) { // Wave 2 immune enemies get speed boost when they reach the second turn (y >= 15) enemy.hasReachedSecondTurn = true; enemy.speed = 2.4 * 0.02; // Set speed to 2.4 times base speed (0.02) } else if (enemy.type === 'tank') { // Tank enemies move to upper quarter vertically and stop there, staying at their spawned x-position var upperQuarterY = (29 + 6) * 0.25; // Upper quarter of map (around row 8.75) var supportPositionY = Math.max(8, upperQuarterY); // Stop at upper quarter // Initialize tank target position if not set if (enemy.tankTargetX === undefined) { enemy.tankTargetX = enemy.currentCellX; // Store spawned x-position enemy.tankTargetY = supportPositionY; // Store target support position enemy.tankMovementComplete = false; } // Use smooth interpolation movement instead of tween animations if (!enemy.tankMovementComplete && enemy.currentCellY >= 4) { // Calculate distance to target var dx = enemy.tankTargetX - enemy.currentCellX; var dy = enemy.tankTargetY - enemy.currentCellY; var distanceToTarget = Math.sqrt(dx * dx + dy * dy); // Check if we've reached the target (within 0.1 cells) if (distanceToTarget < 0.1) { enemy.currentCellX = enemy.tankTargetX; enemy.currentCellY = enemy.tankTargetY; enemy.tankMovementComplete = true; } else { // Use smooth interpolation movement var speed = 0.04; // Movement speed in cells per frame var t = Math.min(1, speed / distanceToTarget); enemy.currentCellX = enemy.currentCellX + (enemy.tankTargetX - enemy.currentCellX) * t; enemy.currentCellY = enemy.currentCellY + (enemy.tankTargetY - enemy.currentCellY) * t; } // Tank enemies do not rotate - keep original rotation } else { // Tank has reached support position - stop moving and face downward to support convoy // Lock tank position at target location enemy.currentCellX = enemy.tankTargetX; // Stay at spawned x-position enemy.currentCellY = enemy.tankTargetY; // Lock at support position // Tank enemies do not rotate - keep original rotation } enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } else { // Handle normal pathfinding enemies if (!enemy.currentTarget && cell && cell.targets && cell.targets.length > 0) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { // Check if current cell has a better target (closer to goal) if (cell && cell.targets && cell.targets.length > 0 && cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell.targets[0]; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Speed boost logic removed to prevent any tank speed boost effects enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox) + Math.PI / 2; enemy.currentCellX += Math.cos(angle - Math.PI / 2) * enemy.speed; enemy.currentCellY += Math.sin(angle - Math.PI / 2) * enemy.speed; } else if (cell && cell.targets && cell.targets.length > 0) { // If no current target, pick the first available target from current cell enemy.currentTarget = cell.targets[0]; } } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Check if ground enemy has moved off the bottom of the screen if (enemy.currentCellY > 35) { // Grid height is 35 (29 + 6) // BOSS Enemy went off-screen - reduce lives by 100 (100% of 100 total lives) if (enemy.isBoss) { lives = Math.max(0, lives - 100); } else { // Enemy went off-screen - reduce lives by 10 (10% of 100 total lives) lives = Math.max(0, lives - 10); } updateUI(); if (lives <= 0) { LK.showGameOver(); } return true; } }; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'trap'; // Increase size of base for easier touch var baseGraphics = self.attachAsset('Box', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); switch (self.towerType) { case 'landmine': baseGraphics.tint = 0x9B8B6D; // Lighter tan/brown camouflage break; case 'sniper': baseGraphics.tint = 0x7A9B5F; // Lighter olive green camouflage break; case 'flamethrower': baseGraphics.tint = 0xBBA385; //{aO} // Lighter sandy brown camouflage break; case 'slow': baseGraphics.tint = 0x7F7F5F; //{aR} // Lighter olive drab camouflage break; case 'poison': baseGraphics.tint = 0x969696; //{aU} // Lighter gray camouflage break; case 'tank': baseGraphics.tint = 0x006400; // Dark green for tank break; default: baseGraphics.tint = 0x8C8C77; //{aX} // Lighter neutral camouflage green-gray } var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var displayTextShadow = self.towerType === 'flamethrower' ? 'Flame' : self.towerType === 'trap' ? 'Shooter' : self.towerType === 'slow' ? 'Missiles' : self.towerType === 'tank' ? 'Tank' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1); var typeLabelShadow = new Text2(displayTextShadow, { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); // Add tower type label var displayText = self.towerType === 'flamethrower' ? 'Flame' : self.towerType === 'trap' ? 'Shooter' : self.towerType === 'slow' ? 'Missiles' : self.towerType === 'tank' ? 'Tank' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1); var typeLabel = new Text2(displayText, { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -20; // Position above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'trap'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; self.isPlaced = false; // Track if tower has been placed on grid // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case 'sniper': // Sniper: massive range to cover entire map from any position if (self.level === self.maxLevel) { return 75 * CELL_SIZE; // Extreme range for max level to cover entire map (1.5x increase) } return (22.5 + (self.level - 1) * 7.5) * CELL_SIZE; // Increased base and scaling (1.5x increase) case 'flamethrower': // Flamethrower: base 4.5, +0.5 per level for cone range (increased aiming distance) return (4.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'landmine': // Landmine: base 2.5, +0.5 per level return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'slow': // Slow: base 8.75, +1.25 per level (2.5x increase) return (8.75 + (self.level - 1) * 1.25) * CELL_SIZE; case 'poison': // Poison: base 3.2, +0.5 per level return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; case 'tank': // Tank: base 14.0, +2.8 per level (3.5x increase) return (14.0 + (self.level - 1) * 2.8) * CELL_SIZE; default: // Trap: base 6, +1.0 per level (double range) return (6 + (self.level - 1) * 1.0) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; // Continuous shooting variables for flamethrower self.isContinuousFiring = false; self.continuousFireTimer = 0; self.continuousFireDuration = 300; // 5 seconds at 60 FPS self.continuousReloadTimer = 0; self.continuousReloadDuration = 180; // 3 seconds at 60 FPS switch (self.id) { case 'landmine': self.fireRate = 30; self.damage = 5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'sniper': self.fireRate = 90; self.damage = 25; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'flamethrower': self.fireRate = 2.5; // Very fast fire rate during continuous shooting (quarter of original delay) self.damage = 8; // Lower per-hit damage but continuous self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 6; break; case 'slow': self.fireRate = 30; self.damage = 15; // Increased from 8 to 15 for stronger missiles self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'poison': self.fireRate = 70; self.damage = 12; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 5; break; case 'tank': self.fireRate = 90; self.damage = 50; // Increased base damage for tank self.range = 4.0 * CELL_SIZE; self.bulletSpeed = 4; break; } var assetToUse = self.id === 'landmine' ? 'landmine' : self.id === 'sniper' ? 'Sniper' : self.id === 'flamethrower' ? 'Flamethrower' : self.id === 'slow' ? 'Rocket' : self.id === 'trap' ? 'Shooter' : self.id === 'tank' ? 'Tank' : 'tower'; var baseGraphics = self.attachAsset(assetToUse, { anchorX: 0.5, anchorY: self.id === 'sniper' ? 0.0 : 0.5 }); // Rotate sniper tower left by 40 degrees if (self.id === 'sniper') { baseGraphics.rotation = -40 * Math.PI / 180; // Convert 40 degrees to radians and make it negative for left rotation } // Flame effects removed for performance switch (self.id) { case 'landmine': // Remove tint to show natural landmine asset break; case 'sniper': // Remove tint to show natural sniper asset break; case 'flamethrower': // Remove tint to show natural flamethrower asset break; case 'slow': // Remove tint to show natural rocket asset break; case 'poison': baseGraphics.tint = 0x00FFAA; break; case 'tank': // Remove tint to show natural tank asset break; case 'trap': baseGraphics.tint = 0x555555; // Dark gray for machine gun break; default: baseGraphics.tint = 0xAAAAAA; } // Add health bar for towers that need them (excluding landmine) self.towerHealthBarOutline = null; self.towerHealthBarBG = null; self.towerHealthBar = null; if (self.id === 'flamethrower' || self.id === 'trap' || self.id === 'sniper' || self.id === 'slow' || self.id === 'tank') { // Create health bar outline self.towerHealthBarOutline = self.attachAsset('towerHealthBarOutline', { anchorX: 0, anchorY: 0.5 }); // Create health bar background (red) self.towerHealthBarBG = self.attachAsset('towerHealthBar', { anchorX: 0, anchorY: 0.5 }); self.towerHealthBarBG.tint = 0xff0000; // Create health bar foreground (green) self.towerHealthBar = self.attachAsset('towerHealthBar', { anchorX: 0, anchorY: 0.5 }); self.towerHealthBar.tint = 0x00ff00; // Position health bars below the level indicators to avoid overlap var healthBarY = CELL_SIZE * 0.7 + 25; // Position below level indicators with some spacing self.towerHealthBarOutline.y = healthBarY; self.towerHealthBarBG.y = healthBarY; self.towerHealthBar.y = healthBarY; // Center health bars horizontally self.towerHealthBarOutline.x = -self.towerHealthBarOutline.width / 2; self.towerHealthBarBG.x = -self.towerHealthBarBG.width / 2; self.towerHealthBar.x = -self.towerHealthBar.width / 2; } var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 6; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { switch (self.id) { case 'landmine': towerLevelIndicator.tint = 0x00AAFF; break; case 'sniper': towerLevelIndicator.tint = 0xFF5500; break; case 'flamethrower': towerLevelIndicator.tint = 0xFF4500; break; case 'slow': towerLevelIndicator.tint = 0x9900FF; break; case 'poison': towerLevelIndicator.tint = 0x00FFAA; break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } // Only render debug when tower is selected to improve performance if (selectedTower === self) { grid.renderDebug(); } }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'landmine') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect self.damage = 5 + self.level * 10; // double the effect self.bulletSpeed = 7 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades self.damage = 5 + self.level * 3; self.bulletSpeed = 7 + self.level * 0.7; } } else { // Special scaling for missiles towers if (self.id === 'slow') { if (self.level === self.maxLevel) { // Extra powerful last upgrade for missiles (triple the effect) self.fireRate = Math.max(3, 30 - self.level * 15); // Much faster fire rate self.damage = 15 + self.level * 30; // Much higher damage scaling self.bulletSpeed = 5 + self.level * 3; // Faster missiles } else { self.fireRate = Math.max(10, 30 - self.level * 5); // Faster than base scaling self.damage = 15 + self.level * 10; // Higher damage per level self.bulletSpeed = 5 + self.level * 1; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range if (distance <= self.getRange()) { // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, landmine towers target any ground enemy in range if (self.id === 'landmine') { // Landmine targets any ground enemy by distance if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } else { // For other towers, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { // Only find targets every 3 frames to reduce performance impact if (LK.ticks % 3 === 0) { self.targetEnemy = self.findTarget(); } // Clean up targeted enemies list for rocket launcher towers if (self.id === 'slow' && self.targetedEnemies) { // Remove enemies that are dead or out of range for (var i = self.targetedEnemies.length - 1; i >= 0; i--) { var targetedEnemy = self.targetedEnemies[i]; if (!targetedEnemy.parent || targetedEnemy.health <= 0 || !self.isInRange(targetedEnemy)) { self.targetedEnemies.splice(i, 1); } } } // Flame animation removed for flamethrower towers // Special handling for landmine towers - explode on contact if (self.id === 'landmine' && self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is close enough to trigger explosion (within 50 pixels) and is ground-based if (distance <= 50 && !self.targetEnemy.isFlying) { // Create landmine explosion effect with enhanced animation var landmineExplosion = new EffectIndicator(self.x, self.y, 'splash'); landmineExplosion.children[0].tint = 0xFF4500; // Orange explosion color landmineExplosion.children[0].width = landmineExplosion.children[0].height = CELL_SIZE * 2.5; landmineExplosion.scaleX = 0.3; landmineExplosion.scaleY = 0.3; landmineExplosion.alpha = 1; game.addChild(landmineExplosion); // Multi-stage landmine explosion animation tween(landmineExplosion, { scaleX: 1.8, scaleY: 1.8, alpha: 0.9 }, { duration: 150, easing: tween.elasticOut, onFinish: function onFinish() { tween(landmineExplosion, { scaleX: 2.5, scaleY: 2.5, alpha: 0 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { landmineExplosion.destroy(); } }); } }); // Create enemy explosion effect with enhanced animation var enemyExplosion = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); enemyExplosion.children[0].tint = 0xFF0000; // Red explosion color enemyExplosion.children[0].width = enemyExplosion.children[0].height = CELL_SIZE * 2; enemyExplosion.scaleX = 0.2; enemyExplosion.scaleY = 0.2; enemyExplosion.alpha = 1; game.addChild(enemyExplosion); // Multi-stage enemy explosion animation tween(enemyExplosion, { scaleX: 1.5, scaleY: 1.5, alpha: 0.8 }, { duration: 120, easing: tween.bounceOut, onFinish: function onFinish() { tween(enemyExplosion, { scaleX: 2.2, scaleY: 2.2, alpha: 0 }, { duration: 250, easing: tween.easeOut, onFinish: function onFinish() { enemyExplosion.destroy(); } }); } }); // Create additional shockwave effect for landmine var shockwave = new EffectIndicator(self.x, self.y, 'splash'); shockwave.children[0].tint = 0xFFFFFF; shockwave.children[0].alpha = 0.3; shockwave.children[0].width = shockwave.children[0].height = CELL_SIZE * 0.5; shockwave.alpha = 0.8; game.addChild(shockwave); // Animate shockwave expanding rapidly tween(shockwave, { scaleX: 4, scaleY: 4, alpha: 0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { shockwave.destroy(); } }); // Mark enemy for destruction self.targetEnemy.health = 0; // Mark landmine tower for destruction self.shouldDestroy = true; return; } } if (self.targetEnemy && self.id !== 'landmine') { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); // Special rotation handling for sniper towers if (self.id === 'sniper') { // Use the main tower graphics (baseGraphics) for sniper rotation var sniperGraphics = self.children[0]; // The sniper asset if (sniperGraphics.targetRotation === undefined) { sniperGraphics.targetRotation = angle; sniperGraphics.rotation = angle; } else { if (Math.abs(angle - sniperGraphics.targetRotation) > 0.1) { // Stop any ongoing rotation tweens tween.stop(sniperGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = sniperGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } sniperGraphics.targetRotation = angle; tween(sniperGraphics, { rotation: currentRotation + angleDiff }, { duration: 200, easing: tween.easeOut }); } } } else { gunContainer.rotation = angle; } // Special continuous firing logic for flamethrower if (self.id === 'flamethrower') { // Handle continuous reload timer if (self.continuousReloadTimer > 0) { self.continuousReloadTimer--; } else if (!self.isContinuousFiring && self.targetEnemy) { // Start continuous firing self.isContinuousFiring = true; self.continuousFireTimer = self.continuousFireDuration; } else if (self.isContinuousFiring) { // Continue firing during the continuous period if (self.continuousFireTimer > 0) { self.continuousFireTimer--; // Fire at the fast rate during continuous shooting if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } else { // End continuous firing, start reload self.isContinuousFiring = false; self.continuousReloadTimer = self.continuousReloadDuration; } } } else { // Normal firing logic for other tower types if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } } }; self.down = function (x, y, obj) { var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; // Initialize tower health based on tower type if (self.id === 'flamethrower' || self.id === 'trap' || self.id === 'sniper' || self.id === 'slow' || self.id === 'tank') { // Set health values based on tower type if (self.id === 'slow') { self.towerHealth = 300; // 3x base health for missiles self.maxTowerHealth = 300; } else if (self.id === 'tank') { self.towerHealth = 500; // 5x base health for tank towers - very strong self.maxTowerHealth = 500; } else if (self.id === 'sniper') { self.towerHealth = 150; // Medium health for sniper self.maxTowerHealth = 150; } else if (self.id === 'flamethrower') { self.towerHealth = 120; // Medium health for flamethrower self.maxTowerHealth = 120; } else { self.towerHealth = 100; // Base tower health for trap/shooter self.maxTowerHealth = 100; } } // Method to handle damage from enemy bullets self.takeDamage = function (damage) { // Special handling for sniper towers - track hits instead of health if (self.id === 'sniper') { // Initialize hit counter if not set if (self.hitCount === undefined) { self.hitCount = 0; } self.hitCount++; // Create hit indicator effect var hitEffect = new EffectIndicator(self.x, self.y, 'sniper'); hitEffect.children[0].tint = 0xFF0000; // Red hit indicator hitEffect.children[0].width = hitEffect.children[0].height = CELL_SIZE; game.addChild(hitEffect); // Update health bar if (self.towerHealthBar) { var healthPercentage = Math.max(0, (5 - self.hitCount) / 5); self.towerHealthBar.width = 60 * healthPercentage; } // Destroy sniper after 5 hits if (self.hitCount >= 5) { // Mark sniper for destruction self.shouldDestroy = true; // Create destruction effect var destructionEffect = new EffectIndicator(self.x, self.y, 'splash'); destructionEffect.children[0].tint = 0xFF4500; // Orange destruction color destructionEffect.children[0].width = destructionEffect.children[0].height = CELL_SIZE * 2; game.addChild(destructionEffect); } } else { // Initialize tower health if not set for non-sniper towers if (self.towerHealth === undefined) { // Missiles towers have much higher health to make them stronger if (self.id === 'slow') { self.towerHealth = 300; // 3x base health for missiles self.maxTowerHealth = 300; } else if (self.id === 'tank') { self.towerHealth = 500; // 5x base health for tank towers - very strong self.maxTowerHealth = 500; } else { self.towerHealth = 100; // Base tower health self.maxTowerHealth = 100; } } self.towerHealth -= damage; // Update health bar if (self.towerHealthBar && self.maxTowerHealth > 0) { var healthPercentage = Math.max(0, self.towerHealth / self.maxTowerHealth); self.towerHealthBar.width = 60 * healthPercentage; } if (self.towerHealth <= 0) { self.towerHealth = 0; // Mark tower for destruction self.shouldDestroy = true; // Create destruction effect var destructionEffect = new EffectIndicator(self.x, self.y, 'splash'); destructionEffect.children[0].tint = 0xFF4500; // Orange destruction color destructionEffect.children[0].width = destructionEffect.children[0].height = CELL_SIZE * 2; game.addChild(destructionEffect); } } }; self.fire = function () { // Landmine towers cannot shoot normally if (self.id === 'landmine') { return; } // Machine gun shooting function if (self.id === 'trap') { if (self.targetEnemy) { // Fire multiple bullets in rapid succession for higher ammunition density var bulletsToFire = 3; // Fire 3 bullets per shot for higher density for (var bulletIndex = 0; bulletIndex < bulletsToFire; bulletIndex++) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type for machine gun bullet.type = 'trap'; // Machine gun bullets are smaller and faster bullet.children[0].tint = 0x555555; // Dark gray for machine gun bullets bullet.children[0].width = 25; bullet.children[0].height = 25; // Add slight delay between bullets for visual effect if (bulletIndex > 0) { bullet.x += (Math.random() - 0.5) * 10; // Small random offset bullet.y += (Math.random() - 0.5) * 10; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); } // Machine gun recoil effect tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; var recoilDistance = 6; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 40, easing: tween.cubicOut, onFinish: function onFinish() { tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 60, easing: tween.cubicIn }); } }); } return; } if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } // For rocket launcher (slow) towers, check if we've already fired at this target if (self.id === 'slow') { // Initialize targeted enemies array if not exists if (!self.targetedEnemies) { self.targetedEnemies = []; } // Check if we've already targeted this enemy var alreadyTargeted = false; for (var i = 0; i < self.targetedEnemies.length; i++) { if (self.targetedEnemies[i] === self.targetEnemy) { alreadyTargeted = true; break; } } // If already targeted, don't fire again if (alreadyTargeted) { return; } // Add this enemy to our targeted list self.targetedEnemies.push(self.targetEnemy); } // Always fire if we have a target enemy - removed health vs potential damage check that was blocking boss attacks if (self.targetEnemy) { // Special handling for flamethrower - create continuous flame spray if (self.id === 'flamethrower') { // Create multiple flame particles for continuous spray effect var flamesToCreate = 3; // Create multiple flames for denser spray for (var flameIndex = 0; flameIndex < flamesToCreate; flameIndex++) { var flameParticle = new Container(); var flameGraphics = flameParticle.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); flameGraphics.width = 25 + Math.random() * 15; flameGraphics.height = 35 + Math.random() * 20; flameGraphics.tint = Math.random() > 0.5 ? 0xFF4500 : 0xFF6600; flameGraphics.alpha = 0.7 + Math.random() * 0.3; // Position flame particle at tower with slight spread var spreadAngle = (Math.random() - 0.5) * 0.4; // 20 degree spread var startAngle = gunContainer.rotation + spreadAngle; var startDistance = 30 + Math.random() * 10; flameParticle.x = self.x + Math.cos(startAngle) * startDistance; flameParticle.y = self.y + Math.sin(startAngle) * startDistance; // Calculate target position with spread var targetDistance = self.getRange() * (0.6 + Math.random() * 0.4); var targetX = self.x + Math.cos(startAngle) * targetDistance; var targetY = self.y + Math.sin(startAngle) * targetDistance; // Store damage info for area effect flameParticle.damage = self.damage; flameParticle.sourceTowerLevel = self.level; game.addChild(flameParticle); // Animate flame particle moving toward target area tween(flameParticle, { x: targetX, y: targetY, scaleX: 1.5, scaleY: 1.5 }, { duration: 400 + Math.random() * 200, easing: tween.easeOut, onFinish: function onFinish() { // Apply area damage when flame reaches target area var coneRange = CELL_SIZE * 1.2; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - flameParticle.x; var dy = enemy.y - flameParticle.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= coneRange) { // Apply flame damage enemy.health -= flameParticle.damage * 0.8; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } // Apply burning effect if (!enemy.isImmune) { enemy.burning = true; enemy.burnDamage = flameParticle.damage * 0.3; enemy.burnDuration = 180; } // Create visual flame effect var flameEffect = new EffectIndicator(enemy.x, enemy.y, 'flamethrower'); game.addChild(flameEffect); } } // Fade out flame particle tween(flameParticle, { alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { flameParticle.destroy(); } }); } }); } } else { // Regular bullet creation for other tower types var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; } // Customize bullet appearance based on tower type switch (self.id) { case 'sniper': bullet.children[0].tint = 0xFF5500; bullet.children[0].width = 15; bullet.children[0].height = 15; break; case 'slow': // Replace bullet graphics with missile asset var missileGraphics = bullet.attachAsset('missiles', { anchorX: 0.5, anchorY: 0.5 }); // Remove the original bullet graphics bullet.removeChild(bullet.children[0]); break; case 'poison': bullet.children[0].tint = 0x00FFAA; bullet.children[0].width = 35; bullet.children[0].height = 35; break; case 'tank': bullet.children[0].tint = 0x8B4513; bullet.children[0].width = 40; bullet.children[0].height = 40; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); } // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; self.isPlaced = true; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { // Store original type before placing tower cell.originalType = cell.type; // Allow placement on any cell type - don't force to wall type } } } self.refreshCellsInRange(); }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'trap'; self.hasEnoughGold = true; var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('Box', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; var updateCounter = 0; self.update = function () { // Only check every 10 frames to reduce performance impact updateCounter++; if (updateCounter % 10 === 0) { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range without placing it var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower immediately without placing it on grid tempTower.destroy(); // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; // Color preview box based on placement validity if (self.canPlace && self.hasEnoughGold) { // Green for valid placement (including tank towers) previewGraphics.tint = 0x00FF00; // Animate green pulsing for valid placement tween.stop(previewGraphics, { alpha: true }); tween(previewGraphics, { alpha: 0.7 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { tween(previewGraphics, { alpha: 1.0 }, { duration: 500, easing: tween.easeInOut }); } }); } else { // Red for invalid placement or insufficient gold previewGraphics.tint = 0xFF0000; // Stop any ongoing animations for invalid placement tween.stop(previewGraphics, { alpha: true }); previewGraphics.alpha = 1.0; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; // Prevent placement in upper sixth of screen (y < 455) var screenY = grid.y + self.gridY * CELL_SIZE; if (screenY < 455) { validGridPlacement = false; } else { // For tank weapons, only allow placement in lower half of screen if (self.towerType === 'tank') { var screenCenterY = grid.y + (29 + 6) * CELL_SIZE / 2; // Grid height / 2 if (screenY < screenCenterY) { validGridPlacement = false; } } // Allow placement anywhere else on the grid for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (!cell) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) { continue; } // Only check non-flying enemies, flying enemies can pass over towers if (!enemy.isFlying) { if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) { self.blockedByEnemy = true; break; } } } } } self.canPlace = validGridPlacement && !self.blockedByEnemy; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; self.checkPlacement(); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var towerTypeName = self.tower.id === 'slow' ? 'Missiles' : self.tower.id === 'tank' ? 'Tank' : self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1); var towerTypeText = new Text2(towerTypeName + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { // Restore original cell type or default to ground (type 1) if no original type stored cell.type = cell.originalType !== undefined ? cell.originalType : 1; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0xffad0e; // Add shadow for start text var startTextShadow = new Text2("Start Battle", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Battle", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0xffad0e; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = i + 1 === totalWaves; // Ensure all types appear in early waves if (i === 0) { block.tint = 0x228B22; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 1) { block.tint = 0x2F4F2F; waveType = "Immune"; enemyType = "immune"; enemyCount = 2; } else if (i === 2) { block.tint = 0x006400; waveType = "Flying"; enemyType = "flying"; enemyCount = 3; } else if (i === 3) { block.tint = 0x3B5323; waveType = "Tank"; enemyType = "tank"; enemyCount = 1; } else if (i === 4) { block.tint = 0x228B22; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 8) { // Wave 9 - Immune block.tint = 0x4A6B4A; waveType = "Immune"; enemyType = "immune"; enemyCount = 2; } else if (i === 9) { // Wave 10 - Flying block.tint = 0x006B40; waveType = "Flying"; enemyType = "flying"; enemyCount = 3; } else if (i === 10) { // Wave 11 - Tank (changed from immune) block.tint = 0x556B22; waveType = "Tank"; enemyType = "tank"; enemyCount = 1; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0x006B22; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'fast': block.tint = 0x228B22; waveType = "Boss Fast"; break; case 'immune': block.tint = 0x4A6B4A; waveType = "Boss Immune"; break; case 'flying': block.tint = 0x006B40; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to black block.tint = 0x000000; } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x228B22; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is tank block.tint = 0x4B6000; waveType = "Tank"; enemyType = "tank"; enemyCount = 1; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0x006B56; waveType = "Flying"; enemyType = "flying"; enemyCount = 3; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is fast block.tint = 0x228B22; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else { block.tint = 0x228B22; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave) { // Change the wave type text to indicate boss waveType = "BOSS"; } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2(waveType, { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "fast"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (final wave) if (waveNumber === totalWaves) { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; for (var i = 0; i < totalWaves + 1; i++) { var marker = self.waveMarkers[i]; if (i === 0) { continue; } var block = marker.children[0]; if (i - 1 < currentWave) { block.alpha = .5; } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { waveTimer++; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; if (currentWave != 1) { var waveType = self.getWaveTypeName(currentWave); var enemyCount = self.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } } }; self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xD2B48C }); /**** * Game Code ****/ var gameState = 'menu'; // 'menu', 'playing' var isHidingUpgradeMenu = false; // Global flag for alternating fly direction var lastDirectionWasRight = false; function spawnFly() { lastDirectionWasRight = !lastDirectionWasRight; // You would set fly.vx and fly.vy here based on your fly object // fly.vx = (lastDirectionWasRight ? 1 : -1) * horizontalSpeed; // fly.vy = downSpeed; // For this tower defense game, we can apply this pattern to flying enemies // by modifying their target lanes based on direction console.log("Fly spawning with direction:", lastDirectionWasRight ? "right" : "left"); } // Mobile detection and sniper tower blocking (function blockSniperTowers() { // Safe mobile detection without navigator.userAgent var isMobile = false; try { if (typeof window !== 'undefined' && window.navigator && window.navigator.userAgent) { isMobile = /Mobi|Android/i.test(window.navigator.userAgent); } else { // Fallback: assume mobile if screen dimensions suggest mobile device isMobile = typeof window !== 'undefined' && window.innerWidth && window.innerWidth < 1024; } } catch (e) { // If all else fails, default to false (desktop) isMobile = false; } if (!isMobile) return; // Only activate on mobile // Override tower creation to block sniper towers var originalTowerConstructor = Tower; Tower = Container.expand(function (id) { // Block sniper tower creation on mobile if (id === 'sniper') { console.warn("[BLOCKED] Sniper tower creation prevented on mobile."); // Return a basic trap tower instead return originalTowerConstructor.call(this, 'trap'); } // Use original constructor for other tower types return originalTowerConstructor.call(this, id); }); // Override getTowerCost to return 0 for sniper on mobile (making it unavailable) var originalGetTowerCost = getTowerCost; getTowerCost = function getTowerCost(towerType) { if (towerType === 'sniper') { return 999999; // Make sniper prohibitively expensive on mobile } return originalGetTowerCost(towerType); }; console.log("[INFO] Sniper tower blocking activated for mobile device."); })(); function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; // Ensure no pre-placed towers exist - clear any that might have been created function clearPrePlacedTowers() { // This function is now only used for initial game setup cleanup // It should not aggressively remove player-placed towers during gameplay // Only clear references, don't destroy actual towers during gameplay selectedTower = null; // Only clear grid cell references if grid exists and game is not in playing state if (grid && grid.cells && gameState !== 'playing') { for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var cell = grid.cells[i][j]; if (cell && cell.towersInRange) { cell.towersInRange.length = 0; } } } } } // Call this function to ensure clean slate - but only after towerLayer is created // clearPrePlacedTowers(); // Moved this call to after towerLayer creation var bullets = []; var defenses = []; var selectedTower = null; var gold = 100; var lives = 100; var score = 0; var currentWave = 0; var selectedDifficulty = null; var totalWaves = 12; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 600; // Reduced from 1500 to 600 frames (10 seconds instead of 25) var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; // Create UI container for game elements var gameUIContainer = new Container(); gameUIContainer.addChild(goldText); gameUIContainer.addChild(livesText); gameUIContainer.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; // Initially hide game UI gameUIContainer.visible = false; LK.gui.top.addChild(gameUIContainer); var lastGold = -1; var lastLives = -1; var lastScore = -1; function updateUI() { if (gold !== lastGold) { goldText.setText('Gold: ' + gold); lastGold = gold; } if (lives !== lastLives) { livesText.setText('Lives: ' + lives); lastLives = lives; } if (score !== lastScore) { scoreText.setText('Score: ' + score); lastScore = score; } } function setGold(value) { gold = value; updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Initialize empty towers array towers = []; // Clear any pre-placed towers that might have been loaded with the map clearPrePlacedTowers(); // Fast enemy can shoot - already implemented // This functionality is already working in the Enemy class update method // Fast enemies have shooting range, damage, cooldown, and create red bullets targeting towers var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); // Create menu container var menuContainer = new Container(); game.addChild(menuContainer); // Create game container for all game elements var gameContainer = new Container(); gameContainer.addChild(debugLayer); gameContainer.addChild(towerLayer); gameContainer.addChild(enemyLayer); game.addChild(gameContainer); // Initially hide game elements gameContainer.visible = false; var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY) { var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); // Only set to blocking type if it's currently a path (type 0) // Ground cells (type 1) can be built on without blocking paths if (cell.type === 0) { cell.type = 1; } } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 30; switch (towerType) { case 'landmine': cost = 10; break; case 'sniper': cost = 40; break; case 'flamethrower': cost = 20; break; case 'slow': cost = 50; break; case 'poison': cost = 55; break; case 'tank': cost = 60; break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'trap'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); // Only recalculate pathfinding if needed if (!grid.pathFind()) { grid.renderDebug(); } return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { // Prevent any clicks on game elements when in menu state if (gameState !== 'playing') { return; } var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); // Apply the same offset as in move handler to ensure consistency when starting drag towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); break; } } }; game.move = function (x, y, obj) { if (isDragging) { // Shift the y position upward by 1.5 tiles to show preview above finger towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } }; game.up = function (x, y, obj) { var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; if (towerPreview.canPlace) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; if (isDragging) { var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } } }; var waveIndicator = null; // Initialize as null, will be created when game starts // Function to create wave indicator with current totalWaves function createWaveIndicator() { if (waveIndicator) { gameContainer.removeChild(waveIndicator); } waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2; waveIndicator.y = 2732 - 80; gameContainer.addChild(waveIndicator); } var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); // Add to game instead of gameContainer to ensure it appears on top of all map elements game.addChild(nextWaveButtonContainer); var towerTypes = ['landmine', 'flamethrower', 'trap', 'sniper', 'slow', 'tank']; // Filter out sniper tower on mobile devices var isMobile = false; try { if (typeof window !== 'undefined' && window.navigator && window.navigator.userAgent) { isMobile = /Mobi|Android/i.test(window.navigator.userAgent); } else { // Fallback: assume mobile if screen dimensions suggest mobile device isMobile = typeof window !== 'undefined' && window.innerWidth && window.innerWidth < 1024; } } catch (e) { // If all else fails, default to false (desktop) isMobile = false; } if (isMobile) { towerTypes = towerTypes.filter(function (type) { return type !== 'sniper'; }); console.log("[INFO] Sniper tower removed from available towers on mobile."); } var sourceTowers = []; var towerSpacing = 350; // Increase spacing for larger towers var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; game.addChild(tower); // Add to game instead of towerLayer to prevent it from being a placeable tower sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; // Hide weapon boxes initially for (var i = 0; i < sourceTowers.length; i++) { sourceTowers[i].visible = false; } // Create new difficulty selection menu // Menu background panel var menuBg = new Container(); var menuBgGraphics = menuBg.attachAsset('MW', { anchorX: 0.5, anchorY: 0.5 }); menuBgGraphics.width = 2050; menuBgGraphics.height = 2732; menuBg.x = 2048 / 2 + 1; menuBg.y = 2732 / 2; menuContainer.addChild(menuBg); // Play menu music automatically when menu is loaded LK.playMusic('Menu1'); // Title - using TDT asset var title = new Container(); var titleGraphics = title.attachAsset('TDT', { anchorX: 0.5, anchorY: 0.5 }); title.x = 2048 / 2; title.y = 2732 * 0.1 + 333; menuContainer.addChild(title); // Difficulty buttons data var difficulties = [{ label: 'HARD – 48 WAVES', level: 'HARD', waves: 48 }, { label: 'MEDIUM – 24 WAVES', level: 'MEDIUM', waves: 24 }, { label: 'EASY – 12 WAVES', level: 'EASY', waves: 12 }]; var difficultyButtons = []; var playButton = null; // Create difficulty buttons for (var i = 0; i < difficulties.length; i++) { var diff = difficulties[i]; var btn = new Container(); var btnBg = btn.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); btnBg.width = 1600; btnBg.height = 240; btnBg.tint = 0x004B00; // Darker green btnBg.alpha = 0.8; var btnText = new Text2(diff.label, { size: 120, fill: 0x00FF00, weight: 800 }); btnText.anchor.set(0.5, 0.5); btn.addChild(btnText); btn.x = 2048 / 2; btn.y = 2732 * (0.25 + i * 0.1) + 333 + 333; btn.difficulty = diff; menuContainer.addChild(btn); difficultyButtons.push(btn); // Add click handler to difficulty button btn.down = function () { selectedDifficulty = this.difficulty.level; totalWaves = this.difficulty.waves; // Hide instruction text once a difficulty is selected if (difficultyInstructionText && difficultyInstructionText.visible) { difficultyInstructionText.visible = false; } // Update all button appearances for (var j = 0; j < difficultyButtons.length; j++) { var button = difficultyButtons[j]; var bg = button.children[0]; if (button.difficulty.level === selectedDifficulty) { bg.tint = 0x006600; // Darker green for selected bg.alpha = 1.0; } else { bg.tint = 0x004B00; // Darker green for unselected bg.alpha = 0.8; } } // Show play button if (playButton) { playButton.visible = true; } }; } // Difficulty instruction text var difficultyInstructionText = new Text2('Please choose a difficulty level!', { size: 80, fill: 0xFFFFFF, weight: 800 }); difficultyInstructionText.anchor.set(0.5, 1.0); difficultyInstructionText.x = 2048 / 2; difficultyInstructionText.y = 2732 - 50; menuContainer.addChild(difficultyInstructionText); // PLAY button - initially hidden playButton = new Container(); var playButtonBg = playButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); playButtonBg.width = 800; playButtonBg.height = 240; playButtonBg.tint = 0x00AA00; var playButtonText = new Text2('PLAY', { size: 160, fill: 0x00FF00, weight: 800 }); playButtonText.anchor.set(0.5, 0.5); playButton.addChild(playButtonText); playButton.x = 2048 / 2; playButton.y = (2732 * 0.45 + 666 + 2732) / 2 + 69; playButton.visible = false; menuContainer.addChild(playButton); playButton.down = function () { if (selectedDifficulty) { // Stop menu music when starting game LK.stopMusic(); // Generate waves based on selected difficulty generateWaves(totalWaves); // Create wave indicator with selected difficulty createWaveIndicator(); // Transition from menu to game gameState = 'playing'; menuContainer.visible = false; gameContainer.visible = true; gameUIContainer.visible = true; // Show weapon boxes when game starts for (var i = 0; i < sourceTowers.length; i++) { sourceTowers[i].visible = true; } } }; // Wave generation function function generateWaves(count) { // The existing wave system will handle wave generation // We just need to set totalWaves which is already done above // The WaveIndicator class will use the totalWaves value console.log('Generated', count, 'waves for difficulty:', selectedDifficulty); } // Function to show menu function showMenu() { gameState = 'menu'; menuContainer.visible = true; gameContainer.visible = false; gameUIContainer.visible = false; } // Function to show game function showGame() { gameState = 'playing'; menuContainer.visible = false; gameContainer.visible = true; gameUIContainer.visible = true; } function onGameStart(callback) { // Store the callback to be executed when game starts if (waveIndicator && waveIndicator.gameStarted) { callback(); } else { // Wait for game to start, then execute callback var checkGameStart = LK.setInterval(function () { if (waveIndicator && waveIndicator.gameStarted) { LK.clearInterval(checkGameStart); callback(); } }, 100); } } // Tank spawning is now handled by the main wave system in game.update // No manual spawning needed here game.update = function () { // Only run game logic when in playing state if (gameState !== 'playing') { return; } if (waveInProgress && waveIndicator) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave === totalWaves; if (isBossWave) { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy = new Enemy(waveType); // Initialize flying enemy direction using global alternating flag if (enemy.isFlying) { // Call the spawnFly function to update the global direction flag spawnFly(); // Set the flying enemy's initial direction based on the global flag enemy.flyingDirection = lastDirectionWasRight ? 1 : -1; // 1 for right, -1 for left } // Add enemy to the appropriate layer based on type for proper display order if (enemy.isFlying) { // Add flying enemy to the top layer to ensure they appear above all other enemies enemyLayerTop.addChild(enemy); // If it's a flying enemy, add its shadow to the middle layer if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else if (enemy.type === 'fast') { // Add fast enemies to the bottom layer to ensure flying enemies appear above them enemyLayerBottom.addChild(enemy); } else { // Add other ground enemies (immune, tank, etc.) to the middle layer enemyLayerMiddle.addChild(enemy); } // Scale difficulty with wave number but don't apply to boss // as bosses already have their health multiplier // Use exponential scaling for health var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // Assign specific lanes for different enemy types to prevent overlapping var gridWidth = 24; var midPoint = Math.floor(gridWidth / 2); // 12 var spawnX; // Define strict non-overlapping lanes with buffer zones for different enemy types if (waveType === 'fast') { // Fast enemies spawn in horizontal lines var enemiesPerLine = 5; // Number of enemies per horizontal line var lineNumber = Math.floor(i / enemiesPerLine); // Which horizontal line this enemy belongs to var positionInLine = i % enemiesPerLine; // Position within the line (0-4) var lineColumns = [6, 9, 12, 15, 18]; // Available columns for horizontal lines - moved left spawnX = lineColumns[positionInLine]; // Position enemy in the line } else if (waveType === 'immune') { // Immune enemies use center-left lanes (columns 11-12) with buffer from fast and tank var immuneColumns = [11, 12]; var availableImmuneColumns = []; for (var col = 0; col < immuneColumns.length; col++) { var columnOccupied = false; for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === immuneColumns[col] && enemies[e].currentCellY < 8) { columnOccupied = true; break; } } if (!columnOccupied) { availableImmuneColumns.push(immuneColumns[col]); } } // If no columns available, queue the enemy by using the first column spawnX = availableImmuneColumns.length > 0 ? availableImmuneColumns[Math.floor(Math.random() * availableImmuneColumns.length)] : immuneColumns[0]; } else if (waveType === 'tank') { // Tank enemies spawn positioning based on wave if (currentWave === 4) { // Wave 4: Left side var tankColumns = [3]; // Left side lane for wave 4 tank enemies } else if (currentWave === 8) { // Wave 8: Right side var tankColumns = [21]; // Right side lane for wave 8 tank enemies } else if (currentWave === 11) { // Wave 11: Center var tankColumns = [12]; // Center lane for wave 11 tank enemies } else { // Default for other waves: right side of road var tankColumns = [21, 22]; // Right edge lanes for tank enemies } spawnX = tankColumns[i % tankColumns.length]; // Cycle through assigned lanes } else { // Flying and other enemy types use middle lanes (columns 12-14) avoiding other types var flyingColumns = [12, 13, 14]; var availableColumns = []; for (var col = 0; col < flyingColumns.length; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === flyingColumns[col] && enemies[e].currentCellY < 8) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(flyingColumns[col]); } } // If no columns available, use the center column spawnX = availableColumns.length > 0 ? availableColumns[Math.floor(Math.random() * availableColumns.length)] : flyingColumns[1]; } // Calculate spawn position with much larger spacing to ensure no visual overlap // Convert units to cell units (pixels / 76 pixels per cell) var enemySpacing; if (waveType === 'fast') { // Calculate spacing between horizontal lines (not individual enemies) var lineSpacing = 300 / CELL_SIZE; // Reduced spacing between horizontal lines (~3.9 cells) var lineNumber = Math.floor(i / 5); // Which horizontal line this enemy belongs to enemySpacing = lineSpacing; // Position fast enemies in their assigned column without horizontal offset enemy.cellX = isBossWave ? 12 : spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = isBossWave ? 12 : spawnX; enemy.currentCellY = -1 - lineNumber * enemySpacing; // Each line spawns with line spacing, not individual spacing } else { if (waveType === 'tank') { enemySpacing = 800 / CELL_SIZE; // Reduced spacing for tank enemies with better lane distribution (~10.5 cells) } else if (waveType === 'immune') { enemySpacing = 400 / CELL_SIZE; // Reduced spacing for immune enemies (~5.3 cells) } else { enemySpacing = 900 / CELL_SIZE; // Large spacing for other enemies (~11.8 cells) } var spawnY = -1 - i * enemySpacing; // Each enemy spawns based on enemy type spacing enemy.cellX = isBossWave ? 12 : spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = isBossWave ? 12 : spawnX; enemy.currentCellY = spawnY; } enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Remove all flame particles and smoke effects from the map when enemy is destroyed for (var flameIndex = game.children.length - 1; flameIndex >= 0; flameIndex--) { var child = game.children[flameIndex]; // Remove EffectIndicator flame effects if (child instanceof EffectIndicator) { var dx = child.x - enemy.x; var dy = child.y - enemy.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= CELL_SIZE * 3) { child.destroy(); } } // Remove flame particles (Container objects with flame graphics) else if (child.children && child.children.length > 0 && child.children[0].tint !== undefined) { var flameGraphics = child.children[0]; // Check for flame particles by tint colors and properties if ((flameGraphics.tint === 0xFF4500 || flameGraphics.tint === 0xFF6600) && flameGraphics.width && flameGraphics.height) { // Stop any ongoing tweens and destroy all flame particles tween.stop(child); child.destroy(); } } // Remove flame particles that have damage property (flamethrower particles) else if (child.damage !== undefined && child.sourceTowerLevel !== undefined) { // Stop any ongoing tweens and destroy flamethrower particles tween.stop(child); child.destroy(); } } // Clean up continuous smoke effect if it exists if (enemy.continuousSmokeEffect && enemy.continuousSmokeEffect.parent) { // Stop any ongoing smoke particle tweens for (var smokeChildIndex = 0; smokeChildIndex < enemy.continuousSmokeEffect.children.length; smokeChildIndex++) { var smokeChild = enemy.continuousSmokeEffect.children[smokeChildIndex]; tween.stop(smokeChild); } enemy.continuousSmokeEffect.destroy(); enemy.continuousSmokeEffect = null; } // Create MASSIVE spectacular death explosion for all enemies except fast enemies (but always for bosses) if (enemy.type !== 'fast' || enemy.isBoss) { // Stage 1: Initial massive impact flash var deathFlash = new EffectIndicator(enemy.x, enemy.y, 'splash'); deathFlash.children[0].tint = 0xFFFFFF; // Bright white impact flash // Scale flash size based on boss status - use Boss asset size for scaling var flashScale = enemy.isBoss ? 4.8 : enemy.type === 'tank' ? 2.4 : 2.0; // Boss uses 4.8x (scaled to boss asset size) deathFlash.children[0].width = deathFlash.children[0].height = CELL_SIZE * flashScale; deathFlash.alpha = 1; deathFlash.scaleX = 0.1; deathFlash.scaleY = 0.1; game.addChild(deathFlash); // Ultra-fast bright flash expansion - scale based on enemy type var flashExpansionScale = enemy.isBoss ? 5.76 : enemy.type === 'tank' ? 2.88 : 2.4; // Boss uses 5.76x (scaled to boss asset size) tween(deathFlash, { scaleX: flashExpansionScale, scaleY: flashExpansionScale, alpha: 0 }, { duration: 80, // Faster than weapon explosions for more impact easing: tween.easeOut }); // Stage 2: ENORMOUS main death explosion fireball - bigger than any weapon var mainDeathExplosion = new EffectIndicator(enemy.x, enemy.y, 'splash'); // Color based on enemy type if (enemy.isBoss) { mainDeathExplosion.children[0].tint = 0xFF0000; // Bright red for boss death } else if (enemy.type === 'tank') { mainDeathExplosion.children[0].tint = 0x8B4513; // Brown for tank enemies } else if (enemy.type === 'immune') { mainDeathExplosion.children[0].tint = 0xAA0000; // Dark red for immune enemies } else if (enemy.isFlying) { mainDeathExplosion.children[0].tint = 0xFFFF00; // Yellow for flying enemies } else { mainDeathExplosion.children[0].tint = 0xFF4500; // Orange for other enemies } mainDeathExplosion.children[0].width = mainDeathExplosion.children[0].height = CELL_SIZE * (enemy.isBoss ? 3.75 : enemy.type === 'tank' ? 3.0 : 2.5); // ENORMOUS - bigger than tank weapons mainDeathExplosion.alpha = 1; mainDeathExplosion.scaleX = 0.2; mainDeathExplosion.scaleY = 0.2; game.addChild(mainDeathExplosion); // Dramatic ENORMOUS fireball expansion with intense bounce effect tween(mainDeathExplosion, { scaleX: enemy.isBoss ? 3.0 : enemy.type === 'tank' ? 2.52 : 2.1, // Much bigger than tank weapon explosions scaleY: enemy.isBoss ? 3.0 : enemy.type === 'tank' ? 2.52 : 2.1, alpha: 0.95 }, { duration: enemy.isBoss ? 400 : 300, // Longer for bosses easing: tween.bounceOut, onFinish: function onFinish() { tween(mainDeathExplosion, { scaleX: enemy.isBoss ? 4.5 : enemy.type === 'tank' ? 4.32 : 3.6, // ENORMOUS final scale - biggest explosions in game scaleY: enemy.isBoss ? 4.5 : enemy.type === 'tank' ? 4.32 : 3.6, alpha: 0 }, { duration: enemy.isBoss ? 1000 : 800, // Very long for dramatic effect easing: tween.easeIn, onFinish: function onFinish() { mainDeathExplosion.destroy(); } }); } }); // Stage 3: Create ENORMOUS debris particle field - much more particles than weapons var particleCount = enemy.isBoss ? 48 : 32; // More particles than tank weapons for (var particleIndex = 0; particleIndex < particleCount; particleIndex++) { var deathParticle = new EffectIndicator(enemy.x, enemy.y, 'splash'); // Varied particle colors based on enemy type var particleColors; if (enemy.isBoss) { particleColors = [0xFF0000, 0xFF3300, 0xFF6600, 0xFF9900, 0xFFCC00, 0xFFFF00, 0xFF0033, 0xFF0066]; } else if (enemy.type === 'tank') { particleColors = [0x8B4513, 0xA0522D, 0xD2691E, 0xFF8C00, 0xCD853F, 0xDEB887, 0x654321, 0x8B7355]; } else if (enemy.type === 'immune') { particleColors = [0xAA0000, 0xBB0000, 0xCC0000, 0xDD0000, 0xEE0000, 0xFF0000, 0x990000, 0x880000]; } else if (enemy.isFlying) { particleColors = [0xFFFF00, 0xFFFF33, 0xFFFF66, 0xFFFF99, 0xFFFFCC, 0xFFFFFF, 0xFFCC00, 0xFF9900]; } else { particleColors = [0xFF4500, 0xFF6600, 0xFF8800, 0xFFAA00, 0xFF2200, 0xFFCC00, 0xFF3300, 0xFFDD00]; } deathParticle.children[0].tint = particleColors[particleIndex % particleColors.length]; deathParticle.children[0].width = deathParticle.children[0].height = CELL_SIZE * (enemy.type === 'tank' ? 0.75 + Math.random() * 0.6 : 0.625 + Math.random() * 0.5); // ENORMOUS particles - bigger than tank weapons deathParticle.alpha = 1.0; deathParticle.scaleX = 0.8 + Math.random() * 0.6; deathParticle.scaleY = 0.8 + Math.random() * 0.6; // ULTRA-MASSIVE particle scatter distance - bigger than tank weapons var particleAngle = particleIndex / particleCount * Math.PI * 2 + (Math.random() - 0.5) * 1.0; var particleDistance = CELL_SIZE * (1.5 + Math.random() * 1.0); // ULTRA-far scatter - bigger than tank weapons if (enemy.isBoss) { particleDistance *= 1.5; // Even further for bosses } var targetX = enemy.x + Math.cos(particleAngle) * particleDistance; var targetY = enemy.y + Math.sin(particleAngle) * particleDistance; game.addChild(deathParticle); // Animate particles with ENORMOUS final scale tween(deathParticle, { x: targetX, y: targetY, scaleX: enemy.type === 'tank' ? 1.8 + Math.random() * 0.72 : 1.5 + Math.random() * 0.6, // ENORMOUS final scale - bigger than tank weapons scaleY: enemy.type === 'tank' ? 1.8 + Math.random() * 0.72 : 1.5 + Math.random() * 0.6, alpha: 0.8 }, { duration: 500 + Math.random() * 400, // Longer duration easing: tween.easeOut, onFinish: function onFinish() { tween(deathParticle, { scaleX: 0.2, scaleY: 0.2, alpha: 0 }, { duration: 600 + Math.random() * 300, // Long fade easing: tween.easeIn, onFinish: function onFinish() { deathParticle.destroy(); } }); } }); } // Stage 4: ENORMOUS multiple shockwaves - bigger than tank weapon shockwaves var primaryDeathShockwave = new EffectIndicator(enemy.x, enemy.y, 'splash'); primaryDeathShockwave.children[0].tint = enemy.isBoss ? 0xFF6666 : 0xFFAAAA; // Light red/pink shockwave primaryDeathShockwave.children[0].alpha = 0.8; primaryDeathShockwave.children[0].width = primaryDeathShockwave.children[0].height = CELL_SIZE * 2.5; primaryDeathShockwave.alpha = 1.0; game.addChild(primaryDeathShockwave); // ENORMOUS expanding death shockwave - bigger than tank weapons tween(primaryDeathShockwave, { scaleX: enemy.isBoss ? 6.0 : enemy.type === 'tank' ? 5.76 : 4.8, // ENORMOUS scale - bigger than tank weapon shockwaves scaleY: enemy.isBoss ? 6.0 : enemy.type === 'tank' ? 5.76 : 4.8, alpha: 0 }, { duration: enemy.isBoss ? 1200 : 1000, // Very long expansion easing: tween.easeOut, onFinish: function onFinish() { primaryDeathShockwave.destroy(); } }); // Multiple additional death shockwaves for ULTRA-MASSIVE effect var shockwaveCount = enemy.isBoss ? 6 : 4; // More shockwaves for bosses for (var shockIndex = 1; shockIndex <= shockwaveCount; shockIndex++) { var additionalDeathShockwave = new EffectIndicator(enemy.x, enemy.y, 'splash'); additionalDeathShockwave.children[0].tint = enemy.isBoss ? 0xFF4444 : 0xFF8888; // Darker red shockwave additionalDeathShockwave.children[0].alpha = 0.6 - shockIndex * 0.08; additionalDeathShockwave.children[0].width = additionalDeathShockwave.children[0].height = CELL_SIZE * (2.5 - shockIndex * 0.2); additionalDeathShockwave.alpha = 0.8 - shockIndex * 0.1; game.addChild(additionalDeathShockwave); // Delayed ENORMOUS death shockwaves tween(additionalDeathShockwave, { scaleX: (enemy.isBoss ? 6.6 : enemy.type === 'tank' ? 6.48 : 5.4) + shockIndex * (enemy.type === 'tank' ? 0.54 : 0.45), // ENORMOUS expanding scale scaleY: (enemy.isBoss ? 6.6 : enemy.type === 'tank' ? 6.48 : 5.4) + shockIndex * (enemy.type === 'tank' ? 0.54 : 0.45), alpha: 0 }, { duration: (enemy.isBoss ? 1300 : 1100) + shockIndex * 150, delay: shockIndex * (enemy.isBoss ? 300 : 250), easing: tween.easeOut, onFinish: function onFinish() { additionalDeathShockwave.destroy(); } }); } // Stage 5: ENORMOUS lingering death smoke cloud - bigger and longer than tank weapons var deathSmokeCloud = new EffectIndicator(enemy.x, enemy.y, 'splash'); deathSmokeCloud.children[0].tint = 0x444444; // Dark gray death smoke deathSmokeCloud.children[0].alpha = 0.8; deathSmokeCloud.children[0].width = deathSmokeCloud.children[0].height = CELL_SIZE * (enemy.isBoss ? 3.0 : enemy.type === 'tank' ? 3.0 : 2.5); // ENORMOUS smoke cloud deathSmokeCloud.alpha = 0; deathSmokeCloud.scaleX = 1.5; deathSmokeCloud.scaleY = 1.5; game.addChild(deathSmokeCloud); // Delayed ENORMOUS death smoke appearance tween(deathSmokeCloud, { alpha: 1.0, scaleX: enemy.isBoss ? 3.0 : enemy.type === 'tank' ? 2.88 : 2.4, // Bigger initial expansion than tank weapons scaleY: enemy.isBoss ? 3.0 : enemy.type === 'tank' ? 2.88 : 2.4 }, { duration: 600, delay: 600, easing: tween.easeOut, onFinish: function onFinish() { // Death smoke slowly dissipates over ULTRA-LONG time tween(deathSmokeCloud, { alpha: 0, scaleX: enemy.isBoss ? 5.4 : enemy.type === 'tank' ? 5.76 : 4.8, // ENORMOUS final scale - bigger than tank weapons scaleY: enemy.isBoss ? 5.4 : enemy.type === 'tank' ? 5.76 : 4.8 }, { duration: enemy.isBoss ? 3000 : 2500, // ULTRA-LONG dissipation - longer than tank weapons easing: tween.easeIn, onFinish: function onFinish() { deathSmokeCloud.destroy(); } }); } }); } // Special boss explosion effect and sound if (enemy.isBoss) { // Create massive boss explosion effect var bossExplosion = new EffectIndicator(enemy.x, enemy.y, 'splash'); bossExplosion.children[0].tint = 0xFFA500; // Orange explosion color bossExplosion.children[0].width = bossExplosion.children[0].height = 150; // Large explosion radius bossExplosion.alpha = 1.0; bossExplosion.scaleX = 0.5; bossExplosion.scaleY = 0.5; game.addChild(bossExplosion); // Multi-stage boss explosion animation tween(bossExplosion, { scaleX: 3.0, scaleY: 3.0, alpha: 0.8 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(bossExplosion, { scaleX: 5.0, scaleY: 5.0, alpha: 0 }, { duration: 700, easing: tween.easeIn, onFinish: function onFinish() { bossExplosion.destroy(); } }); } }); // Play boss explosion sound effect try { var explosionSound = new Audio(); explosionSound.src = "https://cdn.pixabay.com/download/audio/2022/03/02/audio_f27e2020ce.mp3?filename=explosion-6075.mp3"; explosionSound.volume = 1.0; explosionSound.play()["catch"](function (error) { console.log("Could not play boss explosion sound:", error); }); } catch (error) { console.log("Boss explosion sound not available:", error); } } // Boss enemies give more gold and score - significantly increased rewards var goldEarned = enemy.isBoss ? Math.floor(200 + (enemy.waveNumber - 1) * 25) : Math.floor(15 + (enemy.waveNumber - 1) * 3); var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else if (enemy.type === 'fast') { enemyLayerBottom.removeChild(enemy); } else { enemyLayerMiddle.removeChild(enemy); } enemies.splice(a, 1); continue; } // Only update enemy position every few frames to reduce performance impact if (LK.ticks % 2 === 0) { if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else if (enemy.type === 'fast') { enemyLayerBottom.removeChild(enemy); } else { enemyLayerMiddle.removeChild(enemy); } enemies.splice(a, 1); // BOSS Enemy reached the goal - reduce lives by 100 (100% of 100 total lives) if (enemy.isBoss) { lives = Math.max(0, lives - 100); } else { // Each individual enemy reduces lives by 10 (10% of 100 total lives) lives = Math.max(0, lives - 10); } updateUI(); if (lives <= 0) { LK.showGameOver(); } } } } // Only clean bullets every 5 frames to reduce processing if (LK.ticks % 5 === 0) { for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; if (!bullet.parent) { if (bullet.targetEnemy) { var targetEnemy = bullet.targetEnemy; if (targetEnemy && targetEnemy.bulletsTargetingThis) { var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullet); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } } // Clear bullet references bullet.targetEnemy = null; bullets.splice(i, 1); } } } // Check for landmine towers that need to be destroyed after explosion for (var i = towers.length - 1; i >= 0; i--) { var tower = towers[i]; if (tower.shouldDestroy) { // Remove from grid cells var gridX = tower.gridX; var gridY = tower.gridY; for (var j = 0; j < 2; j++) { for (var k = 0; k < 2; k++) { var cell = grid.getCell(gridX + j, gridY + k); if (cell) { // Restore original cell type or default to ground (type 1) if no original type stored cell.type = cell.originalType !== undefined ? cell.originalType : 1; var towerIndex = cell.towersInRange.indexOf(tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } // Remove from towers array and tower layer towers.splice(i, 1); towerLayer.removeChild(tower); // Clean up any upgrade menus or range indicators if (selectedTower === tower) { selectedTower = null; } // Remove any upgrade menus for this tower var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu && child.tower === tower; }); for (var menuIndex = 0; menuIndex < upgradeMenus.length; menuIndex++) { upgradeMenus[menuIndex].destroy(); } // Remove any range indicators for this tower for (var rangeIndex = game.children.length - 1; rangeIndex >= 0; rangeIndex--) { if (game.children[rangeIndex].isTowerRange && game.children[rangeIndex].tower === tower) { game.removeChild(game.children[rangeIndex]); } } // Recalculate pathfinding grid.pathFind(); grid.renderDebug(); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Handle enemy bullets targeting towers
if (self.isEnemyBullet) {
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Damage the tower instead of an enemy
var tower = self.targetEnemy;
if (tower.takeDamage) {
tower.takeDamage(self.damage);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
return;
}
// Original enemy targeting logic
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Special handling for missiles hitting immune, flying, or tank enemies - destroy instantly
if (self.type === 'slow' && (self.targetEnemy.isImmune || self.targetEnemy.isFlying || self.targetEnemy.type === 'tank')) {
// Instantly destroy immune, flying, or tank enemy regardless of health
self.targetEnemy.health = 0;
} else if (self.type === 'tank' && (self.targetEnemy.isImmune || self.targetEnemy.isFlying || self.targetEnemy.type === 'tank' || self.targetEnemy.isBoss)) {
// Tank weapons instantly destroy immune, flying, tank, and boss enemies
self.targetEnemy.health = 0;
} else {
// Apply damage to target enemy normally
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
}
// Create explosion effect for missile hits
if (self.type === 'slow') {
// Store explosion position for area damage calculation
var explosionX = self.targetEnemy.x;
var explosionY = self.targetEnemy.y;
// Increased explosion radius to destroy nearby enemies (especially fast enemies)
var explosionRadius = CELL_SIZE * 2.5; // Increased from smaller radius to 2.5 cells
// Apply area damage to all enemies within explosion radius
for (var i = 0; i < enemies.length; i++) {
var nearbyEnemy = enemies[i];
if (nearbyEnemy !== self.targetEnemy) {
var dx = nearbyEnemy.x - explosionX;
var dy = nearbyEnemy.y - explosionY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= explosionRadius) {
// Instantly destroy fast enemies within explosion radius
if (nearbyEnemy.type === 'fast') {
nearbyEnemy.health = 0;
} else {
// Apply significant damage to other enemy types
nearbyEnemy.health -= self.damage * 1.5; // 1.5x damage for area effect
if (nearbyEnemy.health <= 0) {
nearbyEnemy.health = 0;
} else {
nearbyEnemy.healthBar.width = nearbyEnemy.health / nearbyEnemy.maxHealth * 70;
}
}
// Create individual explosion effect for each affected enemy
var nearbyExplosion = new EffectIndicator(nearbyEnemy.x, nearbyEnemy.y, 'splash');
nearbyExplosion.children[0].tint = 0xFF6600; // Orange-red for area damage
nearbyExplosion.children[0].width = nearbyExplosion.children[0].height = CELL_SIZE * 1.5;
nearbyExplosion.alpha = 0.8;
nearbyExplosion.scaleX = 0.2;
nearbyExplosion.scaleY = 0.2;
game.addChild(nearbyExplosion);
// Animate nearby explosion
tween(nearbyExplosion, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.6
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(nearbyExplosion, {
scaleX: 1.8,
scaleY: 1.8,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
nearbyExplosion.destroy();
}
});
}
});
}
}
}
// Create MASSIVE tank weapon explosion with devastating area damage
if (self.type === 'tank') {
// Store explosion position for area damage calculation
var explosionX = self.targetEnemy.x;
var explosionY = self.targetEnemy.y;
// MASSIVE explosion radius - much larger than missiles for devastating effect
var explosionRadius = CELL_SIZE * 4.0; // 4 cell radius for tank weapons (60% larger than missiles)
// Apply devastating area damage to all enemies within explosion radius
for (var i = 0; i < enemies.length; i++) {
var nearbyEnemy = enemies[i];
if (nearbyEnemy !== self.targetEnemy) {
var dx = nearbyEnemy.x - explosionX;
var dy = nearbyEnemy.y - explosionY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= explosionRadius) {
// Tank weapons cause MASSIVE damage - instantly destroy most enemies
if (nearbyEnemy.type === 'fast' || nearbyEnemy.type === 'flying' || nearbyEnemy.isFlying) {
// Instantly destroy fast and flying enemies
nearbyEnemy.health = 0;
} else if (nearbyEnemy.type === 'immune' || nearbyEnemy.isImmune) {
// Tank weapons instantly destroy immune enemies
nearbyEnemy.health = 0;
} else if (nearbyEnemy.type === 'tank') {
// Tank weapons instantly destroy tank enemies
nearbyEnemy.health = 0;
} else if (nearbyEnemy.isBoss) {
// Tank weapons instantly destroy boss enemies
nearbyEnemy.health = 0;
} else {
// Massive damage to other enemy types
nearbyEnemy.health -= self.damage * 3.0; // 3x damage for devastating effect
if (nearbyEnemy.health <= 0) {
nearbyEnemy.health = 0;
} else {
nearbyEnemy.healthBar.width = nearbyEnemy.health / nearbyEnemy.maxHealth * 70;
}
}
// Create individual massive explosion effect for each affected enemy
var nearbyExplosion = new EffectIndicator(nearbyEnemy.x, nearbyEnemy.y, 'splash');
nearbyExplosion.children[0].tint = 0x8B4513; // Dark brown for tank explosion
nearbyExplosion.children[0].width = nearbyExplosion.children[0].height = CELL_SIZE * 2.0; // Larger than missile explosions
nearbyExplosion.alpha = 1.0;
nearbyExplosion.scaleX = 0.3;
nearbyExplosion.scaleY = 0.3;
game.addChild(nearbyExplosion);
// Animate massive nearby explosion
tween(nearbyExplosion, {
scaleX: 1.8,
scaleY: 1.8,
alpha: 0.8
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(nearbyExplosion, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
nearbyExplosion.destroy();
}
});
}
});
}
}
}
// Create ULTRA-MASSIVE tank weapon explosion with enhanced visual effects
// Stage 1: Initial massive impact flash - GIGANTIC white flash
var impactFlash = new EffectIndicator(explosionX, explosionY, 'splash');
impactFlash.children[0].tint = 0xFFFFFF; // Bright white impact flash
impactFlash.children[0].width = impactFlash.children[0].height = CELL_SIZE * 6.0; // ENORMOUS initial flash
impactFlash.alpha = 1;
impactFlash.scaleX = 0.1;
impactFlash.scaleY = 0.1;
game.addChild(impactFlash);
// Ultra-fast bright flash expansion
tween(impactFlash, {
scaleX: 8.0,
// Much bigger than missile explosions
scaleY: 8.0,
alpha: 0
}, {
duration: 100,
easing: tween.easeOut
});
// Stage 2: MASSIVE main tank explosion fireball - dark brown/orange core
var mainExplosion = new EffectIndicator(explosionX, explosionY, 'splash');
mainExplosion.children[0].tint = 0x8B4513; // Dark brown for tank explosion
mainExplosion.children[0].width = mainExplosion.children[0].height = CELL_SIZE * 10.0; // ENORMOUS main explosion
mainExplosion.alpha = 1;
mainExplosion.scaleX = 0.2;
mainExplosion.scaleY = 0.2;
game.addChild(mainExplosion);
// Dramatic MASSIVE fireball expansion
tween(mainExplosion, {
scaleX: 6.5,
// Bigger than missile explosions
scaleY: 6.5,
alpha: 0.95
}, {
duration: 300,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(mainExplosion, {
scaleX: 10.0,
// ENORMOUS final scale
scaleY: 10.0,
alpha: 0
}, {
duration: 800,
easing: tween.easeIn,
onFinish: function onFinish() {
mainExplosion.destroy();
}
});
}
});
// Stage 3: Create QUADRUPLE the explosion particles for MASSIVE debris effect
for (var particleIndex = 0; particleIndex < 32; particleIndex++) {
// More particles than missiles
var explosionParticle = new EffectIndicator(explosionX, explosionY, 'splash');
// Tank explosion colors - browns and oranges
var particleColors = [0x8B4513, 0xA0522D, 0xD2691E, 0xFF8C00, 0xCD853F, 0xDEB887, 0x654321, 0x8B7355];
explosionParticle.children[0].tint = particleColors[particleIndex % particleColors.length];
explosionParticle.children[0].width = explosionParticle.children[0].height = CELL_SIZE * (2.2 + Math.random() * 1.8); // Bigger particles
explosionParticle.alpha = 0.95;
explosionParticle.scaleX = 0.6 + Math.random() * 0.5;
explosionParticle.scaleY = 0.6 + Math.random() * 0.5;
// Random direction for particle scatter with ULTRA-LONG distance
var particleAngle = particleIndex / 32 * Math.PI * 2 + (Math.random() - 0.5) * 0.8;
var particleDistance = CELL_SIZE * (4.5 + Math.random() * 3.5); // Ultra-far particle scatter
var targetX = explosionX + Math.cos(particleAngle) * particleDistance;
var targetY = explosionY + Math.sin(particleAngle) * particleDistance;
game.addChild(explosionParticle);
// Animate particles flying outward
tween(explosionParticle, {
x: targetX,
y: targetY,
scaleX: 4.0 + Math.random() * 1.5,
// Much bigger final scale
scaleY: 4.0 + Math.random() * 1.5,
alpha: 0.8
}, {
duration: 400 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(explosionParticle, {
scaleX: 0.3,
scaleY: 0.3,
alpha: 0
}, {
duration: 500 + Math.random() * 250,
easing: tween.easeIn,
onFinish: function onFinish() {
explosionParticle.destroy();
}
});
}
});
}
// Stage 4: QUINTUPLE shockwave rings for tank weapons
var primaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
primaryShockwave.children[0].tint = 0xDEB887; // Light brown shockwave
primaryShockwave.children[0].alpha = 0.8;
primaryShockwave.children[0].width = primaryShockwave.children[0].height = CELL_SIZE * 2.0;
primaryShockwave.alpha = 1.0;
game.addChild(primaryShockwave);
// MASSIVE expanding primary shockwave
tween(primaryShockwave, {
scaleX: 15.0,
// Bigger than missile shockwaves
scaleY: 15.0,
alpha: 0
}, {
duration: 900,
easing: tween.easeOut,
onFinish: function onFinish() {
primaryShockwave.destroy();
}
});
// Multiple additional shockwaves for devastating effect
for (var shockIndex = 1; shockIndex <= 4; shockIndex++) {
var additionalShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
additionalShockwave.children[0].tint = 0x8B7355; // Darker brown shockwave
additionalShockwave.children[0].alpha = 0.6 - shockIndex * 0.1;
additionalShockwave.children[0].width = additionalShockwave.children[0].height = CELL_SIZE * (2.0 - shockIndex * 0.2);
additionalShockwave.alpha = 0.8 - shockIndex * 0.1;
game.addChild(additionalShockwave);
// Delayed massive shockwaves
tween(additionalShockwave, {
scaleX: 16.0 + shockIndex * 1.0,
scaleY: 16.0 + shockIndex * 1.0,
alpha: 0
}, {
duration: 1000 + shockIndex * 100,
delay: shockIndex * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
additionalShockwave.destroy();
}
});
}
// Stage 5: MASSIVE lingering smoke cloud
var smokeCloud = new EffectIndicator(explosionX, explosionY, 'splash');
smokeCloud.children[0].tint = 0x696969; // Dark gray smoke
smokeCloud.children[0].alpha = 0.7;
smokeCloud.children[0].width = smokeCloud.children[0].height = CELL_SIZE * 8.0; // Bigger smoke cloud
smokeCloud.alpha = 0;
smokeCloud.scaleX = 1.2;
smokeCloud.scaleY = 1.2;
game.addChild(smokeCloud);
// Delayed massive smoke appearance
tween(smokeCloud, {
alpha: 1.0,
scaleX: 8.0,
scaleY: 8.0
}, {
duration: 500,
delay: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
// Smoke slowly dissipates over very long time
tween(smokeCloud, {
alpha: 0,
scaleX: 14.0,
// ENORMOUS final scale
scaleY: 14.0
}, {
duration: 2000,
// Very long dissipation time
easing: tween.easeIn,
onFinish: function onFinish() {
smokeCloud.destroy();
}
});
}
});
}
// Create ULTRA-MASSIVE spectacular multi-stage missile explosion with ENHANCED particle effects
// Stage 1: Initial impact flash - ULTRA bright white flash (GIGANTIC)
var impactFlash = new EffectIndicator(explosionX, explosionY, 'splash');
impactFlash.children[0].tint = 0xFFFFFF; // Bright white impact flash
impactFlash.children[0].width = impactFlash.children[0].height = CELL_SIZE * 5.0; // GIGANTIC initial flash (increased from 3.5)
impactFlash.alpha = 1;
impactFlash.scaleX = 0.1;
impactFlash.scaleY = 0.1;
game.addChild(impactFlash);
// Ultra-fast bright flash expansion (GIGANTIC SCALE)
tween(impactFlash, {
scaleX: 7.0,
// MUCH bigger scale (increased from 4.5)
scaleY: 7.0,
alpha: 0
}, {
duration: 120,
easing: tween.easeOut
});
// Stage 2: ULTRA-MASSIVE main explosion fireball - orange/red core (ENORMOUS)
var mainExplosion = new EffectIndicator(explosionX, explosionY, 'splash');
mainExplosion.children[0].tint = 0xFF4500; // Orange-red fireball
mainExplosion.children[0].width = mainExplosion.children[0].height = CELL_SIZE * 8.0; // ENORMOUS main explosion (increased from 6.0)
mainExplosion.alpha = 1;
mainExplosion.scaleX = 0.2;
mainExplosion.scaleY = 0.2;
game.addChild(mainExplosion);
// Dramatic ULTRA-MASSIVE fireball expansion with bounce effect
tween(mainExplosion, {
scaleX: 5.5,
// Much bigger scale (increased from 3.8)
scaleY: 5.5,
alpha: 0.95
}, {
duration: 250,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(mainExplosion, {
scaleX: 8.5,
// ENORMOUS final scale (increased from 6.0)
scaleY: 8.5,
alpha: 0
}, {
duration: 700,
easing: tween.easeIn,
onFinish: function onFinish() {
mainExplosion.destroy();
}
});
}
});
// Stage 3: Create TRIPLE the explosion particles for ULTRA-MASSIVE debris effect (TRIPLED)
for (var particleIndex = 0; particleIndex < 24; particleIndex++) {
// Increased from 16 to 24
var explosionParticle = new EffectIndicator(explosionX, explosionY, 'splash');
// Vary particle colors for realistic explosion with MORE variety
var particleColors = [0xFF6600, 0xFF4500, 0xFF8800, 0xFFAA00, 0xFF2200, 0xFFCC00, 0xFF3300, 0xFFDD00];
explosionParticle.children[0].tint = particleColors[particleIndex % particleColors.length];
explosionParticle.children[0].width = explosionParticle.children[0].height = CELL_SIZE * (1.8 + Math.random() * 1.5); // MUCH bigger particles (increased from 1.2 + 1.0)
explosionParticle.alpha = 0.9;
explosionParticle.scaleX = 0.5 + Math.random() * 0.4;
explosionParticle.scaleY = 0.5 + Math.random() * 0.4;
// Random direction for particle scatter with ULTRA-LONG distance
var particleAngle = particleIndex / 24 * Math.PI * 2 + (Math.random() - 0.5) * 0.8;
var particleDistance = CELL_SIZE * (3.5 + Math.random() * 3.0); // ULTRA-FAR particle scatter (increased from 2.5 + 2.0)
var targetX = explosionX + Math.cos(particleAngle) * particleDistance;
var targetY = explosionY + Math.sin(particleAngle) * particleDistance;
game.addChild(explosionParticle);
// Animate particles flying outward with ULTRA-enhanced physics
tween(explosionParticle, {
x: targetX,
y: targetY,
scaleX: 3.5 + Math.random() * 1.2,
// Much bigger final scale (increased from 2.5 + 0.8)
scaleY: 3.5 + Math.random() * 1.2,
alpha: 0.7
}, {
duration: 350 + Math.random() * 250,
// Longer duration
easing: tween.easeOut,
onFinish: function onFinish() {
tween(explosionParticle, {
scaleX: 0.2,
scaleY: 0.2,
alpha: 0
}, {
duration: 450 + Math.random() * 200,
// Longer fade duration
easing: tween.easeIn,
onFinish: function onFinish() {
explosionParticle.destroy();
}
});
}
});
}
// Stage 4: ULTRA-MASSIVE enhanced shockwave with QUADRUPLE rings
var primaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
primaryShockwave.children[0].tint = 0xFFDDDD; // Light pink shockwave
primaryShockwave.children[0].alpha = 0.7;
primaryShockwave.children[0].width = primaryShockwave.children[0].height = CELL_SIZE * 1.5; // Bigger base
primaryShockwave.alpha = 0.9;
game.addChild(primaryShockwave);
// ULTRA-HUGE expanding primary shockwave
tween(primaryShockwave, {
scaleX: 12.0,
// ENORMOUS scale (increased from 8.5)
scaleY: 12.0,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
primaryShockwave.destroy();
}
});
// Secondary ULTRA-MASSIVE shockwave
var secondaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
secondaryShockwave.children[0].tint = 0xFFFFFF; // White secondary wave
secondaryShockwave.children[0].alpha = 0.5;
secondaryShockwave.children[0].width = secondaryShockwave.children[0].height = CELL_SIZE * 1.0;
secondaryShockwave.alpha = 0.8;
game.addChild(secondaryShockwave);
// Delayed ULTRA-MASSIVE secondary shockwave
tween(secondaryShockwave, {
scaleX: 13.0,
// ENORMOUS scale (increased from 9.0)
scaleY: 13.0,
alpha: 0
}, {
duration: 900,
delay: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
secondaryShockwave.destroy();
}
});
// Third shockwave for QUADRUPLE-ring ULTRA-MASSIVE effect
var tertiaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
tertiaryShockwave.children[0].tint = 0xFFAAAA; // Light red tertiary wave
tertiaryShockwave.children[0].alpha = 0.3;
tertiaryShockwave.children[0].width = tertiaryShockwave.children[0].height = CELL_SIZE * 0.8;
tertiaryShockwave.alpha = 0.7;
game.addChild(tertiaryShockwave);
// Third delayed ULTRA-MASSIVE shockwave
tween(tertiaryShockwave, {
scaleX: 14.0,
// ENORMOUS scale (increased from 10.0)
scaleY: 14.0,
alpha: 0
}, {
duration: 1000,
delay: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tertiaryShockwave.destroy();
}
});
// Fourth shockwave for QUADRUPLE-ring effect (NEW!)
var quaternaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
quaternaryShockwave.children[0].tint = 0xFFCCCC; // Very light pink quaternary wave
quaternaryShockwave.children[0].alpha = 0.2;
quaternaryShockwave.children[0].width = quaternaryShockwave.children[0].height = CELL_SIZE * 0.6;
quaternaryShockwave.alpha = 0.6;
game.addChild(quaternaryShockwave);
// Fourth delayed ULTRA-MASSIVE shockwave
tween(quaternaryShockwave, {
scaleX: 15.0,
// ULTRA-ENORMOUS scale
scaleY: 15.0,
alpha: 0
}, {
duration: 1100,
delay: 450,
easing: tween.easeOut,
onFinish: function onFinish() {
quaternaryShockwave.destroy();
}
});
// Stage 5: ULTRA-MASSIVE smoke cloud effect - ENORMOUS and lingers much longer
var smokeCloud = new EffectIndicator(explosionX, explosionY, 'splash');
smokeCloud.children[0].tint = 0x666666; // Dark gray smoke
smokeCloud.children[0].alpha = 0.6;
smokeCloud.children[0].width = smokeCloud.children[0].height = CELL_SIZE * 6.0; // ENORMOUS smoke cloud (increased from 4.0)
smokeCloud.alpha = 0;
smokeCloud.scaleX = 1.0;
smokeCloud.scaleY = 1.0;
game.addChild(smokeCloud);
// Delayed ULTRA-MASSIVE smoke appearance that grows slowly and lingers
tween(smokeCloud, {
alpha: 0.9,
scaleX: 6.0,
// Bigger initial expansion (increased from 4.0)
scaleY: 6.0
}, {
duration: 450,
delay: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
// Smoke slowly dissipates over ULTRA-LONG time
tween(smokeCloud, {
alpha: 0,
scaleX: 10.0,
// ENORMOUS final scale (increased from 7.0)
scaleY: 10.0
}, {
duration: 1500,
// ULTRA-LONG dissipation time (increased from 1200)
easing: tween.easeIn,
onFinish: function onFinish() {
smokeCloud.destroy();
}
});
}
});
}
// Apply special effects based on bullet type
if (self.type === 'flamethrower') {
// Create visual flame effect with enhanced animation
var flameEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'flamethrower');
game.addChild(flameEffect);
// Apply burning effect to target enemy
if (!self.targetEnemy.isImmune) {
self.targetEnemy.burning = true;
self.targetEnemy.burnDamage = self.damage * 0.3; // 30% of original damage per tick
self.targetEnemy.burnDuration = 180; // 3 seconds at 60 FPS
}
// Flame damage to enemies in a cone behind the target
var coneRange = CELL_SIZE * 1.2;
var targetAngle = Math.atan2(self.targetEnemy.y - self.y, self.targetEnemy.x - self.x);
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var flameDx = otherEnemy.x - self.targetEnemy.x;
var flameDy = otherEnemy.y - self.targetEnemy.y;
var flameDistance = Math.sqrt(flameDx * flameDx + flameDy * flameDy);
var enemyAngle = Math.atan2(flameDy, flameDx);
var angleDiff = Math.abs(enemyAngle - targetAngle);
// Normalize angle difference
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
angleDiff = Math.abs(angleDiff);
// Check if enemy is within cone (45 degrees = PI/4 radians)
if (flameDistance <= coneRange && angleDiff <= Math.PI / 4) {
// Apply flame damage (40% of original damage)
otherEnemy.health -= self.damage * 0.4;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
// Apply burning effect
if (!otherEnemy.isImmune) {
otherEnemy.burning = true;
otherEnemy.burnDamage = self.damage * 0.2;
otherEnemy.burnDuration = 120; // 2 seconds for cone targets
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
// Apply slow effect
// Make slow percentage scale with tower level (default 50%, up to 80% at max level)
var slowPct = 0.5;
if (self.sourceTowerLevel !== undefined) {
// Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
self.targetEnemy.slowDuration = 180; // Reset duration
}
}
} else if (self.type === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
// Apply spread angle for machine gun bullets
if (self.type === 'trap' && self.spreadAngle !== undefined) {
angle += self.spreadAngle;
}
// Rotate missile asset to face target direction
if (self.type === 'slow' && self.children.length > 0) {
var missileGraphics = self.children[0];
if (missileGraphics.targetRotation === undefined) {
missileGraphics.targetRotation = angle;
missileGraphics.rotation = angle;
} else {
// Only rotate if angle difference is significant
if (Math.abs(angle - missileGraphics.targetRotation) > 0.1) {
tween.stop(missileGraphics, {
rotation: true
});
// Calculate shortest rotation path
var currentRotation = missileGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
missileGraphics.targetRotation = angle;
tween(missileGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 100,
easing: tween.easeOut
});
}
}
}
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
// DebugCell class removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'flamethrower':
effectGraphics.tint = 0xFF4500;
effectGraphics.width = CELL_SIZE * 1.5;
effectGraphics.height = CELL_SIZE * 0.8; // Elongated flame effect
break;
}
effectGraphics.alpha = 0.7;
self.alpha = 0;
// Animate the effect
tween(self, {
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
// Base enemy class for common functionality
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'normal';
self.speed = .02;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
self.collisionRadius = 25; // Base collision detection radius
self.lastCollisionCheck = 0; // Performance optimization
self.smokeEffectActive = false; // Track if smoke effect is already active
self.lastHealthPercentage = 1.0; // Track last health percentage to detect transitions
// Adjust collision radius based on enemy type
switch (self.type) {
case 'tank':
self.collisionRadius = 40; // Larger radius for tank enemies
break;
case 'fast':
self.collisionRadius = 20; // Smaller radius for fast enemies
break;
case 'flying':
self.collisionRadius = 30; // Medium radius for flying enemies
break;
default:
self.collisionRadius = 25; // Default radius
break;
}
// Boss enemies have larger collision radius
if (self.isBoss) {
self.collisionRadius *= 1.5;
}
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 1; // Normal speed (half of previous 2x)
// Speed boost for wave 5 fast enemies
if (self.waveNumber === 5) {
self.speed *= 1.2; // 1.2x speed for wave 5 fast enemies
}
// Speed boost for wave 6 fast enemies
if (self.waveNumber === 6) {
self.speed *= 1.1; // 1.1x speed for wave 6 fast enemies
}
self.maxHealth = 1;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 200;
// Increase speed for wave 5 immune enemies only
if (self.waveNumber === 5) {
self.speed *= 3; // 3x speed for wave 5 immune enemies
}
// Speed boost for wave 2 immune enemies when they reach the second turn
self.hasReachedSecondTurn = false;
break;
case 'flying':
self.isFlying = true;
self.speed *= 2; // 2x speed for flying enemies
self.maxHealth = 80;
break;
case 'tank':
self.speed *= 0.5; // Half speed for tank enemies
self.maxHealth = 300; // Very high health for tank enemies
break;
default:
// Default enemy uses fast values
self.speed *= 1; // Normal speed (half of previous 2x)
self.maxHealth = 1;
break;
}
if (currentWave === totalWaves && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.isBoss) {
assetId = 'Boss';
} else if (self.type === 'tank') {
assetId = 'Tank';
} else if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale up boss enemies
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Fall back to regular enemy asset if specific type asset not found
// Apply tint to differentiate enemy types
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
break;
case 'immune':
enemyGraphics.tint = 0xAA0000; // Red for immune enemies
break;
case 'flying':
enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
break;
case 'swarm':
enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
break;
}*/
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
shadowGraphics.scaleX = 1.8;
shadowGraphics.scaleY = 1.8;
}
// Position shadow slightly offset
self.shadow.x = 20; // Offset right
self.shadow.y = 20; // Offset down
// Ensure shadow has the same rotation as the enemy
shadowGraphics.rotation = enemyGraphics.rotation;
}
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
self.update = function () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Check for 50% health threshold for smoke effect
var currentHealthPercentage = self.health / self.maxHealth;
if (!self.smokeEffectActive && self.lastHealthPercentage > 0.5 && currentHealthPercentage <= 0.5) {
// Check if this enemy type should have smoke effect (Flying, BOSS, Immune, Tank)
if (self.isFlying || self.isBoss || self.isImmune || self.type === 'tank') {
self.smokeEffectActive = true;
// Create animated smoke steam effect
var smokeEffect = new Container();
smokeEffect.x = self.x;
smokeEffect.y = self.y - enemyGraphics.height / 2 - 10;
// Create multiple smoke particles for realistic steam effect
for (var smokeIndex = 0; smokeIndex < 5; smokeIndex++) {
var smokeParticle = smokeEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
// Set smoke particle properties - scaled up to 2x size
smokeParticle.width = (15 + Math.random() * 10) * 2;
smokeParticle.height = (20 + Math.random() * 15) * 2;
smokeParticle.tint = 0x666666; // Dark gray smoke
smokeParticle.alpha = 0.6 + Math.random() * 0.3;
// Position particles in a small cluster
smokeParticle.x = (Math.random() - 0.5) * 20;
smokeParticle.y = smokeIndex * -8;
// Animate smoke particles rising and fading
tween(smokeParticle, {
y: smokeParticle.y - 60 - Math.random() * 40,
x: smokeParticle.x + (Math.random() - 0.5) * 30,
scaleX: 1.5 + Math.random() * 0.5,
scaleY: 1.5 + Math.random() * 0.5,
alpha: 0
}, {
duration: 2000 + Math.random() * 1000,
delay: smokeIndex * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
smokeParticle.destroy();
}
});
}
game.addChild(smokeEffect);
// Create continuous smoke effect that follows the enemy
self.continuousSmokeEffect = smokeEffect;
// Set up repeating smoke generation
self.smokeTimer = 0;
self.smokeInterval = 60; // Generate new smoke every 60 frames (1 second)
}
}
// Update continuous smoke effect position if active
if (self.smokeEffectActive && self.continuousSmokeEffect && self.continuousSmokeEffect.parent) {
self.continuousSmokeEffect.x = self.x;
self.continuousSmokeEffect.y = self.y - enemyGraphics.height / 2 - 10;
// Generate new smoke particles periodically
self.smokeTimer++;
if (self.smokeTimer >= self.smokeInterval) {
self.smokeTimer = 0;
// Create new smoke particle
var newSmokeParticle = self.continuousSmokeEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
newSmokeParticle.width = (12 + Math.random() * 8) * 2;
newSmokeParticle.height = (16 + Math.random() * 12) * 2;
newSmokeParticle.tint = 0x888888; // Lighter gray for continuous smoke
newSmokeParticle.alpha = 0.4 + Math.random() * 0.2;
newSmokeParticle.x = (Math.random() - 0.5) * 15;
newSmokeParticle.y = 0;
// Animate new smoke particle
tween(newSmokeParticle, {
y: -40 - Math.random() * 20,
x: newSmokeParticle.x + (Math.random() - 0.5) * 20,
scaleX: 1.2 + Math.random() * 0.3,
scaleY: 1.2 + Math.random() * 0.3,
alpha: 0
}, {
duration: 1500 + Math.random() * 500,
easing: tween.easeOut,
onFinish: function onFinish() {
newSmokeParticle.destroy();
}
});
}
}
// Update last health percentage for next frame comparison
self.lastHealthPercentage = currentHealthPercentage;
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed, poisoned, or burned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
self.burning = false;
self.burnEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle burning effect from flamethrower
if (self.burning) {
// Visual indication of burning status
if (!self.burnEffect) {
self.burnEffect = true;
}
// Apply burn damage every 20 frames (3 times per second)
if (LK.ticks % 20 === 0) {
self.health -= self.burnDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.burnDuration--;
if (self.burnDuration <= 0) {
self.burning = false;
self.burnEffect = false;
// Only reset tint if not slowed or poisoned
if (!self.slowed && !self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Enemy overlap avoidance - check every frame for immune and tank enemies for better convoy formation
if (self.type === 'immune' || self.type === 'tank' || LK.ticks % 3 === 0) {
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self && otherEnemy.parent) {
// Prevent collision between flying and ground enemies (fast, immune, tank)
if (self.isFlying && !otherEnemy.isFlying || !self.isFlying && otherEnemy.isFlying) {
continue; // Skip collision detection between flying and ground enemies
}
var dx = otherEnemy.x - self.x;
var dy = otherEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var minDistance = (self.collisionRadius + otherEnemy.collisionRadius) * 1.4; // Increased buffer for better separation
if (distance < minDistance && distance > 0) {
// Much stronger separation force for immune and tank enemies
var overlap = minDistance - distance;
var separationForce;
if (self.type === 'immune' || self.type === 'tank' || otherEnemy.type === 'immune' || otherEnemy.type === 'tank') {
// Extra strong separation for immune/tank enemies to maintain convoy formation
separationForce = Math.pow(overlap / minDistance, 1.5) * 4.0; // Stronger force with smoother scaling
} else {
separationForce = Math.pow(overlap / minDistance, 2) * 2.0; // Original force for other enemies
}
var separationX = dx / distance * separationForce;
var separationY = dy / distance * separationForce;
// Apply separation with stronger horizontal bias to maintain lanes
self.currentCellX -= separationX;
self.currentCellY -= separationY * 0.7; // Reduced Y separation to maintain forward movement
// Apply weaker counter-force to maintain formation
otherEnemy.currentCellX += separationX * 0.3;
otherEnemy.currentCellY += separationY * 0.3;
// Keep enemies within reasonable bounds
self.currentCellX = Math.max(2, Math.min(22, self.currentCellX));
self.currentCellY = Math.max(-20, Math.min(33, self.currentCellY));
otherEnemy.currentCellX = Math.max(2, Math.min(22, otherEnemy.currentCellX));
otherEnemy.currentCellY = Math.max(-20, Math.min(33, otherEnemy.currentCellY));
}
}
}
}
// Fast enemy shooting capability
if (self.type === 'fast') {
// Initialize shooting properties if not already set
if (self.shootingCooldown === undefined) {
self.shootingCooldown = 0;
self.shootingRange = 12 * CELL_SIZE; // 12 cell shooting range (3x increase)
self.shootingDamage = 5;
self.bulletSpeed = 3;
}
// Decrease shooting cooldown
if (self.shootingCooldown > 0) {
self.shootingCooldown--;
}
// Try to find and shoot at towers within range
if (self.shootingCooldown <= 0 && self.currentCellY >= 4) {
var targetTower = null;
var closestDistance = self.shootingRange;
// Find closest tower within shooting range
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Skip landmine towers - enemies cannot see them
if (tower.id === 'landmine') {
continue;
}
// Allow targeting of all other tower types including trap/shooter towers
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.shootingRange && distance < closestDistance) {
closestDistance = distance;
targetTower = tower;
}
}
// Shoot at the target tower if found
if (targetTower) {
var bulletX = self.x;
var bulletY = self.y;
var enemyBullet = new Bullet(bulletX, bulletY, targetTower, self.shootingDamage, self.bulletSpeed);
// Customize enemy bullet appearance
if (enemyBullet.children[0]) {
enemyBullet.children[0].tint = 0xFF0000; // Red color for enemy bullets
enemyBullet.children[0].width = 20;
enemyBullet.children[0].height = 20;
}
// Mark as enemy bullet to handle differently
enemyBullet.isEnemyBullet = true;
game.addChild(enemyBullet);
bullets.push(enemyBullet);
// Set cooldown (shoot every 1.5 seconds at 60 FPS)
self.shootingCooldown = 90;
}
}
}
// Immune enemy shooting capability
if (self.type === 'immune') {
// Initialize shooting properties if not already set
if (self.shootingCooldown === undefined) {
self.shootingCooldown = 0;
self.shootingRange = 8 * CELL_SIZE; // 8 cell shooting range for immune enemies
self.shootingDamage = 8;
self.bulletSpeed = 4;
}
// Decrease shooting cooldown
if (self.shootingCooldown > 0) {
self.shootingCooldown--;
}
// Try to find and shoot at towers within range
if (self.shootingCooldown <= 0 && self.currentCellY >= 4) {
var targetTower = null;
var closestDistance = self.shootingRange;
// Find closest tower within shooting range
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Skip landmine towers - enemies cannot see them
if (tower.id === 'landmine') {
continue;
}
// Allow targeting of all other tower types including trap/shooter towers
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.shootingRange && distance < closestDistance) {
closestDistance = distance;
targetTower = tower;
}
}
// Shoot at the target tower if found
if (targetTower) {
var bulletX = self.x;
var bulletY = self.y;
var enemyBullet = new Bullet(bulletX, bulletY, targetTower, self.shootingDamage, self.bulletSpeed);
// Customize immune enemy bullet appearance
if (enemyBullet.children[0]) {
enemyBullet.children[0].tint = 0xAA0000; // Dark red color for immune enemy bullets
enemyBullet.children[0].width = 25;
enemyBullet.children[0].height = 25;
}
// Mark as enemy bullet to handle differently
enemyBullet.isEnemyBullet = true;
game.addChild(enemyBullet);
bullets.push(enemyBullet);
// Set cooldown (shoot every 1 second at 60 FPS)
self.shootingCooldown = 60;
}
}
}
// Immune enemy trap collision detection
if (self.type === 'immune') {
// Initialize trap contact tracking if not set
if (self.trapContactMap === undefined) {
self.trapContactMap = {};
}
// Check collision with trap towers every 10 frames for performance
if (LK.ticks % 10 === 0) {
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.id === 'trap') {
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if immune enemy is touching the trap (within 80 pixels)
if (distance <= 80) {
// Only reduce health if this trap hasn't been touched before
var trapKey = tower.x + ',' + tower.y; // Unique identifier for this trap
if (!self.trapContactMap[trapKey]) {
self.trapContactMap[trapKey] = true;
// Reduce immune enemy's health by half
self.health = Math.floor(self.health / 2);
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
// Create visual effect for trap contact
var trapEffect = new EffectIndicator(self.x, self.y, 'splash');
trapEffect.children[0].tint = 0xAAAAAA; // Gray color for trap effect
trapEffect.children[0].width = trapEffect.children[0].height = CELL_SIZE * 1.2;
game.addChild(trapEffect);
}
// Break after first trap collision to avoid multiple hits per frame
break;
}
}
}
}
}
// Tank enemy shooting capability
if (self.type === 'tank') {
// Initialize shooting properties if not already set
if (self.shootingCooldown === undefined) {
self.shootingCooldown = 0;
self.shootingRange = 16 * CELL_SIZE; // 2x increased range for better targeting (was 8)
self.shootingDamage = 15; // Increased damage for tank enemies
self.bulletSpeed = 3; // Increased bullet speed
}
// Decrease shooting cooldown
if (self.shootingCooldown > 0) {
self.shootingCooldown--;
}
// Try to find and shoot at towers within range - tank enemies shoot when positioned
if (self.shootingCooldown <= 0 && self.currentCellY >= 4 && self.tankMovementComplete) {
var targetTower = null;
var closestDistance = self.shootingRange;
// Find closest tower within shooting range
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Skip landmine towers - enemies cannot see them
if (tower.id === 'landmine') {
continue;
}
// Allow targeting of all other tower types including trap/shooter towers
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.shootingRange && distance < closestDistance) {
closestDistance = distance;
targetTower = tower;
}
}
// Shoot at the target tower if found
if (targetTower) {
var bulletX = self.x;
var bulletY = self.y;
var enemyBullet = new Bullet(bulletX, bulletY, targetTower, self.shootingDamage, self.bulletSpeed);
// Customize tank enemy bullet appearance
if (enemyBullet.children[0]) {
enemyBullet.children[0].tint = 0x888888; // Gray color for tank enemy bullets
enemyBullet.children[0].width = 35; // Larger bullets for tank enemies
enemyBullet.children[0].height = 35;
}
// Mark as enemy bullet to handle differently
enemyBullet.isEnemyBullet = true;
game.addChild(enemyBullet);
bullets.push(enemyBullet);
// Set cooldown (shoot every 1.5 seconds at 60 FPS for more frequent shooting)
self.shootingCooldown = 90;
}
}
}
// Convoy formation for immune and tank enemies - maintain proper spacing and formation
if ((self.type === 'immune' || self.type === 'tank') && self.currentCellY >= 4) {
// Find nearby same-type enemies to form convoy with
var nearbyConvoyEnemies = [];
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self && otherEnemy.parent && otherEnemy.type === self.type) {
var dx = otherEnemy.x - self.x;
var dy = otherEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 4) {
// Within 4 cells for convoy formation
nearbyConvoyEnemies.push({
enemy: otherEnemy,
distance: distance,
dx: dx,
dy: dy
});
}
}
}
// Apply convoy formation forces
if (nearbyConvoyEnemies.length > 0) {
for (var j = 0; j < nearbyConvoyEnemies.length; j++) {
var convoyData = nearbyConvoyEnemies[j];
var convoyEnemy = convoyData.enemy;
var distance = convoyData.distance;
var dx = convoyData.dx;
var dy = convoyData.dy;
var idealDistance = CELL_SIZE * 2.8; // Ideal convoy spacing
if (distance < idealDistance) {
// Too close - apply separation
var separationForce = (idealDistance - distance) / idealDistance * 0.8;
var separationX = dx / distance * separationForce;
var separationY = dy / distance * separationForce * 0.4; // Less Y separation
self.currentCellX -= separationX;
self.currentCellY -= separationY;
} else if (distance > idealDistance * 1.5) {
// Too far - apply attraction to maintain convoy
var attractionForce = (distance - idealDistance) / distance * 0.3;
var attractionX = dx / distance * attractionForce;
var attractionY = dy / distance * attractionForce * 0.2;
self.currentCellX += attractionX;
self.currentCellY += attractionY;
}
}
}
// Keep enemies within valid bounds
self.currentCellX = Math.max(3, Math.min(21, self.currentCellX));
self.currentCellY = Math.max(-20, Math.min(33, self.currentCellY));
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed && self.burning) {
// Combine all three effects
enemyGraphics.tint = 0x8B4513; // Brown-ish mix
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned && self.burning) {
// Combine poison and burn
enemyGraphics.tint = 0x8B7355; // Brownish-green mix
} else if (self.slowed && self.burning) {
// Combine slow and burn
enemyGraphics.tint = 0xCC4500; // Orange-red mix
} else if (self.burning) {
enemyGraphics.tint = 0xFF4500; // Orange for burning
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox) + Math.PI / 2;
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.25) {
// Further increased threshold to reduce tween frequency
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 150,
// Reduced duration for faster rotation
easing: tween.easeOut
});
}
}
}
}
// Keep health bars positioned below level indicators
var healthBarY = CELL_SIZE * 0.7 + 25;
if (self.towerHealthBarOutline) {
self.towerHealthBarOutline.y = healthBarY;
self.towerHealthBarBG.y = healthBarY;
self.towerHealthBar.y = healthBarY;
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
var GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2("+" + value, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
self.cells = [];
self.spawns = [];
self.goals = [];
for (var i = 0; i < gridWidth; i++) {
self.cells[i] = [];
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j] = {
score: 0,
pathId: 0,
towersInRange: []
};
}
}
/*
Cell Types
0: Transparent floor
1: Wall
2: Spawn
3: Goal
*/
// Create simplified straight road path waypoints with 3-cell width
var pathWaypoints = [{
x: 12,
y: 0
},
// Start point (spawn)
{
x: 12,
y: 8
},
// First horizontal road
{
x: 2,
y: 8
},
// Turn down
{
x: 2,
y: 15
},
// Second horizontal road
{
x: 22,
y: 15
},
// Turn down
{
x: 22,
y: 22
},
// Third horizontal road
{
x: 12,
y: 22
},
// Final segment to goal
{
x: 12,
y: gridHeight - 1
} // End point (goal)
];
// Create path cells array to mark which cells are part of the road
var pathCells = [];
// Generate path between waypoints
for (var w = 0; w < pathWaypoints.length - 1; w++) {
var start = pathWaypoints[w];
var end = pathWaypoints[w + 1];
// Create straight line between waypoints
var steps = Math.max(Math.abs(end.x - start.x), Math.abs(end.y - start.y));
for (var s = 0; s <= steps; s++) {
var t = steps === 0 ? 0 : s / steps;
var pathX = Math.round(start.x + (end.x - start.x) * t);
var pathY = Math.round(start.y + (end.y - start.y) * t);
// Add path width - make road 7 cells wide horizontally, 3 cells wide vertically
for (var offsetX = -3; offsetX <= 3; offsetX++) {
for (var offsetY = -1; offsetY <= 1; offsetY++) {
var cellX = pathX + offsetX;
var cellY = pathY + offsetY;
if (cellX >= 0 && cellX < gridWidth && cellY >= 0 && cellY < gridHeight) {
var key = cellX + ',' + cellY;
if (pathCells.indexOf(key) === -1) {
pathCells.push(key);
}
}
}
}
}
}
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellKey = i + ',' + j;
var isOnPath = pathCells.indexOf(cellKey) !== -1;
// Default to wall, then carve out path and other areas
var cellType = 1; // Wall by default
// Set path cells
if (isOnPath) {
cellType = 0; // Path
}
// Set spawn points at the start of the path
if (i >= 11 && i <= 13 && j === 0) {
cellType = 2; // Spawn
self.spawns.push(cell);
}
// Set goal points at the end of the path
if (i >= 11 && i <= 13 && j === gridHeight - 1) {
cellType = 3; // Goal
self.goals.push(cell);
}
// Keep borders as walls
if (i === 0 || i === gridWidth - 1 || j === 0 || j === gridHeight - 1) {
if (cellType !== 2 && cellType !== 3) {
// Don't override spawn/goal
cellType = 1; // Wall
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
// Render road asset for path cells
if (isOnPath) {
var roadVisual = self.attachAsset('road', {
anchorX: 0.5,
anchorY: 0.6
});
roadVisual.x = i * CELL_SIZE + CELL_SIZE / 2;
roadVisual.y = j * CELL_SIZE + CELL_SIZE / 2;
}
// Debug cells removed for performance
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.left.type != 1) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.right.type != 1) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.right.type != 1) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.left.type != 1) {
processNode(node.downLeft, targetScore, node);
}
targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId != pathId) {
console.warn("Spawn blocked");
return true;
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
// Debug cells removed for performance
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.type == 3) {
// BOSS Enemy reached the goal - reduce lives by 100 (100% of 100 total lives)
if (enemy.isBoss) {
lives = Math.max(0, lives - 100);
} else {
// Regular enemy reached the goal - reduce lives by 10 (10% of 100 total lives)
lives = Math.max(0, lives - 10);
}
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Consistent movement speed for all enemy types during initial phase
var initialSpeed = enemy.speed;
if (enemy.type === 'tank') {
initialSpeed = 0.03; // 3x faster than normal tank speed for smoother entry
}
// Move directly downward
enemy.currentCellY += initialSpeed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) + 90 degrees right
var angle = Math.PI / 2 + Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position (reduce calculation frequency)
if (LK.ticks % 2 === 0) {
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
}
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Special movement for boss enemies - straight line down the middle
if (enemy.isBoss) {
// Boss enemies move straight down the middle column (x = 12)
enemy.currentCellX = 12; // Always stay in middle column
enemy.currentCellY += enemy.speed; // Move straight down
} else {
// Regular flying enemy logic
// Initialize flying enemy direction if not set
if (enemy.flyingDirection === undefined) {
// Use the global alternating direction flag
enemy.flyingDirection = lastDirectionWasRight ? 1 : -1; // 1 for right, -1 for left
enemy.startX = enemy.currentCellX; // Remember starting X position
enemy.hasChangedDirection = false; // Track if enemy has changed direction yet
}
// Assign flying enemies to one of 3 vertical lanes if not already assigned
if (enemy.flyingLane === undefined) {
// Determine lane based on initial direction instead of spawn position
if (enemy.flyingDirection === -1) {
// Flying left: start from right side lanes
enemy.flyingLane = 2; // Right lane - x position 18
enemy.targetX = 18;
} else {
// Flying right: start from left side lanes
enemy.flyingLane = 0; // Left lane - x position 6
enemy.targetX = 6;
}
}
// Move straight down in the assigned lane
enemy.currentCellY += enemy.speed;
// Implement alternating direction movement for flying enemies
if (!enemy.hasChangedDirection && enemy.currentCellY > 15) {
// After reaching middle of map, start moving in assigned direction
enemy.hasChangedDirection = true;
// Set new target X based on direction - enemies move to opposite side
if (enemy.flyingDirection > 0) {
// Moving right - target right side of map
enemy.targetX = 20;
} else {
// Moving left - target left side of map
enemy.targetX = 4;
}
}
// Gradually move to the target X position (lane center or direction target)
var xDiff = enemy.targetX - enemy.currentCellX;
if (Math.abs(xDiff) > 0.1) {
enemy.currentCellX += xDiff * 0.05; // Gradual movement to target
}
}
// Rotate enemy graphic to face downward
var angle = Math.PI; // Face downward (180 degrees)
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
// Update visual position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Check if enemy reached the bottom (goal)
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.type == 3) {
// BOSS Flying enemy reached the goal - reduce lives by 100 (100% of 100 total lives)
if (enemy.isBoss) {
lives = Math.max(0, lives - 100);
} else {
// Flying enemy reached the goal - reduce lives by 10 (10% of 100 total lives)
lives = Math.max(0, lives - 10);
}
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
return true;
}
// Check if enemy has moved off the bottom of the screen
if (enemy.currentCellY > 35) {
// Grid height is 35 (29 + 6)
// BOSS Enemy went off-screen - reduce lives by 100 (100% of 100 total lives)
if (enemy.isBoss) {
lives = Math.max(0, lives - 100);
} else {
// Enemy went off-screen - reduce lives by 10 (10% of 100 total lives)
lives = Math.max(0, lives - 10);
}
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
return true;
}
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (enemy.isBoss) {
// Boss enemies move straight down the middle column regardless of type
enemy.currentCellX = 12; // Always stay in middle column
enemy.currentCellY += enemy.speed; // Move straight down
// Force boss position to center immediately
enemy.cellX = 12;
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
// Set rotation to face straight down without animation
if (enemy.children[0]) {
enemy.children[0].rotation = Math.PI; // Face downward (180 degrees)
enemy.children[0].targetRotation = Math.PI;
}
} else if (enemy.type === 'fast') {
// Fast enemies move straight down without pathfinding or turning
enemy.currentCellY += enemy.speed;
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
// Set rotation to face straight down without animation
if (enemy.children[0]) {
enemy.children[0].rotation = Math.PI; // Face downward (180 degrees)
enemy.children[0].targetRotation = Math.PI;
}
} else if (enemy.type === 'immune' && enemy.waveNumber === 2 && !enemy.hasReachedSecondTurn && enemy.currentCellY >= 15) {
// Wave 2 immune enemies get speed boost when they reach the second turn (y >= 15)
enemy.hasReachedSecondTurn = true;
enemy.speed = 2.4 * 0.02; // Set speed to 2.4 times base speed (0.02)
} else if (enemy.type === 'tank') {
// Tank enemies move to upper quarter vertically and stop there, staying at their spawned x-position
var upperQuarterY = (29 + 6) * 0.25; // Upper quarter of map (around row 8.75)
var supportPositionY = Math.max(8, upperQuarterY); // Stop at upper quarter
// Initialize tank target position if not set
if (enemy.tankTargetX === undefined) {
enemy.tankTargetX = enemy.currentCellX; // Store spawned x-position
enemy.tankTargetY = supportPositionY; // Store target support position
enemy.tankMovementComplete = false;
}
// Use smooth interpolation movement instead of tween animations
if (!enemy.tankMovementComplete && enemy.currentCellY >= 4) {
// Calculate distance to target
var dx = enemy.tankTargetX - enemy.currentCellX;
var dy = enemy.tankTargetY - enemy.currentCellY;
var distanceToTarget = Math.sqrt(dx * dx + dy * dy);
// Check if we've reached the target (within 0.1 cells)
if (distanceToTarget < 0.1) {
enemy.currentCellX = enemy.tankTargetX;
enemy.currentCellY = enemy.tankTargetY;
enemy.tankMovementComplete = true;
} else {
// Use smooth interpolation movement
var speed = 0.04; // Movement speed in cells per frame
var t = Math.min(1, speed / distanceToTarget);
enemy.currentCellX = enemy.currentCellX + (enemy.tankTargetX - enemy.currentCellX) * t;
enemy.currentCellY = enemy.currentCellY + (enemy.tankTargetY - enemy.currentCellY) * t;
}
// Tank enemies do not rotate - keep original rotation
} else {
// Tank has reached support position - stop moving and face downward to support convoy
// Lock tank position at target location
enemy.currentCellX = enemy.tankTargetX; // Stay at spawned x-position
enemy.currentCellY = enemy.tankTargetY; // Lock at support position
// Tank enemies do not rotate - keep original rotation
}
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
} else {
// Handle normal pathfinding enemies
if (!enemy.currentTarget && cell && cell.targets && cell.targets.length > 0) {
enemy.currentTarget = cell.targets[0];
}
if (enemy.currentTarget) {
// Check if current cell has a better target (closer to goal)
if (cell && cell.targets && cell.targets.length > 0 && cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell.targets[0];
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Speed boost logic removed to prevent any tank speed boost effects
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
var angle = Math.atan2(oy, ox) + Math.PI / 2;
enemy.currentCellX += Math.cos(angle - Math.PI / 2) * enemy.speed;
enemy.currentCellY += Math.sin(angle - Math.PI / 2) * enemy.speed;
} else if (cell && cell.targets && cell.targets.length > 0) {
// If no current target, pick the first available target from current cell
enemy.currentTarget = cell.targets[0];
}
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Check if ground enemy has moved off the bottom of the screen
if (enemy.currentCellY > 35) {
// Grid height is 35 (29 + 6)
// BOSS Enemy went off-screen - reduce lives by 100 (100% of 100 total lives)
if (enemy.isBoss) {
lives = Math.max(0, lives - 100);
} else {
// Enemy went off-screen - reduce lives by 10 (10% of 100 total lives)
lives = Math.max(0, lives - 10);
}
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
return true;
}
};
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300;
buttonBackground.height = 100;
buttonBackground.tint = 0x0088FF;
var buttonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 0x000000,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'trap';
// Increase size of base for easier touch
var baseGraphics = self.attachAsset('Box', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
switch (self.towerType) {
case 'landmine':
baseGraphics.tint = 0x9B8B6D; // Lighter tan/brown camouflage
break;
case 'sniper':
baseGraphics.tint = 0x7A9B5F; // Lighter olive green camouflage
break;
case 'flamethrower':
baseGraphics.tint = 0xBBA385; //{aO} // Lighter sandy brown camouflage
break;
case 'slow':
baseGraphics.tint = 0x7F7F5F; //{aR} // Lighter olive drab camouflage
break;
case 'poison':
baseGraphics.tint = 0x969696; //{aU} // Lighter gray camouflage
break;
case 'tank':
baseGraphics.tint = 0x006400; // Dark green for tank
break;
default:
baseGraphics.tint = 0x8C8C77;
//{aX} // Lighter neutral camouflage green-gray
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var displayTextShadow = self.towerType === 'flamethrower' ? 'Flame' : self.towerType === 'trap' ? 'Shooter' : self.towerType === 'slow' ? 'Missiles' : self.towerType === 'tank' ? 'Tank' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
var typeLabelShadow = new Text2(displayTextShadow, {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = -20 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var displayText = self.towerType === 'flamethrower' ? 'Flame' : self.towerType === 'trap' ? 'Shooter' : self.towerType === 'slow' ? 'Missiles' : self.towerType === 'tank' ? 'Tank' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
var typeLabel = new Text2(displayText, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -20; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var costLabelShadow = new Text2(towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 4;
costLabelShadow.y = 24 + 12;
self.addChild(costLabelShadow);
// Add cost label
var costLabel = new Text2(towerCost, {
size: 50,
fill: 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 20 + 12;
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'trap';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
self.isPlaced = false; // Track if tower has been placed on grid
// Standardized method to get the current range of the tower
self.getRange = function () {
// Always calculate range based on tower type and level
switch (self.id) {
case 'sniper':
// Sniper: massive range to cover entire map from any position
if (self.level === self.maxLevel) {
return 75 * CELL_SIZE; // Extreme range for max level to cover entire map (1.5x increase)
}
return (22.5 + (self.level - 1) * 7.5) * CELL_SIZE;
// Increased base and scaling (1.5x increase)
case 'flamethrower':
// Flamethrower: base 4.5, +0.5 per level for cone range (increased aiming distance)
return (4.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'landmine':
// Landmine: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// Slow: base 8.75, +1.25 per level (2.5x increase)
return (8.75 + (self.level - 1) * 1.25) * CELL_SIZE;
case 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'tank':
// Tank: base 14.0, +2.8 per level (3.5x increase)
return (14.0 + (self.level - 1) * 2.8) * CELL_SIZE;
default:
// Trap: base 6, +1.0 per level (double range)
return (6 + (self.level - 1) * 1.0) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
// Continuous shooting variables for flamethrower
self.isContinuousFiring = false;
self.continuousFireTimer = 0;
self.continuousFireDuration = 300; // 5 seconds at 60 FPS
self.continuousReloadTimer = 0;
self.continuousReloadDuration = 180; // 3 seconds at 60 FPS
switch (self.id) {
case 'landmine':
self.fireRate = 30;
self.damage = 5;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'flamethrower':
self.fireRate = 2.5; // Very fast fire rate during continuous shooting (quarter of original delay)
self.damage = 8; // Lower per-hit damage but continuous
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 6;
break;
case 'slow':
self.fireRate = 30;
self.damage = 15; // Increased from 8 to 15 for stronger missiles
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'tank':
self.fireRate = 90;
self.damage = 50; // Increased base damage for tank
self.range = 4.0 * CELL_SIZE;
self.bulletSpeed = 4;
break;
}
var assetToUse = self.id === 'landmine' ? 'landmine' : self.id === 'sniper' ? 'Sniper' : self.id === 'flamethrower' ? 'Flamethrower' : self.id === 'slow' ? 'Rocket' : self.id === 'trap' ? 'Shooter' : self.id === 'tank' ? 'Tank' : 'tower';
var baseGraphics = self.attachAsset(assetToUse, {
anchorX: 0.5,
anchorY: self.id === 'sniper' ? 0.0 : 0.5
});
// Rotate sniper tower left by 40 degrees
if (self.id === 'sniper') {
baseGraphics.rotation = -40 * Math.PI / 180; // Convert 40 degrees to radians and make it negative for left rotation
}
// Flame effects removed for performance
switch (self.id) {
case 'landmine':
// Remove tint to show natural landmine asset
break;
case 'sniper':
// Remove tint to show natural sniper asset
break;
case 'flamethrower':
// Remove tint to show natural flamethrower asset
break;
case 'slow':
// Remove tint to show natural rocket asset
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
case 'tank':
// Remove tint to show natural tank asset
break;
case 'trap':
baseGraphics.tint = 0x555555; // Dark gray for machine gun
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
// Add health bar for towers that need them (excluding landmine)
self.towerHealthBarOutline = null;
self.towerHealthBarBG = null;
self.towerHealthBar = null;
if (self.id === 'flamethrower' || self.id === 'trap' || self.id === 'sniper' || self.id === 'slow' || self.id === 'tank') {
// Create health bar outline
self.towerHealthBarOutline = self.attachAsset('towerHealthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
// Create health bar background (red)
self.towerHealthBarBG = self.attachAsset('towerHealthBar', {
anchorX: 0,
anchorY: 0.5
});
self.towerHealthBarBG.tint = 0xff0000;
// Create health bar foreground (green)
self.towerHealthBar = self.attachAsset('towerHealthBar', {
anchorX: 0,
anchorY: 0.5
});
self.towerHealthBar.tint = 0x00ff00;
// Position health bars below the level indicators to avoid overlap
var healthBarY = CELL_SIZE * 0.7 + 25; // Position below level indicators with some spacing
self.towerHealthBarOutline.y = healthBarY;
self.towerHealthBarBG.y = healthBarY;
self.towerHealthBar.y = healthBarY;
// Center health bars horizontally
self.towerHealthBarOutline.x = -self.towerHealthBarOutline.width / 2;
self.towerHealthBarBG.x = -self.towerHealthBarBG.width / 2;
self.towerHealthBar.x = -self.towerHealthBar.width / 2;
}
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1);
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'landmine':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'flamethrower':
towerLevelIndicator.tint = 0xFF4500;
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF;
break;
case 'poison':
towerLevelIndicator.tint = 0x00FFAA;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
// Only render debug when tower is selected to improve performance
if (selectedTower === self) {
grid.renderDebug();
}
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// Make last upgrade level extra expensive
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
if (self.id === 'landmine') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect
self.damage = 5 + self.level * 10; // double the effect
self.bulletSpeed = 7 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades
self.damage = 5 + self.level * 3;
self.bulletSpeed = 7 + self.level * 0.7;
}
} else {
// Special scaling for missiles towers
if (self.id === 'slow') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for missiles (triple the effect)
self.fireRate = Math.max(3, 30 - self.level * 15); // Much faster fire rate
self.damage = 15 + self.level * 30; // Much higher damage scaling
self.bulletSpeed = 5 + self.level * 3; // Faster missiles
} else {
self.fireRate = Math.max(10, 30 - self.level * 5); // Faster than base scaling
self.damage = 15 + self.level * 10; // Higher damage per level
self.bulletSpeed = 5 + self.level * 1;
}
} else {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for all other towers (double the effect)
self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect
self.damage = 10 + self.level * 20; // double the effect
self.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is in range
if (distance <= self.getRange()) {
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, landmine towers target any ground enemy in range
if (self.id === 'landmine') {
// Landmine targets any ground enemy by distance
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
} else {
// For other towers, use the original path-based targeting
// Get the cell for this enemy
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Use the cell's score (distance to exit) for prioritization
// Lower score means closer to exit
if (cell.score < closestScore) {
closestScore = cell.score;
closestEnemy = enemy;
}
}
}
}
}
}
if (!closestEnemy) {
self.targetEnemy = null;
}
return closestEnemy;
};
self.update = function () {
// Only find targets every 3 frames to reduce performance impact
if (LK.ticks % 3 === 0) {
self.targetEnemy = self.findTarget();
}
// Clean up targeted enemies list for rocket launcher towers
if (self.id === 'slow' && self.targetedEnemies) {
// Remove enemies that are dead or out of range
for (var i = self.targetedEnemies.length - 1; i >= 0; i--) {
var targetedEnemy = self.targetedEnemies[i];
if (!targetedEnemy.parent || targetedEnemy.health <= 0 || !self.isInRange(targetedEnemy)) {
self.targetedEnemies.splice(i, 1);
}
}
}
// Flame animation removed for flamethrower towers
// Special handling for landmine towers - explode on contact
if (self.id === 'landmine' && self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is close enough to trigger explosion (within 50 pixels) and is ground-based
if (distance <= 50 && !self.targetEnemy.isFlying) {
// Create landmine explosion effect with enhanced animation
var landmineExplosion = new EffectIndicator(self.x, self.y, 'splash');
landmineExplosion.children[0].tint = 0xFF4500; // Orange explosion color
landmineExplosion.children[0].width = landmineExplosion.children[0].height = CELL_SIZE * 2.5;
landmineExplosion.scaleX = 0.3;
landmineExplosion.scaleY = 0.3;
landmineExplosion.alpha = 1;
game.addChild(landmineExplosion);
// Multi-stage landmine explosion animation
tween(landmineExplosion, {
scaleX: 1.8,
scaleY: 1.8,
alpha: 0.9
}, {
duration: 150,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(landmineExplosion, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
landmineExplosion.destroy();
}
});
}
});
// Create enemy explosion effect with enhanced animation
var enemyExplosion = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
enemyExplosion.children[0].tint = 0xFF0000; // Red explosion color
enemyExplosion.children[0].width = enemyExplosion.children[0].height = CELL_SIZE * 2;
enemyExplosion.scaleX = 0.2;
enemyExplosion.scaleY = 0.2;
enemyExplosion.alpha = 1;
game.addChild(enemyExplosion);
// Multi-stage enemy explosion animation
tween(enemyExplosion, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0.8
}, {
duration: 120,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(enemyExplosion, {
scaleX: 2.2,
scaleY: 2.2,
alpha: 0
}, {
duration: 250,
easing: tween.easeOut,
onFinish: function onFinish() {
enemyExplosion.destroy();
}
});
}
});
// Create additional shockwave effect for landmine
var shockwave = new EffectIndicator(self.x, self.y, 'splash');
shockwave.children[0].tint = 0xFFFFFF;
shockwave.children[0].alpha = 0.3;
shockwave.children[0].width = shockwave.children[0].height = CELL_SIZE * 0.5;
shockwave.alpha = 0.8;
game.addChild(shockwave);
// Animate shockwave expanding rapidly
tween(shockwave, {
scaleX: 4,
scaleY: 4,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
shockwave.destroy();
}
});
// Mark enemy for destruction
self.targetEnemy.health = 0;
// Mark landmine tower for destruction
self.shouldDestroy = true;
return;
}
}
if (self.targetEnemy && self.id !== 'landmine') {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
// Special rotation handling for sniper towers
if (self.id === 'sniper') {
// Use the main tower graphics (baseGraphics) for sniper rotation
var sniperGraphics = self.children[0]; // The sniper asset
if (sniperGraphics.targetRotation === undefined) {
sniperGraphics.targetRotation = angle;
sniperGraphics.rotation = angle;
} else {
if (Math.abs(angle - sniperGraphics.targetRotation) > 0.1) {
// Stop any ongoing rotation tweens
tween.stop(sniperGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = sniperGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
sniperGraphics.targetRotation = angle;
tween(sniperGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 200,
easing: tween.easeOut
});
}
}
} else {
gunContainer.rotation = angle;
}
// Special continuous firing logic for flamethrower
if (self.id === 'flamethrower') {
// Handle continuous reload timer
if (self.continuousReloadTimer > 0) {
self.continuousReloadTimer--;
} else if (!self.isContinuousFiring && self.targetEnemy) {
// Start continuous firing
self.isContinuousFiring = true;
self.continuousFireTimer = self.continuousFireDuration;
} else if (self.isContinuousFiring) {
// Continue firing during the continuous period
if (self.continuousFireTimer > 0) {
self.continuousFireTimer--;
// Fire at the fast rate during continuous shooting
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
} else {
// End continuous firing, start reload
self.isContinuousFiring = false;
self.continuousReloadTimer = self.continuousReloadDuration;
}
}
} else {
// Normal firing logic for other tower types
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
game.addChild(rangeIndicator);
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
// Initialize tower health based on tower type
if (self.id === 'flamethrower' || self.id === 'trap' || self.id === 'sniper' || self.id === 'slow' || self.id === 'tank') {
// Set health values based on tower type
if (self.id === 'slow') {
self.towerHealth = 300; // 3x base health for missiles
self.maxTowerHealth = 300;
} else if (self.id === 'tank') {
self.towerHealth = 500; // 5x base health for tank towers - very strong
self.maxTowerHealth = 500;
} else if (self.id === 'sniper') {
self.towerHealth = 150; // Medium health for sniper
self.maxTowerHealth = 150;
} else if (self.id === 'flamethrower') {
self.towerHealth = 120; // Medium health for flamethrower
self.maxTowerHealth = 120;
} else {
self.towerHealth = 100; // Base tower health for trap/shooter
self.maxTowerHealth = 100;
}
}
// Method to handle damage from enemy bullets
self.takeDamage = function (damage) {
// Special handling for sniper towers - track hits instead of health
if (self.id === 'sniper') {
// Initialize hit counter if not set
if (self.hitCount === undefined) {
self.hitCount = 0;
}
self.hitCount++;
// Create hit indicator effect
var hitEffect = new EffectIndicator(self.x, self.y, 'sniper');
hitEffect.children[0].tint = 0xFF0000; // Red hit indicator
hitEffect.children[0].width = hitEffect.children[0].height = CELL_SIZE;
game.addChild(hitEffect);
// Update health bar
if (self.towerHealthBar) {
var healthPercentage = Math.max(0, (5 - self.hitCount) / 5);
self.towerHealthBar.width = 60 * healthPercentage;
}
// Destroy sniper after 5 hits
if (self.hitCount >= 5) {
// Mark sniper for destruction
self.shouldDestroy = true;
// Create destruction effect
var destructionEffect = new EffectIndicator(self.x, self.y, 'splash');
destructionEffect.children[0].tint = 0xFF4500; // Orange destruction color
destructionEffect.children[0].width = destructionEffect.children[0].height = CELL_SIZE * 2;
game.addChild(destructionEffect);
}
} else {
// Initialize tower health if not set for non-sniper towers
if (self.towerHealth === undefined) {
// Missiles towers have much higher health to make them stronger
if (self.id === 'slow') {
self.towerHealth = 300; // 3x base health for missiles
self.maxTowerHealth = 300;
} else if (self.id === 'tank') {
self.towerHealth = 500; // 5x base health for tank towers - very strong
self.maxTowerHealth = 500;
} else {
self.towerHealth = 100; // Base tower health
self.maxTowerHealth = 100;
}
}
self.towerHealth -= damage;
// Update health bar
if (self.towerHealthBar && self.maxTowerHealth > 0) {
var healthPercentage = Math.max(0, self.towerHealth / self.maxTowerHealth);
self.towerHealthBar.width = 60 * healthPercentage;
}
if (self.towerHealth <= 0) {
self.towerHealth = 0;
// Mark tower for destruction
self.shouldDestroy = true;
// Create destruction effect
var destructionEffect = new EffectIndicator(self.x, self.y, 'splash');
destructionEffect.children[0].tint = 0xFF4500; // Orange destruction color
destructionEffect.children[0].width = destructionEffect.children[0].height = CELL_SIZE * 2;
game.addChild(destructionEffect);
}
}
};
self.fire = function () {
// Landmine towers cannot shoot normally
if (self.id === 'landmine') {
return;
}
// Machine gun shooting function
if (self.id === 'trap') {
if (self.targetEnemy) {
// Fire multiple bullets in rapid succession for higher ammunition density
var bulletsToFire = 3; // Fire 3 bullets per shot for higher density
for (var bulletIndex = 0; bulletIndex < bulletsToFire; bulletIndex++) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type for machine gun
bullet.type = 'trap';
// Machine gun bullets are smaller and faster
bullet.children[0].tint = 0x555555; // Dark gray for machine gun bullets
bullet.children[0].width = 25;
bullet.children[0].height = 25;
// Add slight delay between bullets for visual effect
if (bulletIndex > 0) {
bullet.x += (Math.random() - 0.5) * 10; // Small random offset
bullet.y += (Math.random() - 0.5) * 10;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
}
// Machine gun recoil effect
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
if (gunContainer._restX === undefined) {
gunContainer._restX = 0;
}
if (gunContainer._restY === undefined) {
gunContainer._restY = 0;
}
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
var recoilDistance = 6;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 40,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 60,
easing: tween.cubicIn
});
}
});
}
return;
}
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
// For rocket launcher (slow) towers, check if we've already fired at this target
if (self.id === 'slow') {
// Initialize targeted enemies array if not exists
if (!self.targetedEnemies) {
self.targetedEnemies = [];
}
// Check if we've already targeted this enemy
var alreadyTargeted = false;
for (var i = 0; i < self.targetedEnemies.length; i++) {
if (self.targetedEnemies[i] === self.targetEnemy) {
alreadyTargeted = true;
break;
}
}
// If already targeted, don't fire again
if (alreadyTargeted) {
return;
}
// Add this enemy to our targeted list
self.targetedEnemies.push(self.targetEnemy);
}
// Always fire if we have a target enemy - removed health vs potential damage check that was blocking boss attacks
if (self.targetEnemy) {
// Special handling for flamethrower - create continuous flame spray
if (self.id === 'flamethrower') {
// Create multiple flame particles for continuous spray effect
var flamesToCreate = 3; // Create multiple flames for denser spray
for (var flameIndex = 0; flameIndex < flamesToCreate; flameIndex++) {
var flameParticle = new Container();
var flameGraphics = flameParticle.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
flameGraphics.width = 25 + Math.random() * 15;
flameGraphics.height = 35 + Math.random() * 20;
flameGraphics.tint = Math.random() > 0.5 ? 0xFF4500 : 0xFF6600;
flameGraphics.alpha = 0.7 + Math.random() * 0.3;
// Position flame particle at tower with slight spread
var spreadAngle = (Math.random() - 0.5) * 0.4; // 20 degree spread
var startAngle = gunContainer.rotation + spreadAngle;
var startDistance = 30 + Math.random() * 10;
flameParticle.x = self.x + Math.cos(startAngle) * startDistance;
flameParticle.y = self.y + Math.sin(startAngle) * startDistance;
// Calculate target position with spread
var targetDistance = self.getRange() * (0.6 + Math.random() * 0.4);
var targetX = self.x + Math.cos(startAngle) * targetDistance;
var targetY = self.y + Math.sin(startAngle) * targetDistance;
// Store damage info for area effect
flameParticle.damage = self.damage;
flameParticle.sourceTowerLevel = self.level;
game.addChild(flameParticle);
// Animate flame particle moving toward target area
tween(flameParticle, {
x: targetX,
y: targetY,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 400 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Apply area damage when flame reaches target area
var coneRange = CELL_SIZE * 1.2;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - flameParticle.x;
var dy = enemy.y - flameParticle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= coneRange) {
// Apply flame damage
enemy.health -= flameParticle.damage * 0.8;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Apply burning effect
if (!enemy.isImmune) {
enemy.burning = true;
enemy.burnDamage = flameParticle.damage * 0.3;
enemy.burnDuration = 180;
}
// Create visual flame effect
var flameEffect = new EffectIndicator(enemy.x, enemy.y, 'flamethrower');
game.addChild(flameEffect);
}
}
// Fade out flame particle
tween(flameParticle, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
flameParticle.destroy();
}
});
}
});
}
} else {
// Regular bullet creation for other tower types
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
// Customize bullet appearance based on tower type
switch (self.id) {
case 'sniper':
bullet.children[0].tint = 0xFF5500;
bullet.children[0].width = 15;
bullet.children[0].height = 15;
break;
case 'slow':
// Replace bullet graphics with missile asset
var missileGraphics = bullet.attachAsset('missiles', {
anchorX: 0.5,
anchorY: 0.5
});
// Remove the original bullet graphics
bullet.removeChild(bullet.children[0]);
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'tank':
bullet.children[0].tint = 0x8B4513;
bullet.children[0].width = 40;
bullet.children[0].height = 40;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
}
// --- Fire recoil effect for gunContainer ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
// Always use the original resting position for recoil, never accumulate offset
if (gunContainer._restX === undefined) {
gunContainer._restX = 0;
}
if (gunContainer._restY === undefined) {
gunContainer._restY = 0;
}
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset to resting position before animating (in case of interrupted tweens)
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Calculate recoil offset (recoil back along the gun's rotation)
var recoilDistance = 8;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
// Animate recoil back from the resting position
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original position/scale
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
self.isPlaced = true;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// Store original type before placing tower
cell.originalType = cell.type;
// Allow placement on any cell type - don't force to wall type
}
}
}
self.refreshCellsInRange();
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'trap';
self.hasEnoughGold = true;
var rangeIndicator = new Container();
self.addChild(rangeIndicator);
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3;
var previewGraphics = self.attachAsset('Box', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
var updateCounter = 0;
self.update = function () {
// Only check every 10 frames to reduce performance impact
updateCounter++;
if (updateCounter % 10 === 0) {
var previousHasEnoughGold = self.hasEnoughGold;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range without placing it
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower immediately without placing it on grid
tempTower.destroy();
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
// Color preview box based on placement validity
if (self.canPlace && self.hasEnoughGold) {
// Green for valid placement (including tank towers)
previewGraphics.tint = 0x00FF00;
// Animate green pulsing for valid placement
tween.stop(previewGraphics, {
alpha: true
});
tween(previewGraphics, {
alpha: 0.7
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(previewGraphics, {
alpha: 1.0
}, {
duration: 500,
easing: tween.easeInOut
});
}
});
} else {
// Red for invalid placement or insufficient gold
previewGraphics.tint = 0xFF0000;
// Stop any ongoing animations for invalid placement
tween.stop(previewGraphics, {
alpha: true
});
previewGraphics.alpha = 1.0;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
// Prevent placement in upper sixth of screen (y < 455)
var screenY = grid.y + self.gridY * CELL_SIZE;
if (screenY < 455) {
validGridPlacement = false;
} else {
// For tank weapons, only allow placement in lower half of screen
if (self.towerType === 'tank') {
var screenCenterY = grid.y + (29 + 6) * CELL_SIZE / 2; // Grid height / 2
if (screenY < screenCenterY) {
validGridPlacement = false;
}
}
// Allow placement anywhere else on the grid
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (!cell) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
self.blockedByEnemy = false;
if (validGridPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) {
continue;
}
// Only check non-flying enemies, flying enemies can pass over towers
if (!enemy.isFlying) {
if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
self.canPlace = validGridPlacement && !self.blockedByEnemy;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
var towerTypeName = self.tower.id === 'slow' ? 'Missiles' : self.tower.id === 'tank' ? 'Tank' : self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1);
var towerTypeText = new Text2(towerTypeName + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' gold');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// Restore original cell type or default to ground (type 1) if no original type stored
cell.type = cell.originalType !== undefined ? cell.originalType : 1;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2;
startBlock.tint = 0xffad0e;
// Add shadow for start text
var startTextShadow = new Text2("Start Battle", {
size: 50,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 4;
startTextShadow.y = 4;
startMarker.addChild(startTextShadow);
var startText = new Text2("Start Battle", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startMarker.addChild(startText);
startMarker.x = -self.indicatorWidth;
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0xffad0e;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = i + 1 === totalWaves;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0x228B22;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x2F4F2F;
waveType = "Immune";
enemyType = "immune";
enemyCount = 2;
} else if (i === 2) {
block.tint = 0x006400;
waveType = "Flying";
enemyType = "flying";
enemyCount = 3;
} else if (i === 3) {
block.tint = 0x3B5323;
waveType = "Tank";
enemyType = "tank";
enemyCount = 1;
} else if (i === 4) {
block.tint = 0x228B22;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 8) {
// Wave 9 - Immune
block.tint = 0x4A6B4A;
waveType = "Immune";
enemyType = "immune";
enemyCount = 2;
} else if (i === 9) {
// Wave 10 - Flying
block.tint = 0x006B40;
waveType = "Flying";
enemyType = "flying";
enemyCount = 3;
} else if (i === 10) {
// Wave 11 - Tank (changed from immune)
block.tint = 0x556B22;
waveType = "Tank";
enemyType = "tank";
enemyCount = 1;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Flying";
block.tint = 0x006B22;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'fast':
block.tint = 0x228B22;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0x4A6B4A;
waveType = "Boss Immune";
break;
case 'flying':
block.tint = 0x006B40;
waveType = "Boss Flying";
break;
}
}
enemyCount = 1;
// Make the wave indicator for boss waves stand out
// Set boss wave color to black
block.tint = 0x000000;
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x228B22;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is tank
block.tint = 0x4B6000;
waveType = "Tank";
enemyType = "tank";
enemyCount = 1;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0x006B56;
waveType = "Flying";
enemyType = "flying";
enemyCount = 3;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is fast
block.tint = 0x228B22;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else {
block.tint = 0x228B22;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave) {
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 4;
waveTypeShadow.y = 4;
marker.addChild(waveTypeShadow);
// Add wave type text - 30% smaller than before
var waveTypeText = new Text2(waveType, {
size: 56,
fill: 0xFFFFFF,
weight: 800
});
waveTypeText.anchor.set(0.5, 0.5);
waveTypeText.y = 0;
marker.addChild(waveTypeText);
// Add shadow for wave number - 20% larger than before
var waveNumShadow = new Text2((i + 1).toString(), {
size: 48,
fill: 0x000000,
weight: 800
});
waveNumShadow.anchor.set(1.0, 1.0);
waveNumShadow.x = blockWidth / 2 - 16 + 5;
waveNumShadow.y = block.height / 2 - 12 + 5;
marker.addChild(waveNumShadow);
// Main wave number text - 20% larger than before
var waveNum = new Text2((i + 1).toString(), {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
waveNum.anchor.set(1.0, 1.0);
waveNum.x = blockWidth / 2 - 16;
waveNum.y = block.height / 2 - 12;
marker.addChild(waveNum);
marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
self.addChild(marker);
self.waveMarkers.push(marker);
}
// Get wave type for a specific wave number
self.getWaveType = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return "fast";
}
// If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType
// then we should return a different boss type
var waveType = self.waveTypes[waveNumber - 1];
return waveType;
};
// Get enemy count for a specific wave number
self.getEnemyCount = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return 10;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Add boss prefix for boss waves (final wave)
if (waveNumber === totalWaves) {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xD2B48C
});
/****
* Game Code
****/
var gameState = 'menu'; // 'menu', 'playing'
var isHidingUpgradeMenu = false;
// Global flag for alternating fly direction
var lastDirectionWasRight = false;
function spawnFly() {
lastDirectionWasRight = !lastDirectionWasRight;
// You would set fly.vx and fly.vy here based on your fly object
// fly.vx = (lastDirectionWasRight ? 1 : -1) * horizontalSpeed;
// fly.vy = downSpeed;
// For this tower defense game, we can apply this pattern to flying enemies
// by modifying their target lanes based on direction
console.log("Fly spawning with direction:", lastDirectionWasRight ? "right" : "left");
}
// Mobile detection and sniper tower blocking
(function blockSniperTowers() {
// Safe mobile detection without navigator.userAgent
var isMobile = false;
try {
if (typeof window !== 'undefined' && window.navigator && window.navigator.userAgent) {
isMobile = /Mobi|Android/i.test(window.navigator.userAgent);
} else {
// Fallback: assume mobile if screen dimensions suggest mobile device
isMobile = typeof window !== 'undefined' && window.innerWidth && window.innerWidth < 1024;
}
} catch (e) {
// If all else fails, default to false (desktop)
isMobile = false;
}
if (!isMobile) return; // Only activate on mobile
// Override tower creation to block sniper towers
var originalTowerConstructor = Tower;
Tower = Container.expand(function (id) {
// Block sniper tower creation on mobile
if (id === 'sniper') {
console.warn("[BLOCKED] Sniper tower creation prevented on mobile.");
// Return a basic trap tower instead
return originalTowerConstructor.call(this, 'trap');
}
// Use original constructor for other tower types
return originalTowerConstructor.call(this, id);
});
// Override getTowerCost to return 0 for sniper on mobile (making it unavailable)
var originalGetTowerCost = getTowerCost;
getTowerCost = function getTowerCost(towerType) {
if (towerType === 'sniper') {
return 999999; // Make sniper prohibitively expensive on mobile
}
return originalGetTowerCost(towerType);
};
console.log("[INFO] Sniper tower blocking activated for mobile device.");
})();
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
// Ensure no pre-placed towers exist - clear any that might have been created
function clearPrePlacedTowers() {
// This function is now only used for initial game setup cleanup
// It should not aggressively remove player-placed towers during gameplay
// Only clear references, don't destroy actual towers during gameplay
selectedTower = null;
// Only clear grid cell references if grid exists and game is not in playing state
if (grid && grid.cells && gameState !== 'playing') {
for (var i = 0; i < grid.cells.length; i++) {
for (var j = 0; j < grid.cells[i].length; j++) {
var cell = grid.cells[i][j];
if (cell && cell.towersInRange) {
cell.towersInRange.length = 0;
}
}
}
}
}
// Call this function to ensure clean slate - but only after towerLayer is created
// clearPrePlacedTowers(); // Moved this call to after towerLayer creation
var bullets = [];
var defenses = [];
var selectedTower = null;
var gold = 100;
var lives = 100;
var score = 0;
var currentWave = 0;
var selectedDifficulty = null;
var totalWaves = 12;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 600; // Reduced from 1500 to 600 frames (10 seconds instead of 25)
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
// Create UI container for game elements
var gameUIContainer = new Container();
gameUIContainer.addChild(goldText);
gameUIContainer.addChild(livesText);
gameUIContainer.addChild(scoreText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
// Initially hide game UI
gameUIContainer.visible = false;
LK.gui.top.addChild(gameUIContainer);
var lastGold = -1;
var lastLives = -1;
var lastScore = -1;
function updateUI() {
if (gold !== lastGold) {
goldText.setText('Gold: ' + gold);
lastGold = gold;
}
if (lives !== lastLives) {
livesText.setText('Lives: ' + lives);
lastLives = lives;
}
if (score !== lastScore) {
scoreText.setText('Score: ' + score);
lastScore = score;
}
}
function setGold(value) {
gold = value;
updateUI();
}
var debugLayer = new Container();
var towerLayer = new Container();
// Initialize empty towers array
towers = [];
// Clear any pre-placed towers that might have been loaded with the map
clearPrePlacedTowers();
// Fast enemy can shoot - already implemented
// This functionality is already working in the Enemy class update method
// Fast enemies have shooting range, damage, cooldown, and create red bullets targeting towers
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
grid.pathFind();
grid.renderDebug();
debugLayer.addChild(grid);
// Create menu container
var menuContainer = new Container();
game.addChild(menuContainer);
// Create game container for all game elements
var gameContainer = new Container();
gameContainer.addChild(debugLayer);
gameContainer.addChild(towerLayer);
gameContainer.addChild(enemyLayer);
game.addChild(gameContainer);
// Initially hide game elements
gameContainer.visible = false;
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
var cells = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cells.push({
cell: cell,
originalType: cell.type
});
// Only set to blocking type if it's currently a path (type 0)
// Ground cells (type 1) can be built on without blocking paths
if (cell.type === 0) {
cell.type = 1;
}
}
}
}
var blocked = grid.pathFind();
for (var i = 0; i < cells.length; i++) {
cells[i].cell.type = cells[i].originalType;
}
grid.pathFind();
grid.renderDebug();
return blocked;
}
function getTowerCost(towerType) {
var cost = 30;
switch (towerType) {
case 'landmine':
cost = 10;
break;
case 'sniper':
cost = 40;
break;
case 'flamethrower':
cost = 20;
break;
case 'slow':
cost = 50;
break;
case 'poison':
cost = 55;
break;
case 'tank':
cost = 60;
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'trap');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
// Only recalculate pathfinding if needed
if (!grid.pathFind()) {
grid.renderDebug();
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
// Prevent any clicks on game elements when in menu state
if (gameState !== 'playing') {
return;
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
// Apply the same offset as in move handler to ensure consistency when starting drag
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
break;
}
}
};
game.move = function (x, y, obj) {
if (isDragging) {
// Shift the y position upward by 1.5 tiles to show preview above finger
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
}
};
game.up = function (x, y, obj) {
var clickedOnTower = false;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var towerLeft = tower.x - tower.width / 2;
var towerRight = tower.x + tower.width / 2;
var towerTop = tower.y - tower.height / 2;
var towerBottom = tower.y + tower.height / 2;
if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) {
clickedOnTower = true;
break;
}
}
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
var menuWidth = 2048;
var menuHeight = 450;
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
hideUpgradeMenu(menu);
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
grid.renderDebug();
}
}
if (isDragging) {
isDragging = false;
if (towerPreview.canPlace) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
}
};
var waveIndicator = null; // Initialize as null, will be created when game starts
// Function to create wave indicator with current totalWaves
function createWaveIndicator() {
if (waveIndicator) {
gameContainer.removeChild(waveIndicator);
}
waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 80;
gameContainer.addChild(waveIndicator);
}
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200;
nextWaveButton.y = 2732 - 100 + 20;
nextWaveButtonContainer.addChild(nextWaveButton);
// Add to game instead of gameContainer to ensure it appears on top of all map elements
game.addChild(nextWaveButtonContainer);
var towerTypes = ['landmine', 'flamethrower', 'trap', 'sniper', 'slow', 'tank'];
// Filter out sniper tower on mobile devices
var isMobile = false;
try {
if (typeof window !== 'undefined' && window.navigator && window.navigator.userAgent) {
isMobile = /Mobi|Android/i.test(window.navigator.userAgent);
} else {
// Fallback: assume mobile if screen dimensions suggest mobile device
isMobile = typeof window !== 'undefined' && window.innerWidth && window.innerWidth < 1024;
}
} catch (e) {
// If all else fails, default to false (desktop)
isMobile = false;
}
if (isMobile) {
towerTypes = towerTypes.filter(function (type) {
return type !== 'sniper';
});
console.log("[INFO] Sniper tower removed from available towers on mobile.");
}
var sourceTowers = [];
var towerSpacing = 350; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
game.addChild(tower); // Add to game instead of towerLayer to prevent it from being a placeable tower
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
// Hide weapon boxes initially
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].visible = false;
}
// Create new difficulty selection menu
// Menu background panel
var menuBg = new Container();
var menuBgGraphics = menuBg.attachAsset('MW', {
anchorX: 0.5,
anchorY: 0.5
});
menuBgGraphics.width = 2050;
menuBgGraphics.height = 2732;
menuBg.x = 2048 / 2 + 1;
menuBg.y = 2732 / 2;
menuContainer.addChild(menuBg);
// Play menu music automatically when menu is loaded
LK.playMusic('Menu1');
// Title - using TDT asset
var title = new Container();
var titleGraphics = title.attachAsset('TDT', {
anchorX: 0.5,
anchorY: 0.5
});
title.x = 2048 / 2;
title.y = 2732 * 0.1 + 333;
menuContainer.addChild(title);
// Difficulty buttons data
var difficulties = [{
label: 'HARD – 48 WAVES',
level: 'HARD',
waves: 48
}, {
label: 'MEDIUM – 24 WAVES',
level: 'MEDIUM',
waves: 24
}, {
label: 'EASY – 12 WAVES',
level: 'EASY',
waves: 12
}];
var difficultyButtons = [];
var playButton = null;
// Create difficulty buttons
for (var i = 0; i < difficulties.length; i++) {
var diff = difficulties[i];
var btn = new Container();
var btnBg = btn.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
btnBg.width = 1600;
btnBg.height = 240;
btnBg.tint = 0x004B00; // Darker green
btnBg.alpha = 0.8;
var btnText = new Text2(diff.label, {
size: 120,
fill: 0x00FF00,
weight: 800
});
btnText.anchor.set(0.5, 0.5);
btn.addChild(btnText);
btn.x = 2048 / 2;
btn.y = 2732 * (0.25 + i * 0.1) + 333 + 333;
btn.difficulty = diff;
menuContainer.addChild(btn);
difficultyButtons.push(btn);
// Add click handler to difficulty button
btn.down = function () {
selectedDifficulty = this.difficulty.level;
totalWaves = this.difficulty.waves;
// Hide instruction text once a difficulty is selected
if (difficultyInstructionText && difficultyInstructionText.visible) {
difficultyInstructionText.visible = false;
}
// Update all button appearances
for (var j = 0; j < difficultyButtons.length; j++) {
var button = difficultyButtons[j];
var bg = button.children[0];
if (button.difficulty.level === selectedDifficulty) {
bg.tint = 0x006600; // Darker green for selected
bg.alpha = 1.0;
} else {
bg.tint = 0x004B00; // Darker green for unselected
bg.alpha = 0.8;
}
}
// Show play button
if (playButton) {
playButton.visible = true;
}
};
}
// Difficulty instruction text
var difficultyInstructionText = new Text2('Please choose a difficulty level!', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
difficultyInstructionText.anchor.set(0.5, 1.0);
difficultyInstructionText.x = 2048 / 2;
difficultyInstructionText.y = 2732 - 50;
menuContainer.addChild(difficultyInstructionText);
// PLAY button - initially hidden
playButton = new Container();
var playButtonBg = playButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
playButtonBg.width = 800;
playButtonBg.height = 240;
playButtonBg.tint = 0x00AA00;
var playButtonText = new Text2('PLAY', {
size: 160,
fill: 0x00FF00,
weight: 800
});
playButtonText.anchor.set(0.5, 0.5);
playButton.addChild(playButtonText);
playButton.x = 2048 / 2;
playButton.y = (2732 * 0.45 + 666 + 2732) / 2 + 69;
playButton.visible = false;
menuContainer.addChild(playButton);
playButton.down = function () {
if (selectedDifficulty) {
// Stop menu music when starting game
LK.stopMusic();
// Generate waves based on selected difficulty
generateWaves(totalWaves);
// Create wave indicator with selected difficulty
createWaveIndicator();
// Transition from menu to game
gameState = 'playing';
menuContainer.visible = false;
gameContainer.visible = true;
gameUIContainer.visible = true;
// Show weapon boxes when game starts
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].visible = true;
}
}
};
// Wave generation function
function generateWaves(count) {
// The existing wave system will handle wave generation
// We just need to set totalWaves which is already done above
// The WaveIndicator class will use the totalWaves value
console.log('Generated', count, 'waves for difficulty:', selectedDifficulty);
}
// Function to show menu
function showMenu() {
gameState = 'menu';
menuContainer.visible = true;
gameContainer.visible = false;
gameUIContainer.visible = false;
}
// Function to show game
function showGame() {
gameState = 'playing';
menuContainer.visible = false;
gameContainer.visible = true;
gameUIContainer.visible = true;
}
function onGameStart(callback) {
// Store the callback to be executed when game starts
if (waveIndicator && waveIndicator.gameStarted) {
callback();
} else {
// Wait for game to start, then execute callback
var checkGameStart = LK.setInterval(function () {
if (waveIndicator && waveIndicator.gameStarted) {
LK.clearInterval(checkGameStart);
callback();
}
}, 100);
}
}
// Tank spawning is now handled by the main wave system in game.update
// No manual spawning needed here
game.update = function () {
// Only run game logic when in playing state
if (gameState !== 'playing') {
return;
}
if (waveInProgress && waveIndicator) {
if (!waveSpawned) {
waveSpawned = true;
// Get wave type and enemy count from the wave indicator
var waveType = waveIndicator.getWaveType(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
// Check if this is a boss wave
var isBossWave = currentWave === totalWaves;
if (isBossWave) {
// Boss waves have just 1 enemy regardless of what the wave indicator says
enemyCount = 1;
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy = new Enemy(waveType);
// Initialize flying enemy direction using global alternating flag
if (enemy.isFlying) {
// Call the spawnFly function to update the global direction flag
spawnFly();
// Set the flying enemy's initial direction based on the global flag
enemy.flyingDirection = lastDirectionWasRight ? 1 : -1; // 1 for right, -1 for left
}
// Add enemy to the appropriate layer based on type for proper display order
if (enemy.isFlying) {
// Add flying enemy to the top layer to ensure they appear above all other enemies
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else if (enemy.type === 'fast') {
// Add fast enemies to the bottom layer to ensure flying enemies appear above them
enemyLayerBottom.addChild(enemy);
} else {
// Add other ground enemies (immune, tank, etc.) to the middle layer
enemyLayerMiddle.addChild(enemy);
}
// Scale difficulty with wave number but don't apply to boss
// as bosses already have their health multiplier
// Use exponential scaling for health
var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// Assign specific lanes for different enemy types to prevent overlapping
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
var spawnX;
// Define strict non-overlapping lanes with buffer zones for different enemy types
if (waveType === 'fast') {
// Fast enemies spawn in horizontal lines
var enemiesPerLine = 5; // Number of enemies per horizontal line
var lineNumber = Math.floor(i / enemiesPerLine); // Which horizontal line this enemy belongs to
var positionInLine = i % enemiesPerLine; // Position within the line (0-4)
var lineColumns = [6, 9, 12, 15, 18]; // Available columns for horizontal lines - moved left
spawnX = lineColumns[positionInLine]; // Position enemy in the line
} else if (waveType === 'immune') {
// Immune enemies use center-left lanes (columns 11-12) with buffer from fast and tank
var immuneColumns = [11, 12];
var availableImmuneColumns = [];
for (var col = 0; col < immuneColumns.length; col++) {
var columnOccupied = false;
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === immuneColumns[col] && enemies[e].currentCellY < 8) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableImmuneColumns.push(immuneColumns[col]);
}
}
// If no columns available, queue the enemy by using the first column
spawnX = availableImmuneColumns.length > 0 ? availableImmuneColumns[Math.floor(Math.random() * availableImmuneColumns.length)] : immuneColumns[0];
} else if (waveType === 'tank') {
// Tank enemies spawn positioning based on wave
if (currentWave === 4) {
// Wave 4: Left side
var tankColumns = [3]; // Left side lane for wave 4 tank enemies
} else if (currentWave === 8) {
// Wave 8: Right side
var tankColumns = [21]; // Right side lane for wave 8 tank enemies
} else if (currentWave === 11) {
// Wave 11: Center
var tankColumns = [12]; // Center lane for wave 11 tank enemies
} else {
// Default for other waves: right side of road
var tankColumns = [21, 22]; // Right edge lanes for tank enemies
}
spawnX = tankColumns[i % tankColumns.length]; // Cycle through assigned lanes
} else {
// Flying and other enemy types use middle lanes (columns 12-14) avoiding other types
var flyingColumns = [12, 13, 14];
var availableColumns = [];
for (var col = 0; col < flyingColumns.length; col++) {
var columnOccupied = false;
// Check if any enemy is already in this column but not yet in view
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === flyingColumns[col] && enemies[e].currentCellY < 8) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(flyingColumns[col]);
}
}
// If no columns available, use the center column
spawnX = availableColumns.length > 0 ? availableColumns[Math.floor(Math.random() * availableColumns.length)] : flyingColumns[1];
}
// Calculate spawn position with much larger spacing to ensure no visual overlap
// Convert units to cell units (pixels / 76 pixels per cell)
var enemySpacing;
if (waveType === 'fast') {
// Calculate spacing between horizontal lines (not individual enemies)
var lineSpacing = 300 / CELL_SIZE; // Reduced spacing between horizontal lines (~3.9 cells)
var lineNumber = Math.floor(i / 5); // Which horizontal line this enemy belongs to
enemySpacing = lineSpacing;
// Position fast enemies in their assigned column without horizontal offset
enemy.cellX = isBossWave ? 12 : spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = isBossWave ? 12 : spawnX;
enemy.currentCellY = -1 - lineNumber * enemySpacing; // Each line spawns with line spacing, not individual spacing
} else {
if (waveType === 'tank') {
enemySpacing = 800 / CELL_SIZE; // Reduced spacing for tank enemies with better lane distribution (~10.5 cells)
} else if (waveType === 'immune') {
enemySpacing = 400 / CELL_SIZE; // Reduced spacing for immune enemies (~5.3 cells)
} else {
enemySpacing = 900 / CELL_SIZE; // Large spacing for other enemies (~11.8 cells)
}
var spawnY = -1 - i * enemySpacing; // Each enemy spawns based on enemy type spacing
enemy.cellX = isBossWave ? 12 : spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = isBossWave ? 12 : spawnX;
enemy.currentCellY = spawnY;
}
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Remove all flame particles and smoke effects from the map when enemy is destroyed
for (var flameIndex = game.children.length - 1; flameIndex >= 0; flameIndex--) {
var child = game.children[flameIndex];
// Remove EffectIndicator flame effects
if (child instanceof EffectIndicator) {
var dx = child.x - enemy.x;
var dy = child.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 3) {
child.destroy();
}
}
// Remove flame particles (Container objects with flame graphics)
else if (child.children && child.children.length > 0 && child.children[0].tint !== undefined) {
var flameGraphics = child.children[0];
// Check for flame particles by tint colors and properties
if ((flameGraphics.tint === 0xFF4500 || flameGraphics.tint === 0xFF6600) && flameGraphics.width && flameGraphics.height) {
// Stop any ongoing tweens and destroy all flame particles
tween.stop(child);
child.destroy();
}
}
// Remove flame particles that have damage property (flamethrower particles)
else if (child.damage !== undefined && child.sourceTowerLevel !== undefined) {
// Stop any ongoing tweens and destroy flamethrower particles
tween.stop(child);
child.destroy();
}
}
// Clean up continuous smoke effect if it exists
if (enemy.continuousSmokeEffect && enemy.continuousSmokeEffect.parent) {
// Stop any ongoing smoke particle tweens
for (var smokeChildIndex = 0; smokeChildIndex < enemy.continuousSmokeEffect.children.length; smokeChildIndex++) {
var smokeChild = enemy.continuousSmokeEffect.children[smokeChildIndex];
tween.stop(smokeChild);
}
enemy.continuousSmokeEffect.destroy();
enemy.continuousSmokeEffect = null;
}
// Create MASSIVE spectacular death explosion for all enemies except fast enemies (but always for bosses)
if (enemy.type !== 'fast' || enemy.isBoss) {
// Stage 1: Initial massive impact flash
var deathFlash = new EffectIndicator(enemy.x, enemy.y, 'splash');
deathFlash.children[0].tint = 0xFFFFFF; // Bright white impact flash
// Scale flash size based on boss status - use Boss asset size for scaling
var flashScale = enemy.isBoss ? 4.8 : enemy.type === 'tank' ? 2.4 : 2.0; // Boss uses 4.8x (scaled to boss asset size)
deathFlash.children[0].width = deathFlash.children[0].height = CELL_SIZE * flashScale;
deathFlash.alpha = 1;
deathFlash.scaleX = 0.1;
deathFlash.scaleY = 0.1;
game.addChild(deathFlash);
// Ultra-fast bright flash expansion - scale based on enemy type
var flashExpansionScale = enemy.isBoss ? 5.76 : enemy.type === 'tank' ? 2.88 : 2.4; // Boss uses 5.76x (scaled to boss asset size)
tween(deathFlash, {
scaleX: flashExpansionScale,
scaleY: flashExpansionScale,
alpha: 0
}, {
duration: 80,
// Faster than weapon explosions for more impact
easing: tween.easeOut
});
// Stage 2: ENORMOUS main death explosion fireball - bigger than any weapon
var mainDeathExplosion = new EffectIndicator(enemy.x, enemy.y, 'splash');
// Color based on enemy type
if (enemy.isBoss) {
mainDeathExplosion.children[0].tint = 0xFF0000; // Bright red for boss death
} else if (enemy.type === 'tank') {
mainDeathExplosion.children[0].tint = 0x8B4513; // Brown for tank enemies
} else if (enemy.type === 'immune') {
mainDeathExplosion.children[0].tint = 0xAA0000; // Dark red for immune enemies
} else if (enemy.isFlying) {
mainDeathExplosion.children[0].tint = 0xFFFF00; // Yellow for flying enemies
} else {
mainDeathExplosion.children[0].tint = 0xFF4500; // Orange for other enemies
}
mainDeathExplosion.children[0].width = mainDeathExplosion.children[0].height = CELL_SIZE * (enemy.isBoss ? 3.75 : enemy.type === 'tank' ? 3.0 : 2.5); // ENORMOUS - bigger than tank weapons
mainDeathExplosion.alpha = 1;
mainDeathExplosion.scaleX = 0.2;
mainDeathExplosion.scaleY = 0.2;
game.addChild(mainDeathExplosion);
// Dramatic ENORMOUS fireball expansion with intense bounce effect
tween(mainDeathExplosion, {
scaleX: enemy.isBoss ? 3.0 : enemy.type === 'tank' ? 2.52 : 2.1,
// Much bigger than tank weapon explosions
scaleY: enemy.isBoss ? 3.0 : enemy.type === 'tank' ? 2.52 : 2.1,
alpha: 0.95
}, {
duration: enemy.isBoss ? 400 : 300,
// Longer for bosses
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(mainDeathExplosion, {
scaleX: enemy.isBoss ? 4.5 : enemy.type === 'tank' ? 4.32 : 3.6,
// ENORMOUS final scale - biggest explosions in game
scaleY: enemy.isBoss ? 4.5 : enemy.type === 'tank' ? 4.32 : 3.6,
alpha: 0
}, {
duration: enemy.isBoss ? 1000 : 800,
// Very long for dramatic effect
easing: tween.easeIn,
onFinish: function onFinish() {
mainDeathExplosion.destroy();
}
});
}
});
// Stage 3: Create ENORMOUS debris particle field - much more particles than weapons
var particleCount = enemy.isBoss ? 48 : 32; // More particles than tank weapons
for (var particleIndex = 0; particleIndex < particleCount; particleIndex++) {
var deathParticle = new EffectIndicator(enemy.x, enemy.y, 'splash');
// Varied particle colors based on enemy type
var particleColors;
if (enemy.isBoss) {
particleColors = [0xFF0000, 0xFF3300, 0xFF6600, 0xFF9900, 0xFFCC00, 0xFFFF00, 0xFF0033, 0xFF0066];
} else if (enemy.type === 'tank') {
particleColors = [0x8B4513, 0xA0522D, 0xD2691E, 0xFF8C00, 0xCD853F, 0xDEB887, 0x654321, 0x8B7355];
} else if (enemy.type === 'immune') {
particleColors = [0xAA0000, 0xBB0000, 0xCC0000, 0xDD0000, 0xEE0000, 0xFF0000, 0x990000, 0x880000];
} else if (enemy.isFlying) {
particleColors = [0xFFFF00, 0xFFFF33, 0xFFFF66, 0xFFFF99, 0xFFFFCC, 0xFFFFFF, 0xFFCC00, 0xFF9900];
} else {
particleColors = [0xFF4500, 0xFF6600, 0xFF8800, 0xFFAA00, 0xFF2200, 0xFFCC00, 0xFF3300, 0xFFDD00];
}
deathParticle.children[0].tint = particleColors[particleIndex % particleColors.length];
deathParticle.children[0].width = deathParticle.children[0].height = CELL_SIZE * (enemy.type === 'tank' ? 0.75 + Math.random() * 0.6 : 0.625 + Math.random() * 0.5); // ENORMOUS particles - bigger than tank weapons
deathParticle.alpha = 1.0;
deathParticle.scaleX = 0.8 + Math.random() * 0.6;
deathParticle.scaleY = 0.8 + Math.random() * 0.6;
// ULTRA-MASSIVE particle scatter distance - bigger than tank weapons
var particleAngle = particleIndex / particleCount * Math.PI * 2 + (Math.random() - 0.5) * 1.0;
var particleDistance = CELL_SIZE * (1.5 + Math.random() * 1.0); // ULTRA-far scatter - bigger than tank weapons
if (enemy.isBoss) {
particleDistance *= 1.5; // Even further for bosses
}
var targetX = enemy.x + Math.cos(particleAngle) * particleDistance;
var targetY = enemy.y + Math.sin(particleAngle) * particleDistance;
game.addChild(deathParticle);
// Animate particles with ENORMOUS final scale
tween(deathParticle, {
x: targetX,
y: targetY,
scaleX: enemy.type === 'tank' ? 1.8 + Math.random() * 0.72 : 1.5 + Math.random() * 0.6,
// ENORMOUS final scale - bigger than tank weapons
scaleY: enemy.type === 'tank' ? 1.8 + Math.random() * 0.72 : 1.5 + Math.random() * 0.6,
alpha: 0.8
}, {
duration: 500 + Math.random() * 400,
// Longer duration
easing: tween.easeOut,
onFinish: function onFinish() {
tween(deathParticle, {
scaleX: 0.2,
scaleY: 0.2,
alpha: 0
}, {
duration: 600 + Math.random() * 300,
// Long fade
easing: tween.easeIn,
onFinish: function onFinish() {
deathParticle.destroy();
}
});
}
});
}
// Stage 4: ENORMOUS multiple shockwaves - bigger than tank weapon shockwaves
var primaryDeathShockwave = new EffectIndicator(enemy.x, enemy.y, 'splash');
primaryDeathShockwave.children[0].tint = enemy.isBoss ? 0xFF6666 : 0xFFAAAA; // Light red/pink shockwave
primaryDeathShockwave.children[0].alpha = 0.8;
primaryDeathShockwave.children[0].width = primaryDeathShockwave.children[0].height = CELL_SIZE * 2.5;
primaryDeathShockwave.alpha = 1.0;
game.addChild(primaryDeathShockwave);
// ENORMOUS expanding death shockwave - bigger than tank weapons
tween(primaryDeathShockwave, {
scaleX: enemy.isBoss ? 6.0 : enemy.type === 'tank' ? 5.76 : 4.8,
// ENORMOUS scale - bigger than tank weapon shockwaves
scaleY: enemy.isBoss ? 6.0 : enemy.type === 'tank' ? 5.76 : 4.8,
alpha: 0
}, {
duration: enemy.isBoss ? 1200 : 1000,
// Very long expansion
easing: tween.easeOut,
onFinish: function onFinish() {
primaryDeathShockwave.destroy();
}
});
// Multiple additional death shockwaves for ULTRA-MASSIVE effect
var shockwaveCount = enemy.isBoss ? 6 : 4; // More shockwaves for bosses
for (var shockIndex = 1; shockIndex <= shockwaveCount; shockIndex++) {
var additionalDeathShockwave = new EffectIndicator(enemy.x, enemy.y, 'splash');
additionalDeathShockwave.children[0].tint = enemy.isBoss ? 0xFF4444 : 0xFF8888; // Darker red shockwave
additionalDeathShockwave.children[0].alpha = 0.6 - shockIndex * 0.08;
additionalDeathShockwave.children[0].width = additionalDeathShockwave.children[0].height = CELL_SIZE * (2.5 - shockIndex * 0.2);
additionalDeathShockwave.alpha = 0.8 - shockIndex * 0.1;
game.addChild(additionalDeathShockwave);
// Delayed ENORMOUS death shockwaves
tween(additionalDeathShockwave, {
scaleX: (enemy.isBoss ? 6.6 : enemy.type === 'tank' ? 6.48 : 5.4) + shockIndex * (enemy.type === 'tank' ? 0.54 : 0.45),
// ENORMOUS expanding scale
scaleY: (enemy.isBoss ? 6.6 : enemy.type === 'tank' ? 6.48 : 5.4) + shockIndex * (enemy.type === 'tank' ? 0.54 : 0.45),
alpha: 0
}, {
duration: (enemy.isBoss ? 1300 : 1100) + shockIndex * 150,
delay: shockIndex * (enemy.isBoss ? 300 : 250),
easing: tween.easeOut,
onFinish: function onFinish() {
additionalDeathShockwave.destroy();
}
});
}
// Stage 5: ENORMOUS lingering death smoke cloud - bigger and longer than tank weapons
var deathSmokeCloud = new EffectIndicator(enemy.x, enemy.y, 'splash');
deathSmokeCloud.children[0].tint = 0x444444; // Dark gray death smoke
deathSmokeCloud.children[0].alpha = 0.8;
deathSmokeCloud.children[0].width = deathSmokeCloud.children[0].height = CELL_SIZE * (enemy.isBoss ? 3.0 : enemy.type === 'tank' ? 3.0 : 2.5); // ENORMOUS smoke cloud
deathSmokeCloud.alpha = 0;
deathSmokeCloud.scaleX = 1.5;
deathSmokeCloud.scaleY = 1.5;
game.addChild(deathSmokeCloud);
// Delayed ENORMOUS death smoke appearance
tween(deathSmokeCloud, {
alpha: 1.0,
scaleX: enemy.isBoss ? 3.0 : enemy.type === 'tank' ? 2.88 : 2.4,
// Bigger initial expansion than tank weapons
scaleY: enemy.isBoss ? 3.0 : enemy.type === 'tank' ? 2.88 : 2.4
}, {
duration: 600,
delay: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
// Death smoke slowly dissipates over ULTRA-LONG time
tween(deathSmokeCloud, {
alpha: 0,
scaleX: enemy.isBoss ? 5.4 : enemy.type === 'tank' ? 5.76 : 4.8,
// ENORMOUS final scale - bigger than tank weapons
scaleY: enemy.isBoss ? 5.4 : enemy.type === 'tank' ? 5.76 : 4.8
}, {
duration: enemy.isBoss ? 3000 : 2500,
// ULTRA-LONG dissipation - longer than tank weapons
easing: tween.easeIn,
onFinish: function onFinish() {
deathSmokeCloud.destroy();
}
});
}
});
}
// Special boss explosion effect and sound
if (enemy.isBoss) {
// Create massive boss explosion effect
var bossExplosion = new EffectIndicator(enemy.x, enemy.y, 'splash');
bossExplosion.children[0].tint = 0xFFA500; // Orange explosion color
bossExplosion.children[0].width = bossExplosion.children[0].height = 150; // Large explosion radius
bossExplosion.alpha = 1.0;
bossExplosion.scaleX = 0.5;
bossExplosion.scaleY = 0.5;
game.addChild(bossExplosion);
// Multi-stage boss explosion animation
tween(bossExplosion, {
scaleX: 3.0,
scaleY: 3.0,
alpha: 0.8
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(bossExplosion, {
scaleX: 5.0,
scaleY: 5.0,
alpha: 0
}, {
duration: 700,
easing: tween.easeIn,
onFinish: function onFinish() {
bossExplosion.destroy();
}
});
}
});
// Play boss explosion sound effect
try {
var explosionSound = new Audio();
explosionSound.src = "https://cdn.pixabay.com/download/audio/2022/03/02/audio_f27e2020ce.mp3?filename=explosion-6075.mp3";
explosionSound.volume = 1.0;
explosionSound.play()["catch"](function (error) {
console.log("Could not play boss explosion sound:", error);
});
} catch (error) {
console.log("Boss explosion sound not available:", error);
}
}
// Boss enemies give more gold and score - significantly increased rewards
var goldEarned = enemy.isBoss ? Math.floor(200 + (enemy.waveNumber - 1) * 25) : Math.floor(15 + (enemy.waveNumber - 1) * 3);
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else if (enemy.type === 'fast') {
enemyLayerBottom.removeChild(enemy);
} else {
enemyLayerMiddle.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
// Only update enemy position every few frames to reduce performance impact
if (LK.ticks % 2 === 0) {
if (grid.updateEnemy(enemy)) {
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else if (enemy.type === 'fast') {
enemyLayerBottom.removeChild(enemy);
} else {
enemyLayerMiddle.removeChild(enemy);
}
enemies.splice(a, 1);
// BOSS Enemy reached the goal - reduce lives by 100 (100% of 100 total lives)
if (enemy.isBoss) {
lives = Math.max(0, lives - 100);
} else {
// Each individual enemy reduces lives by 10 (10% of 100 total lives)
lives = Math.max(0, lives - 10);
}
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
}
}
}
// Only clean bullets every 5 frames to reduce processing
if (LK.ticks % 5 === 0) {
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
if (!bullet.parent) {
if (bullet.targetEnemy) {
var targetEnemy = bullet.targetEnemy;
if (targetEnemy && targetEnemy.bulletsTargetingThis) {
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullet);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
}
// Clear bullet references
bullet.targetEnemy = null;
bullets.splice(i, 1);
}
}
}
// Check for landmine towers that need to be destroyed after explosion
for (var i = towers.length - 1; i >= 0; i--) {
var tower = towers[i];
if (tower.shouldDestroy) {
// Remove from grid cells
var gridX = tower.gridX;
var gridY = tower.gridY;
for (var j = 0; j < 2; j++) {
for (var k = 0; k < 2; k++) {
var cell = grid.getCell(gridX + j, gridY + k);
if (cell) {
// Restore original cell type or default to ground (type 1) if no original type stored
cell.type = cell.originalType !== undefined ? cell.originalType : 1;
var towerIndex = cell.towersInRange.indexOf(tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
// Remove from towers array and tower layer
towers.splice(i, 1);
towerLayer.removeChild(tower);
// Clean up any upgrade menus or range indicators
if (selectedTower === tower) {
selectedTower = null;
}
// Remove any upgrade menus for this tower
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu && child.tower === tower;
});
for (var menuIndex = 0; menuIndex < upgradeMenus.length; menuIndex++) {
upgradeMenus[menuIndex].destroy();
}
// Remove any range indicators for this tower
for (var rangeIndex = game.children.length - 1; rangeIndex >= 0; rangeIndex--) {
if (game.children[rangeIndex].isTowerRange && game.children[rangeIndex].tower === tower) {
game.removeChild(game.children[rangeIndex]);
}
}
// Recalculate pathfinding
grid.pathFind();
grid.renderDebug();
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
};
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
Light grey asphalt texture. Top view
Armed off-road vehicle with camouflage livery. Top view.
Military tank, top view
Army Soldier with gun. Topview.
Bomber Drone with spinning rotors, from top view.
Army box,but no text needed on image. top view
landmine from top view
Sniper man from top view
Rocket launcher cannon, from top view.
Army soldier laying with machine gun, top view from back
Missile, top view