User prompt
Kule yerleştirme noktalarının aralarını aç
User prompt
Kule yerleşim yerlerini göstermek için oyunun orta kısmına yukardan aşagı düz bir şekilde kule yerleşim yeri belirle
User prompt
Beni anlamıyırmusun devam tusuna basılmadıgı sürece dalga 8 başlanamaz
User prompt
Siyahbekran aktifken zehirli kule yanan kule sıcrama kulesi animasyonlarını gizle
User prompt
Siyah ekran aktifken dalga 8 neden başlıyor oyuncu devam et butonuna basmadan dalga 8 başlayamaz
User prompt
Ekran kararması sadece dalga 7 sonrası geçerli dalga8 dalga9vs geçersizdir
User prompt
Oyuncu siyah ekran aktif oldugunda ev tuşuna basarsa oyunu kazandınız çıkmalı oyuncu devam et butonuna tıklarsa oyun devam etmeli
User prompt
Neden dalha 8den sonra kazandınız panosu cıkıyor dalga9 devam etmeli
User prompt
Mary dalga 8de görünür olucak
User prompt
Mary biraz aşagı taşı
User prompt
Biraz aşagı
User prompt
Biraz daha
User prompt
Mary saga kaydır biraz
User prompt
Biraz daha kaydır
User prompt
Biraz saga kaydır
User prompt
Mary küçült
User prompt
Marj orjinal boyutuna geri çevir
User prompt
Biraz küçült
User prompt
100x104 yap
User prompt
Mary 100x100 yap
User prompt
Maey 150x100
User prompt
200x150 yap
User prompt
Mary 250x200 yap
User prompt
Mary 250x200 yap
User prompt
Mary büyült digerlerinin üzerinde görünmesine izin ver
/**** * 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(); } }); } } // Track default bullet hits on swarm enemies for "you can't stop us" sound if ((!self.type || self.type === 'default') && !self.targetEnemy.isFlying && self.targetEnemy.type === 'swarm') { // Initialize separate counter for swarm enemies if it doesn't exist if (!window.swarmBulletHitCount) { window.swarmBulletHitCount = 0; } window.swarmBulletHitCount++; // Play "shoes" sound only once on first default bullet hit on swarm enemy if (window.swarmBulletHitCount === 1 && !window.shoesSoundPlayed) { window.shoesSoundPlayed = true; LK.getSound('shoes').play(); } // Play "you can't stop us" sound only once on 10th default bullet hit on swarm enemy with delay if (window.swarmBulletHitCount === 10 && !youCantStopUsSoundPlayed) { youCantStopUsSoundPlayed = true; // Delay the sound by 1200ms for clear speech tween({}, {}, { duration: 1200, onFinish: function onFinish() { LK.getSound('youcantstopus').play(); // Play browser sound 2 seconds after youcantstopus sound - only once per wave if (!browserSoundPlayedThisWave) { browserSoundPlayedThisWave = true; tween({}, {}, { duration: 2000, onFinish: function onFinish() { LK.getSound('browser').play(); // Play tnk sound 5 seconds after browser sound tween({}, {}, { duration: 5000, onFinish: function onFinish() { LK.getSound('tnk').play(); } }); } }); } } }); } } // Removed browser sound for rapid bullet hits on swarm enemies - now only plays after youcantstopus sound // 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 } // Blood animation removed for swarm enemies per requirements // Skip blood animation for flying enemies, boss enemies, swarm enemies, and immune enemies // No blood animation will be created for any bullet hits on swarm enemies if (false) { // Blood animation completely disabled for swarm enemies } // 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; } // Play krk sound for immune enemies hit by splash damage - only once per wave if (otherEnemy.isImmune && !window.krkSoundPlayedThisWave) { window.krkSoundPlayedThisWave = true; LK.getSound('krk').play(); } } } } } 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.25; if (self.sourceTowerLevel !== undefined) { // Scale: 25% at level 1, 30% at 2, 32% at 3, 35% at 4, 37% at 5, 40% at 6 var slowLevels = [0.25, 0.3, 0.32, 0.35, 0.37, 0.4]; 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') { // Handle poison bullets hitting immune enemies if (self.targetEnemy.isImmune) { // Play mask sound for immune enemies hit by poison bullets - only once per wave if (!window.maskSoundPlayedThisWave) { window.maskSoundPlayedThisWave = true; LK.getSound('mask').play(); } } else { // 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(); } // Removed gogogo sound for sniper bullet hits on normal enemies // 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 = 200; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'blue': self.maxHealth = 150; // Medium health self.speed *= 1.5; // Faster than normal 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 === 7 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 10x health and are larger self.maxHealth *= 10; // Faster speed for bosses self.speed = self.speed * 1.2; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type === 'bigboss') { assetId = 'bigboss'; } else if (self.type === 'blue') { assetId = 'enemy_blue'; } 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, swarm, and blue enemies only (no feet for boss enemies) self.leftFoot = null; self.rightFoot = null; if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue') && !self.isBoss) { // Adjust foot size based on enemy type var footWidth = 18; var footHeight = 13; var footSpacing = 15; var footYPosition = 35; // Create walking feet as regular Container objects (not particles) self.leftFoot = new Container(); var leftFootGraphics = self.leftFoot.attachAsset('walkingFeet', { anchorX: 0.5, anchorY: 0.5 }); leftFootGraphics.width = footWidth; leftFootGraphics.height = footHeight; leftFootGraphics.tint = 0x000000; // Black color for walking feet self.rightFoot = new Container(); var rightFootGraphics = self.rightFoot.attachAsset('walkingFeet', { anchorX: 0.5, anchorY: 0.5 }); rightFootGraphics.width = footWidth; rightFootGraphics.height = footHeight; rightFootGraphics.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; }*/ // Flying enemies no longer use shadows for performance optimization 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, swarm, and blue enemies only (no feet for boss enemies) if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue') && !self.isBoss && self.leftFoot && self.rightFoot) { // 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 // Simplified foot animation - only for normal enemies, swarm enemies get no foot animation if (self.type === 'normal') { // Update foot animation phases self.leftFootPhase += self.walkAnimationSpeed * speedMultiplier * 2; self.rightFootPhase += self.walkAnimationSpeed * speedMultiplier * 2; // 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 = 0.5; // Simplified, reduced movement for swarm 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 } // Simplified animation for swarm enemies if (self.type === 'swarm') { var targetY = combinedBobOffset * animationIntensity; var targetX = horizontalSway * animationIntensity * 0.3; // Much less horizontal movement // Simple direct assignment for swarm enemies instead of tweening enemyGraphics.y = targetY; enemyGraphics.x = targetX; } else { // Normal complex animation for other enemy types 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 (not for swarm) 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; } // Return feet to resting position for normal, swarm, and blue enemies only when not walking if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue') && !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; } // Shadow rendering removed for flying enemies to improve performance // 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(); } }); } // Play we sound only once when boss enemy from Wave 7 first appears on screen if (enemy.waveNumber === 7 && enemy.isBoss && !weSoundPlayed) { weSoundPlayed = true; LK.getSound('we').play(); // Play ek sound after we sound finishes tween({}, {}, { duration: 2000, // Adjust timing based on 'we' sound duration onFinish: function onFinish() { LK.getSound('ek').play(); // Play tw sound after ek sound finishes tween({}, {}, { duration: 2000, // Adjust timing based on 'ek' sound duration onFinish: function onFinish() { LK.getSound('tw').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 // Add exhaust animation for flying enemies (slow tower style) if (!enemy.exhaustTimer) { enemy.exhaustTimer = 0; } enemy.exhaustTimer++; // Create exhaust animation every 25 frames for flying enemies (slower like slow tower) if (enemy.exhaustTimer % 25 === 0) { // Create exhaust particles behind flying enemy using particle pool var exhaustCount = enemies.length > 15 ? 8 : 12; // Reduced count when many enemies like slow tower 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 like slow tower var exhaustColors = [0x2a2a2a, 0x1a1a1a, 0x404040, 0x303030, 0x505050]; exhaustGraphics.tint = exhaustColors[Math.floor(Math.random() * exhaustColors.length)]; // Calculate direction opposite to movement var movementAngle = Math.atan2(oy, ox); var exhaustBaseAngle = movementAngle + Math.PI; // Opposite direction from movement var exhaustAngle = exhaustBaseAngle + (Math.random() - 0.5) * Math.PI * 0.3; // Narrower spread like slow tower var exhaustDistance = 50 + Math.random() * 25; // Moved further back like slow tower exhaustParticle.x = enemy.x + Math.cos(exhaustAngle) * exhaustDistance; exhaustParticle.y = enemy.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 enemy and fading in synchronized direction var targetDistance = exhaustDistance + 35 + Math.random() * 25; // Adjusted for new starting position var targetX = enemy.x + Math.cos(exhaustAngle) * targetDistance; var targetY = enemy.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); } }); } } 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) { // Shadow cleanup removed as flying enemies no longer use shadows // 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 = 15; 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') { // Check if black screen is active var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted; if (!isBlackScreenActive) { 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') { // Check if black screen is active var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted; if (!isBlackScreenActive) { 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') { // Check if black screen is active var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted; if (!isBlackScreenActive) { 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 enemies entering slow tower area damage (bronz sound removed) if (affectedEnemies.length > 0) { slowAreaEnemyCount++; } // 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 slow effect to all affected enemies (visual effects and slow) for (var i = 0; i < affectedEnemies.length; i++) { var affectedEnemy = affectedEnemies[i]; // 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.25; if (self.level !== undefined) { // Scale: 25% at level 1, 30% at 2, 32% at 3, 35% at 4, 37% at 5, 40% at 6 var slowLevels = [0.25, 0.3, 0.32, 0.35, 0.37, 0.4]; 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 } } else { // Play krk sound for immune enemies hit by slow tower - only once per wave if (!window.krkSoundPlayedThisWave) { window.krkSoundPlayedThisWave = true; LK.getSound('krk').play(); } } } 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; // Delay the gzlk sound by 2000ms (2 seconds) tween({}, {}, { duration: 2000, onFinish: function onFinish() { LK.getSound('gzlk').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; // Delay the vasiyet sound by 3000ms (3 seconds) tween({}, {}, { duration: 3000, onFinish: function onFinish() { 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; // --- Extended 50 Wave System --- var waveNum = i + 1; var waveType = "normal"; var enemyType = "normal"; var enemyCount = 8; var isBossWave = waveNum % 10 === 0; // Define wave progression for all 50 waves if (waveNum === 1) { block.tint = 0x0066FF; waveType = "Blue"; enemyType = "blue"; enemyCount = 4; } else if (waveNum === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 5; } else if (waveNum === 3) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 6; } else if (waveNum === 4) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 4; } else if (waveNum === 5) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 4; } else if (waveNum === 6) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 5; } else if (waveNum === 7) { block.tint = 0x8B4513; waveType = "Big Boss"; enemyType = "big"; enemyCount = 1; } else if (waveNum >= 8 && waveNum <= 50) { // Generate varied waves for waves 8-50 var cyclePos = (waveNum - 8) % 7; // Cycle through 7 different types var intensity = Math.floor((waveNum - 8) / 7) + 1; // Increase intensity every 7 waves switch (cyclePos) { case 0: // Immune waves block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = Math.min(15, 6 + intensity * 2); break; case 1: // Fast waves block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = Math.min(12, 5 + intensity); break; case 2: // Flying waves block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = Math.min(10, 4 + intensity); break; case 3: // Swarm waves block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = Math.min(20, 8 + intensity * 2); break; case 4: // Blue waves block.tint = 0x0066FF; waveType = "Blue"; enemyType = "blue"; enemyCount = Math.min(12, 6 + intensity); break; case 5: // Normal waves block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = Math.min(15, 8 + intensity * 2); break; case 6: // Big boss waves (every 7th wave) block.tint = 0x8B4513; waveType = "Big Boss"; enemyType = "big"; enemyCount = Math.min(3, 1 + Math.floor(intensity / 2)); break; } // Special boss waves every 10 waves if (waveNum % 10 === 0) { block.tint = 0x8B4513; waveType = "Mega Boss"; enemyType = "big"; enemyCount = Math.min(5, 2 + Math.floor(waveNum / 20)); } } else { // Fallback for any additional waves block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 8; } // --- End Extended 50 Wave System --- // Mark elite waves with a special visual indicator if (waveNum === 7 && 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 wave 7 if (waveNumber === 7 && 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 = 50; 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 browserSoundPlayedThisWave = false; // Track if browser sound has been played this wave after youcantstopus var warSoundPlayed = false; // Track if war sound has been played globally - only once per game var mezarSoundPlayed = false; // Track if mezar sound has been played globally - only once per game var weSoundPlayed = false; // Track if we sound has been played for wave 7 - only once per game var işSoundPlayed = false; // Track if iş sound has been played after we sound - only once per game var bossWaveCompleted = false; // Track if boss wave (wave 7) has been completed var fadeToBlackStarted = false; // Track if fade to black has started // 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 // Particle lifetime management system var activeParticles = []; // Track all active particles for lifetime management var maxParticleLifetime = 3000; // Maximum particle lifetime in milliseconds (3 seconds) var particleCleanupInterval = 300; // Check for cleanup every 5 seconds (300 frames at 60fps) var lastParticleCleanup = 0; // 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) { var particle = bloodParticlePool.pop(); // Reset lifetime tracking for reused particles particle.creationTime = LK.ticks; particle.maxLifetime = maxParticleLifetime; return particle; } var bloodParticle = new Container(); var bloodGraphics = bloodParticle.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); bloodParticle.bloodGraphics = bloodGraphics; bloodParticle.particleType = 'blood'; bloodParticle.creationTime = LK.ticks; bloodParticle.maxLifetime = maxParticleLifetime; activeParticles.push(bloodParticle); return bloodParticle; } function getPoisonParticle() { if (poisonParticlePool.length > 0) { var particle = poisonParticlePool.pop(); // Reset lifetime tracking for reused particles particle.creationTime = LK.ticks; particle.maxLifetime = maxParticleLifetime; return particle; } var poisonParticle = new Container(); var poisonGraphics = poisonParticle.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); poisonParticle.poisonGraphics = poisonGraphics; poisonParticle.particleType = 'poison'; poisonParticle.creationTime = LK.ticks; poisonParticle.maxLifetime = maxParticleLifetime; activeParticles.push(poisonParticle); return poisonParticle; } function getSmokeParticle() { if (smokeParticlePool.length > 0) { var particle = smokeParticlePool.pop(); // Reset lifetime tracking for reused particles particle.creationTime = LK.ticks; particle.maxLifetime = maxParticleLifetime; return particle; } var smokeParticle = new Container(); var smokeGraphics = smokeParticle.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); smokeParticle.smokeGraphics = smokeGraphics; smokeParticle.particleType = 'smoke'; smokeParticle.creationTime = LK.ticks; smokeParticle.maxLifetime = maxParticleLifetime; activeParticles.push(smokeParticle); return smokeParticle; } function getFireParticle() { if (fireParticlePool.length > 0) { var particle = fireParticlePool.pop(); // Reset lifetime tracking for reused particles particle.creationTime = LK.ticks; particle.maxLifetime = maxParticleLifetime; return particle; } var fireParticle = new Container(); var fireGraphics = fireParticle.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); fireParticle.fireGraphics = fireGraphics; fireParticle.particleType = 'fire'; fireParticle.creationTime = LK.ticks; fireParticle.maxLifetime = maxParticleLifetime; activeParticles.push(fireParticle); return fireParticle; } function getPoisonCloudParticle() { if (poisonCloudPool.length > 0) { var particle = poisonCloudPool.pop(); // Reset lifetime tracking for reused particles particle.creationTime = LK.ticks; particle.maxLifetime = maxParticleLifetime; return particle; } var poisonCloud = new Container(); var cloudGraphics = poisonCloud.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); poisonCloud.cloudGraphics = cloudGraphics; poisonCloud.particleType = 'poisonCloud'; poisonCloud.creationTime = LK.ticks; poisonCloud.maxLifetime = maxParticleLifetime; activeParticles.push(poisonCloud); return poisonCloud; } function getWalkingFeetParticle() { if (walkingFeetPool.length > 0) { var particle = walkingFeetPool.pop(); // Reset lifetime tracking for reused particles particle.creationTime = LK.ticks; particle.maxLifetime = maxParticleLifetime * 10; // Walking feet particles live longer return particle; } var walkingFeet = new Container(); var feetGraphics = walkingFeet.attachAsset('walkingFeet', { anchorX: 0.5, anchorY: 0.5 }); walkingFeet.feetGraphics = feetGraphics; walkingFeet.particleType = 'walkingFeet'; walkingFeet.creationTime = LK.ticks; walkingFeet.maxLifetime = maxParticleLifetime * 10; // Walking feet particles live longer activeParticles.push(walkingFeet); return walkingFeet; } function returnParticle(particle) { if (!particle || !particle.particleType) { if (particle && particle.destroy) { particle.destroy(); } return; } // Remove from active particles tracking var activeIndex = activeParticles.indexOf(particle); if (activeIndex !== -1) { activeParticles.splice(activeIndex, 1); } 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; // Clear lifetime tracking properties particle.creationTime = undefined; particle.maxLifetime = undefined; // 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: 90, height: 90 }); goldBackground.x = 0; // Position background behind the text var goldText = new Text2(gold.toString(), { size: 28, 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: 90, height: 90 }); livesBackground.x = 0; // Position background behind the text var livesText = new Text2(lives.toString(), { size: 28, 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: 90, height: 90 }); scoreIcon.x = -45; // Position icon to the left var scoreText = new Text2(score.toString(), { size: 28, fill: 0xFFFFFF, weight: 800 }); scoreText.anchor.set(0.5, 0.5); scoreText.x = -45; // Position text on top of icon scoreDisplay.addChild(scoreText); // Add ask image asset at top layer with 100x100 size var askDisplay = game.attachAsset('ask', { anchorX: 0.5, anchorY: 0.5, width: 100, height: 100 }); // Position ask display at bottom-left above rapid tower askDisplay.x = 470; // Moved slightly more to the right askDisplay.y = 2732 - 600; // Moved even higher up above rapid tower area // Add mary image asset positioned at bottom of ask asset var maryDisplay = game.attachAsset('mary', { anchorX: 0.5, anchorY: 0.5, width: 850, height: 800 }); // Position mary at bottom of ask asset maryDisplay.x = askDisplay.x; maryDisplay.y = askDisplay.y + 120; // Positioned below ask asset // Add bird image asset positioned above ask asset var birdDisplay = game.attachAsset('bird', { anchorX: 0.5, anchorY: 0.5, width: 100, height: 100 }); // Position bird slightly above ask asset birdDisplay.x = askDisplay.x; birdDisplay.y = askDisplay.y - 120; // Positioned above ask asset // Add bird sound functionality - play chirp sound every 8-12 seconds var birdSoundTimer = 0; var nextBirdSoundTime = 480 + Math.random() * 240; // 8-12 seconds at 60fps birdDisplay.update = function () { // Check if black screen is active var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted; if (!isBlackScreenActive) { birdSoundTimer++; if (birdSoundTimer >= nextBirdSoundTime) { LK.getSound('bird_chirp').play(); birdSoundTimer = 0; nextBirdSoundTime = 480 + Math.random() * 240; // Reset timer for next chirp } } }; // Make birdDisplay interactive to play sound on click birdDisplay.down = function () { LK.getSound('bird_chirp').play(); }; // Add bird to game update so its update method gets called game.addChild(birdDisplay); // Add displays directly to game object instead of LK.gui for better visibility game.addChild(goldDisplay); game.addChild(livesDisplay); game.addChild(scoreDisplay); // Add mary display last so it appears on top of other elements game.addChild(maryDisplay); // Position displays in the top-right corner with absolute coordinates var topMargin = 65; // Moved up slightly more var spacing = 190; // Equalized spacing between displays var rightOffset = 1520; // Moved slightly to the left goldDisplay.x = rightOffset + 30; 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.7; tower.scaleY = 0.7; // Shift default tower icon slightly to the left if (towerTypes[i] === 'default') { tower.x -= 120; } // Shift rapid tower icon further to the left if (towerTypes[i] === 'rapid') { tower.x -= 180; tower.scaleX = 0.6; tower.scaleY = 0.6; } // Shift sniper tower icon further to the left if (towerTypes[i] === 'sniper') { tower.x -= 260; } // Shift slow tower icon to the right if (towerTypes[i] === 'slow') { tower.x += 180; } // Shift splash tower icon to the right - move closer to slow tower if (towerTypes[i] === 'splash') { tower.x += 260; } // Shift poison tower icon to the right if (towerTypes[i] === 'poison') { tower.x += 120; } tower.y = towerY + 70; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; // Start playing background music LK.playMusic('game_music'); game.update = function () { // Particle lifetime management - check and cleanup expired particles if (LK.ticks - lastParticleCleanup >= particleCleanupInterval) { lastParticleCleanup = LK.ticks; // Check all active particles for lifetime expiration for (var p = activeParticles.length - 1; p >= 0; p--) { var particle = activeParticles[p]; if (!particle || !particle.parent || !particle.creationTime) { // Remove invalid particles from tracking activeParticles.splice(p, 1); continue; } // Calculate particle age in milliseconds (convert ticks to ms: ticks * (1000/60)) var particleAge = (LK.ticks - particle.creationTime) * (1000 / 60); // Check if particle has exceeded its maximum lifetime if (particleAge > particle.maxLifetime) { // Force cleanup of expired particle console.log("Cleaning up expired particle:", particle.particleType, "age:", Math.floor(particleAge), "ms"); // Stop any ongoing tweens to prevent memory leaks tween.stop(particle, { x: true, y: true, alpha: true, scaleX: true, scaleY: true, rotation: true }); // Return to pool or destroy returnParticle(particle); } } } // 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; } // Prevent wave progression during black screen var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted; if (waveInProgress && !isBlackScreenActive) { if (!waveSpawned) { waveSpawned = true; // Play war sound only once when Wave 1 starts spawning (after button_start) if (currentWave === 1 && !warSoundPlayed) { warSoundPlayed = true; // Delay war sound to play after button_start sound finishes tween({}, {}, { duration: 1500, // Wait for button_start to finish onFinish: function onFinish() { console.log("Playing war sound for Wave 1"); try { LK.getSound('war').play(); console.log("War sound played successfully"); // Play place sound 5 seconds after war sound tween({}, {}, { duration: 5000, onFinish: function onFinish() { LK.getSound('place').play(); // Play sevda sound 5 seconds after place sound tween({}, {}, { duration: 5000, onFinish: function onFinish() { LK.getSound('sevda').play(); // Play mov sound 5 seconds after sevda sound tween({}, {}, { duration: 5000, onFinish: function onFinish() { LK.getSound('mov').play(); // Play og sound 5 seconds after mov sound tween({}, {}, { duration: 5000, onFinish: function onFinish() { LK.getSound('og').play(); } }); } }); } }); } }); } catch (e) { console.log("Error playing war sound:", e); } } }); } // Play mezar sound only once when Wave 2 starts spawning if (currentWave === 2 && !mezarSoundPlayed) { mezarSoundPlayed = true; LK.getSound('mezar').play(); } // We sound will be played when boss enemy appears on screen (moved to enemy entry logic) // 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 === 7; 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 window.krkSoundPlayedThisWave = false; // Reset mask sound flag for new wave window.maskSoundPlayedThisWave = false; // Reset shoes sound flag for new wave window.shoesSoundPlayed = false; // Reset browser sound flag for new wave window.browserSoundPlayed = false; // Reset browser sound wave flag for new wave browserSoundPlayedThisWave = 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); } else { // Add normal/ground enemies to the bottom layer enemyLayerBottom.addChild(enemy); } // Fixed health values for each wave based on requirements var fixedHealth; switch (currentWave) { case 1: fixedHealth = 195; break; case 2: fixedHealth = 210; break; case 3: fixedHealth = 220; break; case 4: fixedHealth = 250; break; case 5: fixedHealth = 300; break; case 6: fixedHealth = 320; break; case 7: fixedHealth = 2000; break; case 8: fixedHealth = 370; break; case 9: fixedHealth = 400; break; case 10: fixedHealth = 500; break; case 11: fixedHealth = 350; break; default: // For waves 12-50, scale health progressively if (currentWave <= 20) { fixedHealth = 350 + (currentWave - 11) * 30; // 350-620 for waves 12-20 } else if (currentWave <= 30) { fixedHealth = 620 + (currentWave - 20) * 50; // 620-1120 for waves 21-30 } else if (currentWave <= 40) { fixedHealth = 1120 + (currentWave - 30) * 80; // 1120-1920 for waves 31-40 } else { fixedHealth = 1920 + (currentWave - 40) * 100; // 1920+ for waves 41-50 } // Boss waves get extra health if (currentWave % 10 === 0) { fixedHealth = Math.floor(fixedHealth * 2.5); // Boss waves have 2.5x health } break; } // Boss wave special handling - use direct health values for boss enemies if (isBossWave) { if (waveType === 'big') { // Big boss keeps the 2000 health set above } else { // Other bosses keep the 2000 health set above } } enemy.maxHealth = fixedHealth; 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; // Set timer to automatically start next wave after 7 seconds if not at final wave if (currentWave < totalWaves) { waveTimer = nextWaveTime - 420; // 7 seconds = 420 frames at 60fps, so next wave starts in 7 seconds } } } // 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; } // Clean up walking feet for normal, swarm, and blue enemies only (no longer need to return to pool) if ((enemy.type === 'normal' || enemy.type === 'swarm' || enemy.type === 'blue') && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) { // Feet are now regular child objects, they'll be destroyed automatically with the enemy enemy.leftFoot = null; enemy.rightFoot = null; } // Create soul death animation for non-flying enemies if (!enemy.isFlying) { var soulCount = 8; // Number of soul particles for (var soulIdx = 0; soulIdx < soulCount; soulIdx++) { var soulParticle = getSmokeParticle(); var soulGraphics = soulParticle.smokeGraphics; soulGraphics.width = 20 + Math.random() * 30; soulGraphics.height = soulGraphics.width; // Soul colors - grimmer, darker tones var soulColors = [0x404040, 0x505050, 0x333333, 0x666666, 0x2a2a2a]; soulGraphics.tint = soulColors[Math.floor(Math.random() * soulColors.length)]; // Position soul particles at enemy's feet var angle = soulIdx / soulCount * Math.PI * 2 + Math.random() * 0.5; var distance = Math.random() * 20; soulParticle.x = enemy.x + Math.cos(angle) * distance; soulParticle.y = enemy.y + 30 + Math.random() * 10; // Start from ground level soulParticle.alpha = 0.9; soulParticle.scaleX = 0.3 + Math.random() * 0.4; soulParticle.scaleY = 0.3 + Math.random() * 0.4; game.addChild(soulParticle); // Animate soul rising upward and fading var targetY = soulParticle.y - 100 - Math.random() * 60; var targetX = soulParticle.x + (Math.random() - 0.5) * 40; tween(soulParticle, { x: targetX, y: targetY, alpha: 0, scaleX: soulParticle.scaleX * 2.5, scaleY: soulParticle.scaleY * 2.5 }, { duration: 1500 + Math.random() * 800, easing: tween.easeOut, onFinish: function onFinish() { returnParticle(soulParticle); } }); } } // Calculate gold and score rewards with improved scaling var isEliteWave = enemy.waveNumber === 7; var goldEarned = isEliteWave ? Math.floor(18 + (enemy.waveNumber - 1) * 2.5) : Math.floor(1.5 + (enemy.waveNumber - 1) * 0.7); 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(); // Shadow cleanup removed as flying enemies no longer use shadows // 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)) { // Clean up walking feet for normal, swarm, and blue enemies only (no longer need to return to pool) if ((enemy.type === 'normal' || enemy.type === 'swarm' || enemy.type === 'blue') && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) { // Feet are now regular child objects, they'll be destroyed automatically with the enemy 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(); } }); } // Shadow cleanup removed as flying enemies no longer use shadows // 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 // Check if wave 7 (boss wave) is completed if (currentWave >= 7 && enemies.length === 0 && !waveInProgress && !bossWaveCompleted) { bossWaveCompleted = true; // Start fade to black effect after boss wave completion if (!fadeToBlackStarted) { fadeToBlackStarted = true; // Create black overlay var blackOverlay = game.attachAsset('notification', { anchorX: 0, anchorY: 0, width: 2048, height: 2732, x: 0, y: 0 }); blackOverlay.tint = 0x000000; blackOverlay.alpha = 0; // Slowly fade everything to black tween(blackOverlay, { alpha: 1 }, { duration: 3000, // 3 seconds fade easing: tween.easeInOut, onFinish: function onFinish() { // Everything is now black - add center text console.log("Fade to black complete"); // Play 'son' sound when screen goes black LK.getSound('son').play(); // Music continues playing during black screen // No music fade out or restrictions // Create center text that appears after fade var centerText = new Text2("And the war is over", { size: 150, fill: 0xFFFFFF, weight: 800 }); centerText.anchor.set(0.5, 0.5); centerText.x = 2048 / 2; centerText.y = 2732 / 2 - 200; // Move text up to make room for buttons centerText.alpha = 0; game.addChild(centerText); // Create home button var homeButton = new Container(); var homeButtonBg = homeButton.attachAsset('ui_button', { anchorX: 0.5, anchorY: 0.5 }); homeButtonBg.width = 600; homeButtonBg.height = 180; homeButtonBg.tint = 0x4444AA; var homeButtonText = new Text2("home", { size: 80, fill: 0xFFFFFF, weight: 800 }); homeButtonText.anchor.set(0.5, 0.5); homeButton.addChild(homeButtonText); homeButton.x = 2048 / 2 - 350; homeButton.y = 2732 / 2 + 250; homeButton.alpha = 0; game.addChild(homeButton); // Create continue button var continueButton = new Container(); var continueButtonBg = continueButton.attachAsset('ui_button', { anchorX: 0.5, anchorY: 0.5 }); continueButtonBg.width = 600; continueButtonBg.height = 180; continueButtonBg.tint = 0x44AA44; var continueButtonText = new Text2("continue", { size: 80, fill: 0xFFFFFF, weight: 800 }); continueButtonText.anchor.set(0.5, 0.5); continueButton.addChild(continueButtonText); continueButton.x = 2048 / 2 + 350; continueButton.y = 2732 / 2 + 250; continueButton.alpha = 0; game.addChild(continueButton); // Add button click handlers homeButton.down = function () { // Show you win screen instead of reloading LK.showYouWin(); }; continueButton.down = function () { // Continue to wave 8 // Hide the overlay and buttons blackOverlay.alpha = 0; centerText.alpha = 0; homeButton.alpha = 0; continueButton.alpha = 0; // Reset game state for wave 8 currentWave = 8; // Advance to wave 8 waveTimer = 0; // Reset timer to start wave 8 immediately waveInProgress = true; waveSpawned = false; bossWaveCompleted = false; fadeToBlackStarted = false; // Music already playing, no need to restore // Re-enable bird sounds if (birdDisplay) { birdDisplay.update = function () { birdSoundTimer++; if (birdSoundTimer >= nextBirdSoundTime) { LK.getSound('bird_chirp').play(); birdSoundTimer = 0; nextBirdSoundTime = 480 + Math.random() * 240; // Reset timer for next chirp } }; } }; // Fade in the center text and buttons tween(centerText, { alpha: 1 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { // After text fades in, fade in buttons tween(homeButton, { alpha: 1 }, { duration: 1000, easing: tween.easeInOut }); tween(continueButton, { alpha: 1 }, { duration: 1000, easing: tween.easeInOut }); } }); } }); } } // Helper function to check if sounds should be muted during black screen function shouldMuteSound(soundId) { var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted; if (!isBlackScreenActive) { return false; // No black screen, don't mute } // During black screen, only allow 'son' sound to play return soundId !== 'son'; } // Override LK.getSound to apply muting during black screen var originalGetSound = LK.getSound; LK.getSound = function (soundId) { var sound = originalGetSound.call(this, soundId); var originalPlay = sound.play; sound.play = function () { if (shouldMuteSound(soundId)) { return; // Mute this sound during black screen } return originalPlay.call(this); }; return sound; }; // Only show you win for waves beyond 7 (if there are any) if (currentWave > 7 && 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();
}
});
}
}
// Track default bullet hits on swarm enemies for "you can't stop us" sound
if ((!self.type || self.type === 'default') && !self.targetEnemy.isFlying && self.targetEnemy.type === 'swarm') {
// Initialize separate counter for swarm enemies if it doesn't exist
if (!window.swarmBulletHitCount) {
window.swarmBulletHitCount = 0;
}
window.swarmBulletHitCount++;
// Play "shoes" sound only once on first default bullet hit on swarm enemy
if (window.swarmBulletHitCount === 1 && !window.shoesSoundPlayed) {
window.shoesSoundPlayed = true;
LK.getSound('shoes').play();
}
// Play "you can't stop us" sound only once on 10th default bullet hit on swarm enemy with delay
if (window.swarmBulletHitCount === 10 && !youCantStopUsSoundPlayed) {
youCantStopUsSoundPlayed = true;
// Delay the sound by 1200ms for clear speech
tween({}, {}, {
duration: 1200,
onFinish: function onFinish() {
LK.getSound('youcantstopus').play();
// Play browser sound 2 seconds after youcantstopus sound - only once per wave
if (!browserSoundPlayedThisWave) {
browserSoundPlayedThisWave = true;
tween({}, {}, {
duration: 2000,
onFinish: function onFinish() {
LK.getSound('browser').play();
// Play tnk sound 5 seconds after browser sound
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
LK.getSound('tnk').play();
}
});
}
});
}
}
});
}
}
// Removed browser sound for rapid bullet hits on swarm enemies - now only plays after youcantstopus sound
// 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
}
// Blood animation removed for swarm enemies per requirements
// Skip blood animation for flying enemies, boss enemies, swarm enemies, and immune enemies
// No blood animation will be created for any bullet hits on swarm enemies
if (false) {
// Blood animation completely disabled for swarm enemies
}
// 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;
}
// Play krk sound for immune enemies hit by splash damage - only once per wave
if (otherEnemy.isImmune && !window.krkSoundPlayedThisWave) {
window.krkSoundPlayedThisWave = true;
LK.getSound('krk').play();
}
}
}
}
} 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.25;
if (self.sourceTowerLevel !== undefined) {
// Scale: 25% at level 1, 30% at 2, 32% at 3, 35% at 4, 37% at 5, 40% at 6
var slowLevels = [0.25, 0.3, 0.32, 0.35, 0.37, 0.4];
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') {
// Handle poison bullets hitting immune enemies
if (self.targetEnemy.isImmune) {
// Play mask sound for immune enemies hit by poison bullets - only once per wave
if (!window.maskSoundPlayedThisWave) {
window.maskSoundPlayedThisWave = true;
LK.getSound('mask').play();
}
} else {
// 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();
}
// Removed gogogo sound for sniper bullet hits on normal enemies
// 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 = 200;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'blue':
self.maxHealth = 150; // Medium health
self.speed *= 1.5; // Faster than normal
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 === 7 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 10x health and are larger
self.maxHealth *= 10;
// Faster speed for bosses
self.speed = self.speed * 1.2;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.type === 'bigboss') {
assetId = 'bigboss';
} else if (self.type === 'blue') {
assetId = 'enemy_blue';
} 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, swarm, and blue enemies only (no feet for boss enemies)
self.leftFoot = null;
self.rightFoot = null;
if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue') && !self.isBoss) {
// Adjust foot size based on enemy type
var footWidth = 18;
var footHeight = 13;
var footSpacing = 15;
var footYPosition = 35;
// Create walking feet as regular Container objects (not particles)
self.leftFoot = new Container();
var leftFootGraphics = self.leftFoot.attachAsset('walkingFeet', {
anchorX: 0.5,
anchorY: 0.5
});
leftFootGraphics.width = footWidth;
leftFootGraphics.height = footHeight;
leftFootGraphics.tint = 0x000000; // Black color for walking feet
self.rightFoot = new Container();
var rightFootGraphics = self.rightFoot.attachAsset('walkingFeet', {
anchorX: 0.5,
anchorY: 0.5
});
rightFootGraphics.width = footWidth;
rightFootGraphics.height = footHeight;
rightFootGraphics.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;
}*/
// Flying enemies no longer use shadows for performance optimization
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, swarm, and blue enemies only (no feet for boss enemies)
if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue') && !self.isBoss && self.leftFoot && self.rightFoot) {
// 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
// Simplified foot animation - only for normal enemies, swarm enemies get no foot animation
if (self.type === 'normal') {
// Update foot animation phases
self.leftFootPhase += self.walkAnimationSpeed * speedMultiplier * 2;
self.rightFootPhase += self.walkAnimationSpeed * speedMultiplier * 2;
// 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 = 0.5; // Simplified, reduced movement for swarm
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
}
// Simplified animation for swarm enemies
if (self.type === 'swarm') {
var targetY = combinedBobOffset * animationIntensity;
var targetX = horizontalSway * animationIntensity * 0.3; // Much less horizontal movement
// Simple direct assignment for swarm enemies instead of tweening
enemyGraphics.y = targetY;
enemyGraphics.x = targetX;
} else {
// Normal complex animation for other enemy types
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 (not for swarm)
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;
}
// Return feet to resting position for normal, swarm, and blue enemies only when not walking
if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue') && !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;
}
// Shadow rendering removed for flying enemies to improve performance
// 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();
}
});
}
// Play we sound only once when boss enemy from Wave 7 first appears on screen
if (enemy.waveNumber === 7 && enemy.isBoss && !weSoundPlayed) {
weSoundPlayed = true;
LK.getSound('we').play();
// Play ek sound after we sound finishes
tween({}, {}, {
duration: 2000,
// Adjust timing based on 'we' sound duration
onFinish: function onFinish() {
LK.getSound('ek').play();
// Play tw sound after ek sound finishes
tween({}, {}, {
duration: 2000,
// Adjust timing based on 'ek' sound duration
onFinish: function onFinish() {
LK.getSound('tw').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
// Add exhaust animation for flying enemies (slow tower style)
if (!enemy.exhaustTimer) {
enemy.exhaustTimer = 0;
}
enemy.exhaustTimer++;
// Create exhaust animation every 25 frames for flying enemies (slower like slow tower)
if (enemy.exhaustTimer % 25 === 0) {
// Create exhaust particles behind flying enemy using particle pool
var exhaustCount = enemies.length > 15 ? 8 : 12; // Reduced count when many enemies like slow tower
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 like slow tower
var exhaustColors = [0x2a2a2a, 0x1a1a1a, 0x404040, 0x303030, 0x505050];
exhaustGraphics.tint = exhaustColors[Math.floor(Math.random() * exhaustColors.length)];
// Calculate direction opposite to movement
var movementAngle = Math.atan2(oy, ox);
var exhaustBaseAngle = movementAngle + Math.PI; // Opposite direction from movement
var exhaustAngle = exhaustBaseAngle + (Math.random() - 0.5) * Math.PI * 0.3; // Narrower spread like slow tower
var exhaustDistance = 50 + Math.random() * 25; // Moved further back like slow tower
exhaustParticle.x = enemy.x + Math.cos(exhaustAngle) * exhaustDistance;
exhaustParticle.y = enemy.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 enemy and fading in synchronized direction
var targetDistance = exhaustDistance + 35 + Math.random() * 25; // Adjusted for new starting position
var targetX = enemy.x + Math.cos(exhaustAngle) * targetDistance;
var targetY = enemy.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);
}
});
}
}
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) {
// Shadow cleanup removed as flying enemies no longer use shadows
// 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 = 15;
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') {
// Check if black screen is active
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted;
if (!isBlackScreenActive) {
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') {
// Check if black screen is active
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted;
if (!isBlackScreenActive) {
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') {
// Check if black screen is active
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted;
if (!isBlackScreenActive) {
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 enemies entering slow tower area damage (bronz sound removed)
if (affectedEnemies.length > 0) {
slowAreaEnemyCount++;
}
// 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 slow effect to all affected enemies (visual effects and slow)
for (var i = 0; i < affectedEnemies.length; i++) {
var affectedEnemy = affectedEnemies[i];
// 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.25;
if (self.level !== undefined) {
// Scale: 25% at level 1, 30% at 2, 32% at 3, 35% at 4, 37% at 5, 40% at 6
var slowLevels = [0.25, 0.3, 0.32, 0.35, 0.37, 0.4];
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
}
} else {
// Play krk sound for immune enemies hit by slow tower - only once per wave
if (!window.krkSoundPlayedThisWave) {
window.krkSoundPlayedThisWave = true;
LK.getSound('krk').play();
}
}
}
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;
// Delay the gzlk sound by 2000ms (2 seconds)
tween({}, {}, {
duration: 2000,
onFinish: function onFinish() {
LK.getSound('gzlk').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;
// Delay the vasiyet sound by 3000ms (3 seconds)
tween({}, {}, {
duration: 3000,
onFinish: function onFinish() {
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;
// --- Extended 50 Wave System ---
var waveNum = i + 1;
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 8;
var isBossWave = waveNum % 10 === 0;
// Define wave progression for all 50 waves
if (waveNum === 1) {
block.tint = 0x0066FF;
waveType = "Blue";
enemyType = "blue";
enemyCount = 4;
} else if (waveNum === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 5;
} else if (waveNum === 3) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 6;
} else if (waveNum === 4) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 4;
} else if (waveNum === 5) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 4;
} else if (waveNum === 6) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 5;
} else if (waveNum === 7) {
block.tint = 0x8B4513;
waveType = "Big Boss";
enemyType = "big";
enemyCount = 1;
} else if (waveNum >= 8 && waveNum <= 50) {
// Generate varied waves for waves 8-50
var cyclePos = (waveNum - 8) % 7; // Cycle through 7 different types
var intensity = Math.floor((waveNum - 8) / 7) + 1; // Increase intensity every 7 waves
switch (cyclePos) {
case 0:
// Immune waves
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = Math.min(15, 6 + intensity * 2);
break;
case 1:
// Fast waves
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = Math.min(12, 5 + intensity);
break;
case 2:
// Flying waves
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = Math.min(10, 4 + intensity);
break;
case 3:
// Swarm waves
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = Math.min(20, 8 + intensity * 2);
break;
case 4:
// Blue waves
block.tint = 0x0066FF;
waveType = "Blue";
enemyType = "blue";
enemyCount = Math.min(12, 6 + intensity);
break;
case 5:
// Normal waves
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = Math.min(15, 8 + intensity * 2);
break;
case 6:
// Big boss waves (every 7th wave)
block.tint = 0x8B4513;
waveType = "Big Boss";
enemyType = "big";
enemyCount = Math.min(3, 1 + Math.floor(intensity / 2));
break;
}
// Special boss waves every 10 waves
if (waveNum % 10 === 0) {
block.tint = 0x8B4513;
waveType = "Mega Boss";
enemyType = "big";
enemyCount = Math.min(5, 2 + Math.floor(waveNum / 20));
}
} else {
// Fallback for any additional waves
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 8;
}
// --- End Extended 50 Wave System ---
// Mark elite waves with a special visual indicator
if (waveNum === 7 && 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 wave 7
if (waveNumber === 7 && 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 = 50;
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 browserSoundPlayedThisWave = false; // Track if browser sound has been played this wave after youcantstopus
var warSoundPlayed = false; // Track if war sound has been played globally - only once per game
var mezarSoundPlayed = false; // Track if mezar sound has been played globally - only once per game
var weSoundPlayed = false; // Track if we sound has been played for wave 7 - only once per game
var işSoundPlayed = false; // Track if iş sound has been played after we sound - only once per game
var bossWaveCompleted = false; // Track if boss wave (wave 7) has been completed
var fadeToBlackStarted = false; // Track if fade to black has started
// 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
// Particle lifetime management system
var activeParticles = []; // Track all active particles for lifetime management
var maxParticleLifetime = 3000; // Maximum particle lifetime in milliseconds (3 seconds)
var particleCleanupInterval = 300; // Check for cleanup every 5 seconds (300 frames at 60fps)
var lastParticleCleanup = 0;
// 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) {
var particle = bloodParticlePool.pop();
// Reset lifetime tracking for reused particles
particle.creationTime = LK.ticks;
particle.maxLifetime = maxParticleLifetime;
return particle;
}
var bloodParticle = new Container();
var bloodGraphics = bloodParticle.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
bloodParticle.bloodGraphics = bloodGraphics;
bloodParticle.particleType = 'blood';
bloodParticle.creationTime = LK.ticks;
bloodParticle.maxLifetime = maxParticleLifetime;
activeParticles.push(bloodParticle);
return bloodParticle;
}
function getPoisonParticle() {
if (poisonParticlePool.length > 0) {
var particle = poisonParticlePool.pop();
// Reset lifetime tracking for reused particles
particle.creationTime = LK.ticks;
particle.maxLifetime = maxParticleLifetime;
return particle;
}
var poisonParticle = new Container();
var poisonGraphics = poisonParticle.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
poisonParticle.poisonGraphics = poisonGraphics;
poisonParticle.particleType = 'poison';
poisonParticle.creationTime = LK.ticks;
poisonParticle.maxLifetime = maxParticleLifetime;
activeParticles.push(poisonParticle);
return poisonParticle;
}
function getSmokeParticle() {
if (smokeParticlePool.length > 0) {
var particle = smokeParticlePool.pop();
// Reset lifetime tracking for reused particles
particle.creationTime = LK.ticks;
particle.maxLifetime = maxParticleLifetime;
return particle;
}
var smokeParticle = new Container();
var smokeGraphics = smokeParticle.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
smokeParticle.smokeGraphics = smokeGraphics;
smokeParticle.particleType = 'smoke';
smokeParticle.creationTime = LK.ticks;
smokeParticle.maxLifetime = maxParticleLifetime;
activeParticles.push(smokeParticle);
return smokeParticle;
}
function getFireParticle() {
if (fireParticlePool.length > 0) {
var particle = fireParticlePool.pop();
// Reset lifetime tracking for reused particles
particle.creationTime = LK.ticks;
particle.maxLifetime = maxParticleLifetime;
return particle;
}
var fireParticle = new Container();
var fireGraphics = fireParticle.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
fireParticle.fireGraphics = fireGraphics;
fireParticle.particleType = 'fire';
fireParticle.creationTime = LK.ticks;
fireParticle.maxLifetime = maxParticleLifetime;
activeParticles.push(fireParticle);
return fireParticle;
}
function getPoisonCloudParticle() {
if (poisonCloudPool.length > 0) {
var particle = poisonCloudPool.pop();
// Reset lifetime tracking for reused particles
particle.creationTime = LK.ticks;
particle.maxLifetime = maxParticleLifetime;
return particle;
}
var poisonCloud = new Container();
var cloudGraphics = poisonCloud.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
poisonCloud.cloudGraphics = cloudGraphics;
poisonCloud.particleType = 'poisonCloud';
poisonCloud.creationTime = LK.ticks;
poisonCloud.maxLifetime = maxParticleLifetime;
activeParticles.push(poisonCloud);
return poisonCloud;
}
function getWalkingFeetParticle() {
if (walkingFeetPool.length > 0) {
var particle = walkingFeetPool.pop();
// Reset lifetime tracking for reused particles
particle.creationTime = LK.ticks;
particle.maxLifetime = maxParticleLifetime * 10; // Walking feet particles live longer
return particle;
}
var walkingFeet = new Container();
var feetGraphics = walkingFeet.attachAsset('walkingFeet', {
anchorX: 0.5,
anchorY: 0.5
});
walkingFeet.feetGraphics = feetGraphics;
walkingFeet.particleType = 'walkingFeet';
walkingFeet.creationTime = LK.ticks;
walkingFeet.maxLifetime = maxParticleLifetime * 10; // Walking feet particles live longer
activeParticles.push(walkingFeet);
return walkingFeet;
}
function returnParticle(particle) {
if (!particle || !particle.particleType) {
if (particle && particle.destroy) {
particle.destroy();
}
return;
}
// Remove from active particles tracking
var activeIndex = activeParticles.indexOf(particle);
if (activeIndex !== -1) {
activeParticles.splice(activeIndex, 1);
}
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;
// Clear lifetime tracking properties
particle.creationTime = undefined;
particle.maxLifetime = undefined;
// 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: 90,
height: 90
});
goldBackground.x = 0; // Position background behind the text
var goldText = new Text2(gold.toString(), {
size: 28,
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: 90,
height: 90
});
livesBackground.x = 0; // Position background behind the text
var livesText = new Text2(lives.toString(), {
size: 28,
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: 90,
height: 90
});
scoreIcon.x = -45; // Position icon to the left
var scoreText = new Text2(score.toString(), {
size: 28,
fill: 0xFFFFFF,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
scoreText.x = -45; // Position text on top of icon
scoreDisplay.addChild(scoreText);
// Add ask image asset at top layer with 100x100 size
var askDisplay = game.attachAsset('ask', {
anchorX: 0.5,
anchorY: 0.5,
width: 100,
height: 100
});
// Position ask display at bottom-left above rapid tower
askDisplay.x = 470; // Moved slightly more to the right
askDisplay.y = 2732 - 600; // Moved even higher up above rapid tower area
// Add mary image asset positioned at bottom of ask asset
var maryDisplay = game.attachAsset('mary', {
anchorX: 0.5,
anchorY: 0.5,
width: 850,
height: 800
});
// Position mary at bottom of ask asset
maryDisplay.x = askDisplay.x;
maryDisplay.y = askDisplay.y + 120; // Positioned below ask asset
// Add bird image asset positioned above ask asset
var birdDisplay = game.attachAsset('bird', {
anchorX: 0.5,
anchorY: 0.5,
width: 100,
height: 100
});
// Position bird slightly above ask asset
birdDisplay.x = askDisplay.x;
birdDisplay.y = askDisplay.y - 120; // Positioned above ask asset
// Add bird sound functionality - play chirp sound every 8-12 seconds
var birdSoundTimer = 0;
var nextBirdSoundTime = 480 + Math.random() * 240; // 8-12 seconds at 60fps
birdDisplay.update = function () {
// Check if black screen is active
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted;
if (!isBlackScreenActive) {
birdSoundTimer++;
if (birdSoundTimer >= nextBirdSoundTime) {
LK.getSound('bird_chirp').play();
birdSoundTimer = 0;
nextBirdSoundTime = 480 + Math.random() * 240; // Reset timer for next chirp
}
}
};
// Make birdDisplay interactive to play sound on click
birdDisplay.down = function () {
LK.getSound('bird_chirp').play();
};
// Add bird to game update so its update method gets called
game.addChild(birdDisplay);
// Add displays directly to game object instead of LK.gui for better visibility
game.addChild(goldDisplay);
game.addChild(livesDisplay);
game.addChild(scoreDisplay);
// Add mary display last so it appears on top of other elements
game.addChild(maryDisplay);
// Position displays in the top-right corner with absolute coordinates
var topMargin = 65; // Moved up slightly more
var spacing = 190; // Equalized spacing between displays
var rightOffset = 1520; // Moved slightly to the left
goldDisplay.x = rightOffset + 30;
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.7;
tower.scaleY = 0.7;
// Shift default tower icon slightly to the left
if (towerTypes[i] === 'default') {
tower.x -= 120;
}
// Shift rapid tower icon further to the left
if (towerTypes[i] === 'rapid') {
tower.x -= 180;
tower.scaleX = 0.6;
tower.scaleY = 0.6;
}
// Shift sniper tower icon further to the left
if (towerTypes[i] === 'sniper') {
tower.x -= 260;
}
// Shift slow tower icon to the right
if (towerTypes[i] === 'slow') {
tower.x += 180;
}
// Shift splash tower icon to the right - move closer to slow tower
if (towerTypes[i] === 'splash') {
tower.x += 260;
}
// Shift poison tower icon to the right
if (towerTypes[i] === 'poison') {
tower.x += 120;
}
tower.y = towerY + 70;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
// Start playing background music
LK.playMusic('game_music');
game.update = function () {
// Particle lifetime management - check and cleanup expired particles
if (LK.ticks - lastParticleCleanup >= particleCleanupInterval) {
lastParticleCleanup = LK.ticks;
// Check all active particles for lifetime expiration
for (var p = activeParticles.length - 1; p >= 0; p--) {
var particle = activeParticles[p];
if (!particle || !particle.parent || !particle.creationTime) {
// Remove invalid particles from tracking
activeParticles.splice(p, 1);
continue;
}
// Calculate particle age in milliseconds (convert ticks to ms: ticks * (1000/60))
var particleAge = (LK.ticks - particle.creationTime) * (1000 / 60);
// Check if particle has exceeded its maximum lifetime
if (particleAge > particle.maxLifetime) {
// Force cleanup of expired particle
console.log("Cleaning up expired particle:", particle.particleType, "age:", Math.floor(particleAge), "ms");
// Stop any ongoing tweens to prevent memory leaks
tween.stop(particle, {
x: true,
y: true,
alpha: true,
scaleX: true,
scaleY: true,
rotation: true
});
// Return to pool or destroy
returnParticle(particle);
}
}
}
// 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;
}
// Prevent wave progression during black screen
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted;
if (waveInProgress && !isBlackScreenActive) {
if (!waveSpawned) {
waveSpawned = true;
// Play war sound only once when Wave 1 starts spawning (after button_start)
if (currentWave === 1 && !warSoundPlayed) {
warSoundPlayed = true;
// Delay war sound to play after button_start sound finishes
tween({}, {}, {
duration: 1500,
// Wait for button_start to finish
onFinish: function onFinish() {
console.log("Playing war sound for Wave 1");
try {
LK.getSound('war').play();
console.log("War sound played successfully");
// Play place sound 5 seconds after war sound
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
LK.getSound('place').play();
// Play sevda sound 5 seconds after place sound
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
LK.getSound('sevda').play();
// Play mov sound 5 seconds after sevda sound
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
LK.getSound('mov').play();
// Play og sound 5 seconds after mov sound
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
LK.getSound('og').play();
}
});
}
});
}
});
}
});
} catch (e) {
console.log("Error playing war sound:", e);
}
}
});
}
// Play mezar sound only once when Wave 2 starts spawning
if (currentWave === 2 && !mezarSoundPlayed) {
mezarSoundPlayed = true;
LK.getSound('mezar').play();
}
// We sound will be played when boss enemy appears on screen (moved to enemy entry logic)
// 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 === 7;
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
window.krkSoundPlayedThisWave = false;
// Reset mask sound flag for new wave
window.maskSoundPlayedThisWave = false;
// Reset shoes sound flag for new wave
window.shoesSoundPlayed = false;
// Reset browser sound flag for new wave
window.browserSoundPlayed = false;
// Reset browser sound wave flag for new wave
browserSoundPlayedThisWave = 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);
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Fixed health values for each wave based on requirements
var fixedHealth;
switch (currentWave) {
case 1:
fixedHealth = 195;
break;
case 2:
fixedHealth = 210;
break;
case 3:
fixedHealth = 220;
break;
case 4:
fixedHealth = 250;
break;
case 5:
fixedHealth = 300;
break;
case 6:
fixedHealth = 320;
break;
case 7:
fixedHealth = 2000;
break;
case 8:
fixedHealth = 370;
break;
case 9:
fixedHealth = 400;
break;
case 10:
fixedHealth = 500;
break;
case 11:
fixedHealth = 350;
break;
default:
// For waves 12-50, scale health progressively
if (currentWave <= 20) {
fixedHealth = 350 + (currentWave - 11) * 30; // 350-620 for waves 12-20
} else if (currentWave <= 30) {
fixedHealth = 620 + (currentWave - 20) * 50; // 620-1120 for waves 21-30
} else if (currentWave <= 40) {
fixedHealth = 1120 + (currentWave - 30) * 80; // 1120-1920 for waves 31-40
} else {
fixedHealth = 1920 + (currentWave - 40) * 100; // 1920+ for waves 41-50
}
// Boss waves get extra health
if (currentWave % 10 === 0) {
fixedHealth = Math.floor(fixedHealth * 2.5); // Boss waves have 2.5x health
}
break;
}
// Boss wave special handling - use direct health values for boss enemies
if (isBossWave) {
if (waveType === 'big') {
// Big boss keeps the 2000 health set above
} else {
// Other bosses keep the 2000 health set above
}
}
enemy.maxHealth = fixedHealth;
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;
// Set timer to automatically start next wave after 7 seconds if not at final wave
if (currentWave < totalWaves) {
waveTimer = nextWaveTime - 420; // 7 seconds = 420 frames at 60fps, so next wave starts in 7 seconds
}
}
}
// 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;
}
// Clean up walking feet for normal, swarm, and blue enemies only (no longer need to return to pool)
if ((enemy.type === 'normal' || enemy.type === 'swarm' || enemy.type === 'blue') && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) {
// Feet are now regular child objects, they'll be destroyed automatically with the enemy
enemy.leftFoot = null;
enemy.rightFoot = null;
}
// Create soul death animation for non-flying enemies
if (!enemy.isFlying) {
var soulCount = 8; // Number of soul particles
for (var soulIdx = 0; soulIdx < soulCount; soulIdx++) {
var soulParticle = getSmokeParticle();
var soulGraphics = soulParticle.smokeGraphics;
soulGraphics.width = 20 + Math.random() * 30;
soulGraphics.height = soulGraphics.width;
// Soul colors - grimmer, darker tones
var soulColors = [0x404040, 0x505050, 0x333333, 0x666666, 0x2a2a2a];
soulGraphics.tint = soulColors[Math.floor(Math.random() * soulColors.length)];
// Position soul particles at enemy's feet
var angle = soulIdx / soulCount * Math.PI * 2 + Math.random() * 0.5;
var distance = Math.random() * 20;
soulParticle.x = enemy.x + Math.cos(angle) * distance;
soulParticle.y = enemy.y + 30 + Math.random() * 10; // Start from ground level
soulParticle.alpha = 0.9;
soulParticle.scaleX = 0.3 + Math.random() * 0.4;
soulParticle.scaleY = 0.3 + Math.random() * 0.4;
game.addChild(soulParticle);
// Animate soul rising upward and fading
var targetY = soulParticle.y - 100 - Math.random() * 60;
var targetX = soulParticle.x + (Math.random() - 0.5) * 40;
tween(soulParticle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: soulParticle.scaleX * 2.5,
scaleY: soulParticle.scaleY * 2.5
}, {
duration: 1500 + Math.random() * 800,
easing: tween.easeOut,
onFinish: function onFinish() {
returnParticle(soulParticle);
}
});
}
}
// Calculate gold and score rewards with improved scaling
var isEliteWave = enemy.waveNumber === 7;
var goldEarned = isEliteWave ? Math.floor(18 + (enemy.waveNumber - 1) * 2.5) : Math.floor(1.5 + (enemy.waveNumber - 1) * 0.7);
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();
// Shadow cleanup removed as flying enemies no longer use shadows
// 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)) {
// Clean up walking feet for normal, swarm, and blue enemies only (no longer need to return to pool)
if ((enemy.type === 'normal' || enemy.type === 'swarm' || enemy.type === 'blue') && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) {
// Feet are now regular child objects, they'll be destroyed automatically with the enemy
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();
}
});
}
// Shadow cleanup removed as flying enemies no longer use shadows
// 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
// Check if wave 7 (boss wave) is completed
if (currentWave >= 7 && enemies.length === 0 && !waveInProgress && !bossWaveCompleted) {
bossWaveCompleted = true;
// Start fade to black effect after boss wave completion
if (!fadeToBlackStarted) {
fadeToBlackStarted = true;
// Create black overlay
var blackOverlay = game.attachAsset('notification', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732,
x: 0,
y: 0
});
blackOverlay.tint = 0x000000;
blackOverlay.alpha = 0;
// Slowly fade everything to black
tween(blackOverlay, {
alpha: 1
}, {
duration: 3000,
// 3 seconds fade
easing: tween.easeInOut,
onFinish: function onFinish() {
// Everything is now black - add center text
console.log("Fade to black complete");
// Play 'son' sound when screen goes black
LK.getSound('son').play();
// Music continues playing during black screen
// No music fade out or restrictions
// Create center text that appears after fade
var centerText = new Text2("And the war is over", {
size: 150,
fill: 0xFFFFFF,
weight: 800
});
centerText.anchor.set(0.5, 0.5);
centerText.x = 2048 / 2;
centerText.y = 2732 / 2 - 200; // Move text up to make room for buttons
centerText.alpha = 0;
game.addChild(centerText);
// Create home button
var homeButton = new Container();
var homeButtonBg = homeButton.attachAsset('ui_button', {
anchorX: 0.5,
anchorY: 0.5
});
homeButtonBg.width = 600;
homeButtonBg.height = 180;
homeButtonBg.tint = 0x4444AA;
var homeButtonText = new Text2("home", {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
homeButtonText.anchor.set(0.5, 0.5);
homeButton.addChild(homeButtonText);
homeButton.x = 2048 / 2 - 350;
homeButton.y = 2732 / 2 + 250;
homeButton.alpha = 0;
game.addChild(homeButton);
// Create continue button
var continueButton = new Container();
var continueButtonBg = continueButton.attachAsset('ui_button', {
anchorX: 0.5,
anchorY: 0.5
});
continueButtonBg.width = 600;
continueButtonBg.height = 180;
continueButtonBg.tint = 0x44AA44;
var continueButtonText = new Text2("continue", {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
continueButtonText.anchor.set(0.5, 0.5);
continueButton.addChild(continueButtonText);
continueButton.x = 2048 / 2 + 350;
continueButton.y = 2732 / 2 + 250;
continueButton.alpha = 0;
game.addChild(continueButton);
// Add button click handlers
homeButton.down = function () {
// Show you win screen instead of reloading
LK.showYouWin();
};
continueButton.down = function () {
// Continue to wave 8
// Hide the overlay and buttons
blackOverlay.alpha = 0;
centerText.alpha = 0;
homeButton.alpha = 0;
continueButton.alpha = 0;
// Reset game state for wave 8
currentWave = 8; // Advance to wave 8
waveTimer = 0; // Reset timer to start wave 8 immediately
waveInProgress = true;
waveSpawned = false;
bossWaveCompleted = false;
fadeToBlackStarted = false;
// Music already playing, no need to restore
// Re-enable bird sounds
if (birdDisplay) {
birdDisplay.update = function () {
birdSoundTimer++;
if (birdSoundTimer >= nextBirdSoundTime) {
LK.getSound('bird_chirp').play();
birdSoundTimer = 0;
nextBirdSoundTime = 480 + Math.random() * 240; // Reset timer for next chirp
}
};
}
};
// Fade in the center text and buttons
tween(centerText, {
alpha: 1
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
// After text fades in, fade in buttons
tween(homeButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeInOut
});
tween(continueButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeInOut
});
}
});
}
});
}
}
// Helper function to check if sounds should be muted during black screen
function shouldMuteSound(soundId) {
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted;
if (!isBlackScreenActive) {
return false; // No black screen, don't mute
}
// During black screen, only allow 'son' sound to play
return soundId !== 'son';
}
// Override LK.getSound to apply muting during black screen
var originalGetSound = LK.getSound;
LK.getSound = function (soundId) {
var sound = originalGetSound.call(this, soundId);
var originalPlay = sound.play;
sound.play = function () {
if (shouldMuteSound(soundId)) {
return; // Mute this sound during black screen
}
return originalPlay.call(this);
};
return sound;
};
// Only show you win for waves beyond 7 (if there are any)
if (currentWave > 7 && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
};
Kuş bakışı savaş uçağı. 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
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
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ı varyasyonlarını çiz
çarpı şeklinde demir. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Tekerleklerüni gri yap
siyah top. In-Game asset. 2d. High contrast. No shadows
o bir sniper elbisesine sahip
Şişe kırılsın parçaşarı etrafa dagılsın
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
i
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