User prompt
Ben düşmanların her wawede ywrleştirilen son kaleye kadar ilerlemesini istiyorum yarı yolda ölmemeliler
User prompt
Potansiyel sorunları düzelt
User prompt
Oyundaki bütğn hesaplamaları biliyorsun kale yükseltmeleri vs hesaplayarak bana sıfırdan waweler oluştur
User prompt
Dalgalardaki tüm düşmanlar degil sadece 1. Dalgadaki düşmanlar 110 canla başlıcak her yeni wawede canları %30 artıcak
User prompt
1. Dalgadaki düşmanların canını 110 yap
User prompt
Bronz sesi normal düşman için kullanma
User prompt
Ozaman uçan düşmanları azalt ama can barlarını yükselt
User prompt
2. Dalgada 9 adet immune olsun
User prompt
Uçandüşman gölgelendirmeyi kaldır
User prompt
Uçandüşman için gölgeleri kaldır
User prompt
Keskinnişancımermisi normaldüşman için olan gogogo sesini kullanma
User prompt
Gzlk sesi 2 saniye geciktir ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Çalışmıyor
User prompt
Keepmoving sesi swarmdüşman içinde geçerli
User prompt
Youcantstopus sesi artık normaldüşman yerine swarmdüşman için kullanılacak
User prompt
Duramazsın sesüni biriaz geciktir ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Çakışan sesler için geciktirme kullan veya bazı seslerin sesini biraz azalt ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Skor resim varlıgını rakam ile ortala
User prompt
Yavaşkule düşmanları daha az yavaşlatsın ama aynı oranda hasarı biraz fazla olsun
User prompt
Hoparlör beyaz renk olsun
User prompt
Hepsinde çalışmıyor
User prompt
Bunu yapabilirmisin bi deniyrlim ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Poisonkule ımmune düşman etkileşimi için ses ekle adı mask
User prompt
Krk sesi yavaş kule için degil splash kule için
User prompt
Krk sesi bagışıkdüşman ve splash kule etkilesimi için degiştir
/**** * 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 () { 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); // Add more concentrated bullet movement animation if (!self.animationPhase) { self.animationPhase = Math.random() * Math.PI * 2; } self.animationPhase += 0.3; var concentratedBounce = Math.sin(self.animationPhase) * 2; // Reduced from larger values var concentratedSway = Math.cos(self.animationPhase * 0.7) * 1.5; // More controlled sway // Apply subtle animation offset to bullet position var animatedX = self.x + concentratedSway; var animatedY = self.y + concentratedBounce; // Recalculate movement vectors using animated position for smoother trajectory dx = self.targetEnemy.x - animatedX; dy = self.targetEnemy.y - animatedY; distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy 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; } // Play youarekilling me sound on first successful bullet hit with delay // Only for default bullets and normal enemies if (!window.youAreKillingMeSoundPlayed && (!self.type || self.type === 'default') && self.targetEnemy.type === 'normal') { window.youAreKillingMeSoundPlayed = true; // Delay the youarekilling me sound by 1200ms for dramatic effect tween({}, {}, { duration: 1200, onFinish: function onFinish() { LK.getSound('youarekillingme').play(); } }); } // Track default bullet hits and play "it didn't hurt" sound on 7th hit (only for normal enemies) if ((!self.type || self.type === 'default') && !self.targetEnemy.isFlying && self.targetEnemy.type === 'normal') { defaultBulletHitCount++; // Play "it didn't hurt" sound only once on 7th default bullet hit with delay if (defaultBulletHitCount === 7 && !itDidntHurtSoundPlayed) { itDidntHurtSoundPlayed = true; // Delay the sound by 1200ms for clear speech tween({}, {}, { duration: 1200, onFinish: function onFinish() { LK.getSound('itdidnthurt').play(); } }); } // Play "you can't stop us" sound only once on 10th default bullet hit with delay if (defaultBulletHitCount === 10 && !youCantStopUsSoundPlayed) { youCantStopUsSoundPlayed = true; // Delay the sound by 1200ms for clear speech tween({}, {}, { duration: 1200, onFinish: function onFinish() { LK.getSound('youcantstopus').play(); } }); } } // Play "you will never give up, will you?" sound only once on first rapid bullet hit with delay (only for normal enemies) if (self.type === 'rapid' && !youWillNeverGiveUpSoundPlayed && self.targetEnemy.type === 'normal') { youWillNeverGiveUpSoundPlayed = true; // Delay the sound by 1200ms for clear speech tween({}, {}, { duration: 1200, onFinish: function onFinish() { LK.getSound('youwillnevergiveup').play(); } }); } // Track rapid bullet hits and play "gıdıklanıyorum" sound only once on 13th rapid bullet hit with delay (only for normal enemies) if (self.type === 'rapid' && !self.targetEnemy.isFlying && self.targetEnemy.type === 'normal') { // Increment rapid bullet hit counter (using defaultBulletHitCount as rapid bullet counter) if (!window.rapidBulletHitCount) { window.rapidBulletHitCount = 0; } window.rapidBulletHitCount++; // Play "gıdıklanıyorum" sound only once on 13th rapid bullet hit with delay if (window.rapidBulletHitCount === 13 && !gidiklaniyorumSoundPlayed) { gidiklaniyorumSoundPlayed = true; // Delay the sound by 1400ms for clear speech tween({}, {}, { duration: 1400, onFinish: function onFinish() { LK.getSound('gidiklaniyorum').play(); } }); } } // Splash bullet hit detection - goddamit sound now plays on bullet creation instead of hit if (self.type === 'splash' && !self.targetEnemy.isFlying && self.targetEnemy.type === 'normal') { // Sound is now played when bullet is created, not when it hits } // Create blood animation for rapid, sniper, and default bullet hits (before applying special effects) // Skip blood animation for flying enemies, boss enemies, swarm enemies, and immune enemies - using object pool with reduced particle count if ((self.type === 'rapid' || self.type === 'sniper' || !self.type || self.type === 'default') && !self.targetEnemy.isFlying && !self.targetEnemy.isBoss && self.targetEnemy.type !== 'swarm' && !self.targetEnemy.isImmune) { // Dynamic blood particle count based on enemy count - reduce when more than 10 enemies var bloodParticleCount = 4; // Default particle count if (enemies.length > 10) { bloodParticleCount = 2; // Reduce to 2 particles when more than 10 enemies } for (var bloodIdx = 0; bloodIdx < bloodParticleCount; bloodIdx++) { var bloodParticle = getBloodParticle(); var bloodGraphics = bloodParticle.bloodGraphics; bloodGraphics.width = 8 + Math.random() * 12; bloodGraphics.height = bloodGraphics.width; bloodGraphics.tint = 0xFF0000; // Red blood color // Create blood splatter pattern var splatterAngle = bloodIdx / bloodParticleCount * Math.PI * 2 + Math.random() * 0.5; var splatterDistance = 15 + Math.random() * 25; bloodParticle.x = self.targetEnemy.x + Math.cos(splatterAngle) * splatterDistance; bloodParticle.y = self.targetEnemy.y + Math.sin(splatterAngle) * splatterDistance; bloodParticle.alpha = 0.8 + Math.random() * 0.2; bloodParticle.scaleX = 0.5 + Math.random() * 0.4; bloodParticle.scaleY = 0.5 + Math.random() * 0.4; game.addChild(bloodParticle); // Animate blood particles falling and fading var targetY = bloodParticle.y + (30 + Math.random() * 20); var targetX = bloodParticle.x + (Math.random() - 0.5) * 15; tween(bloodParticle, { x: targetX, y: targetY, alpha: 0, scaleX: bloodParticle.scaleX * 0.3, scaleY: bloodParticle.scaleY * 0.3 }, { duration: 800 + Math.random() * 400, easing: tween.easeOut, onFinish: function onFinish() { returnParticle(bloodParticle); } }); } } // Apply special effects based on bullet type if (self.type === 'splash') { // Create black smoke effects using particle pool var smokeCount = enemies.length > 10 ? 8 : 12; // Reduce smoke when many enemies for (var smokeIdx = 0; smokeIdx < smokeCount; smokeIdx++) { var smokeParticle = getSmokeParticle(); var smokeGraphics = smokeParticle.smokeGraphics; smokeGraphics.width = 25 + Math.random() * 35; smokeGraphics.height = smokeGraphics.width; smokeGraphics.tint = 0x2a2a2a; // Dark smoke color smokeParticle.x = self.targetEnemy.x + (Math.random() - 0.5) * 60; smokeParticle.y = self.targetEnemy.y + (Math.random() - 0.5) * 60; smokeParticle.alpha = 0.8 + Math.random() * 0.2; smokeParticle.scaleX = 0.3 + Math.random() * 0.4; smokeParticle.scaleY = 0.3 + Math.random() * 0.4; game.addChild(smokeParticle); // Animate smoke rising and fading var targetY = smokeParticle.y - 80 - Math.random() * 40; var targetX = smokeParticle.x + (Math.random() - 0.5) * 40; tween(smokeParticle, { x: targetX, y: targetY, alpha: 0, scaleX: smokeParticle.scaleX * 2.5, scaleY: smokeParticle.scaleY * 2.5 }, { duration: 1200 + Math.random() * 800, easing: tween.easeOut, onFinish: function onFinish() { returnParticle(smokeParticle); } }); } // Create fire area effect using particle pool var fireAreaRadius = CELL_SIZE * 1.5; var fireCount = enemies.length > 10 ? 8 : 12; // Reduce fire particles when many enemies for (var fireIdx = 0; fireIdx < fireCount; fireIdx++) { var fireParticle = getFireParticle(); var fireGraphics = fireParticle.fireGraphics; fireGraphics.width = 12 + Math.random() * 16; fireGraphics.height = fireGraphics.width; // Fire color gradient: red to orange to yellow var fireColors = [0xff4500, 0xff6600, 0xff8800, 0xffaa00, 0xffcc00]; fireGraphics.tint = fireColors[Math.floor(Math.random() * fireColors.length)]; // Position fire particles in a circle around impact var angle = fireIdx / 8 * Math.PI * 2 + Math.random() * 0.5; var distance = Math.random() * fireAreaRadius; fireParticle.x = self.targetEnemy.x + Math.cos(angle) * distance; fireParticle.y = self.targetEnemy.y + Math.sin(angle) * distance; fireParticle.alpha = 0.9; fireParticle.scaleX = 0.5 + Math.random() * 0.5; fireParticle.scaleY = 0.5 + Math.random() * 0.5; game.addChild(fireParticle); // Animate fire flickering and burning out tween(fireParticle, { alpha: 0, scaleX: fireParticle.scaleX * 1.8, scaleY: fireParticle.scaleY * 1.8, y: fireParticle.y - 20 - Math.random() * 30 }, { duration: 600 + Math.random() * 400, easing: tween.easeOut, onFinish: function onFinish() { returnParticle(fireParticle); } }); } // Visual splash effect removed - no green flash // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'slow') { // Get the range from the source tower for slow effect area var slowRadius = self.sourceTower ? self.sourceTower.getRange() : CELL_SIZE * 3.5; // Use tower's actual range var affectedEnemies = []; // Find all enemies within slow radius from impact point for (var i = 0; i < enemies.length; i++) { var nearbyEnemy = enemies[i]; if (nearbyEnemy && nearbyEnemy.parent && nearbyEnemy.health > 0) { var dx = nearbyEnemy.x - self.targetEnemy.x; // Use impact point as center var dy = nearbyEnemy.y - self.targetEnemy.y; // Use impact point as center var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= slowRadius && !nearbyEnemy.isImmune) { affectedEnemies.push(nearbyEnemy); } } } // Apply slow effect to all affected enemies for (var i = 0; i < affectedEnemies.length; i++) { var affectedEnemy = affectedEnemies[i]; // Create visual slow effect for each affected enemy var slowEffect = new EffectIndicator(affectedEnemy.x, affectedEnemy.y, 'slow'); game.addChild(slowEffect); // Apply slow effect // Make slow percentage scale with source tower level if available 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 (!affectedEnemy.slowed) { affectedEnemy.originalSpeed = affectedEnemy.speed; affectedEnemy.speed *= 1 - slowPct; // Slow by X% affectedEnemy.slowed = true; affectedEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { affectedEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'poison') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Increment poison bullet hit counter for tracking (only for normal enemies) if (!self.targetEnemy.isFlying && self.targetEnemy.type === 'normal') { poisonBulletHitCount++; // Play gogogo sound only once for poison bullets on first poison bullet hit if (poisonBulletHitCount === 1 && !gogogoPoisonSoundPlayed) { gogogoPoisonSoundPlayed = true; LK.getSound('gogogo').play(); } // Play whofarted sound only once on second poison bullet hit with delay if (poisonBulletHitCount === 2 && !whoFartedSoundPlayed) { whoFartedSoundPlayed = true; // Delay the whofarted sound by 800ms tween({}, {}, { duration: 800, onFinish: function onFinish() { LK.getSound('whofarted').play(); } }); } } // Play poison bullet hit sound every 3rd hit for non-flying enemies (separate from the counter above) if (!self.targetEnemy.isFlying) { // Initialize global poison hit counter if not exists if (!window.globalPoisonHitCount) { window.globalPoisonHitCount = 0; } window.globalPoisonHitCount++; // Play poison bullet hit sound every 3rd hit if (window.globalPoisonHitCount % 3 === 0) { LK.getSound('poison_bullet_hit').play(); } } // Coughing animation removed for poison bullets // Poison bullet impact animation removed // 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') { // Sniper hit - play becareful sound only once on first contact (only for normal enemies) if (!beCarefulSoundPlayed && self.targetEnemy.type === 'normal') { beCarefulSoundPlayed = true; LK.getSound('becareful').play(); } // Track sniper bullet hits and play "sniperr" sound only once on 3rd hit (only for normal enemies) if (!self.targetEnemy.isFlying && self.targetEnemy.type === 'normal') { sniperBulletHitCount++; // Play "sniperr" sound only once on 3rd sniper bullet hit if (sniperBulletHitCount === 3 && !sniperrSoundPlayed) { sniperrSoundPlayed = true; LK.getSound('sniperr').play(); } // Play "keepmoving" sound only once on 4th sniper bullet hit if (sniperBulletHitCount === 4 && !keepMovingSoundPlayed) { keepMovingSoundPlayed = true; LK.getSound('keepmoving').play(); } // Play "gogogo" sound only once on 7th sniper bullet hit if (sniperBulletHitCount === 7 && !gogogoSniperSoundPlayed) { gogogoSniperSoundPlayed = true; LK.getSound('gogogo').play(); } } // Track sniper bullet hits on immune enemies and play "shy" sound on 3rd hit - only once per wave if (self.targetEnemy.isImmune && !self.targetEnemy.isFlying) { // Initialize counter if it doesn't exist if (!window.sniperImmuneHitCounter) { window.sniperImmuneHitCounter = 0; } window.sniperImmuneHitCounter++; if (window.sniperImmuneHitCounter === 3 && !shySoundPlayedThisWave) { shySoundPlayedThisWave = true; // Delay the shy sound by 2000ms (2 seconds) tween({}, {}, { duration: 2000, onFinish: function onFinish() { LK.getSound('shy').play(); } }); } } } self.destroy(); } else { var angle = Math.atan2(dy, dx); // Apply concentrated movement with subtle animation var baseMovementX = Math.cos(angle) * self.speed; var baseMovementY = Math.sin(angle) * self.speed; // Add very subtle concentrated bouncing to movement var concentratedBounceX = Math.sin(self.animationPhase * 1.2) * 0.8; // Much smaller bounce var concentratedBounceY = Math.cos(self.animationPhase * 1.5) * 0.6; // Reduced bounce self.x += baseMovementX + concentratedBounceX; self.y += baseMovementY + concentratedBounceY; } }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); cellGraphics.tint = 0x880000; return; } numberLabel.visible = false; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; } else { cellGraphics.tint = 0x88 - tint << 8 | tint; } // Hide direction arrows by not displaying them while (debugArrows.length > 0) { self.removeChild(debugArrows.pop()); } // Direction arrows are now hidden break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0x008800; numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); }; }); // This update method was incorrectly placed here and should be 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; } 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 = .01; 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; // 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 *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'big': self.maxHealth = 300; // Much stronger than normal enemies self.speed *= 0.7; // Slower movement break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && 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.type === 'bigboss') { assetId = 'bigboss'; } else if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.25, scaleY: 1.25 }); // Add walking feet for normal and swarm enemies only (no feet for boss enemies) self.leftFoot = null; self.rightFoot = null; if ((self.type === 'normal' || self.type === 'swarm') && !self.isBoss) { // Adjust foot size based on enemy type var footWidth = 18; var footHeight = 13; var footSpacing = 15; var footYPosition = 35; // Use normal elliptical feet self.leftFoot = getWalkingFeetParticle(); self.leftFoot.feetGraphics.width = footWidth; self.leftFoot.feetGraphics.height = footHeight; self.leftFoot.feetGraphics.tint = 0x000000; // Black color for walking feet self.rightFoot = getWalkingFeetParticle(); self.rightFoot.feetGraphics.width = footWidth; self.rightFoot.feetGraphics.height = footHeight; self.rightFoot.feetGraphics.tint = 0x000000; // Black color for walking feet // Position feet relative to enemy size self.leftFoot.x = -footSpacing; // Position to the left self.leftFoot.y = footYPosition; // Position lower at bottom of enemy self.addChild(self.leftFoot); self.rightFoot.x = footSpacing; // Position to the right self.rightFoot.y = footYPosition; // Position lower at bottom of enemy self.addChild(self.rightFoot); // Initialize foot animation variables self.leftFootPhase = 0; self.rightFootPhase = Math.PI; // Start opposite phase for alternating steps } // Scale up boss enemies if (self.isBoss) { if (self.type === 'bigboss') { enemyGraphics.scaleX = 2.5; enemyGraphics.scaleY = 2.5; } else { 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, scaleX: 1.25, scaleY: 1.25 }); // 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; // Initialize health bar visibility - hide by default until damage is taken healthBarOutline.visible = false; healthBarBG.visible = false; healthBar.visible = false; self.hasBeenDamaged = false; // Track if enemy has taken damage // Initialize walking animation variables self.walkAnimationPhase = Math.random() * Math.PI * 2; // Random starting phase for each enemy self.walkAnimationSpeed = 0.15; // Animation speed self.walkBobAmount = 3; // How much to bob up and down self.lastWalkingState = false; self.isCurrentlyMakingSound = false; // Flag to limit footstep sounds self.hasPlayedFirstPoisonSound = false; // Track if gogogo sound has been played for this enemy self.update = function () { // Track last health for damage animation if (self.lastHealth === undefined) { self.lastHealth = self.health; } // Check if enemy took damage this frame (blood animation now handled in Bullet class) if (self.lastHealth > self.health) { // Show health bar when enemy takes damage for the first time if (!self.hasBeenDamaged) { self.hasBeenDamaged = true; healthBarOutline.visible = true; healthBarBG.visible = true; healthBar.visible = true; } } // Hide health bar when enemy is at full health if (self.health >= self.maxHealth && self.hasBeenDamaged) { healthBarOutline.visible = false; healthBarBG.visible = false; healthBar.visible = false; } else if (self.hasBeenDamaged && self.health < self.maxHealth) { // Keep health bar visible when damaged but not at full health healthBarOutline.visible = true; healthBarBG.visible = true; healthBar.visible = true; } // Update last health self.lastHealth = self.health; if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = 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 } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Only show slow effect tint when both poisoned and slowed enemyGraphics.tint = 0x9900FF; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } // Walking animation logic - enhanced for realism (disabled for flying enemies) // Check if enemy is on screen (has appeared) for animation and sound var isOnScreen = self.currentCellY >= -1; // Enemy is visible or about to be visible var isCurrentlyWalking = isOnScreen && !self.isFlying && (self.currentTarget && (self.currentTarget.x !== self.currentCellX || self.currentTarget.y !== self.currentCellY) || self.currentCellY < 4); // Batch walking animations - only animate every 3rd frame when many enemies var shouldAnimate = enemies.length <= 15 || (LK.ticks + self.waveNumber) % 3 === 0; // Update walking animation if enemy is moving and on screen (but not for flying enemies) if (isCurrentlyWalking && shouldAnimate) { // Advance animation phase based on actual movement speed for realistic timing var speedMultiplier = self.speed * 100; // Scale animation speed with movement speed self.walkAnimationPhase += self.walkAnimationSpeed * speedMultiplier; // Create more realistic walking motion with multiple animation components var primaryBob = Math.sin(self.walkAnimationPhase) * self.walkBobAmount; var secondaryBob = Math.sin(self.walkAnimationPhase * 2) * (self.walkBobAmount * 0.3); var combinedBobOffset = primaryBob + secondaryBob; // Add subtle horizontal sway for more natural movement var horizontalSway = Math.sin(self.walkAnimationPhase * 0.5) * (self.walkBobAmount * 0.2); // Animate feet for normal and swarm enemies only (no feet for boss enemies) if ((self.type === 'normal' || self.type === 'swarm') && !self.isBoss && self.leftFoot && self.rightFoot) { // Update foot animation phases self.leftFootPhase += self.walkAnimationSpeed * speedMultiplier * 2; self.rightFootPhase += self.walkAnimationSpeed * speedMultiplier * 2; // Keep feet at fixed positions var footYPos = 35; var footSpacing = 15; self.leftFoot.y = footYPos; // Fixed position self.leftFoot.x = -footSpacing; // Fixed position self.rightFoot.y = footYPos; // Fixed position self.rightFoot.x = footSpacing; // Fixed position // Use tween for smooth scaling animation var leftFootScale = 1 + Math.abs(Math.sin(self.leftFootPhase)) * 0.4; // Bigger scaling effect var rightFootScale = 1 + Math.abs(Math.sin(self.rightFootPhase)) * 0.4; // Bigger scaling effect // Apply scaling animation using tween for smoothness if (self.leftFoot && !self.leftFoot.isAnimating) { self.leftFoot.isAnimating = true; tween(self.leftFoot, { scaleX: leftFootScale, scaleY: leftFootScale }, { duration: 100, easing: tween.easeInOut, onFinish: function onFinish() { if (self.leftFoot) { self.leftFoot.isAnimating = false; } } }); } if (self.rightFoot && !self.rightFoot.isAnimating) { self.rightFoot.isAnimating = true; tween(self.rightFoot, { scaleX: rightFootScale, scaleY: rightFootScale }, { duration: 100, easing: tween.easeInOut, onFinish: function onFinish() { if (self.rightFoot) { self.rightFoot.isAnimating = false; } } }); } // Synchronize feet rotation with enemy rotation if (self.leftFoot && self.rightFoot && enemyGraphics.targetRotation !== undefined) { var targetFootRotation = enemyGraphics.targetRotation; // Smoothly rotate feet to match enemy direction if (Math.abs(targetFootRotation - (self.leftFoot.rotation || 0)) > 0.05) { tween(self.leftFoot, { rotation: targetFootRotation }, { duration: 250, easing: tween.easeOut }); } if (Math.abs(targetFootRotation - (self.rightFoot.rotation || 0)) > 0.05) { tween(self.rightFoot, { rotation: targetFootRotation }, { duration: 250, easing: tween.easeOut }); } } } // Apply different animation intensity based on enemy type var animationIntensity = 1; switch (self.type) { case 'fast': animationIntensity = 1.5; // More energetic movement break; case 'immune': animationIntensity = 0.8; // More controlled movement break; case 'swarm': animationIntensity = 1.3; // Quick, jittery movement break; } // Apply boss scaling for more imposing movement if (self.isBoss) { animationIntensity *= 0.7; // Slower, more deliberate movement combinedBobOffset *= 1.2; // But with more weight } // Smooth animation transitions using tweening var targetY = combinedBobOffset * animationIntensity; var targetX = horizontalSway * animationIntensity; // Use tween for smoother animation instead of direct assignment if (!self.animatingMovement) { self.animatingMovement = true; tween(enemyGraphics, { y: targetY, x: targetX }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { self.animatingMovement = false; } }); } // Add slight rotation for more dynamic movement var walkRotation = Math.sin(self.walkAnimationPhase * 1.5) * 0.05; // Very subtle rotation if (!self.animatingRotation) { self.animatingRotation = true; tween(enemyGraphics, { rotation: enemyGraphics.rotation + walkRotation }, { duration: 100, easing: tween.easeInOut, onFinish: function onFinish() { self.animatingRotation = false; } }); } // Play footstep sound at specific points in the walking cycle when enemy is on screen (not for flying enemies) var walkCycle = Math.floor(self.walkAnimationPhase / (Math.PI / 2)) % 4; var lastWalkCycle = Math.floor((self.walkAnimationPhase - self.walkAnimationSpeed * speedMultiplier) / (Math.PI / 2)) % 4; // Play walking sound for non-flying enemies with reduced frequency to avoid audio overload if (walkCycle !== lastWalkCycle && walkCycle % 2 === 0 && !self.isCurrentlyMakingSound) { // Limit walking sounds based on enemy count to prevent audio chaos var enemyCount = enemies ? enemies.length : 0; var shouldPlaySound = false; // Play sound based on enemy count - fewer enemies = more sounds if (enemyCount <= 5) { shouldPlaySound = true; // Always play for small groups } else if (enemyCount <= 15) { shouldPlaySound = Math.random() < 0.3; // 30% chance for medium groups } else { shouldPlaySound = Math.random() < 0.1; // 10% chance for large groups } if (shouldPlaySound) { self.isCurrentlyMakingSound = true; LK.getSound('walking').play(); // Reset sound flag after a short delay tween({}, {}, { duration: 300, onFinish: function onFinish() { self.isCurrentlyMakingSound = false; } }); } } } else { // Smoothly return to resting position when not walking (only for non-flying enemies) if (!self.isFlying && (enemyGraphics.y !== 0 || enemyGraphics.x !== 0)) { tween.stop(enemyGraphics, { y: true, x: true, rotation: true }); tween(enemyGraphics, { y: 0, x: 0, rotation: enemyGraphics.targetRotation || 0 }, { duration: 200, easing: tween.easeOut }); self.animatingMovement = false; self.animatingRotation = false; } // Reset feet to resting position for normal and swarm enemies only when not walking if ((self.type === 'normal' || self.type === 'swarm') && !self.isBoss && self.leftFoot && self.rightFoot && !isCurrentlyWalking) { // Stop any ongoing animations tween.stop(self.leftFoot, { scaleX: true, scaleY: true }); tween.stop(self.rightFoot, { scaleX: true, scaleY: true }); // Reset animation flags with null checks if (self.leftFoot) { self.leftFoot.isAnimating = false; } if (self.rightFoot) { self.rightFoot.isAnimating = false; } // Return feet to normal scale and fixed positions, maintaining rotation sync var restFootYPos = 35; var restFootSpacing = 15; tween(self.leftFoot, { y: restFootYPos, x: -restFootSpacing, scaleX: 1, scaleY: 1, rotation: enemyGraphics.targetRotation || 0 }, { duration: 200, easing: tween.easeOut }); tween(self.rightFoot, { y: restFootYPos, x: restFootSpacing, scaleX: 1, scaleY: 1, rotation: enemyGraphics.targetRotation || 0 }, { duration: 200, easing: tween.easeOut }); } } 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); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { 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: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); // BossEnemy class removed - using regular enemies with health multipliers instead 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 */ for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; if (i > 11 - 3 && i <= 11 + 3) { if (j === 0) { cellType = 2; self.spawns.push(cell); } else if (j <= 4) { cellType = 0; } else if (j === gridHeight - 1) { cellType = 3; self.goals.push(cell); } else if (j >= gridHeight - 4) { cellType = 0; } } 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 = []; if (j > 3 && j <= gridHeight - 4) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { // Check if we can use cached pathfinding result var currentTime = LK.ticks; // Adjust cache timeout based on activity var activityLevel = enemies.length + towers.length; if (activityLevel < 10) { dynamicCacheTimeout = 600; // 10 seconds for low activity } else if (activityLevel < 20) { dynamicCacheTimeout = 450; // 7.5 seconds for medium activity } else { dynamicCacheTimeout = 300; // 5 seconds for high activity } if (pathfindingCache && currentTime - lastPathfindTime < dynamicCacheTimeout) { // Use cached result for (var i = 0; i < self.cells.length; i++) { for (var j = 0; j < self.cells[i].length; j++) { var cell = self.cells[i][j]; var cachedCell = pathfindingCache[i][j]; if (cachedCell) { cell.score = cachedCell.score; cell.pathId = cachedCell.pathId; cell.targets = cachedCell.targets; } } } maxScore = pathfindingCache.maxScore; pathId = pathfindingCache.pathId; return false; // No blocking found in cache } 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; } } // Cache the successful pathfinding result pathfindingCache = { maxScore: maxScore, pathId: pathId }; // Deep copy cell data for cache for (var i = 0; i < self.cells.length; i++) { pathfindingCache[i] = []; for (var j = 0; j < self.cells[i].length; j++) { var cell = self.cells[i][j]; pathfindingCache[i][j] = { score: cell.score, pathId: cell.pathId, targets: cell.targets.slice() // Copy array }; } } lastPathfindTime = LK.ticks; console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { 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; // Sync shadow animation with main enemy enemy.shadow.children[0].y = enemy.children[0].y * 0.5; // Shadow moves less than main sprite } } // 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) { // Enhanced walking animation for pre-entry movement (disabled for flying enemies) if (!enemy.isFlying) { var speedMultiplier = enemy.speed * 100; enemy.walkAnimationPhase += enemy.walkAnimationSpeed * speedMultiplier; // Create more realistic pre-entry walking motion var primaryBob = Math.sin(enemy.walkAnimationPhase) * enemy.walkBobAmount; var secondaryBob = Math.sin(enemy.walkAnimationPhase * 2) * (enemy.walkBobAmount * 0.2); var bobOffset = primaryBob + secondaryBob; // Add slight horizontal movement for pre-entry var horizontalSway = Math.sin(enemy.walkAnimationPhase * 0.7) * (enemy.walkBobAmount * 0.15); if (enemy.children[0]) { // Apply different animation styles based on enemy type during pre-entry var animationIntensity = 1; switch (enemy.type) { case 'fast': animationIntensity = 1.4; break; case 'swarm': animationIntensity = 1.2; break; } if (enemy.isBoss) { animationIntensity *= 0.8; bobOffset *= 1.1; } // Smooth animation application var targetY = bobOffset * animationIntensity; var targetX = horizontalSway * animationIntensity; if (!enemy.preEntryAnimating) { enemy.preEntryAnimating = true; tween(enemy.children[0], { y: targetY, x: targetX }, { duration: 60, easing: tween.easeOut, onFinish: function onFinish() { enemy.preEntryAnimating = false; } }); } } // Play footstep sound for pre-entry movement var walkCycle = Math.floor(enemy.walkAnimationPhase / (Math.PI / 2)) % 4; var lastWalkCycle = Math.floor((enemy.walkAnimationPhase - enemy.walkAnimationSpeed) / (Math.PI / 2)) % 4; } // Footstep sounds removed for enemies // Move directly downward enemy.currentCellY += enemy.speed; // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) var angle = 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 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); // Play goddayfordie sound when normal enemies first enter the screen if (enemy.type === 'normal' && !godDayForDieSoundPlayed) { godDayForDieSoundPlayed = true; LK.getSound('goddayfordie').play(); } // Play fast enemy sounds when fast enemies first enter the screen if (enemy.type === 'fast' && !fastEnemySoundPlayed) { fastEnemySoundPlayed = true; // Play first sound when enemy enters screen LK.getSound('fast_enemy_sound').play(); // Play second sound after a 5 second delay tween({}, {}, { duration: 5000, // 5 second delay between fast enemy sounds onFinish: function onFinish() { LK.getSound('for_the_fallen').play(); } }); } } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } // Flying enemies move without wing-flapping animation - simple smooth movement // No complex animation for flying enemies var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction 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); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { if (cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); // Check if enemy is close to reaching the goal (within 2 cells) and play "we are coming for you" sound once (only for normal enemies) if (!weAreComingForYouSoundPlayed && cell.score < 20000 && enemy.type === 'normal') { // Close to goal weAreComingForYouSoundPlayed = true; // Play the sound and make enemy disappear after sound finishes LK.getSound('wearecomingforyou').play(); // Use tween to delay enemy disappearing until sound finishes tween({}, {}, { duration: 2000, // Approximate sound duration onFinish: function onFinish() { // Make the enemy disappear by removing it from the game if (enemy.parent) { // 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 { enemyLayerBottom.removeChild(enemy); } // Remove from enemies array var enemyIndex = enemies.indexOf(enemy); if (enemyIndex !== -1) { enemies.splice(enemyIndex, 1); } } } }); } if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } // Enhanced walking animation for normal pathfinding movement (disabled for flying enemies) if (!enemy.isFlying) { var speedMultiplier = enemy.speed * 100; enemy.walkAnimationPhase += enemy.walkAnimationSpeed * speedMultiplier; // Create realistic walking motion with multiple components var primaryBob = Math.sin(enemy.walkAnimationPhase) * enemy.walkBobAmount; var secondaryBob = Math.sin(enemy.walkAnimationPhase * 2.2) * (enemy.walkBobAmount * 0.25); var bobOffset = primaryBob + secondaryBob; // Add natural horizontal sway var horizontalSway = Math.sin(enemy.walkAnimationPhase * 0.6) * (enemy.walkBobAmount * 0.18); if (enemy.children[0]) { // Apply type-specific animation characteristics var animationIntensity = 1; switch (enemy.type) { case 'fast': animationIntensity = 1.6; // Very energetic break; case 'immune': animationIntensity = 0.75; // More controlled break; case 'swarm': animationIntensity = 1.3; // Quick and jittery // Add random jitter for swarm enemies bobOffset += (Math.random() - 0.5) * enemy.walkBobAmount * 0.2; break; } if (enemy.isBoss) { animationIntensity *= 0.7; // Slower but more imposing bobOffset *= 1.3; // More pronounced movement } // Apply smooth animation transitions var targetY = bobOffset * animationIntensity; var targetX = horizontalSway * animationIntensity; if (!enemy.pathfindingAnimating) { enemy.pathfindingAnimating = true; tween(enemy.children[0], { y: targetY, x: targetX }, { duration: 45, easing: tween.easeOut, onFinish: function onFinish() { enemy.pathfindingAnimating = false; } }); } } // Play footstep sound for normal pathfinding movement var walkCycle = Math.floor(enemy.walkAnimationPhase / (Math.PI / 2)) % 4; var lastWalkCycle = Math.floor((enemy.walkAnimationPhase - enemy.walkAnimationSpeed) / (Math.PI / 2)) % 4; } // Footstep sounds removed for enemies var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('next_wave_bg', { anchorX: 0.5, anchorY: 0.5 }); // buttonBackground.tint = 0x0088FF; // Removed to show original image colors 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; // Removed to show original image colors self.alpha = 1; } else { self.enabled = false; self.visible = false; // buttonBackground.tint = 0x888888; // Removed to show original image colors self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; } }; 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 || 'default'; // Get appropriate asset for this tower type var assetId = 'tower_' + self.towerType; // Increase size of base for easier touch var baseGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, scaleY: 1.2 }); var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { 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 typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { 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); // Check if tower type can be placed (for poison and slow towers) var canPlace = canPlaceTowerType(self.towerType); // Hide tower icon instead of making it transparent self.visible = canAfford && canPlace; }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // 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: base 5, +0.8 per level, but final upgrade gets a huge boost if (self.level === self.maxLevel) { return 12 * CELL_SIZE; // Significantly increased range for max level } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case 'splash': // Splash: base 2, +0.2 per level (max ~4 blocks at max level) return (2 + (self.level - 1) * 0.2) * CELL_SIZE; case 'rapid': // Rapid: base 2.5, +0.5 per level return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'slow': // Slow: base 3.5, +0.5 per level return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'poison': // Poison: base 3.2, +0.5 per level return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; default: // Default: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': 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 'splash': self.fireRate = 75; self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; case 'slow': self.fireRate = 50; self.damage = 8; 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; } // Get appropriate asset for this tower type var assetId = 'tower_' + self.id; var baseGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.4, scaleY: 1.4 }); baseGraphics.alpha = 0; // Hide tower base graphics 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('tower_level', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('tower_level', { 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); // Get appropriate defense asset for this tower type var defenseAssetId = 'defense_' + self.id; var gunGraphics = gunContainer.attachAsset(defenseAssetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); // Make poison tower defense graphics transparent if (self.id === 'poison') { gunGraphics.alpha = 0; } // Make slow tower defense graphics transparent if (self.id === 'slow') { gunGraphics.alpha = 0; } 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 'rapid': towerLevelIndicator.tint = 0x00AAFF; break; case 'sniper': towerLevelIndicator.tint = 0xFF5500; break; case 'splash': towerLevelIndicator.tint = 0x33CC00; break; case 'slow': towerLevelIndicator.tint = 0x9900FF; break; case 'poison': towerLevelIndicator.tint = 0x00FFAA; break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); // Add upgrade warning indicator var upgradeWarning = self.attachAsset('upgrade_warning', { anchorX: 0.5, anchorY: 0.5 }); upgradeWarning.x = -CELL_SIZE * 1.1; // Position further left, outside tower graphic upgradeWarning.y = -CELL_SIZE * 1.1; // Position further up, outside tower graphic upgradeWarning.visible = false; // Hidden by default upgradeWarning.tint = 0xFFD700; // Gold color for upgrade indication self.upgradeWarning = upgradeWarning; 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); } } } } 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 === 'rapid') { 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 { 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(); //Play tower upgrade sound LK.getSound('tower_upgrade').play(); 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 () { // Cache last target if still valid and in range if (self.targetEnemy && self.targetEnemy.parent && self.targetEnemy.health > 0) { if (self.isInRange(self.targetEnemy)) { return self.targetEnemy; } } var closestEnemy = null; var closestScore = Infinity; var towerRange = self.getRange(); // Use spatial partitioning to get only nearby enemies var nearbyEnemies = spatialGrid.getEnemiesInRange(self.x, self.y, towerRange); for (var i = 0; i < nearbyEnemies.length; i++) { var enemy = nearbyEnemies[i]; if (!enemy.parent || enemy.health <= 0) continue; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distanceSquared = dx * dx + dy * dy; var rangeSquared = towerRange * towerRange; // Check if enemy is in range using squared distance (faster than sqrt) if (distanceSquared <= rangeSquared) { // 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 (distanceSquared < closestScore) { closestScore = distanceSquared; closestEnemy = enemy; } } } else { // For ground enemies, 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 () { self.targetEnemy = self.findTarget(); if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); // Only rotate gun for non-poison and non-slow towers if (self.id !== 'poison' && self.id !== 'slow') { gunContainer.rotation = angle; } if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } // Continuous poison cloud animation for poison towers if (self.id === 'poison') { self.poisonCloudTimer++; // Count poison towers to balance animation frequency var poisonTowerCount = 0; for (var t = 0; t < towers.length; t++) { if (towers[t].id === 'poison') { poisonTowerCount++; } } // Adjust frequency based on poison tower count - reduce frequency with more towers var poisonFrequency = 20; // Reduced base frequency for faster animation if (poisonTowerCount > 3) { // Increase interval (reduce frequency) when more than 3 poison towers poisonFrequency = 20 + (poisonTowerCount - 3) * 10; // Reduced multiplier for faster animation } // Create poison clouds at adjusted frequency if (self.poisonCloudTimer % poisonFrequency === 0) { self.createContinuousPoisonClouds(); } } // Continuous motor exhaust animation for splash towers if (self.id === 'splash') { self.exhaustTimer++; // Count splash towers to balance animation frequency var splashTowerCount = 0; for (var t = 0; t < towers.length; t++) { if (towers[t].id === 'splash') { splashTowerCount++; } } // Adjust frequency based on splash tower count - reduce frequency with more towers var exhaustFrequency = 25; // Base frequency for exhaust animation if (splashTowerCount > 2) { // Increase interval (reduce frequency) when more than 2 splash towers exhaustFrequency = 25 + (splashTowerCount - 2) * 8; } // Create motor exhaust at adjusted frequency if (self.exhaustTimer % exhaustFrequency === 0) { self.createMotorExhaust(); } } // Continuous flame animation for slow towers if (self.id === 'slow') { self.flameTimer++; // Count slow towers to balance animation frequency var slowTowerCount = 0; for (var t = 0; t < towers.length; t++) { if (towers[t].id === 'slow') { slowTowerCount++; } } // Adjust frequency based on slow tower count - reduce frequency with more towers var flameFrequency = 18; // Base frequency for flame animation (faster than exhaust) if (slowTowerCount > 2) { // Increase interval (reduce frequency) when more than 2 slow towers flameFrequency = 18 + (slowTowerCount - 2) * 6; } // Create continuous flames at adjusted frequency if (self.flameTimer % flameFrequency === 0) { self.createContinuousFlames(); } } // Check if tower can be upgraded and has enough gold self.checkUpgradeAvailability(); }; 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(); }; self.fire = function () { // Slow towers apply area damage to ALL enemies in range if (self.id === 'slow') { // Apply slow effect and area damage to all enemies in range var slowRadius = self.getRange(); var affectedEnemies = []; // Find all enemies within slow radius from tower for (var i = 0; i < enemies.length; i++) { var nearbyEnemy = enemies[i]; if (nearbyEnemy && nearbyEnemy.parent && nearbyEnemy.health > 0 && !nearbyEnemy.isFlying) { var dx = nearbyEnemy.x - self.x; var dy = nearbyEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= slowRadius) { affectedEnemies.push(nearbyEnemy); } } } // Apply area damage to only one enemy (the first one in the list) if (affectedEnemies.length > 0) { var damagedEnemy = affectedEnemies[0]; // Apply area damage to only one enemy (including immune ones) damagedEnemy.health -= self.damage; if (damagedEnemy.health <= 0) { damagedEnemy.health = 0; } else { damagedEnemy.healthBar.width = damagedEnemy.health / damagedEnemy.maxHealth * 70; } // Play area damage sound every 4 seconds (240 frames at 60 FPS) if (!self.lastAreaSoundTime) { self.lastAreaSoundTime = 0; } if (LK.ticks - self.lastAreaSoundTime >= 240) { LK.getSound('alanhasar').play(); self.lastAreaSoundTime = LK.ticks; } } // Play welcometohell sound only once for the first enemy entering slow tower area damage if (affectedEnemies.length > 0 && !welcomeToHellSoundPlayed) { welcomeToHellSoundPlayed = true; LK.getSound('welcometohell').play(); } // Count and play bronz sound for 5th enemy entering slow tower area damage if (affectedEnemies.length > 0) { slowAreaEnemyCount++; if (slowAreaEnemyCount === 5 && !bronzSoundPlayed) { bronzSoundPlayed = true; // Delay the bronz sound by 800ms tween({}, {}, { duration: 800, onFinish: function onFinish() { LK.getSound('bronz').play(); } }); } } // Track enemies entering and exiting slow area for (var i = 0; i < affectedEnemies.length; i++) { var affectedEnemy = affectedEnemies[i]; if (enemiesInSlowArea.indexOf(affectedEnemy) === -1) { enemiesInSlowArea.push(affectedEnemy); } } // Apply effects to all affected enemies (visual effects and slow) for (var i = 0; i < affectedEnemies.length; i++) { var affectedEnemy = affectedEnemies[i]; // Play krk sound for immune enemies hit by slow tower - only once per wave if (affectedEnemy.isImmune && !window.krkSoundPlayedThisWave) { window.krkSoundPlayedThisWave = true; LK.getSound('krk').play(); } // Create flame animation for each affected enemy using particle pool var flameCount = 8; // Number of flame particles per enemy for (var flameIdx = 0; flameIdx < flameCount; flameIdx++) { var flameParticle = getFireParticle(); var flameGraphics = flameParticle.fireGraphics; flameGraphics.width = 15 + Math.random() * 20; flameGraphics.height = flameGraphics.width; // Flame color palette var flameColors = [0xFF4500, 0xFF6600, 0xFF8800, 0xFFAA00, 0xFFCC00]; flameGraphics.tint = flameColors[Math.floor(Math.random() * flameColors.length)]; // Position flame particles around the enemy var angle = flameIdx / flameCount * Math.PI * 2 + Math.random() * 0.5; var distance = Math.random() * 40; flameParticle.x = affectedEnemy.x + Math.cos(angle) * distance; flameParticle.y = affectedEnemy.y + Math.sin(angle) * distance; flameParticle.alpha = 0.9; flameParticle.scaleX = 0.5 + Math.random() * 0.5; flameParticle.scaleY = 0.5 + Math.random() * 0.5; game.addChild(flameParticle); // Animate flame flickering and burning out tween(flameParticle, { alpha: 0, scaleX: flameParticle.scaleX * 1.8, scaleY: flameParticle.scaleY * 1.8, y: flameParticle.y - 20 - Math.random() * 30 }, { duration: 600 + Math.random() * 400, easing: tween.easeOut, onFinish: function onFinish() { returnParticle(flameParticle); } }); } // Apply slow effect only to non-immune enemies if (!affectedEnemy.isImmune) { var slowPct = 0.5; if (self.level !== 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.level - 1)); slowPct = slowLevels[idx]; } if (!affectedEnemy.slowed) { affectedEnemy.originalSpeed = affectedEnemy.speed; affectedEnemy.speed *= 1 - slowPct; // Slow by X% affectedEnemy.slowed = true; affectedEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { affectedEnemy.slowDuration = 180; // Reset duration } } } return; // Exit early for slow towers } if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { 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 default towers, randomly use bullet_5 asset 20% of the time if (self.id === 'default' && Math.random() < 0.2) { bullet.isBullet5 = true; // Replace default bullet graphics with bullet_5 asset if (bullet.children[0].parent) { bullet.removeChild(bullet.children[0]); } var bullet5Graphics = bullet.attachAsset('bullet_5', { anchorX: 0.5, anchorY: 0.5 }); } // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0x00AAFF; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'sniper': bullet.children[0].tint = 0xFF5500; bullet.children[0].width = 15; bullet.children[0].height = 15; break; case 'splash': // Play goddamit sound only once per wave on first splash bullet creation for normal enemies only if (!window.splashBulletSoundPlayed && self.targetEnemy.type === 'normal') { window.splashBulletSoundPlayed = true; // Delay the sound by 1200ms for clear speech tween({}, {}, { duration: 1200, onFinish: function onFinish() { LK.getSound('goddamit').play(); } }); } // Remove old bullet graphics and add splash-specific asset if (bullet.children[0].parent) { bullet.removeChild(bullet.children[0]); } var splashBulletGraphics = bullet.attachAsset('bullet_splash', { anchorX: 0.5, anchorY: 0.5 }); break; case 'poison': // Hide the poison bullet graphic bullet.children[0].alpha = 0; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); //Play tower shooting sound (except for poison and slow towers) if (self.id !== 'poison' && self.id !== 'slow') { // Play wifi sound for default tower bullets targeting bagışıkdüşman - only once per wave if (self.id === 'default' && self.targetEnemy && self.targetEnemy.isImmune && !wifiSoundPlayedThisWave) { wifiSoundPlayedThisWave = true; LK.getSound('wifi').play(); // Play taksi sound for default tower bullet_5 theme targeting bagışıkdüşman - only once per wave } else if (self.id === 'default' && bullet.isBullet5 && self.targetEnemy && self.targetEnemy.isImmune && !taksiSoundPlayedThisWave) { taksiSoundPlayedThisWave = true; // Delay the taksi sound by 2000ms tween({}, {}, { duration: 2000, onFinish: function onFinish() { LK.getSound('taksi').play(); } }); // Play gözlük sound for sniper tower bullets targeting bagışıkdüşman on 1st hit - only once per wave } else if (self.id === 'sniper' && self.targetEnemy && self.targetEnemy.isImmune) { sniperBulletImmuneHitCount++; if (sniperBulletImmuneHitCount === 1 && !gözlükSoundPlayedThisWave) { gözlükSoundPlayedThisWave = true; LK.getSound('gozluk').play(); } else { LK.getSound('tower_shoot').play(); } // Play vasiyet sound for rapid tower bullets targeting bagışıkdüşman on 3rd hit - only once per wave } else if (self.id === 'rapid' && self.targetEnemy && self.targetEnemy.isImmune) { rapidBulletImmuneHitCount++; if (rapidBulletImmuneHitCount === 3 && !vasiyetSoundPlayedThisWave) { vasiyetSoundPlayedThisWave = true; LK.getSound('vasiyet').play(); } else { LK.getSound('tower_shoot').play(); } // Play shy sound for sniper tower bullets targeting bagışıkdüşman on 3rd hit - only once per wave } else if (self.id === 'sniper' && self.targetEnemy && self.targetEnemy.isImmune) { // Don't increment counter here - it should be incremented in bullet hit logic LK.getSound('tower_shoot').play(); } else { LK.getSound('tower_shoot').play(); } } // --- Fire recoil effect for gunContainer (not for poison towers) --- if (self.id !== 'poison') { // 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; // Only set cell type to 1 (wall) for non-poison towers // Poison towers don't block enemy movement if (self.id !== 'poison') { for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; } } } } self.refreshCellsInRange(); // Invalidate pathfinding cache when tower is placed pathfindingCache = null; // Initialize continuous poison cloud animation for poison towers if (self.id === 'poison') { self.poisonCloudTimer = 0; self.createContinuousPoisonClouds = function () { // Create reduced poison cloud particles around the tower continuously using particle pool var cloudCount = enemies.length > 15 ? 8 : 12; // Reduced cloud count for less intensive animation for (var cloudIdx = 0; cloudIdx < cloudCount; cloudIdx++) { var poisonCloud = getPoisonCloudParticle(); var cloudGraphics = poisonCloud.cloudGraphics; cloudGraphics.width = 25 + Math.random() * 35; // Smaller cloud size cloudGraphics.height = cloudGraphics.width; // Enhanced poison color palette with toxic variations var toxicColors = [0x00FF00, 0x00DD00, 0x00BB00, 0x228B22, 0x32CD32, 0x7CFC00, 0x9ACD32, 0x00FF7F]; cloudGraphics.tint = toxicColors[Math.floor(Math.random() * toxicColors.length)]; // Position cloud particles in multiple concentric circles around the tower var ringNumber = Math.floor(cloudIdx / 4); // Adjusted for smaller cloud count var angleInRing = cloudIdx % 4 * (Math.PI * 2 / 4) + Math.random() * 0.6; // Reduced randomness var baseDistance = 35 + ringNumber * 20; // Slightly smaller distance var distance = baseDistance + Math.random() * 20; // Reduced spread poisonCloud.x = self.x + Math.cos(angleInRing) * distance; poisonCloud.y = self.y + Math.sin(angleInRing) * distance; poisonCloud.alpha = 0.5 + Math.random() * 0.3; // Slightly more transparent poisonCloud.scaleX = 0.4 + Math.random() * 0.4; // Smaller initial scale poisonCloud.scaleY = 0.4 + Math.random() * 0.4; game.addChild(poisonCloud); // Reduced poison cloud animation with swirling motion var targetScale = poisonCloud.scaleX * (2.2 + Math.random() * 1.5); // Smaller target scale // Create swirling motion around the tower var swirl = Math.random() > 0.5 ? 1 : -1; var swirlAngle = angleInRing + swirl * (Math.PI * 1.2 + Math.random() * Math.PI * 0.6); // Reduced swirl var swirlRadius = distance * (0.7 + Math.random() * 0.3); // Tighter radius var targetX = self.x + Math.cos(swirlAngle) * swirlRadius + (Math.random() - 0.5) * 30; // Less spread var targetY = poisonCloud.y - (25 + Math.random() * 35); // Reduced vertical movement // Add pulsing effect by varying the target scale over time var pulseScale = targetScale * (0.7 + Math.random() * 0.3); // Less pulsing var rotationSpeed = swirl * (Math.PI * 2 + Math.random() * Math.PI * 1.2); // Slower rotation tween(poisonCloud, { x: targetX, y: targetY, alpha: 0, scaleX: pulseScale, scaleY: pulseScale, rotation: rotationSpeed }, { duration: 1000 + Math.random() * 400, // Shorter duration easing: tween.easeOut, onFinish: function onFinish() { returnParticle(poisonCloud); } }); } }; } // Initialize motor exhaust particle effects for splash towers if (self.id === 'splash') { self.exhaustTimer = 0; self.createMotorExhaust = function () { // Play motor exhaust sound effect LK.getSound('egzos').play(); // Create motor exhaust particles behind the splash tower using particle pool var exhaustCount = enemies.length > 15 ? 8 : 12; // Reduced count when many enemies for (var exhaustIdx = 0; exhaustIdx < exhaustCount; exhaustIdx++) { var exhaustParticle = getSmokeParticle(); var exhaustGraphics = exhaustParticle.smokeGraphics; exhaustGraphics.width = 15 + Math.random() * 25; exhaustGraphics.height = exhaustGraphics.width; // Motor exhaust color palette - dark grays and blacks var exhaustColors = [0x2a2a2a, 0x1a1a1a, 0x404040, 0x303030, 0x505050]; exhaustGraphics.tint = exhaustColors[Math.floor(Math.random() * exhaustColors.length)]; // Synchronize exhaust direction with gun rotation - emit from opposite direction of gun var gunRotation = gunContainer.rotation || 0; // Get current gun rotation var exhaustBaseAngle = gunRotation + Math.PI; // Opposite direction from gun var exhaustAngle = exhaustBaseAngle + (Math.random() - 0.5) * Math.PI * 0.3; // Narrower spread around opposite direction var exhaustDistance = 50 + Math.random() * 25; // Moved further back (50-75 instead of 30-50) exhaustParticle.x = self.x + Math.cos(exhaustAngle) * exhaustDistance; exhaustParticle.y = self.y + Math.sin(exhaustAngle) * exhaustDistance; exhaustParticle.alpha = 0.7 + Math.random() * 0.3; exhaustParticle.scaleX = 0.4 + Math.random() * 0.4; exhaustParticle.scaleY = 0.4 + Math.random() * 0.4; game.addChild(exhaustParticle); // Animate exhaust particles moving away from tower and fading in synchronized direction var targetDistance = exhaustDistance + 35 + Math.random() * 25; // Adjusted for new starting position var targetX = self.x + Math.cos(exhaustAngle) * targetDistance; var targetY = self.y + Math.sin(exhaustAngle) * targetDistance; tween(exhaustParticle, { x: targetX, y: targetY, alpha: 0, scaleX: exhaustParticle.scaleX * 2.0, scaleY: exhaustParticle.scaleY * 2.0 }, { duration: 800 + Math.random() * 400, easing: tween.easeOut, onFinish: function onFinish() { returnParticle(exhaustParticle); } }); } }; } // Initialize continuous flame animation for slow towers if (self.id === 'slow') { self.flameTimer = 0; self.createContinuousFlames = function () { // Play flame sound effect LK.getSound('flame_sound').play(); // Create continuous flame particles around the slow tower using particle pool var flameCount = enemies.length > 15 ? 6 : 9; // Reduced flame count for less intensive animation for (var flameIdx = 0; flameIdx < flameCount; flameIdx++) { var flameParticle = getFireParticle(); var flameGraphics = flameParticle.fireGraphics; flameGraphics.width = 15 + Math.random() * 25; // Reduced flame size flameGraphics.height = flameGraphics.width; // Enhanced flame color palette with intense fire colors var flameColors = [0xFF4500, 0xFF6600, 0xFF2200, 0xFF8800, 0xFFAA00, 0xFF0000, 0xCC4400, 0xDD3300]; flameGraphics.tint = flameColors[Math.floor(Math.random() * flameColors.length)]; // Position flame particles in multiple concentric circles around the tower var ringNumber = Math.floor(flameIdx / 6); // 3 rings of 6 particles each var angleInRing = flameIdx % 6 * (Math.PI * 2 / 6) + Math.random() * 0.6; var baseDistance = 35 + ringNumber * 20; var distance = baseDistance + Math.random() * 25; flameParticle.x = self.x + Math.cos(angleInRing) * distance; flameParticle.y = self.y + Math.sin(angleInRing) * distance; flameParticle.alpha = 0.7 + Math.random() * 0.3; flameParticle.scaleX = 0.6 + Math.random() * 0.5; flameParticle.scaleY = 0.6 + Math.random() * 0.5; game.addChild(flameParticle); // Reduced flame animation with flickering motion and upward movement var targetScale = flameParticle.scaleX * (1.8 + Math.random() * 1.2); // Smaller target scale // Create flickering motion around the tower var flicker = Math.random() > 0.5 ? 1 : -1; var flickerAngle = angleInRing + flicker * (Math.PI * 0.6 + Math.random() * Math.PI * 0.4); // Less flickering var flickerRadius = distance * (0.8 + Math.random() * 0.2); // Tighter radius var targetX = self.x + Math.cos(flickerAngle) * flickerRadius + (Math.random() - 0.5) * 25; // Less spread var targetY = flameParticle.y - (20 + Math.random() * 25); // Less vertical movement // Add pulsing effect by varying the target scale over time var pulseScale = targetScale * (0.8 + Math.random() * 0.4); // Less pulsing variation var rotationSpeed = flicker * (Math.PI * 2 + Math.random() * Math.PI * 1.5); tween(flameParticle, { x: targetX, y: targetY, alpha: 0, scaleX: pulseScale, scaleY: pulseScale, rotation: rotationSpeed }, { duration: 1000 + Math.random() * 500, easing: tween.easeOut, onFinish: function onFinish() { returnParticle(flameParticle); } }); } }; } }; self.checkUpgradeAvailability = function () { if (self.level >= self.maxLevel) { // Tower is at max level, hide warning if (self.upgradeWarning) { self.upgradeWarning.visible = false; } return; } // Calculate upgrade cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } // Show warning if player has enough gold for upgrade var canAffordUpgrade = gold >= upgradeCost; if (self.upgradeWarning) { if (canAffordUpgrade && !self.upgradeWarning.visible) { // Create pulsing animation var _pulseWarning = function pulseWarning() { if (self.upgradeWarning && self.upgradeWarning.visible) { tween(self.upgradeWarning, { scaleX: 1.3, scaleY: 1.3, alpha: 0.7 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (self.upgradeWarning && self.upgradeWarning.visible) { tween(self.upgradeWarning, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { // Continue pulsing if still visible if (self.upgradeWarning && self.upgradeWarning.visible) { _pulseWarning(); } } }); } } }); } }; // Start pulsing animation when warning becomes visible self.upgradeWarning.visible = true; self.upgradeWarning.alpha = 1; self.upgradeWarning.scaleX = 1; self.upgradeWarning.scaleY = 1; _pulseWarning(); } else if (!canAffordUpgrade) { // Stop animation and hide warning if (self.upgradeWarning.visible) { tween.stop(self.upgradeWarning, { scaleX: true, scaleY: true, alpha: true }); self.upgradeWarning.visible = false; } } } }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; 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('towerpreview', { 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; self.update = function () { 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 var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; // Remove old preview graphics and add new one with correct asset if (previewGraphics.parent) { self.removeChild(previewGraphics); } // Get appropriate asset for this tower type var previewAssetId = 'towerpreview_' + self.towerType; previewGraphics = self.attachAsset(previewAssetId, { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2.4; previewGraphics.height = CELL_SIZE * 2.4; // Set color based on tower type var towerColor = 0xFFFFFF; // Default white switch (self.towerType) { case 'rapid': towerColor = 0x00AAFF; // Blue break; case 'sniper': towerColor = 0xFF5500; // Orange break; case 'splash': towerColor = 0x33CC00; // Green break; case 'slow': towerColor = 0x9900FF; // Purple break; case 'poison': towerColor = 0x00FFAA; // Cyan break; default: towerColor = 0xAAAAAA; // Gray for default } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } else { previewGraphics.tint = towerColor; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) { validGridPlacement = false; } else { 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 || cell.type !== 0) { 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 && canPlaceTowerType(self.towerType); 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 towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' 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); //Play tower sell sound LK.getSound('tower_sell').play(); 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; // Reset placement flags for all tower types when sold if (self.tower.id === 'poison') { poisonTowerPlaced = false; } else if (self.tower.id === 'slow') { slowTowerPlaced = false; } else if (self.tower.id === 'default') { defaultTowerPlaced = false; } else if (self.tower.id === 'rapid') { rapidTowerPlaced = false; } else if (self.tower.id === 'sniper') { sniperTowerPlaced = false; } else if (self.tower.id === 'splash') { splashTowerPlaced = false; } for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { // Only reset cell type if this wasn't a poison tower // Poison towers don't block movement so cells should already be type 0 if (self.tower.id !== 'poison') { cell.type = 0; } 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); // Invalidate pathfinding cache when tower is sold pathfindingCache = null; 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 = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { 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 Game", { 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) { //Play start button sound LK.getSound('button_start').play(); self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; // Make started image asset transparent using tween tween(startBlock, { alpha: 0 }, { duration: 500, easing: tween.easeOut }); // Animate yellow frame to shrink to normal block size var normalBlockHeight = 70; var normalFrameHeight = normalBlockHeight + 32; // Add some padding around the normal block // Animate horizontal bars (top and bottom) height reduction tween(indicator, { height: 16 }, { duration: 500, easing: tween.easeOut }); tween(indicator2, { height: 16 }, { duration: 500, easing: tween.easeOut }); // Animate vertical bars (left and right) height reduction tween(leftWall, { height: normalFrameHeight }, { duration: 500, easing: tween.easeOut }); tween(rightWall, { height: normalFrameHeight }, { duration: 500, easing: tween.easeOut }); // Adjust position of horizontal bars to match new frame size tween(indicator, { y: -(normalFrameHeight / 2 - 8) }, { duration: 500, easing: tween.easeOut }); tween(indicator2, { y: normalFrameHeight / 2 - 8 }, { duration: 500, easing: tween.easeOut }); } }; 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; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 9; } else if (i === 1) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 7; } else if (i === 2) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 4; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 7; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 10; } else if (i === 5) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (isBossWave) { // Wave 10 gets big enemy, other boss waves get strengthened types if (i + 1 === 10) { enemyType = "big"; block.tint = 0x8B4513; // Brown color for big enemy waveType = "Big"; enemyCount = 1; // Single big enemy } else { // Replace other boss waves with multiple stronger enemies var strengthenedTypes = ['immune', 'fast', 'immune', 'flying', 'fast']; var typeIndex = Math.floor((i + 1) / 10) - 1; enemyType = strengthenedTypes[typeIndex % strengthenedTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Elite Normal"; enemyCount = 5; break; case 'fast': block.tint = 0x00AAFF; waveType = "Elite Fast"; enemyCount = 4; break; case 'immune': block.tint = 0xAA0000; waveType = "Elite Immune"; enemyCount = 3; break; case 'flying': block.tint = 0xFFFF00; waveType = "Elite Flying"; enemyCount = 4; break; case 'big': block.tint = 0x8B4513; waveType = "Big"; enemyCount = 1; break; } } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 10; } else { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark elite waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a star indicator to the wave marker for elite waves var eliteIndicator = marker.attachAsset('star_score', { anchorX: 0.5, anchorY: 0.5 }); eliteIndicator.width = 30; eliteIndicator.height = 30; eliteIndicator.tint = 0xFFD700; // Gold color eliteIndicator.y = -block.height / 2 - 15; } // 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 "normal"; } // 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 elite prefix for every 10th wave if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "Elite " + typeName; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('star_score', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('star_score', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('star_score', { 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('star_score', { 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]; // Make all indicator blocks transparent tween(block, { alpha: 0.3 }, { duration: 300, easing: tween.easeOut }); if (i - 1 < currentWave) { block.alpha = .15; // Even more transparent for completed waves } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { waveTimer++; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; } } }; self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ // Add main background var mainBackground = game.attachAsset('main_background', { anchorX: 0, anchorY: 0, x: 0, y: 0 }); var isHidingUpgradeMenu = false; 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 pathfindingCache = null; var lastPathfindTime = 0; var pathfindCacheTimeout = 300; // Cache for 5 seconds (300 ticks at 60fps) var dynamicCacheTimeout = 300; // Dynamic cache timeout that adjusts based on activity var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; var gold = 180; var lives = 100; var score = 0; var currentWave = 0; var totalWaves = 11; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave var poisonBulletHitCount = 0; // Counter for poison bullet hits var gogogoPoisonSoundPlayed = false; // Global flag to ensure gogogo sound only plays once for poison bullets var gogogoSniperSoundPlayed = false; // Global flag to ensure gogogo sound only plays once for sniper bullets var whoFartedSoundPlayed = false; // Global flag to ensure whofarted sound only plays once ever var splashBulletSoundPlayed = false; // Global flag to ensure splash bullet sound only plays once ever var defaultBulletHitCount = 0; // Counter for default bullet hits var itDidntHurtSoundPlayed = false; // Global flag to ensure it didn't hurt sound only plays once ever var youCantStopUsSoundPlayed = false; // Global flag to ensure you can't stop us sound only plays once ever var weAreComingForYouSoundPlayed = false; // Global flag to ensure we are coming for you sound only plays once ever var youWillNeverGiveUpSoundPlayed = false; // Global flag to ensure you will never give up sound only plays once ever var godDayForDieSoundPlayed = false; // Global flag to ensure god day for die sound only plays once ever var enemiesReachedGoalCount = 0; // Counter for enemies that reach the goal var weWinSoundPlayed = false; // Global flag to ensure wewin sound only plays once ever var gidiklaniyorumSoundPlayed = false; // Global flag to ensure gidiklaniyorum sound only plays once ever var beCarefulSoundPlayed = false; // Global flag to ensure becareful sound only plays once ever var sniperBulletHitCount = 0; // Counter for sniper bullet hits var sniperrSoundPlayed = false; // Global flag to ensure sniperr sound only plays once ever var keepMovingSoundPlayed = false; // Global flag to ensure keepmoving sound only plays once ever var poisonTowerPlaced = false; // Track if poison tower has been placed var slowTowerPlaced = false; // Track if slow tower has been placed var defaultTowerPlaced = false; // Track if default tower has been placed var rapidTowerPlaced = false; // Track if rapid tower has been placed var sniperTowerPlaced = false; // Track if sniper tower has been placed var splashTowerPlaced = false; // Track if splash tower has been placed var fastEnemySoundPlayed = false; // Track if fast enemy sound has been played this wave var fastEnemySoundTimer = 0; // Timer for random fast enemy sound timing var welcomeToHellSoundPlayed = false; // Global flag to ensure welcometohell sound only plays once ever var bronzSoundPlayed = false; // Global flag to ensure bronz sound only plays once ever var slowAreaEnemyCount = 0; // Counter for enemies entering slow tower area damage var sunSoundPlayed = false; // Global flag to ensure sun sound only plays once ever var enemiesInSlowArea = []; // Track enemies currently in slow area var wifiSoundPlayedThisWave = false; // Track if wifi sound has been played this wave for bagışıkdüşman var taksiSoundPlayedThisWave = false; // Track if taksi sound has been played this wave for bagışıkdüşman hit by bullet_5 var vasiyetSoundPlayedThisWave = false; // Track if vasiyet sound has been played this wave for bagışıkdüşman hit by rapid bullets var rapidBulletImmuneHitCount = 0; // Counter for rapid bullet hits on immune enemies var gözlükSoundPlayedThisWave = false; // Track if gözlük sound has been played this wave for bagışıkdüşman hit by sniper bullets var sniperBulletImmuneHitCount = 0; // Counter for sniper bullet hits on immune enemies var slowBulletImmuneHitCount = 0; // Counter for slow bullet hits on immune enemies var shySoundPlayedThisWave = false; // Track if shy sound has been played this wave for immune enemies hit by slow bullets var krkSoundPlayedThisWave = false; // Track if krk sound has been played this wave for immune enemies hit by slow towers // Expanded object pools for various particle types var bloodParticlePool = []; var poisonParticlePool = []; var smokeParticlePool = []; var fireParticlePool = []; var poisonCloudPool = []; var walkingFeetPool = []; var maxPoolSize = 30; // Increased pool size for all particle types // Spatial partitioning for optimized enemy targeting var spatialGrid = { cellSize: CELL_SIZE * 2, // Each spatial cell covers 2x2 game cells width: 12, // 24 / 2 height: 18, // 36 / 2 cells: [], init: function init() { this.cells = []; for (var x = 0; x < this.width; x++) { this.cells[x] = []; for (var y = 0; y < this.height; y++) { this.cells[x][y] = []; } } }, clear: function clear() { for (var x = 0; x < this.width; x++) { for (var y = 0; y < this.height; y++) { this.cells[x][y].length = 0; } } }, addEnemy: function addEnemy(enemy) { var gridX = Math.floor(enemy.x / this.cellSize); var gridY = Math.floor(enemy.y / this.cellSize); if (gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height) { this.cells[gridX][gridY].push(enemy); } }, getEnemiesInRange: function getEnemiesInRange(x, y, range) { var enemies = []; var startX = Math.max(0, Math.floor((x - range) / this.cellSize)); var endX = Math.min(this.width - 1, Math.floor((x + range) / this.cellSize)); var startY = Math.max(0, Math.floor((y - range) / this.cellSize)); var endY = Math.min(this.height - 1, Math.floor((y + range) / this.cellSize)); for (var gx = startX; gx <= endX; gx++) { for (var gy = startY; gy <= endY; gy++) { var cellEnemies = this.cells[gx][gy]; for (var i = 0; i < cellEnemies.length; i++) { enemies.push(cellEnemies[i]); } } } return enemies; } }; spatialGrid.init(); function getBloodParticle() { if (bloodParticlePool.length > 0) { return bloodParticlePool.pop(); } var bloodParticle = new Container(); var bloodGraphics = bloodParticle.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); bloodParticle.bloodGraphics = bloodGraphics; bloodParticle.particleType = 'blood'; return bloodParticle; } function getPoisonParticle() { if (poisonParticlePool.length > 0) { return poisonParticlePool.pop(); } var poisonParticle = new Container(); var poisonGraphics = poisonParticle.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); poisonParticle.poisonGraphics = poisonGraphics; poisonParticle.particleType = 'poison'; return poisonParticle; } function getSmokeParticle() { if (smokeParticlePool.length > 0) { return smokeParticlePool.pop(); } var smokeParticle = new Container(); var smokeGraphics = smokeParticle.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); smokeParticle.smokeGraphics = smokeGraphics; smokeParticle.particleType = 'smoke'; return smokeParticle; } function getFireParticle() { if (fireParticlePool.length > 0) { return fireParticlePool.pop(); } var fireParticle = new Container(); var fireGraphics = fireParticle.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); fireParticle.fireGraphics = fireGraphics; fireParticle.particleType = 'fire'; return fireParticle; } function getPoisonCloudParticle() { if (poisonCloudPool.length > 0) { return poisonCloudPool.pop(); } var poisonCloud = new Container(); var cloudGraphics = poisonCloud.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); poisonCloud.cloudGraphics = cloudGraphics; poisonCloud.particleType = 'poisonCloud'; return poisonCloud; } function getWalkingFeetParticle() { if (walkingFeetPool.length > 0) { return walkingFeetPool.pop(); } var walkingFeet = new Container(); var feetGraphics = walkingFeet.attachAsset('walkingFeet', { anchorX: 0.5, anchorY: 0.5 }); walkingFeet.feetGraphics = feetGraphics; walkingFeet.particleType = 'walkingFeet'; return walkingFeet; } function returnParticle(particle) { if (!particle || !particle.particleType) { if (particle && particle.destroy) { particle.destroy(); } return; } var pool; // Dynamic pool sizing based on enemy count var dynamicMaxSize = Math.min(50, Math.max(20, enemies.length * 2)); var maxSize = dynamicMaxSize; switch (particle.particleType) { case 'blood': pool = bloodParticlePool; break; case 'poison': pool = poisonParticlePool; break; case 'smoke': pool = smokeParticlePool; break; case 'fire': pool = fireParticlePool; break; case 'poisonCloud': pool = poisonCloudPool; break; case 'walkingFeet': pool = walkingFeetPool; break; case 'bossCircularFeet': // Boss circular feet are not pooled, destroy them directly particle.destroy(); return; default: particle.destroy(); return; } if (pool.length < maxSize) { // Reset particle properties particle.alpha = 1; particle.scaleX = 1; particle.scaleY = 1; particle.rotation = 0; // Reset graphics tint if (particle.bloodGraphics) particle.bloodGraphics.tint = 0xFFFFFF; if (particle.poisonGraphics) particle.poisonGraphics.tint = 0xFFFFFF; if (particle.smokeGraphics) particle.smokeGraphics.tint = 0xFFFFFF; if (particle.fireGraphics) particle.fireGraphics.tint = 0xFFFFFF; if (particle.cloudGraphics) particle.cloudGraphics.tint = 0xFFFFFF; if (particle.feetGraphics) particle.feetGraphics.tint = 0xFFFFFF; tween.stop(particle, { x: true, y: true, alpha: true, scaleX: true, scaleY: true, rotation: true }); if (particle.parent) { particle.parent.removeChild(particle); } pool.push(particle); } else { particle.destroy(); } } // Legacy function for compatibility function returnBloodParticle(particle) { returnParticle(particle); } // Create gold display container with background image and text var goldDisplay = new Container(); var goldBackground = goldDisplay.attachAsset('coin_gold', { anchorX: 0.5, anchorY: 0.5, width: 120, height: 120 }); goldBackground.x = 0; // Position background behind the text var goldText = new Text2(gold.toString(), { size: 36, fill: 0x000000, weight: 800 }); goldText.anchor.set(0.5, 0.5); goldText.x = 0; // Position text on top of background goldDisplay.addChild(goldText); // Create lives display container with background image and text var livesDisplay = new Container(); var livesBackground = livesDisplay.attachAsset('heart_life', { anchorX: 0.5, anchorY: 0.5, width: 120, height: 120 }); livesBackground.x = 0; // Position background behind the text var livesText = new Text2(lives.toString(), { size: 36, fill: 0x000000, weight: 800 }); livesText.anchor.set(0.5, 0.5); livesText.x = 0; // Position text on top of background livesDisplay.addChild(livesText); // Create score display container with image and text var scoreDisplay = new Container(); var scoreIcon = scoreDisplay.attachAsset('score_icon', { anchorX: 0.5, anchorY: 0.5, width: 120, height: 120 }); scoreIcon.x = -45; // Position icon to the left var scoreText = new Text2(score.toString(), { size: 36, fill: 0xFFFFFF, weight: 800 }); scoreText.anchor.set(0.5, 0.5); scoreText.x = -45; // Position text on top of icon scoreDisplay.addChild(scoreText); // Add displays directly to game object instead of LK.gui for better visibility game.addChild(goldDisplay); game.addChild(livesDisplay); game.addChild(scoreDisplay); // Position displays in the top-right corner with absolute coordinates var topMargin = 85; // Moved up a little bit more var spacing = 190; // Equalized spacing between displays var rightOffset = 1400; // Moved left from previous position goldDisplay.x = rightOffset; goldDisplay.y = topMargin; livesDisplay.x = rightOffset + spacing; livesDisplay.y = topMargin; scoreDisplay.x = rightOffset + spacing * 2 + 50; scoreDisplay.y = topMargin; function updateUI() { goldText.setText(gold.toString()); livesText.setText(lives.toString()); scoreText.setText(score.toString()); } function setGold(value) { gold = value; updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy 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); // Grid cells hidden from visual display // game.addChild(debugLayer); // Debug layer hidden game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY, towerType) { // Poison towers never block paths since enemies can pass through them if (towerType === 'poison') { return false; } 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 }); 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 = 5; switch (towerType) { case 'rapid': cost = 15; break; case 'sniper': cost = 25; break; case 'splash': cost = 35; break; case 'slow': cost = 45; break; case 'poison': cost = 55; break; } return cost; } function getTowerSellValue(totalValue) { return totalValue; } function canPlaceTowerType(towerType) { switch (towerType) { case 'poison': return !poisonTowerPlaced; case 'slow': return !slowTowerPlaced; case 'default': return !defaultTowerPlaced; case 'rapid': return !rapidTowerPlaced; case 'sniper': return !sniperTowerPlaced; case 'splash': return !splashTowerPlaced; default: return false; // No unknown tower types allowed } } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (!canPlaceTowerType(towerType)) { var notification = game.addChild(new Notification(towerType.charAt(0).toUpperCase() + towerType.slice(1) + " tower can only be placed once!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); // Track placement of all tower types if (towerType === 'poison') { poisonTowerPlaced = true; } else if (towerType === 'slow') { slowTowerPlaced = true; } else if (towerType === 'default') { defaultTowerPlaced = true; } else if (towerType === 'rapid') { rapidTowerPlaced = true; } else if (towerType === 'sniper') { sniperTowerPlaced = true; } else if (towerType === 'splash') { splashTowerPlaced = true; } //Play tower placement sound - special sounds for poison, splash and sniper towers if (towerType === 'poison') { LK.getSound('poison_impact').play(); } else if (towerType === 'splash') { LK.getSound('splash_place').play(); } else if (towerType === 'sniper') { LK.getSound('yessir').play(); } else { LK.getSound('tower_place').play(); } 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) { 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) { if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType)) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } 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 = new WaveIndicator(); waveIndicator.x = 2048 / 2; waveIndicator.y = 2732 - 80; game.addChild(waveIndicator); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 / 2; nextWaveButton.y = 2732 - 200; game.addChild(nextWaveButton); var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; var sourceTowers = []; var towerSpacing = 300; // 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; // Apply smaller scale to all tower icons tower.scaleX = 0.8; tower.scaleY = 0.8; // Shift rapid tower icon slightly to the left if (towerTypes[i] === 'rapid') { tower.x -= 60; tower.scaleX = 0.7; tower.scaleY = 0.7; } // Shift sniper tower icon to the left if (towerTypes[i] === 'sniper') { tower.x -= 140; } // Shift slow tower icon to the right if (towerTypes[i] === 'slow') { tower.x += 80; } // Shift splash tower icon to the right - move closer to slow tower if (towerTypes[i] === 'splash') { tower.x += 160; } // Shift poison tower icon to the left if (towerTypes[i] === 'poison') { tower.x -= 20; } tower.y = towerY + 50; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; // Start playing background music LK.playMusic('game_music'); game.update = function () { // Update spatial partitioning grid for enemy targeting optimization spatialGrid.clear(); for (var i = 0; i < enemies.length; i++) { spatialGrid.addEnemy(enemies[i]); } // Visibility culling - hide objects outside screen bounds var screenMargin = 200; // Extra margin for smooth transitions for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var isVisible = enemy.x > -screenMargin && enemy.x < 2048 + screenMargin && enemy.y > -screenMargin && enemy.y < 2732 + screenMargin; enemy.visible = isVisible; if (enemy.shadow) enemy.shadow.visible = isVisible; } if (waveInProgress) { 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 % 10 === 0 && currentWave > 0; if (isBossWave && (waveType !== 'swarm' || waveType === 'big')) { // Boss waves and big enemies have just 1 enemy regardless of what the wave indicator says enemyCount = 1; } // goddayfordie sound is now played when normal enemies enter screen // Reset sound flags for all enemies when new waves start (excluding the global first-time sounds) // Reset normal enemy specific sound flags to allow them to play again in new waves window.youAreKillingMeSoundPlayed = false; itDidntHurtSoundPlayed = false; youCantStopUsSoundPlayed = false; weAreComingForYouSoundPlayed = false; youwillnevergiveupSoundPlayed = false; gidiklaniyorumSoundPlayed = false; gogogoPoisonSoundPlayed = false; beCarefulSoundPlayed = false; sniperrSoundPlayed = false; keepMovingSoundPlayed = false; gogogoSniperSoundPlayed = false; whoFartedSoundPlayed = false; // Reset goddayfordie sound flag to allow it to play again for normal enemies godDayForDieSoundPlayed = false; // Reset goddamit sound flag to allow it to play once per wave for splash bullets hitting normal enemies window.splashBulletSoundPlayed = false; window.splashBulletHitCount = 0; // Reset fast enemy sound flag for new wave fastEnemySoundPlayed = false; fastEnemySoundTimer = 0; // Reset slow tower area sound flags to allow them to repeat each wave welcomeToHellSoundPlayed = false; bronzSoundPlayed = false; sunSoundPlayed = false; slowAreaEnemyCount = 0; // Reset counter for bronz sound timing // Reset wifi sound flag for new wave wifiSoundPlayedThisWave = false; // Reset taksi sound flag for new wave taksiSoundPlayedThisWave = false; // Reset vasiyet sound flag for new wave vasiyetSoundPlayedThisWave = false; rapidBulletImmuneHitCount = 0; // Reset gözlük sound flag for new wave gözlükSoundPlayedThisWave = false; sniperBulletImmuneHitCount = 0; // Reset shy sound flag for new wave shySoundPlayedThisWave = false; slowBulletImmuneHitCount = 0; // Reset krk sound flag for new wave krkSoundPlayedThisWave = false; // Reset sniper immune hit counter for new wave window.sniperImmuneHitCounter = 0; // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy = new Enemy(waveType); // Add enemy to the appropriate layer based on type if (enemy.isFlying) { // Add flying enemy to the top layer enemyLayerTop.addChild(enemy); // If it's a flying enemy, add its shadow to the middle layer if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else { // Add normal/ground enemies to the bottom layer enemyLayerBottom.addChild(enemy); } // Scale difficulty with wave number // Increase enemy health by 30% each wave var healthMultiplier = 1 + currentWave * 0.3; // 30% increase per wave // Elite waves (every 10th) get extra health boost if (isBossWave && waveType !== 'swarm') { if (waveType === 'big') { healthMultiplier *= 5; // Big enemies get 5x health multiplier } else { healthMultiplier *= 3; // Triple health for other elite enemies } } enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // All enemy types now spawn in the middle 6 tiles at the top spacing var gridWidth = 24; var midPoint = Math.floor(gridWidth / 2); // 12 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; 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 === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading enemy.cellX = spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = 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; } } // Check for enemies exiting slow tower area for (var i = enemiesInSlowArea.length - 1; i >= 0; i--) { var enemy = enemiesInSlowArea[i]; var stillInSlowArea = false; // Check if enemy is still in range of any slow tower for (var t = 0; t < towers.length; t++) { var tower = towers[t]; if (tower.id === 'slow') { var dx = enemy.x - tower.x; var dy = enemy.y - tower.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= tower.getRange()) { stillInSlowArea = true; break; } } } // If enemy is no longer in slow area or has been destroyed, remove from tracking if (!stillInSlowArea || !enemy.parent || enemy.health <= 0) { enemiesInSlowArea.splice(i, 1); // Play sun sound if this was the last enemy and sound hasn't been played yet if (enemiesInSlowArea.length === 0 && !sunSoundPlayed) { sunSoundPlayed = true; LK.getSound('sun').play(); } } } 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; } // Return walking feet to pool for normal and swarm enemies only if ((enemy.type === 'normal' || enemy.type === 'swarm') && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) { returnParticle(enemy.leftFoot); returnParticle(enemy.rightFoot); enemy.leftFoot = null; enemy.rightFoot = null; } // Calculate gold and score rewards var isEliteWave = enemy.waveNumber % 10 === 0 && enemy.waveNumber > 0; var goldEarned = isEliteWave ? Math.floor(15 + (enemy.waveNumber - 1) * 2) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for elite enemies var scoreValue = isEliteWave ? 25 : 5; score += scoreValue; 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 { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Return walking feet to pool for normal and swarm enemies only before removal if ((enemy.type === 'normal' || enemy.type === 'swarm') && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) { returnParticle(enemy.leftFoot); returnParticle(enemy.rightFoot); enemy.leftFoot = null; enemy.rightFoot = null; } // Increment counter for enemies reaching goal enemiesReachedGoalCount++; // Play "wewin" sound on 5th enemy reaching goal with delay if (enemiesReachedGoalCount === 5 && !weWinSoundPlayed) { weWinSoundPlayed = true; // Delay the sound by 1200ms for clear speech tween({}, {}, { duration: 1200, onFinish: function onFinish() { LK.getSound('wewin').play(); } }); } // 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 { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { LK.showGameOver(); } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } // Fast enemy sounds are now handled in enemy update logic when they appear on screen 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 () {
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);
// Add more concentrated bullet movement animation
if (!self.animationPhase) {
self.animationPhase = Math.random() * Math.PI * 2;
}
self.animationPhase += 0.3;
var concentratedBounce = Math.sin(self.animationPhase) * 2; // Reduced from larger values
var concentratedSway = Math.cos(self.animationPhase * 0.7) * 1.5; // More controlled sway
// Apply subtle animation offset to bullet position
var animatedX = self.x + concentratedSway;
var animatedY = self.y + concentratedBounce;
// Recalculate movement vectors using animated position for smoother trajectory
dx = self.targetEnemy.x - animatedX;
dy = self.targetEnemy.y - animatedY;
distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Apply damage to target enemy
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;
}
// Play youarekilling me sound on first successful bullet hit with delay
// Only for default bullets and normal enemies
if (!window.youAreKillingMeSoundPlayed && (!self.type || self.type === 'default') && self.targetEnemy.type === 'normal') {
window.youAreKillingMeSoundPlayed = true;
// Delay the youarekilling me sound by 1200ms for dramatic effect
tween({}, {}, {
duration: 1200,
onFinish: function onFinish() {
LK.getSound('youarekillingme').play();
}
});
}
// Track default bullet hits and play "it didn't hurt" sound on 7th hit (only for normal enemies)
if ((!self.type || self.type === 'default') && !self.targetEnemy.isFlying && self.targetEnemy.type === 'normal') {
defaultBulletHitCount++;
// Play "it didn't hurt" sound only once on 7th default bullet hit with delay
if (defaultBulletHitCount === 7 && !itDidntHurtSoundPlayed) {
itDidntHurtSoundPlayed = true;
// Delay the sound by 1200ms for clear speech
tween({}, {}, {
duration: 1200,
onFinish: function onFinish() {
LK.getSound('itdidnthurt').play();
}
});
}
// Play "you can't stop us" sound only once on 10th default bullet hit with delay
if (defaultBulletHitCount === 10 && !youCantStopUsSoundPlayed) {
youCantStopUsSoundPlayed = true;
// Delay the sound by 1200ms for clear speech
tween({}, {}, {
duration: 1200,
onFinish: function onFinish() {
LK.getSound('youcantstopus').play();
}
});
}
}
// Play "you will never give up, will you?" sound only once on first rapid bullet hit with delay (only for normal enemies)
if (self.type === 'rapid' && !youWillNeverGiveUpSoundPlayed && self.targetEnemy.type === 'normal') {
youWillNeverGiveUpSoundPlayed = true;
// Delay the sound by 1200ms for clear speech
tween({}, {}, {
duration: 1200,
onFinish: function onFinish() {
LK.getSound('youwillnevergiveup').play();
}
});
}
// Track rapid bullet hits and play "gıdıklanıyorum" sound only once on 13th rapid bullet hit with delay (only for normal enemies)
if (self.type === 'rapid' && !self.targetEnemy.isFlying && self.targetEnemy.type === 'normal') {
// Increment rapid bullet hit counter (using defaultBulletHitCount as rapid bullet counter)
if (!window.rapidBulletHitCount) {
window.rapidBulletHitCount = 0;
}
window.rapidBulletHitCount++;
// Play "gıdıklanıyorum" sound only once on 13th rapid bullet hit with delay
if (window.rapidBulletHitCount === 13 && !gidiklaniyorumSoundPlayed) {
gidiklaniyorumSoundPlayed = true;
// Delay the sound by 1400ms for clear speech
tween({}, {}, {
duration: 1400,
onFinish: function onFinish() {
LK.getSound('gidiklaniyorum').play();
}
});
}
}
// Splash bullet hit detection - goddamit sound now plays on bullet creation instead of hit
if (self.type === 'splash' && !self.targetEnemy.isFlying && self.targetEnemy.type === 'normal') {
// Sound is now played when bullet is created, not when it hits
}
// Create blood animation for rapid, sniper, and default bullet hits (before applying special effects)
// Skip blood animation for flying enemies, boss enemies, swarm enemies, and immune enemies - using object pool with reduced particle count
if ((self.type === 'rapid' || self.type === 'sniper' || !self.type || self.type === 'default') && !self.targetEnemy.isFlying && !self.targetEnemy.isBoss && self.targetEnemy.type !== 'swarm' && !self.targetEnemy.isImmune) {
// Dynamic blood particle count based on enemy count - reduce when more than 10 enemies
var bloodParticleCount = 4; // Default particle count
if (enemies.length > 10) {
bloodParticleCount = 2; // Reduce to 2 particles when more than 10 enemies
}
for (var bloodIdx = 0; bloodIdx < bloodParticleCount; bloodIdx++) {
var bloodParticle = getBloodParticle();
var bloodGraphics = bloodParticle.bloodGraphics;
bloodGraphics.width = 8 + Math.random() * 12;
bloodGraphics.height = bloodGraphics.width;
bloodGraphics.tint = 0xFF0000; // Red blood color
// Create blood splatter pattern
var splatterAngle = bloodIdx / bloodParticleCount * Math.PI * 2 + Math.random() * 0.5;
var splatterDistance = 15 + Math.random() * 25;
bloodParticle.x = self.targetEnemy.x + Math.cos(splatterAngle) * splatterDistance;
bloodParticle.y = self.targetEnemy.y + Math.sin(splatterAngle) * splatterDistance;
bloodParticle.alpha = 0.8 + Math.random() * 0.2;
bloodParticle.scaleX = 0.5 + Math.random() * 0.4;
bloodParticle.scaleY = 0.5 + Math.random() * 0.4;
game.addChild(bloodParticle);
// Animate blood particles falling and fading
var targetY = bloodParticle.y + (30 + Math.random() * 20);
var targetX = bloodParticle.x + (Math.random() - 0.5) * 15;
tween(bloodParticle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: bloodParticle.scaleX * 0.3,
scaleY: bloodParticle.scaleY * 0.3
}, {
duration: 800 + Math.random() * 400,
easing: tween.easeOut,
onFinish: function onFinish() {
returnParticle(bloodParticle);
}
});
}
}
// Apply special effects based on bullet type
if (self.type === 'splash') {
// Create black smoke effects using particle pool
var smokeCount = enemies.length > 10 ? 8 : 12; // Reduce smoke when many enemies
for (var smokeIdx = 0; smokeIdx < smokeCount; smokeIdx++) {
var smokeParticle = getSmokeParticle();
var smokeGraphics = smokeParticle.smokeGraphics;
smokeGraphics.width = 25 + Math.random() * 35;
smokeGraphics.height = smokeGraphics.width;
smokeGraphics.tint = 0x2a2a2a; // Dark smoke color
smokeParticle.x = self.targetEnemy.x + (Math.random() - 0.5) * 60;
smokeParticle.y = self.targetEnemy.y + (Math.random() - 0.5) * 60;
smokeParticle.alpha = 0.8 + Math.random() * 0.2;
smokeParticle.scaleX = 0.3 + Math.random() * 0.4;
smokeParticle.scaleY = 0.3 + Math.random() * 0.4;
game.addChild(smokeParticle);
// Animate smoke rising and fading
var targetY = smokeParticle.y - 80 - Math.random() * 40;
var targetX = smokeParticle.x + (Math.random() - 0.5) * 40;
tween(smokeParticle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: smokeParticle.scaleX * 2.5,
scaleY: smokeParticle.scaleY * 2.5
}, {
duration: 1200 + Math.random() * 800,
easing: tween.easeOut,
onFinish: function onFinish() {
returnParticle(smokeParticle);
}
});
}
// Create fire area effect using particle pool
var fireAreaRadius = CELL_SIZE * 1.5;
var fireCount = enemies.length > 10 ? 8 : 12; // Reduce fire particles when many enemies
for (var fireIdx = 0; fireIdx < fireCount; fireIdx++) {
var fireParticle = getFireParticle();
var fireGraphics = fireParticle.fireGraphics;
fireGraphics.width = 12 + Math.random() * 16;
fireGraphics.height = fireGraphics.width;
// Fire color gradient: red to orange to yellow
var fireColors = [0xff4500, 0xff6600, 0xff8800, 0xffaa00, 0xffcc00];
fireGraphics.tint = fireColors[Math.floor(Math.random() * fireColors.length)];
// Position fire particles in a circle around impact
var angle = fireIdx / 8 * Math.PI * 2 + Math.random() * 0.5;
var distance = Math.random() * fireAreaRadius;
fireParticle.x = self.targetEnemy.x + Math.cos(angle) * distance;
fireParticle.y = self.targetEnemy.y + Math.sin(angle) * distance;
fireParticle.alpha = 0.9;
fireParticle.scaleX = 0.5 + Math.random() * 0.5;
fireParticle.scaleY = 0.5 + Math.random() * 0.5;
game.addChild(fireParticle);
// Animate fire flickering and burning out
tween(fireParticle, {
alpha: 0,
scaleX: fireParticle.scaleX * 1.8,
scaleY: fireParticle.scaleY * 1.8,
y: fireParticle.y - 20 - Math.random() * 30
}, {
duration: 600 + Math.random() * 400,
easing: tween.easeOut,
onFinish: function onFinish() {
returnParticle(fireParticle);
}
});
}
// Visual splash effect removed - no green flash
// Splash damage to nearby enemies
var splashRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var splashDx = otherEnemy.x - self.targetEnemy.x;
var splashDy = otherEnemy.y - self.targetEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
// Apply splash damage (50% of original damage)
otherEnemy.health -= self.damage * 0.5;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
} else if (self.type === 'slow') {
// Get the range from the source tower for slow effect area
var slowRadius = self.sourceTower ? self.sourceTower.getRange() : CELL_SIZE * 3.5; // Use tower's actual range
var affectedEnemies = [];
// Find all enemies within slow radius from impact point
for (var i = 0; i < enemies.length; i++) {
var nearbyEnemy = enemies[i];
if (nearbyEnemy && nearbyEnemy.parent && nearbyEnemy.health > 0) {
var dx = nearbyEnemy.x - self.targetEnemy.x; // Use impact point as center
var dy = nearbyEnemy.y - self.targetEnemy.y; // Use impact point as center
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= slowRadius && !nearbyEnemy.isImmune) {
affectedEnemies.push(nearbyEnemy);
}
}
}
// Apply slow effect to all affected enemies
for (var i = 0; i < affectedEnemies.length; i++) {
var affectedEnemy = affectedEnemies[i];
// Create visual slow effect for each affected enemy
var slowEffect = new EffectIndicator(affectedEnemy.x, affectedEnemy.y, 'slow');
game.addChild(slowEffect);
// Apply slow effect
// Make slow percentage scale with source tower level if available
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 (!affectedEnemy.slowed) {
affectedEnemy.originalSpeed = affectedEnemy.speed;
affectedEnemy.speed *= 1 - slowPct; // Slow by X%
affectedEnemy.slowed = true;
affectedEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
affectedEnemy.slowDuration = 180; // Reset duration
}
}
} else if (self.type === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Increment poison bullet hit counter for tracking (only for normal enemies)
if (!self.targetEnemy.isFlying && self.targetEnemy.type === 'normal') {
poisonBulletHitCount++;
// Play gogogo sound only once for poison bullets on first poison bullet hit
if (poisonBulletHitCount === 1 && !gogogoPoisonSoundPlayed) {
gogogoPoisonSoundPlayed = true;
LK.getSound('gogogo').play();
}
// Play whofarted sound only once on second poison bullet hit with delay
if (poisonBulletHitCount === 2 && !whoFartedSoundPlayed) {
whoFartedSoundPlayed = true;
// Delay the whofarted sound by 800ms
tween({}, {}, {
duration: 800,
onFinish: function onFinish() {
LK.getSound('whofarted').play();
}
});
}
}
// Play poison bullet hit sound every 3rd hit for non-flying enemies (separate from the counter above)
if (!self.targetEnemy.isFlying) {
// Initialize global poison hit counter if not exists
if (!window.globalPoisonHitCount) {
window.globalPoisonHitCount = 0;
}
window.globalPoisonHitCount++;
// Play poison bullet hit sound every 3rd hit
if (window.globalPoisonHitCount % 3 === 0) {
LK.getSound('poison_bullet_hit').play();
}
}
// Coughing animation removed for poison bullets
// Poison bullet impact animation removed
// 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') {
// Sniper hit - play becareful sound only once on first contact (only for normal enemies)
if (!beCarefulSoundPlayed && self.targetEnemy.type === 'normal') {
beCarefulSoundPlayed = true;
LK.getSound('becareful').play();
}
// Track sniper bullet hits and play "sniperr" sound only once on 3rd hit (only for normal enemies)
if (!self.targetEnemy.isFlying && self.targetEnemy.type === 'normal') {
sniperBulletHitCount++;
// Play "sniperr" sound only once on 3rd sniper bullet hit
if (sniperBulletHitCount === 3 && !sniperrSoundPlayed) {
sniperrSoundPlayed = true;
LK.getSound('sniperr').play();
}
// Play "keepmoving" sound only once on 4th sniper bullet hit
if (sniperBulletHitCount === 4 && !keepMovingSoundPlayed) {
keepMovingSoundPlayed = true;
LK.getSound('keepmoving').play();
}
// Play "gogogo" sound only once on 7th sniper bullet hit
if (sniperBulletHitCount === 7 && !gogogoSniperSoundPlayed) {
gogogoSniperSoundPlayed = true;
LK.getSound('gogogo').play();
}
}
// Track sniper bullet hits on immune enemies and play "shy" sound on 3rd hit - only once per wave
if (self.targetEnemy.isImmune && !self.targetEnemy.isFlying) {
// Initialize counter if it doesn't exist
if (!window.sniperImmuneHitCounter) {
window.sniperImmuneHitCounter = 0;
}
window.sniperImmuneHitCounter++;
if (window.sniperImmuneHitCounter === 3 && !shySoundPlayedThisWave) {
shySoundPlayedThisWave = true;
// Delay the shy sound by 2000ms (2 seconds)
tween({}, {}, {
duration: 2000,
onFinish: function onFinish() {
LK.getSound('shy').play();
}
});
}
}
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
// Apply concentrated movement with subtle animation
var baseMovementX = Math.cos(angle) * self.speed;
var baseMovementY = Math.sin(angle) * self.speed;
// Add very subtle concentrated bouncing to movement
var concentratedBounceX = Math.sin(self.animationPhase * 1.2) * 0.8; // Much smaller bounce
var concentratedBounceY = Math.cos(self.animationPhase * 1.5) * 0.6; // Reduced bounce
self.x += baseMovementX + concentratedBounceX;
self.y += baseMovementY + concentratedBounceY;
}
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.setText("-");
cellGraphics.tint = 0x880000;
return;
}
numberLabel.visible = false;
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff;
} else {
cellGraphics.tint = 0x88 - tint << 8 | tint;
}
// Hide direction arrows by not displaying them
while (debugArrows.length > 0) {
self.removeChild(debugArrows.pop());
}
// Direction arrows are now hidden
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.tint = 0x008800;
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 1000) / 10);
};
});
// This update method was incorrectly placed here and should be 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;
}
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 = .01;
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;
// 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 *= 2; // Twice as fast
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'big':
self.maxHealth = 300; // Much stronger than normal enemies
self.speed *= 0.7; // Slower movement
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && 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.type === 'bigboss') {
assetId = 'bigboss';
} else if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.25,
scaleY: 1.25
});
// Add walking feet for normal and swarm enemies only (no feet for boss enemies)
self.leftFoot = null;
self.rightFoot = null;
if ((self.type === 'normal' || self.type === 'swarm') && !self.isBoss) {
// Adjust foot size based on enemy type
var footWidth = 18;
var footHeight = 13;
var footSpacing = 15;
var footYPosition = 35;
// Use normal elliptical feet
self.leftFoot = getWalkingFeetParticle();
self.leftFoot.feetGraphics.width = footWidth;
self.leftFoot.feetGraphics.height = footHeight;
self.leftFoot.feetGraphics.tint = 0x000000; // Black color for walking feet
self.rightFoot = getWalkingFeetParticle();
self.rightFoot.feetGraphics.width = footWidth;
self.rightFoot.feetGraphics.height = footHeight;
self.rightFoot.feetGraphics.tint = 0x000000; // Black color for walking feet
// Position feet relative to enemy size
self.leftFoot.x = -footSpacing; // Position to the left
self.leftFoot.y = footYPosition; // Position lower at bottom of enemy
self.addChild(self.leftFoot);
self.rightFoot.x = footSpacing; // Position to the right
self.rightFoot.y = footYPosition; // Position lower at bottom of enemy
self.addChild(self.rightFoot);
// Initialize foot animation variables
self.leftFootPhase = 0;
self.rightFootPhase = Math.PI; // Start opposite phase for alternating steps
}
// Scale up boss enemies
if (self.isBoss) {
if (self.type === 'bigboss') {
enemyGraphics.scaleX = 2.5;
enemyGraphics.scaleY = 2.5;
} else {
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,
scaleX: 1.25,
scaleY: 1.25
});
// 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;
// Initialize health bar visibility - hide by default until damage is taken
healthBarOutline.visible = false;
healthBarBG.visible = false;
healthBar.visible = false;
self.hasBeenDamaged = false; // Track if enemy has taken damage
// Initialize walking animation variables
self.walkAnimationPhase = Math.random() * Math.PI * 2; // Random starting phase for each enemy
self.walkAnimationSpeed = 0.15; // Animation speed
self.walkBobAmount = 3; // How much to bob up and down
self.lastWalkingState = false;
self.isCurrentlyMakingSound = false; // Flag to limit footstep sounds
self.hasPlayedFirstPoisonSound = false; // Track if gogogo sound has been played for this enemy
self.update = function () {
// Track last health for damage animation
if (self.lastHealth === undefined) {
self.lastHealth = self.health;
}
// Check if enemy took damage this frame (blood animation now handled in Bullet class)
if (self.lastHealth > self.health) {
// Show health bar when enemy takes damage for the first time
if (!self.hasBeenDamaged) {
self.hasBeenDamaged = true;
healthBarOutline.visible = true;
healthBarBG.visible = true;
healthBar.visible = true;
}
}
// Hide health bar when enemy is at full health
if (self.health >= self.maxHealth && self.hasBeenDamaged) {
healthBarOutline.visible = false;
healthBarBG.visible = false;
healthBar.visible = false;
} else if (self.hasBeenDamaged && self.health < self.maxHealth) {
// Keep health bar visible when damaged but not at full health
healthBarOutline.visible = true;
healthBarBG.visible = true;
healthBar.visible = true;
}
// Update last health
self.lastHealth = self.health;
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = 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
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
// Only show slow effect tint when both poisoned and slowed
enemyGraphics.tint = 0x9900FF;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
// Walking animation logic - enhanced for realism (disabled for flying enemies)
// Check if enemy is on screen (has appeared) for animation and sound
var isOnScreen = self.currentCellY >= -1; // Enemy is visible or about to be visible
var isCurrentlyWalking = isOnScreen && !self.isFlying && (self.currentTarget && (self.currentTarget.x !== self.currentCellX || self.currentTarget.y !== self.currentCellY) || self.currentCellY < 4);
// Batch walking animations - only animate every 3rd frame when many enemies
var shouldAnimate = enemies.length <= 15 || (LK.ticks + self.waveNumber) % 3 === 0;
// Update walking animation if enemy is moving and on screen (but not for flying enemies)
if (isCurrentlyWalking && shouldAnimate) {
// Advance animation phase based on actual movement speed for realistic timing
var speedMultiplier = self.speed * 100; // Scale animation speed with movement speed
self.walkAnimationPhase += self.walkAnimationSpeed * speedMultiplier;
// Create more realistic walking motion with multiple animation components
var primaryBob = Math.sin(self.walkAnimationPhase) * self.walkBobAmount;
var secondaryBob = Math.sin(self.walkAnimationPhase * 2) * (self.walkBobAmount * 0.3);
var combinedBobOffset = primaryBob + secondaryBob;
// Add subtle horizontal sway for more natural movement
var horizontalSway = Math.sin(self.walkAnimationPhase * 0.5) * (self.walkBobAmount * 0.2);
// Animate feet for normal and swarm enemies only (no feet for boss enemies)
if ((self.type === 'normal' || self.type === 'swarm') && !self.isBoss && self.leftFoot && self.rightFoot) {
// Update foot animation phases
self.leftFootPhase += self.walkAnimationSpeed * speedMultiplier * 2;
self.rightFootPhase += self.walkAnimationSpeed * speedMultiplier * 2;
// Keep feet at fixed positions
var footYPos = 35;
var footSpacing = 15;
self.leftFoot.y = footYPos; // Fixed position
self.leftFoot.x = -footSpacing; // Fixed position
self.rightFoot.y = footYPos; // Fixed position
self.rightFoot.x = footSpacing; // Fixed position
// Use tween for smooth scaling animation
var leftFootScale = 1 + Math.abs(Math.sin(self.leftFootPhase)) * 0.4; // Bigger scaling effect
var rightFootScale = 1 + Math.abs(Math.sin(self.rightFootPhase)) * 0.4; // Bigger scaling effect
// Apply scaling animation using tween for smoothness
if (self.leftFoot && !self.leftFoot.isAnimating) {
self.leftFoot.isAnimating = true;
tween(self.leftFoot, {
scaleX: leftFootScale,
scaleY: leftFootScale
}, {
duration: 100,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.leftFoot) {
self.leftFoot.isAnimating = false;
}
}
});
}
if (self.rightFoot && !self.rightFoot.isAnimating) {
self.rightFoot.isAnimating = true;
tween(self.rightFoot, {
scaleX: rightFootScale,
scaleY: rightFootScale
}, {
duration: 100,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.rightFoot) {
self.rightFoot.isAnimating = false;
}
}
});
}
// Synchronize feet rotation with enemy rotation
if (self.leftFoot && self.rightFoot && enemyGraphics.targetRotation !== undefined) {
var targetFootRotation = enemyGraphics.targetRotation;
// Smoothly rotate feet to match enemy direction
if (Math.abs(targetFootRotation - (self.leftFoot.rotation || 0)) > 0.05) {
tween(self.leftFoot, {
rotation: targetFootRotation
}, {
duration: 250,
easing: tween.easeOut
});
}
if (Math.abs(targetFootRotation - (self.rightFoot.rotation || 0)) > 0.05) {
tween(self.rightFoot, {
rotation: targetFootRotation
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
// Apply different animation intensity based on enemy type
var animationIntensity = 1;
switch (self.type) {
case 'fast':
animationIntensity = 1.5; // More energetic movement
break;
case 'immune':
animationIntensity = 0.8; // More controlled movement
break;
case 'swarm':
animationIntensity = 1.3; // Quick, jittery movement
break;
}
// Apply boss scaling for more imposing movement
if (self.isBoss) {
animationIntensity *= 0.7; // Slower, more deliberate movement
combinedBobOffset *= 1.2; // But with more weight
}
// Smooth animation transitions using tweening
var targetY = combinedBobOffset * animationIntensity;
var targetX = horizontalSway * animationIntensity;
// Use tween for smoother animation instead of direct assignment
if (!self.animatingMovement) {
self.animatingMovement = true;
tween(enemyGraphics, {
y: targetY,
x: targetX
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
self.animatingMovement = false;
}
});
}
// Add slight rotation for more dynamic movement
var walkRotation = Math.sin(self.walkAnimationPhase * 1.5) * 0.05; // Very subtle rotation
if (!self.animatingRotation) {
self.animatingRotation = true;
tween(enemyGraphics, {
rotation: enemyGraphics.rotation + walkRotation
}, {
duration: 100,
easing: tween.easeInOut,
onFinish: function onFinish() {
self.animatingRotation = false;
}
});
}
// Play footstep sound at specific points in the walking cycle when enemy is on screen (not for flying enemies)
var walkCycle = Math.floor(self.walkAnimationPhase / (Math.PI / 2)) % 4;
var lastWalkCycle = Math.floor((self.walkAnimationPhase - self.walkAnimationSpeed * speedMultiplier) / (Math.PI / 2)) % 4;
// Play walking sound for non-flying enemies with reduced frequency to avoid audio overload
if (walkCycle !== lastWalkCycle && walkCycle % 2 === 0 && !self.isCurrentlyMakingSound) {
// Limit walking sounds based on enemy count to prevent audio chaos
var enemyCount = enemies ? enemies.length : 0;
var shouldPlaySound = false;
// Play sound based on enemy count - fewer enemies = more sounds
if (enemyCount <= 5) {
shouldPlaySound = true; // Always play for small groups
} else if (enemyCount <= 15) {
shouldPlaySound = Math.random() < 0.3; // 30% chance for medium groups
} else {
shouldPlaySound = Math.random() < 0.1; // 10% chance for large groups
}
if (shouldPlaySound) {
self.isCurrentlyMakingSound = true;
LK.getSound('walking').play();
// Reset sound flag after a short delay
tween({}, {}, {
duration: 300,
onFinish: function onFinish() {
self.isCurrentlyMakingSound = false;
}
});
}
}
} else {
// Smoothly return to resting position when not walking (only for non-flying enemies)
if (!self.isFlying && (enemyGraphics.y !== 0 || enemyGraphics.x !== 0)) {
tween.stop(enemyGraphics, {
y: true,
x: true,
rotation: true
});
tween(enemyGraphics, {
y: 0,
x: 0,
rotation: enemyGraphics.targetRotation || 0
}, {
duration: 200,
easing: tween.easeOut
});
self.animatingMovement = false;
self.animatingRotation = false;
}
// Reset feet to resting position for normal and swarm enemies only when not walking
if ((self.type === 'normal' || self.type === 'swarm') && !self.isBoss && self.leftFoot && self.rightFoot && !isCurrentlyWalking) {
// Stop any ongoing animations
tween.stop(self.leftFoot, {
scaleX: true,
scaleY: true
});
tween.stop(self.rightFoot, {
scaleX: true,
scaleY: true
});
// Reset animation flags with null checks
if (self.leftFoot) {
self.leftFoot.isAnimating = false;
}
if (self.rightFoot) {
self.rightFoot.isAnimating = false;
}
// Return feet to normal scale and fixed positions, maintaining rotation sync
var restFootYPos = 35;
var restFootSpacing = 15;
tween(self.leftFoot, {
y: restFootYPos,
x: -restFootSpacing,
scaleX: 1,
scaleY: 1,
rotation: enemyGraphics.targetRotation || 0
}, {
duration: 200,
easing: tween.easeOut
});
tween(self.rightFoot, {
y: restFootYPos,
x: restFootSpacing,
scaleX: 1,
scaleY: 1,
rotation: enemyGraphics.targetRotation || 0
}, {
duration: 200,
easing: tween.easeOut
});
}
}
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);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
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: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
// BossEnemy class removed - using regular enemies with health multipliers instead
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
*/
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0;
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = 0;
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = 0;
}
}
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 = [];
if (j > 3 && j <= gridHeight - 4) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
// Check if we can use cached pathfinding result
var currentTime = LK.ticks;
// Adjust cache timeout based on activity
var activityLevel = enemies.length + towers.length;
if (activityLevel < 10) {
dynamicCacheTimeout = 600; // 10 seconds for low activity
} else if (activityLevel < 20) {
dynamicCacheTimeout = 450; // 7.5 seconds for medium activity
} else {
dynamicCacheTimeout = 300; // 5 seconds for high activity
}
if (pathfindingCache && currentTime - lastPathfindTime < dynamicCacheTimeout) {
// Use cached result
for (var i = 0; i < self.cells.length; i++) {
for (var j = 0; j < self.cells[i].length; j++) {
var cell = self.cells[i][j];
var cachedCell = pathfindingCache[i][j];
if (cachedCell) {
cell.score = cachedCell.score;
cell.pathId = cachedCell.pathId;
cell.targets = cachedCell.targets;
}
}
}
maxScore = pathfindingCache.maxScore;
pathId = pathfindingCache.pathId;
return false; // No blocking found in cache
}
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;
}
}
// Cache the successful pathfinding result
pathfindingCache = {
maxScore: maxScore,
pathId: pathId
};
// Deep copy cell data for cache
for (var i = 0; i < self.cells.length; i++) {
pathfindingCache[i] = [];
for (var j = 0; j < self.cells[i].length; j++) {
var cell = self.cells[i][j];
pathfindingCache[i][j] = {
score: cell.score,
pathId: cell.pathId,
targets: cell.targets.slice() // Copy array
};
}
}
lastPathfindTime = LK.ticks;
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
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;
// Sync shadow animation with main enemy
enemy.shadow.children[0].y = enemy.children[0].y * 0.5; // Shadow moves less than main sprite
}
}
// 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) {
// Enhanced walking animation for pre-entry movement (disabled for flying enemies)
if (!enemy.isFlying) {
var speedMultiplier = enemy.speed * 100;
enemy.walkAnimationPhase += enemy.walkAnimationSpeed * speedMultiplier;
// Create more realistic pre-entry walking motion
var primaryBob = Math.sin(enemy.walkAnimationPhase) * enemy.walkBobAmount;
var secondaryBob = Math.sin(enemy.walkAnimationPhase * 2) * (enemy.walkBobAmount * 0.2);
var bobOffset = primaryBob + secondaryBob;
// Add slight horizontal movement for pre-entry
var horizontalSway = Math.sin(enemy.walkAnimationPhase * 0.7) * (enemy.walkBobAmount * 0.15);
if (enemy.children[0]) {
// Apply different animation styles based on enemy type during pre-entry
var animationIntensity = 1;
switch (enemy.type) {
case 'fast':
animationIntensity = 1.4;
break;
case 'swarm':
animationIntensity = 1.2;
break;
}
if (enemy.isBoss) {
animationIntensity *= 0.8;
bobOffset *= 1.1;
}
// Smooth animation application
var targetY = bobOffset * animationIntensity;
var targetX = horizontalSway * animationIntensity;
if (!enemy.preEntryAnimating) {
enemy.preEntryAnimating = true;
tween(enemy.children[0], {
y: targetY,
x: targetX
}, {
duration: 60,
easing: tween.easeOut,
onFinish: function onFinish() {
enemy.preEntryAnimating = false;
}
});
}
}
// Play footstep sound for pre-entry movement
var walkCycle = Math.floor(enemy.walkAnimationPhase / (Math.PI / 2)) % 4;
var lastWalkCycle = Math.floor((enemy.walkAnimationPhase - enemy.walkAnimationSpeed) / (Math.PI / 2)) % 4;
}
// Footstep sounds removed for enemies
// Move directly downward
enemy.currentCellY += enemy.speed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = 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
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);
// Play goddayfordie sound when normal enemies first enter the screen
if (enemy.type === 'normal' && !godDayForDieSoundPlayed) {
godDayForDieSoundPlayed = true;
LK.getSound('goddayfordie').play();
}
// Play fast enemy sounds when fast enemies first enter the screen
if (enemy.type === 'fast' && !fastEnemySoundPlayed) {
fastEnemySoundPlayed = true;
// Play first sound when enemy enters screen
LK.getSound('fast_enemy_sound').play();
// Play second sound after a 5 second delay
tween({}, {}, {
duration: 5000,
// 5 second delay between fast enemy sounds
onFinish: function onFinish() {
LK.getSound('for_the_fallen').play();
}
});
}
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
// Flying enemies move without wing-flapping animation - simple smooth movement
// No complex animation for flying enemies
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
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);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget) {
enemy.currentTarget = cell.targets[0];
}
if (enemy.currentTarget) {
if (cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
// Check if enemy is close to reaching the goal (within 2 cells) and play "we are coming for you" sound once (only for normal enemies)
if (!weAreComingForYouSoundPlayed && cell.score < 20000 && enemy.type === 'normal') {
// Close to goal
weAreComingForYouSoundPlayed = true;
// Play the sound and make enemy disappear after sound finishes
LK.getSound('wearecomingforyou').play();
// Use tween to delay enemy disappearing until sound finishes
tween({}, {}, {
duration: 2000,
// Approximate sound duration
onFinish: function onFinish() {
// Make the enemy disappear by removing it from the game
if (enemy.parent) {
// 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 {
enemyLayerBottom.removeChild(enemy);
}
// Remove from enemies array
var enemyIndex = enemies.indexOf(enemy);
if (enemyIndex !== -1) {
enemies.splice(enemyIndex, 1);
}
}
}
});
}
if (dist < enemy.speed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
// Enhanced walking animation for normal pathfinding movement (disabled for flying enemies)
if (!enemy.isFlying) {
var speedMultiplier = enemy.speed * 100;
enemy.walkAnimationPhase += enemy.walkAnimationSpeed * speedMultiplier;
// Create realistic walking motion with multiple components
var primaryBob = Math.sin(enemy.walkAnimationPhase) * enemy.walkBobAmount;
var secondaryBob = Math.sin(enemy.walkAnimationPhase * 2.2) * (enemy.walkBobAmount * 0.25);
var bobOffset = primaryBob + secondaryBob;
// Add natural horizontal sway
var horizontalSway = Math.sin(enemy.walkAnimationPhase * 0.6) * (enemy.walkBobAmount * 0.18);
if (enemy.children[0]) {
// Apply type-specific animation characteristics
var animationIntensity = 1;
switch (enemy.type) {
case 'fast':
animationIntensity = 1.6; // Very energetic
break;
case 'immune':
animationIntensity = 0.75; // More controlled
break;
case 'swarm':
animationIntensity = 1.3; // Quick and jittery
// Add random jitter for swarm enemies
bobOffset += (Math.random() - 0.5) * enemy.walkBobAmount * 0.2;
break;
}
if (enemy.isBoss) {
animationIntensity *= 0.7; // Slower but more imposing
bobOffset *= 1.3; // More pronounced movement
}
// Apply smooth animation transitions
var targetY = bobOffset * animationIntensity;
var targetX = horizontalSway * animationIntensity;
if (!enemy.pathfindingAnimating) {
enemy.pathfindingAnimating = true;
tween(enemy.children[0], {
y: targetY,
x: targetX
}, {
duration: 45,
easing: tween.easeOut,
onFinish: function onFinish() {
enemy.pathfindingAnimating = false;
}
});
}
}
// Play footstep sound for normal pathfinding movement
var walkCycle = Math.floor(enemy.walkAnimationPhase / (Math.PI / 2)) % 4;
var lastWalkCycle = Math.floor((enemy.walkAnimationPhase - enemy.walkAnimationSpeed) / (Math.PI / 2)) % 4;
}
// Footstep sounds removed for enemies
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('next_wave_bg', {
anchorX: 0.5,
anchorY: 0.5
});
// buttonBackground.tint = 0x0088FF; // Removed to show original image colors
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; // Removed to show original image colors
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
// buttonBackground.tint = 0x888888; // Removed to show original image colors
self.alpha = 0.7;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
}
};
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 || 'default';
// Get appropriate asset for this tower type
var assetId = 'tower_' + self.towerType;
// Increase size of base for easier touch
var baseGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2
});
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
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 typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
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);
// Check if tower type can be placed (for poison and slow towers)
var canPlace = canPlaceTowerType(self.towerType);
// Hide tower icon instead of making it transparent
self.visible = canAfford && canPlace;
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// 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: base 5, +0.8 per level, but final upgrade gets a huge boost
if (self.level === self.maxLevel) {
return 12 * CELL_SIZE; // Significantly increased range for max level
}
return (5 + (self.level - 1) * 0.8) * CELL_SIZE;
case 'splash':
// Splash: base 2, +0.2 per level (max ~4 blocks at max level)
return (2 + (self.level - 1) * 0.2) * CELL_SIZE;
case 'rapid':
// Rapid: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// Slow: base 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
default:
// Default: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
switch (self.id) {
case 'rapid':
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 'splash':
self.fireRate = 75;
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 50;
self.damage = 8;
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;
}
// Get appropriate asset for this tower type
var assetId = 'tower_' + self.id;
var baseGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.4,
scaleY: 1.4
});
baseGraphics.alpha = 0; // Hide tower base graphics
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('tower_level', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('tower_level', {
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);
// Get appropriate defense asset for this tower type
var defenseAssetId = 'defense_' + self.id;
var gunGraphics = gunContainer.attachAsset(defenseAssetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
// Make poison tower defense graphics transparent
if (self.id === 'poison') {
gunGraphics.alpha = 0;
}
// Make slow tower defense graphics transparent
if (self.id === 'slow') {
gunGraphics.alpha = 0;
}
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 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF;
break;
case 'poison':
towerLevelIndicator.tint = 0x00FFAA;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
// Add upgrade warning indicator
var upgradeWarning = self.attachAsset('upgrade_warning', {
anchorX: 0.5,
anchorY: 0.5
});
upgradeWarning.x = -CELL_SIZE * 1.1; // Position further left, outside tower graphic
upgradeWarning.y = -CELL_SIZE * 1.1; // Position further up, outside tower graphic
upgradeWarning.visible = false; // Hidden by default
upgradeWarning.tint = 0xFFD700; // Gold color for upgrade indication
self.upgradeWarning = upgradeWarning;
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);
}
}
}
}
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 === 'rapid') {
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 {
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();
//Play tower upgrade sound
LK.getSound('tower_upgrade').play();
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 () {
// Cache last target if still valid and in range
if (self.targetEnemy && self.targetEnemy.parent && self.targetEnemy.health > 0) {
if (self.isInRange(self.targetEnemy)) {
return self.targetEnemy;
}
}
var closestEnemy = null;
var closestScore = Infinity;
var towerRange = self.getRange();
// Use spatial partitioning to get only nearby enemies
var nearbyEnemies = spatialGrid.getEnemiesInRange(self.x, self.y, towerRange);
for (var i = 0; i < nearbyEnemies.length; i++) {
var enemy = nearbyEnemies[i];
if (!enemy.parent || enemy.health <= 0) continue;
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distanceSquared = dx * dx + dy * dy;
var rangeSquared = towerRange * towerRange;
// Check if enemy is in range using squared distance (faster than sqrt)
if (distanceSquared <= rangeSquared) {
// 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 (distanceSquared < closestScore) {
closestScore = distanceSquared;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, 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 () {
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
// Only rotate gun for non-poison and non-slow towers
if (self.id !== 'poison' && self.id !== 'slow') {
gunContainer.rotation = angle;
}
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
// Continuous poison cloud animation for poison towers
if (self.id === 'poison') {
self.poisonCloudTimer++;
// Count poison towers to balance animation frequency
var poisonTowerCount = 0;
for (var t = 0; t < towers.length; t++) {
if (towers[t].id === 'poison') {
poisonTowerCount++;
}
}
// Adjust frequency based on poison tower count - reduce frequency with more towers
var poisonFrequency = 20; // Reduced base frequency for faster animation
if (poisonTowerCount > 3) {
// Increase interval (reduce frequency) when more than 3 poison towers
poisonFrequency = 20 + (poisonTowerCount - 3) * 10; // Reduced multiplier for faster animation
}
// Create poison clouds at adjusted frequency
if (self.poisonCloudTimer % poisonFrequency === 0) {
self.createContinuousPoisonClouds();
}
}
// Continuous motor exhaust animation for splash towers
if (self.id === 'splash') {
self.exhaustTimer++;
// Count splash towers to balance animation frequency
var splashTowerCount = 0;
for (var t = 0; t < towers.length; t++) {
if (towers[t].id === 'splash') {
splashTowerCount++;
}
}
// Adjust frequency based on splash tower count - reduce frequency with more towers
var exhaustFrequency = 25; // Base frequency for exhaust animation
if (splashTowerCount > 2) {
// Increase interval (reduce frequency) when more than 2 splash towers
exhaustFrequency = 25 + (splashTowerCount - 2) * 8;
}
// Create motor exhaust at adjusted frequency
if (self.exhaustTimer % exhaustFrequency === 0) {
self.createMotorExhaust();
}
}
// Continuous flame animation for slow towers
if (self.id === 'slow') {
self.flameTimer++;
// Count slow towers to balance animation frequency
var slowTowerCount = 0;
for (var t = 0; t < towers.length; t++) {
if (towers[t].id === 'slow') {
slowTowerCount++;
}
}
// Adjust frequency based on slow tower count - reduce frequency with more towers
var flameFrequency = 18; // Base frequency for flame animation (faster than exhaust)
if (slowTowerCount > 2) {
// Increase interval (reduce frequency) when more than 2 slow towers
flameFrequency = 18 + (slowTowerCount - 2) * 6;
}
// Create continuous flames at adjusted frequency
if (self.flameTimer % flameFrequency === 0) {
self.createContinuousFlames();
}
}
// Check if tower can be upgraded and has enough gold
self.checkUpgradeAvailability();
};
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();
};
self.fire = function () {
// Slow towers apply area damage to ALL enemies in range
if (self.id === 'slow') {
// Apply slow effect and area damage to all enemies in range
var slowRadius = self.getRange();
var affectedEnemies = [];
// Find all enemies within slow radius from tower
for (var i = 0; i < enemies.length; i++) {
var nearbyEnemy = enemies[i];
if (nearbyEnemy && nearbyEnemy.parent && nearbyEnemy.health > 0 && !nearbyEnemy.isFlying) {
var dx = nearbyEnemy.x - self.x;
var dy = nearbyEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= slowRadius) {
affectedEnemies.push(nearbyEnemy);
}
}
}
// Apply area damage to only one enemy (the first one in the list)
if (affectedEnemies.length > 0) {
var damagedEnemy = affectedEnemies[0];
// Apply area damage to only one enemy (including immune ones)
damagedEnemy.health -= self.damage;
if (damagedEnemy.health <= 0) {
damagedEnemy.health = 0;
} else {
damagedEnemy.healthBar.width = damagedEnemy.health / damagedEnemy.maxHealth * 70;
}
// Play area damage sound every 4 seconds (240 frames at 60 FPS)
if (!self.lastAreaSoundTime) {
self.lastAreaSoundTime = 0;
}
if (LK.ticks - self.lastAreaSoundTime >= 240) {
LK.getSound('alanhasar').play();
self.lastAreaSoundTime = LK.ticks;
}
}
// Play welcometohell sound only once for the first enemy entering slow tower area damage
if (affectedEnemies.length > 0 && !welcomeToHellSoundPlayed) {
welcomeToHellSoundPlayed = true;
LK.getSound('welcometohell').play();
}
// Count and play bronz sound for 5th enemy entering slow tower area damage
if (affectedEnemies.length > 0) {
slowAreaEnemyCount++;
if (slowAreaEnemyCount === 5 && !bronzSoundPlayed) {
bronzSoundPlayed = true;
// Delay the bronz sound by 800ms
tween({}, {}, {
duration: 800,
onFinish: function onFinish() {
LK.getSound('bronz').play();
}
});
}
}
// Track enemies entering and exiting slow area
for (var i = 0; i < affectedEnemies.length; i++) {
var affectedEnemy = affectedEnemies[i];
if (enemiesInSlowArea.indexOf(affectedEnemy) === -1) {
enemiesInSlowArea.push(affectedEnemy);
}
}
// Apply effects to all affected enemies (visual effects and slow)
for (var i = 0; i < affectedEnemies.length; i++) {
var affectedEnemy = affectedEnemies[i];
// Play krk sound for immune enemies hit by slow tower - only once per wave
if (affectedEnemy.isImmune && !window.krkSoundPlayedThisWave) {
window.krkSoundPlayedThisWave = true;
LK.getSound('krk').play();
}
// Create flame animation for each affected enemy using particle pool
var flameCount = 8; // Number of flame particles per enemy
for (var flameIdx = 0; flameIdx < flameCount; flameIdx++) {
var flameParticle = getFireParticle();
var flameGraphics = flameParticle.fireGraphics;
flameGraphics.width = 15 + Math.random() * 20;
flameGraphics.height = flameGraphics.width;
// Flame color palette
var flameColors = [0xFF4500, 0xFF6600, 0xFF8800, 0xFFAA00, 0xFFCC00];
flameGraphics.tint = flameColors[Math.floor(Math.random() * flameColors.length)];
// Position flame particles around the enemy
var angle = flameIdx / flameCount * Math.PI * 2 + Math.random() * 0.5;
var distance = Math.random() * 40;
flameParticle.x = affectedEnemy.x + Math.cos(angle) * distance;
flameParticle.y = affectedEnemy.y + Math.sin(angle) * distance;
flameParticle.alpha = 0.9;
flameParticle.scaleX = 0.5 + Math.random() * 0.5;
flameParticle.scaleY = 0.5 + Math.random() * 0.5;
game.addChild(flameParticle);
// Animate flame flickering and burning out
tween(flameParticle, {
alpha: 0,
scaleX: flameParticle.scaleX * 1.8,
scaleY: flameParticle.scaleY * 1.8,
y: flameParticle.y - 20 - Math.random() * 30
}, {
duration: 600 + Math.random() * 400,
easing: tween.easeOut,
onFinish: function onFinish() {
returnParticle(flameParticle);
}
});
}
// Apply slow effect only to non-immune enemies
if (!affectedEnemy.isImmune) {
var slowPct = 0.5;
if (self.level !== 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.level - 1));
slowPct = slowLevels[idx];
}
if (!affectedEnemy.slowed) {
affectedEnemy.originalSpeed = affectedEnemy.speed;
affectedEnemy.speed *= 1 - slowPct; // Slow by X%
affectedEnemy.slowed = true;
affectedEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
affectedEnemy.slowDuration = 180; // Reset duration
}
}
}
return; // Exit early for slow towers
}
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
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 default towers, randomly use bullet_5 asset 20% of the time
if (self.id === 'default' && Math.random() < 0.2) {
bullet.isBullet5 = true;
// Replace default bullet graphics with bullet_5 asset
if (bullet.children[0].parent) {
bullet.removeChild(bullet.children[0]);
}
var bullet5Graphics = bullet.attachAsset('bullet_5', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Customize bullet appearance based on tower type
switch (self.id) {
case 'rapid':
bullet.children[0].tint = 0x00AAFF;
bullet.children[0].width = 20;
bullet.children[0].height = 20;
break;
case 'sniper':
bullet.children[0].tint = 0xFF5500;
bullet.children[0].width = 15;
bullet.children[0].height = 15;
break;
case 'splash':
// Play goddamit sound only once per wave on first splash bullet creation for normal enemies only
if (!window.splashBulletSoundPlayed && self.targetEnemy.type === 'normal') {
window.splashBulletSoundPlayed = true;
// Delay the sound by 1200ms for clear speech
tween({}, {}, {
duration: 1200,
onFinish: function onFinish() {
LK.getSound('goddamit').play();
}
});
}
// Remove old bullet graphics and add splash-specific asset
if (bullet.children[0].parent) {
bullet.removeChild(bullet.children[0]);
}
var splashBulletGraphics = bullet.attachAsset('bullet_splash', {
anchorX: 0.5,
anchorY: 0.5
});
break;
case 'poison':
// Hide the poison bullet graphic
bullet.children[0].alpha = 0;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
//Play tower shooting sound (except for poison and slow towers)
if (self.id !== 'poison' && self.id !== 'slow') {
// Play wifi sound for default tower bullets targeting bagışıkdüşman - only once per wave
if (self.id === 'default' && self.targetEnemy && self.targetEnemy.isImmune && !wifiSoundPlayedThisWave) {
wifiSoundPlayedThisWave = true;
LK.getSound('wifi').play();
// Play taksi sound for default tower bullet_5 theme targeting bagışıkdüşman - only once per wave
} else if (self.id === 'default' && bullet.isBullet5 && self.targetEnemy && self.targetEnemy.isImmune && !taksiSoundPlayedThisWave) {
taksiSoundPlayedThisWave = true;
// Delay the taksi sound by 2000ms
tween({}, {}, {
duration: 2000,
onFinish: function onFinish() {
LK.getSound('taksi').play();
}
});
// Play gözlük sound for sniper tower bullets targeting bagışıkdüşman on 1st hit - only once per wave
} else if (self.id === 'sniper' && self.targetEnemy && self.targetEnemy.isImmune) {
sniperBulletImmuneHitCount++;
if (sniperBulletImmuneHitCount === 1 && !gözlükSoundPlayedThisWave) {
gözlükSoundPlayedThisWave = true;
LK.getSound('gozluk').play();
} else {
LK.getSound('tower_shoot').play();
}
// Play vasiyet sound for rapid tower bullets targeting bagışıkdüşman on 3rd hit - only once per wave
} else if (self.id === 'rapid' && self.targetEnemy && self.targetEnemy.isImmune) {
rapidBulletImmuneHitCount++;
if (rapidBulletImmuneHitCount === 3 && !vasiyetSoundPlayedThisWave) {
vasiyetSoundPlayedThisWave = true;
LK.getSound('vasiyet').play();
} else {
LK.getSound('tower_shoot').play();
}
// Play shy sound for sniper tower bullets targeting bagışıkdüşman on 3rd hit - only once per wave
} else if (self.id === 'sniper' && self.targetEnemy && self.targetEnemy.isImmune) {
// Don't increment counter here - it should be incremented in bullet hit logic
LK.getSound('tower_shoot').play();
} else {
LK.getSound('tower_shoot').play();
}
}
// --- Fire recoil effect for gunContainer (not for poison towers) ---
if (self.id !== 'poison') {
// 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;
// Only set cell type to 1 (wall) for non-poison towers
// Poison towers don't block enemy movement
if (self.id !== 'poison') {
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 1;
}
}
}
}
self.refreshCellsInRange();
// Invalidate pathfinding cache when tower is placed
pathfindingCache = null;
// Initialize continuous poison cloud animation for poison towers
if (self.id === 'poison') {
self.poisonCloudTimer = 0;
self.createContinuousPoisonClouds = function () {
// Create reduced poison cloud particles around the tower continuously using particle pool
var cloudCount = enemies.length > 15 ? 8 : 12; // Reduced cloud count for less intensive animation
for (var cloudIdx = 0; cloudIdx < cloudCount; cloudIdx++) {
var poisonCloud = getPoisonCloudParticle();
var cloudGraphics = poisonCloud.cloudGraphics;
cloudGraphics.width = 25 + Math.random() * 35; // Smaller cloud size
cloudGraphics.height = cloudGraphics.width;
// Enhanced poison color palette with toxic variations
var toxicColors = [0x00FF00, 0x00DD00, 0x00BB00, 0x228B22, 0x32CD32, 0x7CFC00, 0x9ACD32, 0x00FF7F];
cloudGraphics.tint = toxicColors[Math.floor(Math.random() * toxicColors.length)];
// Position cloud particles in multiple concentric circles around the tower
var ringNumber = Math.floor(cloudIdx / 4); // Adjusted for smaller cloud count
var angleInRing = cloudIdx % 4 * (Math.PI * 2 / 4) + Math.random() * 0.6; // Reduced randomness
var baseDistance = 35 + ringNumber * 20; // Slightly smaller distance
var distance = baseDistance + Math.random() * 20; // Reduced spread
poisonCloud.x = self.x + Math.cos(angleInRing) * distance;
poisonCloud.y = self.y + Math.sin(angleInRing) * distance;
poisonCloud.alpha = 0.5 + Math.random() * 0.3; // Slightly more transparent
poisonCloud.scaleX = 0.4 + Math.random() * 0.4; // Smaller initial scale
poisonCloud.scaleY = 0.4 + Math.random() * 0.4;
game.addChild(poisonCloud);
// Reduced poison cloud animation with swirling motion
var targetScale = poisonCloud.scaleX * (2.2 + Math.random() * 1.5); // Smaller target scale
// Create swirling motion around the tower
var swirl = Math.random() > 0.5 ? 1 : -1;
var swirlAngle = angleInRing + swirl * (Math.PI * 1.2 + Math.random() * Math.PI * 0.6); // Reduced swirl
var swirlRadius = distance * (0.7 + Math.random() * 0.3); // Tighter radius
var targetX = self.x + Math.cos(swirlAngle) * swirlRadius + (Math.random() - 0.5) * 30; // Less spread
var targetY = poisonCloud.y - (25 + Math.random() * 35); // Reduced vertical movement
// Add pulsing effect by varying the target scale over time
var pulseScale = targetScale * (0.7 + Math.random() * 0.3); // Less pulsing
var rotationSpeed = swirl * (Math.PI * 2 + Math.random() * Math.PI * 1.2); // Slower rotation
tween(poisonCloud, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: pulseScale,
scaleY: pulseScale,
rotation: rotationSpeed
}, {
duration: 1000 + Math.random() * 400,
// Shorter duration
easing: tween.easeOut,
onFinish: function onFinish() {
returnParticle(poisonCloud);
}
});
}
};
}
// Initialize motor exhaust particle effects for splash towers
if (self.id === 'splash') {
self.exhaustTimer = 0;
self.createMotorExhaust = function () {
// Play motor exhaust sound effect
LK.getSound('egzos').play();
// Create motor exhaust particles behind the splash tower using particle pool
var exhaustCount = enemies.length > 15 ? 8 : 12; // Reduced count when many enemies
for (var exhaustIdx = 0; exhaustIdx < exhaustCount; exhaustIdx++) {
var exhaustParticle = getSmokeParticle();
var exhaustGraphics = exhaustParticle.smokeGraphics;
exhaustGraphics.width = 15 + Math.random() * 25;
exhaustGraphics.height = exhaustGraphics.width;
// Motor exhaust color palette - dark grays and blacks
var exhaustColors = [0x2a2a2a, 0x1a1a1a, 0x404040, 0x303030, 0x505050];
exhaustGraphics.tint = exhaustColors[Math.floor(Math.random() * exhaustColors.length)];
// Synchronize exhaust direction with gun rotation - emit from opposite direction of gun
var gunRotation = gunContainer.rotation || 0; // Get current gun rotation
var exhaustBaseAngle = gunRotation + Math.PI; // Opposite direction from gun
var exhaustAngle = exhaustBaseAngle + (Math.random() - 0.5) * Math.PI * 0.3; // Narrower spread around opposite direction
var exhaustDistance = 50 + Math.random() * 25; // Moved further back (50-75 instead of 30-50)
exhaustParticle.x = self.x + Math.cos(exhaustAngle) * exhaustDistance;
exhaustParticle.y = self.y + Math.sin(exhaustAngle) * exhaustDistance;
exhaustParticle.alpha = 0.7 + Math.random() * 0.3;
exhaustParticle.scaleX = 0.4 + Math.random() * 0.4;
exhaustParticle.scaleY = 0.4 + Math.random() * 0.4;
game.addChild(exhaustParticle);
// Animate exhaust particles moving away from tower and fading in synchronized direction
var targetDistance = exhaustDistance + 35 + Math.random() * 25; // Adjusted for new starting position
var targetX = self.x + Math.cos(exhaustAngle) * targetDistance;
var targetY = self.y + Math.sin(exhaustAngle) * targetDistance;
tween(exhaustParticle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: exhaustParticle.scaleX * 2.0,
scaleY: exhaustParticle.scaleY * 2.0
}, {
duration: 800 + Math.random() * 400,
easing: tween.easeOut,
onFinish: function onFinish() {
returnParticle(exhaustParticle);
}
});
}
};
}
// Initialize continuous flame animation for slow towers
if (self.id === 'slow') {
self.flameTimer = 0;
self.createContinuousFlames = function () {
// Play flame sound effect
LK.getSound('flame_sound').play();
// Create continuous flame particles around the slow tower using particle pool
var flameCount = enemies.length > 15 ? 6 : 9; // Reduced flame count for less intensive animation
for (var flameIdx = 0; flameIdx < flameCount; flameIdx++) {
var flameParticle = getFireParticle();
var flameGraphics = flameParticle.fireGraphics;
flameGraphics.width = 15 + Math.random() * 25; // Reduced flame size
flameGraphics.height = flameGraphics.width;
// Enhanced flame color palette with intense fire colors
var flameColors = [0xFF4500, 0xFF6600, 0xFF2200, 0xFF8800, 0xFFAA00, 0xFF0000, 0xCC4400, 0xDD3300];
flameGraphics.tint = flameColors[Math.floor(Math.random() * flameColors.length)];
// Position flame particles in multiple concentric circles around the tower
var ringNumber = Math.floor(flameIdx / 6); // 3 rings of 6 particles each
var angleInRing = flameIdx % 6 * (Math.PI * 2 / 6) + Math.random() * 0.6;
var baseDistance = 35 + ringNumber * 20;
var distance = baseDistance + Math.random() * 25;
flameParticle.x = self.x + Math.cos(angleInRing) * distance;
flameParticle.y = self.y + Math.sin(angleInRing) * distance;
flameParticle.alpha = 0.7 + Math.random() * 0.3;
flameParticle.scaleX = 0.6 + Math.random() * 0.5;
flameParticle.scaleY = 0.6 + Math.random() * 0.5;
game.addChild(flameParticle);
// Reduced flame animation with flickering motion and upward movement
var targetScale = flameParticle.scaleX * (1.8 + Math.random() * 1.2); // Smaller target scale
// Create flickering motion around the tower
var flicker = Math.random() > 0.5 ? 1 : -1;
var flickerAngle = angleInRing + flicker * (Math.PI * 0.6 + Math.random() * Math.PI * 0.4); // Less flickering
var flickerRadius = distance * (0.8 + Math.random() * 0.2); // Tighter radius
var targetX = self.x + Math.cos(flickerAngle) * flickerRadius + (Math.random() - 0.5) * 25; // Less spread
var targetY = flameParticle.y - (20 + Math.random() * 25); // Less vertical movement
// Add pulsing effect by varying the target scale over time
var pulseScale = targetScale * (0.8 + Math.random() * 0.4); // Less pulsing variation
var rotationSpeed = flicker * (Math.PI * 2 + Math.random() * Math.PI * 1.5);
tween(flameParticle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: pulseScale,
scaleY: pulseScale,
rotation: rotationSpeed
}, {
duration: 1000 + Math.random() * 500,
easing: tween.easeOut,
onFinish: function onFinish() {
returnParticle(flameParticle);
}
});
}
};
}
};
self.checkUpgradeAvailability = function () {
if (self.level >= self.maxLevel) {
// Tower is at max level, hide warning
if (self.upgradeWarning) {
self.upgradeWarning.visible = false;
}
return;
}
// Calculate upgrade cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
}
// Show warning if player has enough gold for upgrade
var canAffordUpgrade = gold >= upgradeCost;
if (self.upgradeWarning) {
if (canAffordUpgrade && !self.upgradeWarning.visible) {
// Create pulsing animation
var _pulseWarning = function pulseWarning() {
if (self.upgradeWarning && self.upgradeWarning.visible) {
tween(self.upgradeWarning, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.upgradeWarning && self.upgradeWarning.visible) {
tween(self.upgradeWarning, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Continue pulsing if still visible
if (self.upgradeWarning && self.upgradeWarning.visible) {
_pulseWarning();
}
}
});
}
}
});
}
};
// Start pulsing animation when warning becomes visible
self.upgradeWarning.visible = true;
self.upgradeWarning.alpha = 1;
self.upgradeWarning.scaleX = 1;
self.upgradeWarning.scaleY = 1;
_pulseWarning();
} else if (!canAffordUpgrade) {
// Stop animation and hide warning
if (self.upgradeWarning.visible) {
tween.stop(self.upgradeWarning, {
scaleX: true,
scaleY: true,
alpha: true
});
self.upgradeWarning.visible = false;
}
}
}
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'default';
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('towerpreview', {
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;
self.update = function () {
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
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
// Remove old preview graphics and add new one with correct asset
if (previewGraphics.parent) {
self.removeChild(previewGraphics);
}
// Get appropriate asset for this tower type
var previewAssetId = 'towerpreview_' + self.towerType;
previewGraphics = self.attachAsset(previewAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2.4;
previewGraphics.height = CELL_SIZE * 2.4;
// Set color based on tower type
var towerColor = 0xFFFFFF; // Default white
switch (self.towerType) {
case 'rapid':
towerColor = 0x00AAFF; // Blue
break;
case 'sniper':
towerColor = 0xFF5500; // Orange
break;
case 'splash':
towerColor = 0x33CC00; // Green
break;
case 'slow':
towerColor = 0x9900FF; // Purple
break;
case 'poison':
towerColor = 0x00FFAA; // Cyan
break;
default:
towerColor = 0xAAAAAA;
// Gray for default
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
} else {
previewGraphics.tint = towerColor;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
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 || cell.type !== 0) {
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 && canPlaceTowerType(self.towerType);
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 towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' 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);
//Play tower sell sound
LK.getSound('tower_sell').play();
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;
// Reset placement flags for all tower types when sold
if (self.tower.id === 'poison') {
poisonTowerPlaced = false;
} else if (self.tower.id === 'slow') {
slowTowerPlaced = false;
} else if (self.tower.id === 'default') {
defaultTowerPlaced = false;
} else if (self.tower.id === 'rapid') {
rapidTowerPlaced = false;
} else if (self.tower.id === 'sniper') {
sniperTowerPlaced = false;
} else if (self.tower.id === 'splash') {
splashTowerPlaced = false;
}
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// Only reset cell type if this wasn't a poison tower
// Poison towers don't block movement so cells should already be type 0
if (self.tower.id !== 'poison') {
cell.type = 0;
}
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);
// Invalidate pathfinding cache when tower is sold
pathfindingCache = null;
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 = 0x00AA00;
// Add shadow for start text
var startTextShadow = new Text2("Start Game", {
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 Game", {
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) {
//Play start button sound
LK.getSound('button_start').play();
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
// Make started image asset transparent using tween
tween(startBlock, {
alpha: 0
}, {
duration: 500,
easing: tween.easeOut
});
// Animate yellow frame to shrink to normal block size
var normalBlockHeight = 70;
var normalFrameHeight = normalBlockHeight + 32; // Add some padding around the normal block
// Animate horizontal bars (top and bottom) height reduction
tween(indicator, {
height: 16
}, {
duration: 500,
easing: tween.easeOut
});
tween(indicator2, {
height: 16
}, {
duration: 500,
easing: tween.easeOut
});
// Animate vertical bars (left and right) height reduction
tween(leftWall, {
height: normalFrameHeight
}, {
duration: 500,
easing: tween.easeOut
});
tween(rightWall, {
height: normalFrameHeight
}, {
duration: 500,
easing: tween.easeOut
});
// Adjust position of horizontal bars to match new frame size
tween(indicator, {
y: -(normalFrameHeight / 2 - 8)
}, {
duration: 500,
easing: tween.easeOut
});
tween(indicator2, {
y: normalFrameHeight / 2 - 8
}, {
duration: 500,
easing: tween.easeOut
});
}
};
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;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 9;
} else if (i === 1) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 7;
} else if (i === 2) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 4;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 7;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 10;
} else if (i === 5) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (isBossWave) {
// Wave 10 gets big enemy, other boss waves get strengthened types
if (i + 1 === 10) {
enemyType = "big";
block.tint = 0x8B4513; // Brown color for big enemy
waveType = "Big";
enemyCount = 1; // Single big enemy
} else {
// Replace other boss waves with multiple stronger enemies
var strengthenedTypes = ['immune', 'fast', 'immune', 'flying', 'fast'];
var typeIndex = Math.floor((i + 1) / 10) - 1;
enemyType = strengthenedTypes[typeIndex % strengthenedTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
waveType = "Elite Normal";
enemyCount = 5;
break;
case 'fast':
block.tint = 0x00AAFF;
waveType = "Elite Fast";
enemyCount = 4;
break;
case 'immune':
block.tint = 0xAA0000;
waveType = "Elite Immune";
enemyCount = 3;
break;
case 'flying':
block.tint = 0xFFFF00;
waveType = "Elite Flying";
enemyCount = 4;
break;
case 'big':
block.tint = 0x8B4513;
waveType = "Big";
enemyCount = 1;
break;
}
}
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 10;
} else {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark elite waves with a special visual indicator
if (isBossWave && enemyType !== 'swarm') {
// Add a star indicator to the wave marker for elite waves
var eliteIndicator = marker.attachAsset('star_score', {
anchorX: 0.5,
anchorY: 0.5
});
eliteIndicator.width = 30;
eliteIndicator.height = 30;
eliteIndicator.tint = 0xFFD700; // Gold color
eliteIndicator.y = -block.height / 2 - 15;
}
// 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 "normal";
}
// 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 elite prefix for every 10th wave
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "Elite " + typeName;
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('star_score', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('star_score', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('star_score', {
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('star_score', {
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];
// Make all indicator blocks transparent
tween(block, {
alpha: 0.3
}, {
duration: 300,
easing: tween.easeOut
});
if (i - 1 < currentWave) {
block.alpha = .15; // Even more transparent for completed waves
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
}
}
};
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
// Add main background
var mainBackground = game.attachAsset('main_background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
var isHidingUpgradeMenu = false;
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 pathfindingCache = null;
var lastPathfindTime = 0;
var pathfindCacheTimeout = 300; // Cache for 5 seconds (300 ticks at 60fps)
var dynamicCacheTimeout = 300; // Dynamic cache timeout that adjusts based on activity
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
var gold = 180;
var lives = 100;
var score = 0;
var currentWave = 0;
var totalWaves = 11;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var poisonBulletHitCount = 0; // Counter for poison bullet hits
var gogogoPoisonSoundPlayed = false; // Global flag to ensure gogogo sound only plays once for poison bullets
var gogogoSniperSoundPlayed = false; // Global flag to ensure gogogo sound only plays once for sniper bullets
var whoFartedSoundPlayed = false; // Global flag to ensure whofarted sound only plays once ever
var splashBulletSoundPlayed = false; // Global flag to ensure splash bullet sound only plays once ever
var defaultBulletHitCount = 0; // Counter for default bullet hits
var itDidntHurtSoundPlayed = false; // Global flag to ensure it didn't hurt sound only plays once ever
var youCantStopUsSoundPlayed = false; // Global flag to ensure you can't stop us sound only plays once ever
var weAreComingForYouSoundPlayed = false; // Global flag to ensure we are coming for you sound only plays once ever
var youWillNeverGiveUpSoundPlayed = false; // Global flag to ensure you will never give up sound only plays once ever
var godDayForDieSoundPlayed = false; // Global flag to ensure god day for die sound only plays once ever
var enemiesReachedGoalCount = 0; // Counter for enemies that reach the goal
var weWinSoundPlayed = false; // Global flag to ensure wewin sound only plays once ever
var gidiklaniyorumSoundPlayed = false; // Global flag to ensure gidiklaniyorum sound only plays once ever
var beCarefulSoundPlayed = false; // Global flag to ensure becareful sound only plays once ever
var sniperBulletHitCount = 0; // Counter for sniper bullet hits
var sniperrSoundPlayed = false; // Global flag to ensure sniperr sound only plays once ever
var keepMovingSoundPlayed = false; // Global flag to ensure keepmoving sound only plays once ever
var poisonTowerPlaced = false; // Track if poison tower has been placed
var slowTowerPlaced = false; // Track if slow tower has been placed
var defaultTowerPlaced = false; // Track if default tower has been placed
var rapidTowerPlaced = false; // Track if rapid tower has been placed
var sniperTowerPlaced = false; // Track if sniper tower has been placed
var splashTowerPlaced = false; // Track if splash tower has been placed
var fastEnemySoundPlayed = false; // Track if fast enemy sound has been played this wave
var fastEnemySoundTimer = 0; // Timer for random fast enemy sound timing
var welcomeToHellSoundPlayed = false; // Global flag to ensure welcometohell sound only plays once ever
var bronzSoundPlayed = false; // Global flag to ensure bronz sound only plays once ever
var slowAreaEnemyCount = 0; // Counter for enemies entering slow tower area damage
var sunSoundPlayed = false; // Global flag to ensure sun sound only plays once ever
var enemiesInSlowArea = []; // Track enemies currently in slow area
var wifiSoundPlayedThisWave = false; // Track if wifi sound has been played this wave for bagışıkdüşman
var taksiSoundPlayedThisWave = false; // Track if taksi sound has been played this wave for bagışıkdüşman hit by bullet_5
var vasiyetSoundPlayedThisWave = false; // Track if vasiyet sound has been played this wave for bagışıkdüşman hit by rapid bullets
var rapidBulletImmuneHitCount = 0; // Counter for rapid bullet hits on immune enemies
var gözlükSoundPlayedThisWave = false; // Track if gözlük sound has been played this wave for bagışıkdüşman hit by sniper bullets
var sniperBulletImmuneHitCount = 0; // Counter for sniper bullet hits on immune enemies
var slowBulletImmuneHitCount = 0; // Counter for slow bullet hits on immune enemies
var shySoundPlayedThisWave = false; // Track if shy sound has been played this wave for immune enemies hit by slow bullets
var krkSoundPlayedThisWave = false; // Track if krk sound has been played this wave for immune enemies hit by slow towers
// Expanded object pools for various particle types
var bloodParticlePool = [];
var poisonParticlePool = [];
var smokeParticlePool = [];
var fireParticlePool = [];
var poisonCloudPool = [];
var walkingFeetPool = [];
var maxPoolSize = 30; // Increased pool size for all particle types
// Spatial partitioning for optimized enemy targeting
var spatialGrid = {
cellSize: CELL_SIZE * 2,
// Each spatial cell covers 2x2 game cells
width: 12,
// 24 / 2
height: 18,
// 36 / 2
cells: [],
init: function init() {
this.cells = [];
for (var x = 0; x < this.width; x++) {
this.cells[x] = [];
for (var y = 0; y < this.height; y++) {
this.cells[x][y] = [];
}
}
},
clear: function clear() {
for (var x = 0; x < this.width; x++) {
for (var y = 0; y < this.height; y++) {
this.cells[x][y].length = 0;
}
}
},
addEnemy: function addEnemy(enemy) {
var gridX = Math.floor(enemy.x / this.cellSize);
var gridY = Math.floor(enemy.y / this.cellSize);
if (gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height) {
this.cells[gridX][gridY].push(enemy);
}
},
getEnemiesInRange: function getEnemiesInRange(x, y, range) {
var enemies = [];
var startX = Math.max(0, Math.floor((x - range) / this.cellSize));
var endX = Math.min(this.width - 1, Math.floor((x + range) / this.cellSize));
var startY = Math.max(0, Math.floor((y - range) / this.cellSize));
var endY = Math.min(this.height - 1, Math.floor((y + range) / this.cellSize));
for (var gx = startX; gx <= endX; gx++) {
for (var gy = startY; gy <= endY; gy++) {
var cellEnemies = this.cells[gx][gy];
for (var i = 0; i < cellEnemies.length; i++) {
enemies.push(cellEnemies[i]);
}
}
}
return enemies;
}
};
spatialGrid.init();
function getBloodParticle() {
if (bloodParticlePool.length > 0) {
return bloodParticlePool.pop();
}
var bloodParticle = new Container();
var bloodGraphics = bloodParticle.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
bloodParticle.bloodGraphics = bloodGraphics;
bloodParticle.particleType = 'blood';
return bloodParticle;
}
function getPoisonParticle() {
if (poisonParticlePool.length > 0) {
return poisonParticlePool.pop();
}
var poisonParticle = new Container();
var poisonGraphics = poisonParticle.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
poisonParticle.poisonGraphics = poisonGraphics;
poisonParticle.particleType = 'poison';
return poisonParticle;
}
function getSmokeParticle() {
if (smokeParticlePool.length > 0) {
return smokeParticlePool.pop();
}
var smokeParticle = new Container();
var smokeGraphics = smokeParticle.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
smokeParticle.smokeGraphics = smokeGraphics;
smokeParticle.particleType = 'smoke';
return smokeParticle;
}
function getFireParticle() {
if (fireParticlePool.length > 0) {
return fireParticlePool.pop();
}
var fireParticle = new Container();
var fireGraphics = fireParticle.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
fireParticle.fireGraphics = fireGraphics;
fireParticle.particleType = 'fire';
return fireParticle;
}
function getPoisonCloudParticle() {
if (poisonCloudPool.length > 0) {
return poisonCloudPool.pop();
}
var poisonCloud = new Container();
var cloudGraphics = poisonCloud.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
poisonCloud.cloudGraphics = cloudGraphics;
poisonCloud.particleType = 'poisonCloud';
return poisonCloud;
}
function getWalkingFeetParticle() {
if (walkingFeetPool.length > 0) {
return walkingFeetPool.pop();
}
var walkingFeet = new Container();
var feetGraphics = walkingFeet.attachAsset('walkingFeet', {
anchorX: 0.5,
anchorY: 0.5
});
walkingFeet.feetGraphics = feetGraphics;
walkingFeet.particleType = 'walkingFeet';
return walkingFeet;
}
function returnParticle(particle) {
if (!particle || !particle.particleType) {
if (particle && particle.destroy) {
particle.destroy();
}
return;
}
var pool;
// Dynamic pool sizing based on enemy count
var dynamicMaxSize = Math.min(50, Math.max(20, enemies.length * 2));
var maxSize = dynamicMaxSize;
switch (particle.particleType) {
case 'blood':
pool = bloodParticlePool;
break;
case 'poison':
pool = poisonParticlePool;
break;
case 'smoke':
pool = smokeParticlePool;
break;
case 'fire':
pool = fireParticlePool;
break;
case 'poisonCloud':
pool = poisonCloudPool;
break;
case 'walkingFeet':
pool = walkingFeetPool;
break;
case 'bossCircularFeet':
// Boss circular feet are not pooled, destroy them directly
particle.destroy();
return;
default:
particle.destroy();
return;
}
if (pool.length < maxSize) {
// Reset particle properties
particle.alpha = 1;
particle.scaleX = 1;
particle.scaleY = 1;
particle.rotation = 0;
// Reset graphics tint
if (particle.bloodGraphics) particle.bloodGraphics.tint = 0xFFFFFF;
if (particle.poisonGraphics) particle.poisonGraphics.tint = 0xFFFFFF;
if (particle.smokeGraphics) particle.smokeGraphics.tint = 0xFFFFFF;
if (particle.fireGraphics) particle.fireGraphics.tint = 0xFFFFFF;
if (particle.cloudGraphics) particle.cloudGraphics.tint = 0xFFFFFF;
if (particle.feetGraphics) particle.feetGraphics.tint = 0xFFFFFF;
tween.stop(particle, {
x: true,
y: true,
alpha: true,
scaleX: true,
scaleY: true,
rotation: true
});
if (particle.parent) {
particle.parent.removeChild(particle);
}
pool.push(particle);
} else {
particle.destroy();
}
}
// Legacy function for compatibility
function returnBloodParticle(particle) {
returnParticle(particle);
}
// Create gold display container with background image and text
var goldDisplay = new Container();
var goldBackground = goldDisplay.attachAsset('coin_gold', {
anchorX: 0.5,
anchorY: 0.5,
width: 120,
height: 120
});
goldBackground.x = 0; // Position background behind the text
var goldText = new Text2(gold.toString(), {
size: 36,
fill: 0x000000,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
goldText.x = 0; // Position text on top of background
goldDisplay.addChild(goldText);
// Create lives display container with background image and text
var livesDisplay = new Container();
var livesBackground = livesDisplay.attachAsset('heart_life', {
anchorX: 0.5,
anchorY: 0.5,
width: 120,
height: 120
});
livesBackground.x = 0; // Position background behind the text
var livesText = new Text2(lives.toString(), {
size: 36,
fill: 0x000000,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
livesText.x = 0; // Position text on top of background
livesDisplay.addChild(livesText);
// Create score display container with image and text
var scoreDisplay = new Container();
var scoreIcon = scoreDisplay.attachAsset('score_icon', {
anchorX: 0.5,
anchorY: 0.5,
width: 120,
height: 120
});
scoreIcon.x = -45; // Position icon to the left
var scoreText = new Text2(score.toString(), {
size: 36,
fill: 0xFFFFFF,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
scoreText.x = -45; // Position text on top of icon
scoreDisplay.addChild(scoreText);
// Add displays directly to game object instead of LK.gui for better visibility
game.addChild(goldDisplay);
game.addChild(livesDisplay);
game.addChild(scoreDisplay);
// Position displays in the top-right corner with absolute coordinates
var topMargin = 85; // Moved up a little bit more
var spacing = 190; // Equalized spacing between displays
var rightOffset = 1400; // Moved left from previous position
goldDisplay.x = rightOffset;
goldDisplay.y = topMargin;
livesDisplay.x = rightOffset + spacing;
livesDisplay.y = topMargin;
scoreDisplay.x = rightOffset + spacing * 2 + 50;
scoreDisplay.y = topMargin;
function updateUI() {
goldText.setText(gold.toString());
livesText.setText(lives.toString());
scoreText.setText(score.toString());
}
function setGold(value) {
gold = value;
updateUI();
}
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
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); // Grid cells hidden from visual display
// game.addChild(debugLayer); // Debug layer hidden
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY, towerType) {
// Poison towers never block paths since enemies can pass through them
if (towerType === 'poison') {
return false;
}
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
});
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 = 5;
switch (towerType) {
case 'rapid':
cost = 15;
break;
case 'sniper':
cost = 25;
break;
case 'splash':
cost = 35;
break;
case 'slow':
cost = 45;
break;
case 'poison':
cost = 55;
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return totalValue;
}
function canPlaceTowerType(towerType) {
switch (towerType) {
case 'poison':
return !poisonTowerPlaced;
case 'slow':
return !slowTowerPlaced;
case 'default':
return !defaultTowerPlaced;
case 'rapid':
return !rapidTowerPlaced;
case 'sniper':
return !sniperTowerPlaced;
case 'splash':
return !splashTowerPlaced;
default:
return false;
// No unknown tower types allowed
}
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (!canPlaceTowerType(towerType)) {
var notification = game.addChild(new Notification(towerType.charAt(0).toUpperCase() + towerType.slice(1) + " tower can only be placed once!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
if (gold >= towerCost) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
// Track placement of all tower types
if (towerType === 'poison') {
poisonTowerPlaced = true;
} else if (towerType === 'slow') {
slowTowerPlaced = true;
} else if (towerType === 'default') {
defaultTowerPlaced = true;
} else if (towerType === 'rapid') {
rapidTowerPlaced = true;
} else if (towerType === 'sniper') {
sniperTowerPlaced = true;
} else if (towerType === 'splash') {
splashTowerPlaced = true;
}
//Play tower placement sound - special sounds for poison, splash and sniper towers
if (towerType === 'poison') {
LK.getSound('poison_impact').play();
} else if (towerType === 'splash') {
LK.getSound('splash_place').play();
} else if (towerType === 'sniper') {
LK.getSound('yessir').play();
} else {
LK.getSound('tower_place').play();
}
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) {
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) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType)) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} 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 = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 80;
game.addChild(waveIndicator);
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 / 2;
nextWaveButton.y = 2732 - 200;
game.addChild(nextWaveButton);
var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
var sourceTowers = [];
var towerSpacing = 300; // 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;
// Apply smaller scale to all tower icons
tower.scaleX = 0.8;
tower.scaleY = 0.8;
// Shift rapid tower icon slightly to the left
if (towerTypes[i] === 'rapid') {
tower.x -= 60;
tower.scaleX = 0.7;
tower.scaleY = 0.7;
}
// Shift sniper tower icon to the left
if (towerTypes[i] === 'sniper') {
tower.x -= 140;
}
// Shift slow tower icon to the right
if (towerTypes[i] === 'slow') {
tower.x += 80;
}
// Shift splash tower icon to the right - move closer to slow tower
if (towerTypes[i] === 'splash') {
tower.x += 160;
}
// Shift poison tower icon to the left
if (towerTypes[i] === 'poison') {
tower.x -= 20;
}
tower.y = towerY + 50;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
// Start playing background music
LK.playMusic('game_music');
game.update = function () {
// Update spatial partitioning grid for enemy targeting optimization
spatialGrid.clear();
for (var i = 0; i < enemies.length; i++) {
spatialGrid.addEnemy(enemies[i]);
}
// Visibility culling - hide objects outside screen bounds
var screenMargin = 200; // Extra margin for smooth transitions
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var isVisible = enemy.x > -screenMargin && enemy.x < 2048 + screenMargin && enemy.y > -screenMargin && enemy.y < 2732 + screenMargin;
enemy.visible = isVisible;
if (enemy.shadow) enemy.shadow.visible = isVisible;
}
if (waveInProgress) {
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 % 10 === 0 && currentWave > 0;
if (isBossWave && (waveType !== 'swarm' || waveType === 'big')) {
// Boss waves and big enemies have just 1 enemy regardless of what the wave indicator says
enemyCount = 1;
}
// goddayfordie sound is now played when normal enemies enter screen
// Reset sound flags for all enemies when new waves start (excluding the global first-time sounds)
// Reset normal enemy specific sound flags to allow them to play again in new waves
window.youAreKillingMeSoundPlayed = false;
itDidntHurtSoundPlayed = false;
youCantStopUsSoundPlayed = false;
weAreComingForYouSoundPlayed = false;
youwillnevergiveupSoundPlayed = false;
gidiklaniyorumSoundPlayed = false;
gogogoPoisonSoundPlayed = false;
beCarefulSoundPlayed = false;
sniperrSoundPlayed = false;
keepMovingSoundPlayed = false;
gogogoSniperSoundPlayed = false;
whoFartedSoundPlayed = false;
// Reset goddayfordie sound flag to allow it to play again for normal enemies
godDayForDieSoundPlayed = false;
// Reset goddamit sound flag to allow it to play once per wave for splash bullets hitting normal enemies
window.splashBulletSoundPlayed = false;
window.splashBulletHitCount = 0;
// Reset fast enemy sound flag for new wave
fastEnemySoundPlayed = false;
fastEnemySoundTimer = 0;
// Reset slow tower area sound flags to allow them to repeat each wave
welcomeToHellSoundPlayed = false;
bronzSoundPlayed = false;
sunSoundPlayed = false;
slowAreaEnemyCount = 0; // Reset counter for bronz sound timing
// Reset wifi sound flag for new wave
wifiSoundPlayedThisWave = false;
// Reset taksi sound flag for new wave
taksiSoundPlayedThisWave = false;
// Reset vasiyet sound flag for new wave
vasiyetSoundPlayedThisWave = false;
rapidBulletImmuneHitCount = 0;
// Reset gözlük sound flag for new wave
gözlükSoundPlayedThisWave = false;
sniperBulletImmuneHitCount = 0;
// Reset shy sound flag for new wave
shySoundPlayedThisWave = false;
slowBulletImmuneHitCount = 0;
// Reset krk sound flag for new wave
krkSoundPlayedThisWave = false;
// Reset sniper immune hit counter for new wave
window.sniperImmuneHitCounter = 0;
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy = new Enemy(waveType);
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Scale difficulty with wave number
// Increase enemy health by 30% each wave
var healthMultiplier = 1 + currentWave * 0.3; // 30% increase per wave
// Elite waves (every 10th) get extra health boost
if (isBossWave && waveType !== 'swarm') {
if (waveType === 'big') {
healthMultiplier *= 5; // Big enemies get 5x health multiplier
} else {
healthMultiplier *= 3; // Triple health for other elite enemies
}
}
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// All enemy types now spawn in the middle 6 tiles at the top spacing
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
// Find a column that isn't occupied by another enemy that's not yet in view
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; 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 === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
// If all columns are occupied, use original random method
var spawnX;
if (availableColumns.length > 0) {
// Choose a random unoccupied column
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
// Fallback to random if all columns are occupied
spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
}
var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = 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;
}
}
// Check for enemies exiting slow tower area
for (var i = enemiesInSlowArea.length - 1; i >= 0; i--) {
var enemy = enemiesInSlowArea[i];
var stillInSlowArea = false;
// Check if enemy is still in range of any slow tower
for (var t = 0; t < towers.length; t++) {
var tower = towers[t];
if (tower.id === 'slow') {
var dx = enemy.x - tower.x;
var dy = enemy.y - tower.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= tower.getRange()) {
stillInSlowArea = true;
break;
}
}
}
// If enemy is no longer in slow area or has been destroyed, remove from tracking
if (!stillInSlowArea || !enemy.parent || enemy.health <= 0) {
enemiesInSlowArea.splice(i, 1);
// Play sun sound if this was the last enemy and sound hasn't been played yet
if (enemiesInSlowArea.length === 0 && !sunSoundPlayed) {
sunSoundPlayed = true;
LK.getSound('sun').play();
}
}
}
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;
}
// Return walking feet to pool for normal and swarm enemies only
if ((enemy.type === 'normal' || enemy.type === 'swarm') && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) {
returnParticle(enemy.leftFoot);
returnParticle(enemy.rightFoot);
enemy.leftFoot = null;
enemy.rightFoot = null;
}
// Calculate gold and score rewards
var isEliteWave = enemy.waveNumber % 10 === 0 && enemy.waveNumber > 0;
var goldEarned = isEliteWave ? Math.floor(15 + (enemy.waveNumber - 1) * 2) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5);
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Give more score for elite enemies
var scoreValue = isEliteWave ? 25 : 5;
score += scoreValue;
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 {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Return walking feet to pool for normal and swarm enemies only before removal
if ((enemy.type === 'normal' || enemy.type === 'swarm') && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) {
returnParticle(enemy.leftFoot);
returnParticle(enemy.rightFoot);
enemy.leftFoot = null;
enemy.rightFoot = null;
}
// Increment counter for enemies reaching goal
enemiesReachedGoalCount++;
// Play "wewin" sound on 5th enemy reaching goal with delay
if (enemiesReachedGoalCount === 5 && !weWinSoundPlayed) {
weWinSoundPlayed = true;
// Delay the sound by 1200ms for clear speech
tween({}, {}, {
duration: 1200,
onFinish: function onFinish() {
LK.getSound('wewin').play();
}
});
}
// 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 {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
lives = Math.max(0, lives - 1);
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Fast enemy sounds are now handled in enemy update logic when they appear on screen
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
};
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
Taş duvar. In-Game asset. 2d. High contrast. No shadows
Kuş bakışı savaş uçağı. In-Game asset. 2d. High contrast. No shadows
Üstten bakış yere sabitlenmiş silah. In-Game asset. 2d. High contrast. No shadows
Alev. In-Game asset. 2d. High contrast. No shadows
Yandan bakış tank. In-Game asset. 2d. High contrast. No shadows
Sniper soilder. In-Game asset. 2d. High contrast. No shadows
Top atan makina yukardan bakış. In-Game asset. 2d. High contrast. No shadows
Ortadaki $ kaldır düz sarı renk
Kalp. In-Game asset. 2d. High contrast. No shadows
Yeşil bir şişe içinde yeşil sıvı var. In-Game asset. 2d. High contrast. No shadows
Taç. In-Game asset. 2d. High contrast. No shadows
Tepeden bakış tank. In-Game asset. 2d. High contrast. No shadows
Yıldız. In-Game asset. 2d. High contrast. No shadows
Yukarı yükseltme oku yeşil. In-Game asset. 2d. High contrast. No shadows
Alev makinesi. In-Game asset. 2d. High contrast. No shadows
Asker kaskı. Önden bakış kayışı yok In-Game asset. 2d. High contrast. No shadows
Anime
Anime
Kask ve pantolonu siyah renk olsun
Tahta bir tabela üzerinde fort yazıyor. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Farklı varyasyonlarda üret
Farklı varyasyonlarını çiz
Şişe rengini saydam yap
tower_shoot
Sound effect
footstep
Sound effect
splash_impact
Sound effect
tower_place
Sound effect
button_start
Sound effect
game_music
Music
tower_upgrade
Sound effect
tower_sell
Sound effect
poison_impact
Sound effect
poison_bullet_hit
Sound effect
splash_place
Sound effect
gogogo
Sound effect
whofarted
Sound effect
youarekillingme
Sound effect
itdidnthurt
Sound effect
youcantstopus
Sound effect
wearecomingforyou
Sound effect
youwillnevergiveup
Sound effect
giddayfordie
Sound effect
goddayfordie
Sound effect
wewin
Sound effect
gidiklaniyorum
Sound effect
yessir
Sound effect
becareful
Sound effect
sniperr
Sound effect
keepmoving
Sound effect
walking
Sound effect
gooddayfordie
Sound effect
bullet_casing_drop
Sound effect
goddamit
Sound effect
egzos
Sound effect
fast_enemy_sound
Sound effect
for_the_fallen
Sound effect
slow_bullet_shoot
Sound effect
flame_sound
Sound effect
alanhasar
Sound effect
welcometohell
Sound effect
sun
Sound effect
wifi
Sound effect
taksi
Sound effect
vasiyet
Sound effect
gzlk
Sound effect
gozluk
Sound effect
shy
Sound effect
krk
Sound effect
mask
Sound effect
shoes
Sound effect
browser
Sound effect
tnk
Sound effect
bird_chirp
Sound effect
war
Sound effect
place
Sound effect
sevda
Sound effect
mov
Sound effect
og
Sound effect
mezar
Sound effect
we
Sound effect
ek
Sound effect
tw
Sound effect
son
Sound effect
miss
Sound effect
xr
Sound effect
yar
Sound effect
upit
Sound effect
df
Sound effect
guys
Sound effect
n
Sound effect
wc
Sound effect
water
Sound effect
funny
Sound effect
z
Sound effect
borc
Sound effect
sm
Sound effect
db
Sound effect
wu
Sound effect
bolt
Sound effect
hamburger
Sound effect
kola
Sound effect
hungry
Sound effect
sh
Sound effect
dog
Sound effect
hav
Sound effect
ner
Sound effect
hlp
Sound effect
hv2
Sound effect
hv3
Sound effect
hv4
Sound effect