User prompt
Ayaklar çok küçük
User prompt
Olmuyor
User prompt
Yok bacaklar büyüyüp küçülmüyor sabit duruyor ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Şuan ayaklar gözüküyor ama ayaklarda hareket animasyonu yok ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Ayak vx varlıgına geçişte degil bekleme süresinden sonra dönüşte kayboluyor
User prompt
Hala olmuyor
User prompt
Sol ayagı dönüşte kayboluyor hala
User prompt
Hala kayboluyor
User prompt
Olmamış
User prompt
Vex düşman dönüşte ayakları kayboluyor ne yapmalı ?
User prompt
Vx görüntü varlığının alt birimi ayak oluştur ve animasyon ekle ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Vex saga dönüşte görüntü varlıgı ters dönüyor düzelt
User prompt
Vex saga dönüşte aktif olan vx görüntüsü 1 saniye sonra kaybolup yerini vx2 görüntüsğne bırakmalı ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Vex saga dönüşte aktif olan vx görüntüsü 1 saniye sonra kaybolup yerini vx2 görüntüsğne bırakmalı ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Vex düşmanı için yeni görüntü varlıgı ekle vx görüntülendikten 1 saniye sonra devreye girecek adı vx2 ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
15px yap ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Vx görüntüsünün alt 5px bölümüne büyüyüp küçülme animasyonu ekle vex düşmanı dururken çalışmasın ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Vx görüntüsü aktif oldugunda alt kısımlarını büyüyüp küçülme animasyonu ekle ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Vx görüntüsü aktif oldugunda alt kısımlarını büyüyüp küçülme animasyonu ekle ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Vex dişman sağa ilerlemeye başladıktan sonra yürüme animasyonu bir türlü düzelmiyor
User prompt
Zehir kule animasyonunuda aynı oranda arttır ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Yavaşkule animasyonu arttır daha gerçekçi yap ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Yavaşlama kulesi animasyonunu uygun resim varlıkları ile degiştirerek daha gerçekçi bir alev animasyonu yarat ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Yavaşlama kulesi animasyonunu daha gerçekçi yap ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Yavaşlama kulesine siyah dumanlar ekle
/**** * 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; // Apply animation reduction for splash bullets to minimize load var animationFactor = self.animationReductionFactor || 1.0; // Default to full animation var concentratedBounce = Math.sin(self.animationPhase) * 2 * animationFactor; // Reduced from larger values var concentratedSway = Math.cos(self.animationPhase * 0.7) * 1.5 * animationFactor; // 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 (skip damage for vex enemies to make them immortal) if (self.targetEnemy.type !== 'vex') { 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 trackTween({}, {}, { 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 trackTween({}, {}, { 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(); // Play borc sound 4 seconds after tnk sound tween({}, {}, { duration: 4000, onFinish: function onFinish() { LK.getSound('borc').play(); // Play db sound 4 seconds after borc sound finishes tween({}, {}, { duration: 4000, onFinish: function onFinish() { LK.getSound('db').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 ? 3 : 4; // Further reduced smoke particles (was 4-6, now 3-4) for (var smokeIdx = 0; smokeIdx < smokeCount; smokeIdx++) { var smokeParticle = getSmokeParticle(); var smokeGraphics = smokeParticle.smokeGraphics; smokeGraphics.width = 15 + Math.random() * 20; 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 = 8 + Math.random() * 12; 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 tower bullets on 2nd hit - only once per wave if (otherEnemy.isImmune && self.type === 'splash') { if (typeof window.splashImmuneHitCounter === 'undefined') { window.splashImmuneHitCounter = 0; } window.splashImmuneHitCounter++; if (window.splashImmuneHitCounter === 2 && !window.krkSoundPlayedThisWave) { window.krkSoundPlayedThisWave = true; try { LK.getSound('krk').play(); } catch (e) { console.log("Error playing krk sound:", e); } } } // Krk sound moved to splash tower bullet hit logic - not played here anymore } } } } 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 (exclude fast enemies from wave 5) if (poisonBulletHitCount === 1 && !gogogoPoisonSoundPlayed && !(self.targetEnemy.type === 'fast' && self.targetEnemy.waveNumber === 5)) { gogogoPoisonSoundPlayed = true; LK.getSound('gogogo').play(); } // Play whofarted sound only once on second poison bullet hit with delay (exclude fast enemies from wave 5) if (poisonBulletHitCount === 2 && !whoFartedSoundPlayed && !(self.targetEnemy.type === 'fast' && self.targetEnemy.waveNumber === 5)) { 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(); // } } // Shy sound logic moved to sniper tower fire method to ensure it only triggers for sniper towers } 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 with animation reduction factor var animationFactor = self.animationReductionFactor || 1.0; // Default to full animation var concentratedBounceX = Math.sin(self.animationPhase * 1.2) * 0.8 * animationFactor; // Much smaller bounce var concentratedBounceY = Math.cos(self.animationPhase * 1.5) * 0.6 * animationFactor; // 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 'vex': self.maxHealth = 200; // Stronger than blue enemies self.speed *= 2.0; // Much faster movement break; case 'rat': self.maxHealth = 2000; // High health rat enemy self.speed *= 1.0; // Normal speed movement break; case 'big': self.maxHealth = 300; // Much stronger than normal enemies self.speed *= 0.7; // Slower movement break; case 'kırık': self.maxHealth = 100; // Normal health like other enemies self.speed *= 1.5; // Faster movement (increased from 0.8 to 1.5) // Removed immortal property - now mortal 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 === 'vex') { assetId = 'enemy_vex'; } else if (self.type === 'rat') { assetId = 'enemy_rat'; } else if (self.type === 'kırık') { assetId = 'enemy_kirik'; } else if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: self.type === 'rat' ? 1.45 : 1.25, scaleY: self.type === 'rat' ? 1.45 : 1.25 }); // Add walking feet for normal, swarm, blue, and kırık enemies only (no feet for boss enemies) self.leftFoot = null; self.rightFoot = null; if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue' || self.type === 'vex' || self.type === 'rat' || self.type === 'kırık') && !self.isBoss) { // Adjust foot size and asset based on enemy type var footWidth, footHeight, footSpacing, footAsset; if (self.type === 'vex') { // Vex gets special feet - smaller size footWidth = 16; footHeight = 12; footSpacing = 18; footAsset = 'vexFeet'; } else if (self.type === 'rat') { // Rat enemy gets elliptical feet with wider spacing footWidth = 28; footHeight = 22; footSpacing = 25; footAsset = 'walkingFeet'; } else if (self.type === 'kırık') { // Kırık enemy gets bigger feet footWidth = 24; footHeight = 18; footSpacing = 20; footAsset = 'walkingFeet'; } else { // Regular feet for other enemies footWidth = 18; footHeight = 13; footSpacing = 15; footAsset = 'walkingFeet'; } var footYPosition = 35; // Create walking feet as regular Container objects (not particles) self.leftFoot = new Container(); var leftFootGraphics = self.leftFoot.attachAsset(footAsset, { 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(footAsset, { 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 = 1.8; enemyGraphics.scaleY = 1.8; } else { enemyGraphics.scaleX = 1.6; enemyGraphics.scaleY = 1.6; } } // 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 kırık enemy special properties if (self.type === 'kırık') { // Health bar will show normally when damaged like other enemies self.kırıkPhase = 'goingToTabela'; // 'goingToTabela' or 'returningToStart' // Set tabela position as target (one cell down and one cell left from previous position) self.tabelaX = (2048 - 360 - 76) / CELL_SIZE; // Convert pixel position to grid self.tabelaY = (2732 / 2 + 76 - (200 - CELL_SIZE * 4)) / CELL_SIZE; // Convert pixel position to grid self.startX = self.currentCellX; // Remember starting position self.startY = self.currentCellY; } // 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 () { // Clean up targeting arrays periodically if (LK.ticks % 300 === 0) { // Every 5 seconds if (self.bulletsTargetingThis) { self.bulletsTargetingThis = self.bulletsTargetingThis.filter(function (bullet) { return bullet && bullet.parent && bullet.targetEnemy === self; }); } } // 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, blue, vex, kırık and immune enemies only (no feet for boss enemies) if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue' || self.type === 'vex' || self.type === 'kırık' || self.type === 'immune') && !self.isBoss && self.leftFoot && self.rightFoot) { // Keep feet at fixed positions with type-specific spacing var footYPos = self.type === 'rat' ? 40 : 35; var footSpacing; if (self.type === 'vex') { footSpacing = 18; // Special spacing for vex } else if (self.type === 'rat') { footSpacing = 25; // Wider spacing for rat } else if (self.type === 'kırık') { footSpacing = 20; // Wider spacing for kırık } else { footSpacing = 15; // Default spacing } 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 - normal, vex, rat, kırık and immune enemies get foot animation, swarm enemies get no foot animation if (self.type === 'normal' || self.type === 'vex' || self.type === 'rat' || self.type === 'kırık' || self.type === 'immune') { // Update foot animation phases with type-specific multipliers var animationMultiplier; if (self.type === 'vex') { animationMultiplier = 2.5; // Special animation for vex } else if (self.type === 'rat') { animationMultiplier = 2.3; // Fast animation for rat, slightly less than vex } else if (self.type === 'kırık') { animationMultiplier = 3; // Faster animation for kırık } else if (self.type === 'immune') { animationMultiplier = 1.5; // Simplified animation for immune } else { animationMultiplier = 2; // Default animation } self.leftFootPhase += self.walkAnimationSpeed * speedMultiplier * animationMultiplier; self.rightFootPhase += self.walkAnimationSpeed * speedMultiplier * animationMultiplier; // Use simple scaling animation with type-specific scaling var scaleMultiplier; if (self.type === 'vex') { scaleMultiplier = 0.5; // Special scaling for vex } else if (self.type === 'rat') { scaleMultiplier = 1.45; // Scale to 1.45 for rat feet } else if (self.type === 'kırık') { scaleMultiplier = 0.6; // Bigger scaling for kırık } else if (self.type === 'immune') { scaleMultiplier = 0.3; // Simplified scaling for immune } else { scaleMultiplier = 0.4; // Default scaling } var leftFootScale = 1 + Math.abs(Math.sin(self.leftFootPhase)) * scaleMultiplier; var rightFootScale = 1 + Math.abs(Math.sin(self.rightFootPhase)) * scaleMultiplier; // Apply scaling animation using pooled animation objects for other enemy types if (self.type === 'immune') { // Direct scaling for immune enemies for simplicity self.leftFoot.scaleX = leftFootScale; self.leftFoot.scaleY = leftFootScale; self.rightFoot.scaleX = rightFootScale; self.rightFoot.scaleY = rightFootScale; } else if (self.type === 'rat') { // Stabilized foot animation for rat enemies using tween for smooth movement if (self.leftFoot && !self.leftFoot.isAnimating) { self.leftFoot.isAnimating = true; tween(self.leftFoot, { scaleX: leftFootScale, scaleY: leftFootScale }, { duration: 120, 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: 120, easing: tween.easeInOut, onFinish: function onFinish() { if (self.rightFoot) { self.rightFoot.isAnimating = false; } } }); } } else { // Apply scaling animation using pooled animation objects for other enemy types if (self.leftFoot && !self.leftFoot.isAnimating) { self.leftFoot.isAnimating = true; createPooledAnimation(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; createPooledAnimation(self.rightFoot, { scaleX: rightFootScale, scaleY: rightFootScale }, { duration: 100, easing: tween.easeInOut, onFinish: function onFinish() { if (self.rightFoot) { self.rightFoot.isAnimating = false; } } }); } } // Simplified rotation synchronization for immune enemies if (self.leftFoot && self.rightFoot && enemyGraphics.targetRotation !== undefined) { var targetFootRotation = enemyGraphics.targetRotation; if (self.type === 'immune') { // Direct rotation for immune enemies for simplicity self.leftFoot.rotation = targetFootRotation; self.rightFoot.rotation = targetFootRotation; } else { // Smoothly rotate feet to match enemy direction for other enemy types using pooled animations if (Math.abs(targetFootRotation - (self.leftFoot.rotation || 0)) > 0.05) { createPooledAnimation(self.leftFoot, { rotation: targetFootRotation }, { duration: 250, easing: tween.easeOut }); } if (Math.abs(targetFootRotation - (self.rightFoot.rotation || 0)) > 0.05) { createPooledAnimation(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.2; // Simplified energetic movement (reduced from 1.5) 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 and fast enemies if (self.type === 'swarm' || self.type === 'fast') { var targetY = combinedBobOffset * animationIntensity; var targetX = horizontalSway * animationIntensity * 0.3; // Much less horizontal movement // Simple direct assignment for swarm and fast 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 pooled animation for smoother animation instead of direct assignment if (!self.animatingMovement) { self.animatingMovement = true; createPooledAnimation(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 or fast) if (self.type !== 'swarm' && self.type !== 'fast') { var walkRotation = Math.sin(self.walkAnimationPhase * 1.5) * 0.05; // Very subtle rotation if (!self.animatingRotation) { self.animatingRotation = true; createPooledAnimation(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 } // Special handling for vex enemies - they should always play walking sound when moving if (self.type === 'vex' && isCurrentlyWalking) { shouldPlaySound = true; // Always play sound for vex enemies when they are walking } // Special handling for swarm enemies - they should also play walking sound when moving if (self.type === 'swarm' && isCurrentlyWalking) { shouldPlaySound = true; // Always play sound for swarm enemies when they are walking } 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, blue, and kırık enemies only when not walking if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue' || self.type === 'kırık') && !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 - adjust for kırık and rat enemies var restFootYPos = 35; var restFootSpacing = self.type === 'kırık' ? 20 : self.type === 'rat' ? 25 : 15; // Wider spacing for kırık and rat enemies 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; // Greatly increased cache timeout based on activity for better performance var activityLevel = enemies.length + towers.length; if (activityLevel < 5) { dynamicCacheTimeout = 900; // 15 seconds for very low activity } else if (activityLevel < 10) { dynamicCacheTimeout = 600; // 10 seconds for low activity } else if (activityLevel < 20) { dynamicCacheTimeout = 450; // 7.5 seconds for medium activity } else { dynamicCacheTimeout = 150; // 2.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 guys sound when vex enemies first enter the screen if (enemy.type === 'vex' && !window.guysSoundPlayed) { window.guysSoundPlayed = true; LK.getSound('guys').play(); } // Play wu sound when rat enemies first enter the screen if (enemy.type === 'rat' && !window.wuSoundPlayed) { window.wuSoundPlayed = true; LK.getSound('wu').play(); // Play bolt sound 3 seconds after wu sound tween({}, {}, { duration: 3000, onFinish: function onFinish() { LK.getSound('bolt').play(); // Play hamburger sound 10 seconds after bolt sound tween({}, {}, { duration: 10000, onFinish: function onFinish() { LK.getSound('hamburger').play(); // Play kola sound 10 seconds after hamburger sound (reduced from 3 to 10 seconds total delay) tween({}, {}, { duration: 10000, onFinish: function onFinish() { console.log("Playing kola sound"); LK.getSound('kola').play(); // Play hungry sound 10 seconds after kola sound tween({}, {}, { duration: 10000, onFinish: function onFinish() { console.log("Playing hungry sound"); LK.getSound('hungry').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(); // Spawn 1 rat enemy when for_the_fallen sound starts playing - ONLY in Wave 5 if (currentWave === 5) { var ratEnemy = new Enemy('rat'); // Add rat enemy to the appropriate layer if (ratEnemy.isFlying) { enemyLayerTop.addChild(ratEnemy); } else { enemyLayerBottom.addChild(ratEnemy); } // Set rat enemy health for Wave 5 ratEnemy.maxHealth = 1500; ratEnemy.health = ratEnemy.maxHealth; // Rat enemy spawns from a random column in the middle area var gridWidth = 24; var midPoint = Math.floor(gridWidth / 2); // 12 var spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 var spawnY = -1 - Math.random() * 5; // Random distance above the grid ratEnemy.cellX = spawnX; ratEnemy.cellY = 5; // Position after entry ratEnemy.currentCellX = spawnX; ratEnemy.currentCellY = spawnY; ratEnemy.waveNumber = currentWave; enemies.push(ratEnemy); } } }); } // Play xr sound when flying enemies from Wave 4 first enter the screen if (enemy.type === 'flying' && enemy.waveNumber === 4 && !window.xrSoundPlayed) { window.xrSoundPlayed = true; LK.getSound('xr').play(); // Play yarış sound after xr sound finishes - increased delay to ensure xr sound completes tween({}, {}, { duration: 4000, // Wait longer for xr sound to finish completely onFinish: function onFinish() { LK.getSound('yar').play(); // Play upit sound after yar sound finishes tween({}, {}, { duration: 5000, // Wait longer for yar sound to finish completely onFinish: function onFinish() { LK.getSound('upit').play(); // Play water sound 3 seconds after upit sound finishes tween({}, {}, { duration: 4000, // Wait for upit sound to finish + 3 second delay onFinish: function onFinish() { LK.getSound('water').play(); // Play funny sound 3 seconds after water sound finishes tween({}, {}, { duration: 5000, // Wait for water sound to finish + 3 second delay onFinish: function onFinish() { LK.getSound('funny').play(); // Play z sound 5 seconds after funny sound finishes tween({}, {}, { duration: 5000, // Wait 5 seconds after funny sound onFinish: function onFinish() { LK.getSound('z').play(); // Play sm sound 5 seconds after z sound finishes tween({}, {}, { duration: 5000, onFinish: function onFinish() { LK.getSound('sm').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(); // Play sh sound after tw sound finishes tween({}, {}, { duration: 15000, // Adjust timing based on 'tw' sound duration onFinish: function onFinish() { LK.getSound('sh').play(); } }); } }); } }); } // df sound is now played when Wave 8 starts, not when enemies enter screen } return false; } // Handle vex enemy - move to y13, then move right 3 cells, then stop if (enemy.type === 'vex') { // Initialize vex movement phase if not set if (!enemy.vexPhase) { enemy.vexPhase = 'movingDown'; enemy.targetX = enemy.currentCellX + 3; // Target position 3 cells to the right } if (enemy.vexPhase === 'movingDown') { // Check if vex enemy has reached y13 if (enemy.currentCellY >= 13) { // Switch to moving right phase enemy.vexPhase = 'movingRight'; enemy.currentCellY = 13; // Ensure we're exactly at y13 // Play sound 'n' when vex enemy turns right and track when it finishes LK.getSound('n').play(); // Use tween to track when 'n' sound finishes (assume it's about 2-3 seconds) tween({}, {}, { duration: 2500, // Estimated duration of 'n' sound in milliseconds onFinish: function onFinish() { nSoundFinished = true; // Mark that 'n' sound has finished } }); // Simplified asset switching - remove the problematic rotation logic if (enemy.children[0] && enemy.children[0].parent) { enemy.removeChild(enemy.children[0]); var vxGraphics = enemy.attachAsset('vx', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.25, scaleY: 1.25 }); vxGraphics.rotation = 0; // Face right immediately } } else { // Continue moving down to y13 enemy.currentCellY += enemy.speed; // Add walking animation for vex enemy during y13 progression if (!enemy.isFlying) { var speedMultiplier = enemy.speed * 100; enemy.walkAnimationPhase += enemy.walkAnimationSpeed * speedMultiplier; // Create realistic 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 walking var horizontalSway = Math.sin(enemy.walkAnimationPhase * 0.7) * (enemy.walkBobAmount * 0.15); if (enemy.children[0]) { var animationIntensity = 1; if (enemy.isBoss) { animationIntensity *= 0.8; bobOffset *= 1.1; } // Apply animation var targetY = bobOffset * animationIntensity; var targetX = horizontalSway * animationIntensity; if (!enemy.vexDownAnimating) { enemy.vexDownAnimating = true; tween(enemy.children[0], { y: targetY, x: targetX }, { duration: 60, easing: tween.easeOut, onFinish: function onFinish() { enemy.vexDownAnimating = false; } }); } } // Animate vex enemy feet during downward movement with special vex feet if (enemy.type === 'vex' && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) { // Ensure feet remain visible during downward movement - force visibility every frame enemy.leftFoot.visible = true; enemy.rightFoot.visible = true; // Keep feet at proper positions during downward movement var footSpacing = 18; // Special spacing for vex var footYPos = 35; enemy.leftFoot.x = -footSpacing; enemy.leftFoot.y = footYPos; enemy.rightFoot.x = footSpacing; enemy.rightFoot.y = footYPos; // Update foot animation phases for vex with enhanced animation var animationMultiplier = 3.0; // Increased animation speed for vex enemy.leftFootPhase += enemy.walkAnimationSpeed * speedMultiplier * animationMultiplier; enemy.rightFootPhase += enemy.walkAnimationSpeed * speedMultiplier * animationMultiplier; // Enhanced scaling for vex special feet with alternating pattern var scaleMultiplier = 0.7; // Increased scaling for more visible animation var leftFootScale = 1 + Math.abs(Math.sin(enemy.leftFootPhase)) * scaleMultiplier; var rightFootScale = 1 + Math.abs(Math.sin(enemy.rightFootPhase)) * scaleMultiplier; // Add vertical bobbing motion to feet for more realistic walking var leftFootBob = Math.sin(enemy.leftFootPhase) * 2; var rightFootBob = Math.sin(enemy.rightFootPhase) * 2; // Apply scaling and position animation for vex feet with shorter duration for snappier movement if (enemy.leftFoot && !enemy.leftFoot.isAnimating) { enemy.leftFoot.isAnimating = true; tween(enemy.leftFoot, { scaleX: leftFootScale, scaleY: leftFootScale, y: footYPos + leftFootBob }, { duration: 60, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.leftFoot) { enemy.leftFoot.isAnimating = false; } } }); } if (enemy.rightFoot && !enemy.rightFoot.isAnimating) { enemy.rightFoot.isAnimating = true; tween(enemy.rightFoot, { scaleX: rightFootScale, scaleY: rightFootScale, y: footYPos + rightFootBob }, { duration: 60, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.rightFoot) { enemy.rightFoot.isAnimating = false; } } }); } } } } // Rotate enemy graphic to face downward while moving 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 }); var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } else if (enemy.vexPhase === 'movingRight') { // Check if vex enemy has moved 3 cells to the right if (enemy.currentCellX >= enemy.targetX) { // Stop moving - we've reached the target position enemy.vexPhase = 'waiting'; enemy.currentCellX = enemy.targetX; // Ensure exact position enemy.waitTimer = 0; // Initialize wait timer // Play 'wc' sound when starting to wait, but only if 'n' sound has finished if (nSoundFinished) { // Add additional delay even when 'n' sound is marked as finished to ensure proper timing tween({}, {}, { duration: 2000, // 2 second additional delay (1 second more than before) onFinish: function onFinish() { LK.getSound('wc').play(); } }); } else { // Wait for 'n' sound to finish before playing 'wc' var _checkNSoundFinished = function checkNSoundFinished() { if (nSoundFinished) { // Add delay to ensure 'n' sound has completely finished tween({}, {}, { duration: 2500, // 2.5 second delay after 'n' finishes (1 second more than before) onFinish: function onFinish() { LK.getSound('wc').play(); } }); } else { // Check again in 100ms tween({}, {}, { duration: 100, onFinish: _checkNSoundFinished }); } }; _checkNSoundFinished(); } } else { // Continue moving right enemy.currentCellX += enemy.speed; // DO NOT change foot positions during turning movement - keep feet stable } } else if (enemy.vexPhase === 'waiting') { // Wait for 8 seconds (480 frames at 60 FPS) if (!enemy.waitTimer) enemy.waitTimer = 0; enemy.waitTimer++; if (enemy.waitTimer >= 480) { // Activate vx asset after 5 second wait if (enemy.children[0] && enemy.children[0].parent) { enemy.removeChild(enemy.children[0]); } var vxGraphics = enemy.attachAsset('vx', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.25, scaleY: 1.25 }); // No rotation animation - vx asset appears without rotation vxGraphics.rotation = 0; // Face right immediately without animation // Start returning to spawn area enemy.vexPhase = 'returningToSpawn'; enemy.returnTargetX = 14; // Return to original spawn column enemy.returnTargetY = -3; // Return above the spawn area } } else if (enemy.vexPhase === 'returningToSpawn') { // Initialize safety timer if not set if (!enemy.returningSafetyTimer) { enemy.returningSafetyTimer = 0; } enemy.returningSafetyTimer++; // Safety timeout: force delete after 4 seconds (400 frames at 60fps) if (enemy.returningSafetyTimer > 400) { // Force delete vex enemy after timeout if (enemy.parent) { if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } } var enemyIndex = enemies.indexOf(enemy); if (enemyIndex !== -1) { enemies.splice(enemyIndex, 1); } // Once vex enemy exits, allow wave 2 to start if (currentWave === 1) { // Automatically start wave 2 after vex enemy exits currentWave = 2; waveTimer = 0; waveInProgress = true; waveSpawned = false; } return true; // Return true to indicate enemy reached goal (exit condition) } // Move back to spawn area first var dx = enemy.returnTargetX - enemy.currentCellX; var dy = enemy.returnTargetY - enemy.currentCellY; var distance = Math.sqrt(dx * dx + dy * dy); // Improved deletion conditions - delete vex enemy when it's completely off-screen if (enemy.currentCellY <= -3 || enemy.y <= -100) { // Delete vex enemy immediately when it's completely off-screen if (enemy.parent) { if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } } var enemyIndex = enemies.indexOf(enemy); if (enemyIndex !== -1) { enemies.splice(enemyIndex, 1); } // Once vex enemy exits, allow wave 2 to start if (currentWave === 1) { // Automatically start wave 2 after vex enemy exits currentWave = 2; waveTimer = 0; waveInProgress = true; waveSpawned = false; } return true; // Return true to indicate enemy reached goal (exit condition) } else { // Move toward spawn area var angle = Math.atan2(dy, dx); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; // Rotate enemy to face 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 }); var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } else if (enemy.vexPhase === 'exitingFromSpawn') { // Move upward to exit off the top of the screen enemy.currentCellY -= enemy.speed; // Rotate enemy to face upward var angle = -Math.PI / 2; // Face upward 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 }); var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Check if enemy has moved far enough off screen to be removed if (enemy.currentCellY < -5) { // Remove vex enemy completely off screen if (enemy.parent) { if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } } var enemyIndex = enemies.indexOf(enemy); if (enemyIndex !== -1) { enemies.splice(enemyIndex, 1); } // Once vex enemy exits, allow wave 2 to start if (currentWave === 1) { // Automatically start wave 2 after vex enemy exits currentWave = 2; waveTimer = 0; waveInProgress = true; waveSpawned = false; } return true; // Return true to indicate enemy reached goal (exit condition) } // Keep enemy image straight during rightward movement (like a crab) - no rotation // Vex enemy moves sideways without rotating the image asset // Add walking animation for vex enemy during rightward movement if (!enemy.isFlying) { var speedMultiplier = enemy.speed * 100; enemy.walkAnimationPhase += enemy.walkAnimationSpeed * speedMultiplier; // Create realistic 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 walking var horizontalSway = Math.sin(enemy.walkAnimationPhase * 0.7) * (enemy.walkBobAmount * 0.15); if (enemy.children[0]) { var animationIntensity = 1; if (enemy.isBoss) { animationIntensity *= 0.8; bobOffset *= 1.1; } // Apply animation var targetY = bobOffset * animationIntensity; var targetX = horizontalSway * animationIntensity; if (!enemy.vexRightAnimating) { enemy.vexRightAnimating = true; tween(enemy.children[0], { y: targetY, x: targetX }, { duration: 60, easing: tween.easeOut, onFinish: function onFinish() { enemy.vexRightAnimating = false; } }); } } // Animate vex enemy feet during rightward movement with special vex feet if (enemy.type === 'vex' && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) { // Ensure feet remain visible during rightward movement enemy.leftFoot.visible = true; enemy.rightFoot.visible = true; // Keep feet at proper positions during sideways movement var footSpacing = 18; // Special spacing for vex var footYPos = 35; enemy.leftFoot.x = -footSpacing; enemy.leftFoot.y = footYPos; enemy.rightFoot.x = footSpacing; enemy.rightFoot.y = footYPos; // Update foot animation phases for vex with enhanced animation var animationMultiplier = 3.5; // Increased for more active sideways movement enemy.leftFootPhase += enemy.walkAnimationSpeed * speedMultiplier * animationMultiplier; enemy.rightFootPhase += enemy.walkAnimationSpeed * speedMultiplier * animationMultiplier; // Enhanced scaling for vex special feet with more pronounced movement var scaleMultiplier = 0.8; // Increased scaling for more visible animation var leftFootScale = 1 + Math.abs(Math.sin(enemy.leftFootPhase)) * scaleMultiplier; var rightFootScale = 1 + Math.abs(Math.sin(enemy.rightFootPhase)) * scaleMultiplier; // Add horizontal stepping motion for sideways movement var leftFootStep = Math.cos(enemy.leftFootPhase) * 3; var rightFootStep = Math.cos(enemy.rightFootPhase) * 3; // Add vertical lifting motion for more realistic stepping var leftFootLift = Math.abs(Math.sin(enemy.leftFootPhase)) * 4; var rightFootLift = Math.abs(Math.sin(enemy.rightFootPhase)) * 4; // Apply scaling and position animation for vex feet with shorter duration for snappier movement if (enemy.leftFoot && !enemy.leftFoot.isAnimating) { enemy.leftFoot.isAnimating = true; tween(enemy.leftFoot, { scaleX: leftFootScale, scaleY: leftFootScale, x: -footSpacing + leftFootStep, y: footYPos - leftFootLift }, { duration: 50, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.leftFoot) { enemy.leftFoot.isAnimating = false; } } }); } if (enemy.rightFoot && !enemy.rightFoot.isAnimating) { enemy.rightFoot.isAnimating = true; tween(enemy.rightFoot, { scaleX: rightFootScale, scaleY: rightFootScale, x: footSpacing + rightFootStep, y: footYPos - rightFootLift }, { duration: 50, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.rightFoot) { enemy.rightFoot.isAnimating = false; } } }); } } } } else if (enemy.vexPhase === 'waiting' || enemy.vexPhase === 'returningToSpawn') { // Preserve vex enemy waiting foot positions during turning movement phases if (enemy.type === 'vex' && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) { // Force both feet to be visible during all turning phases with maximum visibility enemy.leftFoot.visible = true; enemy.rightFoot.visible = true; enemy.leftFoot.alpha = 1; // Ensure full opacity enemy.rightFoot.alpha = 1; // Ensure full opacity // Ensure feet are properly attached to parent if (!enemy.leftFoot.parent) { enemy.addChild(enemy.leftFoot); } if (!enemy.rightFoot.parent) { enemy.addChild(enemy.rightFoot); } var footSpacing = 18; // Special spacing for vex var footYPos = 35; // Always use standard positioning during turning phases to prevent feet from disappearing enemy.leftFoot.x = -footSpacing; enemy.leftFoot.y = footYPos; enemy.rightFoot.x = footSpacing; enemy.rightFoot.y = footYPos; // Force both feet to front of display list to ensure visibility enemy.setChildIndex(enemy.leftFoot, enemy.children.length - 1); enemy.setChildIndex(enemy.rightFoot, enemy.children.length - 1); // Stop any ongoing foot animations to prevent position changes tween.stop(enemy.leftFoot, { scaleX: true, scaleY: true, x: true, y: true }); tween.stop(enemy.rightFoot, { scaleX: true, scaleY: true, x: true, y: true }); // Reset foot animation flags and ensure feet are properly scaled if (enemy.leftFoot) { enemy.leftFoot.isAnimating = false; enemy.leftFoot.scaleX = 1; enemy.leftFoot.scaleY = 1; } if (enemy.rightFoot) { enemy.rightFoot.isAnimating = false; enemy.rightFoot.scaleX = 1; enemy.rightFoot.scaleY = 1; } } } // If stopped, just maintain position (no movement) // Update enemy's position enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; return false; // Don't use normal pathfinding } // 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 40 frames for flying enemies (slightly increased frequency) // Reduce frequency for waves 8+ to minimize animation load var exhaustFrequency = currentWave >= 8 ? 80 : 40; // Slightly decreased interval for better visual effect if (enemy.exhaustTimer % exhaustFrequency === 0) { // Create exhaust particles behind flying enemy using particle pool // Slightly increased exhaust particle count for better visual effect var exhaustCount = enemies.length > 10 ? 2 : 3; // Increased to max 3 particles when few enemies, 2 when many if (currentWave === 7 || currentWave >= 8) { exhaustCount = Math.floor(exhaustCount * 0.15); // Slightly larger particles for boss wave and waves 8+ } for (var exhaustIdx = 0; exhaustIdx < exhaustCount; exhaustIdx++) { var exhaustParticle = getSmokeParticle(); var exhaustGraphics = exhaustParticle.smokeGraphics; exhaustGraphics.width = 20 + Math.random() * 28; 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.2; // Narrower spread var exhaustDistance = 30 + Math.random() * 15; // Smaller distance exhaustParticle.x = enemy.x + Math.cos(exhaustAngle) * exhaustDistance; exhaustParticle.y = enemy.y + Math.sin(exhaustAngle) * exhaustDistance; exhaustParticle.alpha = 0.5 + Math.random() * 0.2; exhaustParticle.scaleX = 0.2 + Math.random() * 0.2; exhaustParticle.scaleY = 0.2 + Math.random() * 0.2; game.addChild(exhaustParticle); // Animate exhaust particles moving away from enemy and fading in synchronized direction var targetDistance = exhaustDistance + 20 + Math.random() * 15; // Smaller movement range 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 * 1.5, scaleY: exhaustParticle.scaleY * 1.5 }, { duration: 500 + Math.random() * 200, 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 kırık enemy - move in L-shape: straight down, then straight right to tabela, then return if (enemy.type === 'kırık' && enemy.isImmortal) { // Initialize kırık phase if not set if (!enemy.kırıkPhase) { enemy.kırıkPhase = 'movingDown'; enemy.startX = enemy.currentCellX; // Remember starting position enemy.startY = enemy.currentCellY; } // Calculate tabela position in grid coordinates (where tabela display is positioned) var tabelaGridX = (2048 - 360 - 76 - grid.x) / CELL_SIZE; var tabelaGridY = (2732 / 2 + 76 - grid.y) / CELL_SIZE; if (enemy.kırıkPhase === 'movingDown') { // Move straight down like blue enemy until reaching bottom of screen 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 }); var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Check if reached bottom of screen (around where goals are) if (enemy.currentCellY >= 34) { enemy.kırıkPhase = 'goingToTabela'; } } else if (enemy.kırıkPhase === 'goingToTabela') { // Move straight horizontally toward tabela (L-shape movement) enemy.currentCellX += enemy.speed; // Move straight right // Rotate enemy graphic to face right (0 radians) var angle = 0; // Face right if (enemy.children[0]) { if (enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Check if reached tabela X position if (enemy.currentCellX >= tabelaGridX) { enemy.kırıkPhase = 'returningToStart'; } } else if (enemy.kırıkPhase === 'returningToStart') { // Move straight left back to starting position enemy.currentCellX -= enemy.speed; // Move straight left // Rotate enemy graphic to face left (PI radians) var angle = Math.PI; // Face left if (enemy.children[0]) { if (enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Check if reached start position if (enemy.currentCellX <= enemy.startX) { enemy.kırıkPhase = 'movingDown'; // Restart the cycle } } // Update visual position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; return false; // Don't use normal pathfinding } // 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 () { // Next wave button is removed - always keep it hidden and disabled self.enabled = false; self.visible = false; self.alpha = 0; }; self.down = function () { if (!self.enabled) { return; } // Don't allow manual wave progression during black screen (wave 7 completion) var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7; if (isBlackScreenActive) { return; // Block manual wave progression during black screen } // Check if any vex enemies are still alive to block manual wave progression var vexEnemiesAlive = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].type === 'vex') { vexEnemiesAlive = true; break; } } // Block manual progression to wave 2 if vex enemies are still alive if (vexEnemiesAlive && currentWave === 1) { var notification = game.addChild(new Notification("Defeat all vex enemies before starting next wave!")); notification.x = 2048 / 2; notification.y = grid.height - 50; 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: 80, fill: 0xFFFFFF, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 50; notificationGraphics.alpha = 0; // Make background transparent 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; // Add pulsing animation for tower icons before game starts if (!waveIndicator.gameStarted && self.visible && !self.isPulsing) { var _pulseIcon = function pulseIcon() { if (!waveIndicator.gameStarted && self.visible) { // First pulse - grow tween(self, { scaleX: self.scaleX * 1.15, scaleY: self.scaleY * 1.15 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { if (!waveIndicator.gameStarted && self.visible) { // Second pulse - shrink back tween(self, { scaleX: self.scaleX / 1.15, scaleY: self.scaleY / 1.15 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { // Wait before next pulse cycle tween({}, {}, { duration: 600, onFinish: function onFinish() { if (!waveIndicator.gameStarted && self.visible) { _pulseIcon(); // Continue pulsing } else { self.isPulsing = false; } } }); } }); } else { self.isPulsing = false; } } }); } else { self.isPulsing = false; } }; self.isPulsing = true; _pulseIcon(); } else if (waveIndicator.gameStarted && self.isPulsing) { // Stop pulsing when game starts tween.stop(self, { scaleX: true, scaleY: true }); self.isPulsing = false; } }; 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 3.0, +0.3 per level (increased effect area) return (3.0 + (self.level - 1) * 0.3) * 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, width: 40, height: 40 }); 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 () { // Prevent upgrading before game starts if (!waveIndicator || !waveIndicator.gameStarted) { var notification = game.addChild(new Notification("Cannot upgrade towers before game starts!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } 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, but exclude vex enemies if (self.targetEnemy && self.targetEnemy.parent && self.targetEnemy.health > 0 && self.targetEnemy.type !== 'vex') { 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; // Skip vex enemies - towers ignore them completely if (enemy.type === 'vex') continue; // Kırık enemies can now be targeted like other enemies 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 () { // Clean up enemy targeting arrays if (LK.ticks % 180 === 0) { // Every 3 seconds if (self.targetEnemy && (!self.targetEnemy.parent || self.targetEnemy.health <= 0)) { self.targetEnemy = null; } } 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 - only for wave 7 var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7; 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 - only for wave 7 var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7; 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 // Reduce frequency significantly for waves 8+ to minimize animation load if (currentWave >= 8) { exhaustFrequency = 120; // Significantly increased frequency for waves 8+ } if (splashTowerCount > 2) { // Increase interval (reduce frequency) when more than 2 splash towers exhaustFrequency = exhaustFrequency + (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 - only for wave 7 var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7; 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 (excluding blue enemies, Wave 5, and Wave 7 big enemy) if (affectedEnemies.length > 0 && !welcomeToHellSoundPlayed) { // Filter out blue enemies, Wave 5 enemies, and Wave 7 big enemy from welcometohell sound triggering var validEnemiesForWelcome = affectedEnemies.filter(function (enemy) { // Exclude big enemy in wave 7 if (enemy.waveNumber === 7 && enemy.type === 'big') return false; return enemy.type !== 'blue' && enemy.waveNumber !== 5 && enemy.waveNumber !== 7; }); if (validEnemiesForWelcome.length > 0) { welcomeToHellSoundPlayed = true; LK.getSound('welcometohell').play(); } } // Count enemies entering slow tower area damage (bronz sound removed for blue enemies and all Wave 5 enemies) if (affectedEnemies.length > 0) { // Filter out blue enemies and all Wave 5 enemies from bronz sound triggering AND counting var validEnemiesForBronz = affectedEnemies.filter(function (enemy) { return enemy.type !== 'blue' && enemy.waveNumber !== 5; }); if (validEnemiesForBronz.length > 0) { slowAreaEnemyCount += validEnemiesForBronz.length; } } // 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 for (var i = 0; i < affectedEnemies.length; i++) { var affectedEnemy = affectedEnemies[i]; // Create flame animation for each affected enemy using particle pool var flameCount = 4; // Reduced from 8 to 4 flame particles per enemy for (var flameIdx = 0; flameIdx < flameCount; flameIdx++) { var flameParticle = getFireParticle(); var flameGraphics = flameParticle.fireGraphics; flameGraphics.width = 12 + Math.random() * 16; // Reduced size from 15+20 to 12+16 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() * 30; // Reduced distance from 40 to 30 flameParticle.x = affectedEnemy.x + Math.cos(angle) * distance; flameParticle.y = affectedEnemy.y + Math.sin(angle) * distance; flameParticle.alpha = 0.8; // Reduced from 0.9 to 0.8 flameParticle.scaleX = 0.4 + Math.random() * 0.4; // Reduced from 0.5+0.5 to 0.4+0.4 flameParticle.scaleY = 0.4 + Math.random() * 0.4; game.addChild(flameParticle); // Animate flame flickering and burning out tween(flameParticle, { alpha: 0, scaleX: flameParticle.scaleX * 1.6, // Reduced from 1.8 to 1.6 scaleY: flameParticle.scaleY * 1.6, y: flameParticle.y - 15 - Math.random() * 20 // Reduced from 20+30 to 15+20 }, { duration: 500 + Math.random() * 300, //{tm} // Reduced from 600+400 to 500+300 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 { // Krk sound moved to splash tower bullet hit logic - not played here anymore } } 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 }); // Reduce splash bullet animation complexity to minimize load // Apply much smaller bullet animation with greatly reduced movement for splash bullets bullet.animationReductionFactor = 0.1; // Reduce movement animation to 10% for splash bullets (reduced from 30%) 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('gozluk').play(); } }); } // Play shy sound for sniper tower bullets targeting bagışıkdüşman on 3rd hit - only once per wave if (typeof window.sniperImmuneHitCounter === 'undefined') { 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() { try { LK.getSound('shy').play(); } catch (e) { console.log("Error playing shy sound:", e); } } }); } LK.getSound('tower_shoot').play(); // Play krk sound for splash tower bullets targeting bagışıkdüşman on 2nd hit - only once per wave } else if (self.id === 'splash' && self.targetEnemy && self.targetEnemy.isImmune) { if (typeof window.splashImmuneHitCounter === 'undefined') { window.splashImmuneHitCounter = 0; } window.splashImmuneHitCounter++; if (window.splashImmuneHitCounter === 2 && !window.krkSoundPlayedThisWave) { window.krkSoundPlayedThisWave = true; try { LK.getSound('krk').play(); } catch (e) { console.log("Error playing krk sound:", e); } } 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(); } } 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; // Hide the placement square for this position if (waveIndicator && waveIndicator.placementFrame) { var placementSquares = waveIndicator.placementFrame.children; // Define the placement positions to match with squares var placementPositions = [{ x: 11, y: 6 }, // First square { x: 11, y: 11 }, // Second square { x: 11, y: 16 }, // Third square { x: 11, y: 21 }, // Fourth square { x: 11, y: 26 }, // Fifth square { x: 11, y: 31 } // Sixth square ]; // Find which placement square corresponds to this position for (var i = 0; i < placementPositions.length; i++) { if (placementPositions[i].x === gridX && placementPositions[i].y === gridY) { // Hide the corresponding placement square if (placementSquares[i]) { placementSquares[i].visible = false; } break; } } } // 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(); // Only invalidate pathfinding cache for non-poison towers (since poison towers don't block paths) if (self.id !== 'poison') { pathfindingCache = null; } // Initialize continuous poison cloud animation for poison towers if (self.id === 'poison') { self.poisonCloudTimer = 0; self.createContinuousPoisonClouds = function () { // Check if black screen is active - only for wave 7 var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7; if (isBlackScreenActive) { return; // Hide poison animation during black screen } // 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 () { // Check if black screen is active - only for wave 7 var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7; if (isBlackScreenActive) { return; // Hide splash animation during black screen } // 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 ? 4 : 6; // Further reduced count when many enemies // Reduce exhaust particle count for boss waves and waves 8+ to minimize animation load if (currentWave === 7 || currentWave >= 8) { exhaustCount = Math.floor(exhaustCount * 0.2); // Further reduce particle count for boss wave and waves 8+ } 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 () { // Check if black screen is active - only for wave 7 var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7; if (isBlackScreenActive) { return; // Hide slow tower animation during black screen } // 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); } }); } // Create black smoke particles along with flames for enhanced visual effect var smokeCount = enemies.length > 15 ? 4 : 6; // Black smoke particles for (var smokeIdx = 0; smokeIdx < smokeCount; smokeIdx++) { var smokeParticle = getSmokeParticle(); var smokeGraphics = smokeParticle.smokeGraphics; smokeGraphics.width = 20 + Math.random() * 30; smokeGraphics.height = smokeGraphics.width; // Dark smoke colors - various shades of black and dark gray var smokeColors = [0x000000, 0x1a1a1a, 0x2a2a2a, 0x404040, 0x333333]; smokeGraphics.tint = smokeColors[Math.floor(Math.random() * smokeColors.length)]; // Position smoke particles in outer rings around the tower var smokeRingNumber = Math.floor(smokeIdx / 3); // 2 rings of 3 particles each var smokeAngleInRing = smokeIdx % 3 * (Math.PI * 2 / 3) + Math.random() * 0.8; var smokeBaseDistance = 50 + smokeRingNumber * 25; // Outer rings for smoke var smokeDistance = smokeBaseDistance + Math.random() * 30; smokeParticle.x = self.x + Math.cos(smokeAngleInRing) * smokeDistance; smokeParticle.y = self.y + Math.sin(smokeAngleInRing) * smokeDistance; smokeParticle.alpha = 0.6 + Math.random() * 0.3; smokeParticle.scaleX = 0.4 + Math.random() * 0.4; smokeParticle.scaleY = 0.4 + Math.random() * 0.4; game.addChild(smokeParticle); // Animate black smoke rising and dispersing var smokeTargetScale = smokeParticle.scaleX * (2.0 + Math.random() * 1.0); var smokeDrift = Math.random() > 0.5 ? 1 : -1; var smokeDriftAngle = smokeAngleInRing + smokeDrift * (Math.PI * 0.4 + Math.random() * Math.PI * 0.3); var smokeDriftRadius = smokeDistance * (0.9 + Math.random() * 0.2); var smokeTargetX = self.x + Math.cos(smokeDriftAngle) * smokeDriftRadius + (Math.random() - 0.5) * 35; var smokeTargetY = smokeParticle.y - (30 + Math.random() * 40); // Rise higher than flames var smokeRotationSpeed = smokeDrift * (Math.PI * 1.5 + Math.random() * Math.PI * 1.0); tween(smokeParticle, { x: smokeTargetX, y: smokeTargetY, alpha: 0, scaleX: smokeTargetScale, scaleY: smokeTargetScale, rotation: smokeRotationSpeed }, { duration: 1200 + Math.random() * 600, // Smoke lasts slightly longer than flames easing: tween.easeOut, onFinish: function onFinish() { returnParticle(smokeParticle); } }); } }; } }; 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 AND game has started var canAffordUpgrade = gold >= upgradeCost && waveIndicator && waveIndicator.gameStarted; 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 = false; // Define the exact placement squares where towers can be placed var allowedPlacements = [{ x: 11, y: 6 }, // First placement square { x: 11, y: 11 }, // Second placement square { x: 11, y: 16 }, // Third placement square { x: 11, y: 21 }, // Fourth placement square { x: 11, y: 26 }, // Fifth placement square { x: 11, y: 31 } // Sixth placement square ]; // Check if current grid position matches any allowed placement for (var p = 0; p < allowedPlacements.length; p++) { var placement = allowedPlacements[p]; if (self.gridX === placement.x && self.gridY === placement.y) { validGridPlacement = true; break; } } // If placement is valid, check if cells are actually available if (validGridPlacement) { 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) { // Prevent tower selling until wave 8 if (currentWave < 8) { var notification = game.addChild(new Notification("Tower selling is prohibited until wave 8!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); // Decrement tower placement counter towersPlacedCount--; //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; // Show the placement square again for this position if (waveIndicator && waveIndicator.placementFrame) { var placementSquares = waveIndicator.placementFrame.children; // Define the placement positions to match with squares var placementPositions = [{ x: 11, y: 6 }, // First square { x: 11, y: 11 }, // Second square { x: 11, y: 16 }, // Third square { x: 11, y: 21 }, // Fourth square { x: 11, y: 26 }, // Fifth square { x: 11, y: 31 } // Sixth square ]; // Find which placement square corresponds to this tower's position for (var i = 0; i < placementPositions.length; i++) { if (placementPositions[i].x === gridX && placementPositions[i].y === gridY) { // Show the corresponding placement square again if (placementSquares[i]) { placementSquares[i].visible = true; } break; } } } // 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); // Only invalidate pathfinding cache and recalculate for non-poison towers if (self.tower.id !== 'poison') { 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("Place 6 Towers", { 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("Place 6 Towers", { 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) { // Check if minimum towers have been placed if (towersPlacedCount < minTowersRequired) { var notification = game.addChild(new Notification("Place " + minTowersRequired + " towers before starting! (" + towersPlacedCount + "/" + minTowersRequired + ")")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } //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 }); // Hide tower placement frame when game starts if (self.placementFrame) { tween(self.placementFrame, { alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { if (self.placementFrame && self.placementFrame.parent) { self.placementFrame.parent.removeChild(self.placementFrame); } } }); } // 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 = 4; } else if (waveNum === 3) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 5; } else if (waveNum === 4) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 3; } else if (waveNum === 5) { block.tint = 0x00AAFF; // Blue color for fast enemies waveType = "Fast"; enemyType = "fast"; enemyCount = 3; // 3 fast enemies, rat enemy spawned when for_the_fallen sound plays } else if (waveNum === 6) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 3; } 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 = 3; // Changed from 4 to 3 for waves 8+ break; case 1: // Fast waves block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 3; // Changed from 4 to 3 for waves 8+ break; case 2: // Flying waves block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 3; // Changed from 4 to 3 for waves 8+ break; case 3: // Swarm waves block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 3; // Changed from 4 to 3 for waves 8+ break; case 4: // Normal waves (blue enemies only allowed in wave 1) block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 3; // Changed from 4 to 3 for waves 8+ break; case 5: // Normal waves block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 3; // Changed from 4 to 3 for waves 8+ break; case 6: // Big boss waves (every 7th wave) block.tint = 0x8B4513; waveType = "Big Boss"; enemyType = "big"; enemyCount = 1; // Boss waves always have 1 enemy break; } // Special boss waves every 10 waves if (waveNum % 10 === 0) { block.tint = 0x8B4513; waveType = "Mega Boss"; enemyType = "big"; enemyCount = 1; // Boss waves always have 1 enemy } } else { // Fallback for any additional waves beyond 50 block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 4; } // --- 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; } var baseCount = self.enemyCounts[waveNumber - 1]; // Apply limits for waves 8 and above if (waveNumber === 8) { baseCount = 3; // Ensure exactly 3 enemies for wave 8 to match waves 9+ } else if (waveNumber > 8) { var waveType = self.waveTypes[waveNumber - 1]; if (waveType === 'big') { // Boss waves limited to 1 baseCount = 1; } else { // All enemy types limited to 3 for waves 9+ baseCount = Math.min(3, baseCount); } } return baseCount; }; // 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; // Create tower placement area frame (only visible before game starts) var placementFrame = new Container(); placementFrame.x = grid.x; placementFrame.y = grid.y; // Create 6 placement squares in gridx11 area (column 11) var placementSquares = []; var _loop = function _loop() { square = placementFrame.attachAsset('placement_dot', { anchorX: 0.5, anchorY: 0.5 }); square.width = CELL_SIZE * 0.8; square.height = CELL_SIZE * 0.8; square.tint = 0x00FF00; // Green color square.alpha = 0.7; // Semi-transparent with better visibility square.x = 11 * CELL_SIZE + CELL_SIZE / 2; // Column 11, centered square.y = (6 + squareIndex * 5) * CELL_SIZE + CELL_SIZE / 2; // Spaced 5 cells apart vertically starting from row 6, centered // Add pulsating heart-like animation to make placement dots eye-catching function pulsatePlacementDot(dot, delay) { // Start with a small delay to stagger the animations tween({}, {}, { duration: delay, onFinish: function onFinish() { function heartBeat() { // First pulse - bigger scale tween(dot, { scaleX: 1.4, scaleY: 1.4, alpha: 0.9 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { // Return to normal size tween(dot, { scaleX: 1.0, scaleY: 1.0, alpha: 0.7 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { // Second pulse - smaller scale (like heartbeat) tween(dot, { scaleX: 1.2, scaleY: 1.2, alpha: 0.8 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { // Return to normal again tween(dot, { scaleX: 1.0, scaleY: 1.0, alpha: 0.7 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { // Wait before next heartbeat cycle tween({}, {}, { duration: 1200, onFinish: function onFinish() { if (dot.parent && dot.visible) { heartBeat(); // Continue pulsating if still visible } } }); } }); } }); } }); } }); } heartBeat(); // Start the pulsating animation } }); } // Start pulsating animation for each placement square with staggered timing pulsatePlacementDot(square, squareIndex * 200); // 200ms delay between each dot placementSquares.push(square); }, square; for (var squareIndex = 0; squareIndex < 6; squareIndex++) { _loop(); } // Add placement frame to game game.addChild(placementFrame); // Store reference to hide it later self.placementFrame = placementFrame; self.addChild(self.positionIndicator); self.update = function () { // Update start button text based on tower count if (!self.gameStarted) { // Show different text based on tower count if (towersPlacedCount >= minTowersRequired) { startText.setText("start"); startTextShadow.setText("start"); startBlock.tint = 0x00AA00; // Green when ready } else { startText.setText("place tower"); startTextShadow.setText("place tower"); startBlock.tint = 0x888888; // Gray when not ready } startTextShadow.x = 4; startTextShadow.y = 4; } 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; } // Check if black screen is active to prevent wave progression var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7; if (currentWave < totalWaves && !isBlackScreenActive) { 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 ****/ // Initialize activeParticles array to prevent undefined error // Add tween tracking and cleanup functions var activeTweens = []; var maxActiveTweens = 200; function trackTween(target, properties, config) { // Clean up completed tweens activeTweens = activeTweens.filter(function (tweenData) { return tweenData.isActive; }); // Enforce limit on active tweens if (activeTweens.length >= maxActiveTweens) { // Stop oldest tweens var oldestTweens = activeTweens.splice(0, 10); oldestTweens.forEach(function (tweenData) { tween.stop(tweenData.target, tweenData.properties); }); } // Create wrapped config with cleanup var wrappedConfig = Object.assign({}, config); var originalOnFinish = config.onFinish; var tweenData = { target: target, properties: properties, isActive: true, startTime: LK.ticks }; wrappedConfig.onFinish = function () { tweenData.isActive = false; if (originalOnFinish) originalOnFinish(); }; activeTweens.push(tweenData); return tween(target, properties, wrappedConfig); } function cleanupInactiveTweens() { // Remove completed tweens from tracking activeTweens = activeTweens.filter(function (tweenData) { return tweenData.isActive; }); // Force cleanup very old tweens (over 30 seconds) var currentTime = LK.ticks; activeTweens = activeTweens.filter(function (tweenData) { if (currentTime - tweenData.startTime > 1800) { // 30 seconds at 60fps tween.stop(tweenData.target, tweenData.properties); return false; } return true; }); } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } var activeParticles = activeParticles || []; // Ensure activeParticles is defined activeParticles = activeParticles.filter(function (particle) { return particle && particle.parent; }); var activeSounds = activeSounds || []; // Ensure activeSounds is defined activeSounds = activeSounds.filter(function (sound) { if (sound && sound.isPlaying) { return true; } else { // Force cleanup of stopped sounds forceCleanupSound(sound); return false; } }); // Particle lifetime management - check and cleanup expired particles 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 lastArrayCleanupTime = 0; // Track when we last cleaned up arrays var arrayCleanupInterval = 300; // Clean up arrays every 5 seconds (300 frames at 60fps) var defenses = []; var selectedTower = null; var gold = 183; var lives = 30; var score = 0; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var spawnInProgress = false; // Additional flag to prevent double spawning var enemiesSpawnedThisWave = 0; // Counter to track spawned enemies per wave var nextWaveTime = 12000 / 2; var towersPlacedCount = 0; // Counter for placed towers var minTowersRequired = 6; // Minimum towers required to start game var placementNotification = null; // Notification for tower placement guidance 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 wuSoundPlayed = false; // Track if wu sound has been played for rat enemies - 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 animationObjectPool = []; // Pool for reusable animation objects var maxPoolSize = 30; // Increased pool size for all particle types var maxAnimationPoolSize = 20; // Maximum size for animation object pool // Particle lifetime management system var activeParticles = []; // Track all active particles for lifetime management var maxActiveParticles = 100; // Maximum size limit for activeParticles array var maxParticleLifetime = 3000; // Maximum particle lifetime in milliseconds (3 seconds) var particleCleanupInterval = 120; // Clean up expired particles every 2 seconds (120 frames at 60fps) var lastParticleCleanup = 0; var wave1VexSpawned = false; // Track if vex enemy has been spawned in wave 1 var missSoundStarted = false; // Track if miss sound has started var vexSpawnTimerStarted = false; // Track if vex spawn timer has started var vexSpawnTimer = 0; // Timer for vex spawn after miss sound var nSoundFinished = false; // Track if 'n' sound has finished playing // Sound pooling system var soundPool = {}; var maxSoundInstances = 8; // Maximum instances per sound type var soundCleanupInterval = 1200; // Clean up idle sounds every 20 seconds (1200 frames at 60fps) var lastSoundCleanup = 0; var activeSounds = []; // Track currently playing sounds var maxActiveSounds = 50; // Maximum size limit for activeSounds array // 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 getAnimationObject() { if (animationObjectPool.length > 0) { var animObj = animationObjectPool.pop(); // Reset animation object properties animObj.isActive = false; animObj.target = null; animObj.properties = {}; animObj.config = {}; animObj.startTime = 0; return animObj; } // Create new animation object if pool is empty var animationObject = { isActive: false, target: null, properties: {}, config: {}, startTime: 0, currentValues: {}, initialValues: {} }; return animationObject; } function returnAnimationObject(animObj) { if (!animObj) return; // Stop any active tweens if (animObj.target && animObj.isActive) { tween.stop(animObj.target, animObj.properties); } // Reset object state animObj.isActive = false; animObj.target = null; animObj.properties = {}; animObj.config = {}; animObj.startTime = 0; animObj.currentValues = {}; animObj.initialValues = {}; // Return to pool if not at capacity if (animationObjectPool.length < maxAnimationPoolSize) { animationObjectPool.push(animObj); } } function createPooledAnimation(target, properties, config) { if (!target || _typeof(properties) !== 'object') return null; var animObj = getAnimationObject(); animObj.target = target; animObj.properties = Object.assign({}, properties); animObj.config = Object.assign({}, config); animObj.isActive = true; animObj.startTime = LK.ticks; // Store initial values for (var prop in properties) { animObj.initialValues[prop] = target[prop] || 0; animObj.currentValues[prop] = animObj.initialValues[prop]; } // Create the actual tween with cleanup callback var originalOnFinish = config.onFinish; config.onFinish = function () { if (originalOnFinish) originalOnFinish(); returnAnimationObject(animObj); }; tween(target, properties, config); return animObj; } 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(); } } // Sound pool management functions with stricter limits function getPooledSound(soundId) { // Initialize pool for this sound type if it doesn't exist if (!soundPool[soundId]) { soundPool[soundId] = { available: [], active: [], createdCount: 0 }; } var pool = soundPool[soundId]; var sound; // Try to reuse an available sound instance if (pool.available.length > 0) { sound = pool.available.pop(); // Reset sound properties if needed if (sound.stop) { sound.stop(); } } else if (pool.createdCount < maxSoundInstances) { // Create new sound instance if under limit sound = LK.getSound(soundId); pool.createdCount++; // Wrap the original play method to track usage var originalPlay = sound.play; sound.play = function () { // Move to active list var availableIndex = pool.available.indexOf(sound); if (availableIndex !== -1) { pool.available.splice(availableIndex, 1); } if (pool.active.indexOf(sound) === -1) { pool.active.push(sound); } // Track globally for cleanup sound.lastPlayTime = LK.ticks; sound.isPlaying = true; if (activeSounds.indexOf(sound) === -1) { activeSounds.push(sound); } return originalPlay.call(this); }; } else { // Pool limit reached, force cleanup oldest sound and reuse if (pool.active.length > 0) { sound = pool.active[0]; // Force stop and clear audio buffer references forceCleanupSound(sound); // Remove from active list pool.active.splice(0, 1); } else if (pool.available.length > 0) { sound = pool.available.pop(); // Force cleanup of unused instance forceCleanupSound(sound); } else { // Last resort: create temporary sound (will be cleaned up aggressively) sound = LK.getSound(soundId); } } return sound; } function forceCleanupSound(sound) { // Force stop the sound if (sound && sound.stop) { try { sound.stop(); } catch (e) { // Ignore errors if sound is already stopped } } // Clear audio buffer references if (sound) { sound.isPlaying = false; sound.lastPlayTime = 0; // Clear internal references if available if (sound._source) { sound._source = null; } if (sound._buffer) { sound._buffer = null; } if (sound.source) { sound.source = null; } // Additional cleanup for audio context if (sound.context) { sound.context.close(); sound.context = null; } } } function returnSoundToPool(sound, soundId) { if (!soundPool[soundId]) return; var pool = soundPool[soundId]; // Force cleanup before returning to pool forceCleanupSound(sound); // Remove from active list var activeIndex = pool.active.indexOf(sound); if (activeIndex !== -1) { pool.active.splice(activeIndex, 1); } // Add to available list with stricter size limit var maxAvailable = Math.floor(maxSoundInstances / 3); // Reduced from /2 to /3 if (pool.available.indexOf(sound) === -1 && pool.available.length < maxAvailable) { pool.available.push(sound); } else { // Force cleanup if pool is full forceCleanupSound(sound); } // Remove from global tracking var globalIndex = activeSounds.indexOf(sound); if (globalIndex !== -1) { activeSounds.splice(globalIndex, 1); } } function cleanupIdleSounds() { // More aggressive cleanup with stricter limits var aggressiveCleanupThreshold = soundCleanupInterval / 4; // Clean up 4x more frequently // Clean up sounds that haven't been used recently for (var i = activeSounds.length - 1; i >= 0; i--) { var sound = activeSounds[i]; if (sound.lastPlayTime && LK.ticks - sound.lastPlayTime > aggressiveCleanupThreshold) { // Force cleanup idle sound forceCleanupSound(sound); // Remove from active sounds immediately activeSounds.splice(i, 1); // Return to pool or destroy for (var soundId in soundPool) { var pool = soundPool[soundId]; var activeIndex = pool.active.indexOf(sound); if (activeIndex !== -1) { pool.active.splice(activeIndex, 1); break; } } } } // Enforce stricter pool size limits for (var soundId in soundPool) { var pool = soundPool[soundId]; // Reduce available pool size to 1/4 of max instances var maxAvailable = Math.floor(maxSoundInstances / 4); while (pool.available.length > maxAvailable) { var excessSound = pool.available.pop(); forceCleanupSound(excessSound); pool.createdCount = Math.max(0, pool.createdCount - 1); } // Force cleanup active sounds if too many var maxActive = Math.floor(maxSoundInstances / 2); while (pool.active.length > maxActive) { var excessActiveSound = pool.active.shift(); forceCleanupSound(excessActiveSound); // Remove from global tracking var globalIndex = activeSounds.indexOf(excessActiveSound); if (globalIndex !== -1) { activeSounds.splice(globalIndex, 1); } } } // Enforce global activeSounds limit more strictly if (activeSounds.length > maxActiveSounds) { // Sort by last play time and remove oldest activeSounds.sort(function (a, b) { return (a.lastPlayTime || 0) - (b.lastPlayTime || 0); }); while (activeSounds.length > maxActiveSounds / 2) { var oldestSound = activeSounds.shift(); forceCleanupSound(oldestSound); } } } // Override LK.getSound to use pooled sounds for performance-critical sounds var originalLKGetSound = LK.getSound; LK.getSound = function (soundId) { // List of sounds that benefit from pooling (frequently played sounds) var pooledSounds = ['walking', 'tower_shoot', 'footstep', 'egzos', 'flame_sound', 'poison_bullet_hit', 'bullet_casing_drop', 'bird_chirp']; if (pooledSounds.indexOf(soundId) !== -1) { return getPooledSound(soundId); } else { // Use original method for less frequently played sounds return originalLKGetSound.call(this, soundId); } }; // 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: 200, height: 160 }); // Position mary at bottom of ask asset maryDisplay.x = askDisplay.x + 70; // Moved slightly more to the right maryDisplay.y = askDisplay.y + 150; // Moved slightly down // Initially hide mary - will be shown when wave 8 starts maryDisplay.visible = false; // 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 - only for wave 7 var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7; 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 tabela display in right middle edge area var tabelaDisplay = game.attachAsset('tabela', { anchorX: 0.5, anchorY: 0.5, width: 160, height: 160 }); // Position tabela one cell down and one cell left from previous position tabelaDisplay.x = 2048 - 360 - 76; // Move one cell left (76px) tabelaDisplay.y = 2732 / 2 + 76 - 650; // Move one cell down (76px) then 650px up // Add displays directly to game object instead of LK.gui for better visibility game.addChild(goldDisplay); game.addChild(livesDisplay); game.addChild(scoreDisplay); game.addChild(tabelaDisplay); // Add mary display last so it appears on top of other elements game.addChild(maryDisplay); // Add update method to mary to check wave progression maryDisplay.update = function () { // Show mary starting from wave 8 if (currentWave >= 8) { maryDisplay.visible = true; } }; // 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) { // Define the exact placement squares where towers can be placed var allowedPlacements = [{ x: 11, y: 6 }, // First placement square { x: 11, y: 11 }, // Second placement square { x: 11, y: 16 }, // Third placement square { x: 11, y: 21 }, // Fourth placement square { x: 11, y: 26 }, // Fifth placement square { x: 11, y: 31 } // Sixth placement square ]; // Check if current grid position matches any allowed placement var isAllowedPlacement = false; for (var p = 0; p < allowedPlacements.length; p++) { var placement = allowedPlacements[p]; if (gridX === placement.x && gridY === placement.y) { isAllowedPlacement = true; break; } } // Block placement if not in allowed positions if (!isAllowedPlacement) { return true; // Block placement outside allowed squares } // Poison towers never block paths since enemies can pass through them if (towerType === 'poison') { return false; } // In single column formation, only check vertically adjacent towers for (var checkY = gridY - 2; checkY <= gridY + 3; checkY++) { // Skip the cells that would be occupied by the new tower if (checkY >= gridY && checkY < gridY + 2) { continue; } // Only check within the center column (gridX 11-12) for (var checkX = gridX; checkX < gridX + 2; checkX++) { var checkCell = grid.getCell(checkX, checkY); if (checkCell && checkCell.type === 1) { // Found a vertically adjacent tower in same column, prevent placement return true; } } } 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); // Increment tower placement counter towersPlacedCount++; // 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) { // Clean up any orphaned event listeners if (LK.ticks % 600 === 0) { // Every 10 seconds // Force cleanup of objects without parents for (var i = game.children.length - 1; i >= 0; i--) { var child = game.children[i]; if (child && typeof child.down === 'function' && !child.parent) { // Remove orphaned child with event listeners game.removeChild(child); } } } 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; // Snap to nearest valid placement square var allowedPlacements = [{ x: 11, y: 6 }, // First placement square { x: 11, y: 11 }, // Second placement square { x: 11, y: 16 }, // Third placement square { x: 11, y: 21 }, // Fourth placement square { x: 11, y: 26 }, // Fifth placement square { x: 11, y: 31 } // Sixth placement square ]; // Find the nearest valid placement square var nearestSquare = null; var nearestDistance = Infinity; var dropX = x; var dropY = y - CELL_SIZE * 1.5; // Account for the offset used during dragging for (var p = 0; p < allowedPlacements.length; p++) { var placement = allowedPlacements[p]; var squareWorldX = grid.x + placement.x * CELL_SIZE + CELL_SIZE; var squareWorldY = grid.y + placement.y * CELL_SIZE + CELL_SIZE; var dx = dropX - squareWorldX; var dy = dropY - squareWorldY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < nearestDistance) { nearestDistance = distance; nearestSquare = placement; } } // Snap to the nearest square if within reasonable range (2 grid cells) if (nearestSquare && nearestDistance < CELL_SIZE * 2) { towerPreview.gridX = nearestSquare.x; towerPreview.gridY = nearestSquare.y; towerPreview.x = grid.x + nearestSquare.x * CELL_SIZE + CELL_SIZE / 2; towerPreview.y = grid.y + nearestSquare.y * CELL_SIZE + CELL_SIZE / 2; towerPreview.checkPlacement(); } if (towerPreview.canPlace) { var blockResult = wouldBlockPath(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); if (!blockResult) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { // Check if blocking is due to adjacent towers var hasAdjacentTower = false; for (var checkX = towerPreview.gridX - 2; checkX <= towerPreview.gridX + 3; checkX++) { for (var checkY = towerPreview.gridY - 2; checkY <= towerPreview.gridY + 3; checkY++) { // Skip the cells that would be occupied by the new tower if (checkX >= towerPreview.gridX && checkX < towerPreview.gridX + 2 && checkY >= towerPreview.gridY && checkY < towerPreview.gridY + 2) { continue; } var checkCell = grid.getCell(checkX, checkY); if (checkCell && checkCell.type === 1) { hasAdjacentTower = true; break; } } if (hasAdjacentTower) break; } var notificationText = hasAdjacentTower ? "Cannot place towers next to each other!" : "Tower would block the path!"; var notification = game.addChild(new Notification(notificationText)); 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); // Show placement notification function function showPlacementNotification() { if (!placementNotification) { placementNotification = new Notification("Place the towers on the green spots."); placementNotification.x = 2048 / 2; placementNotification.y = 2732 / 2; // Position at center of screen game.addChild(placementNotification); // Make notification persistent by overriding its update method placementNotification.update = function () { // Only hide notification after second tower is placed if (towersPlacedCount >= 2) { this.destroy(); placementNotification = null; } }; } } // Automatically place default tower at enemy spawn point nokta1 (first allowed placement position) var autoPlaceTower = function autoPlaceTower() { // Use the first placement position which corresponds to nokta1 (enemy spawn area) var nokta1GridX = 11; // First placement square x coordinate var nokta1GridY = 6; // First placement square y coordinate // Check if we can place a default tower here if (canPlaceTowerType('default') && gold >= getTowerCost('default')) { // Place the default tower automatically if (placeTower(nokta1GridX, nokta1GridY, 'default')) { console.log("Default tower automatically placed at nokta1 spawn point"); // Show placement notification after first tower is placed showPlacementNotification(); } } }; // Place tower immediately when game initializes autoPlaceTower(); 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]; // Remove invalid particles from tracking if (!particle || !particle.parent || typeof particle.creationTime !== "number") { if (particle && particle.parent) { if (particle.destroy) particle.destroy(); } 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 }); // Remove from parent if still attached if (particle.parent) { particle.parent.removeChild(particle); } // Return to pool or destroy returnParticle(particle); } } // Enforce maxActiveParticles limit if (activeParticles.length > maxActiveParticles) { // Remove oldest particles first for (var p = 0; p < activeParticles.length - maxActiveParticles; p++) { var particle = activeParticles[p]; if (particle && particle.parent) { particle.parent.removeChild(particle); } if (particle && particle.destroy) particle.destroy(); } activeParticles.splice(0, activeParticles.length - maxActiveParticles); } } // Sound pool cleanup - check and cleanup idle sounds if (LK.ticks - lastSoundCleanup >= soundCleanupInterval) { lastSoundCleanup = LK.ticks; cleanupIdleSounds(); } // Tween cleanup - clean up inactive tweens every 5 seconds if (LK.ticks % 300 === 0) { cleanupInactiveTweens(); } // 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 - only for wave 7 var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7; // Also prevent wave 8 from starting automatically during black screen var preventWave8AutoStart = isBlackScreenActive && currentWave === 7; // Check if any vex enemies are still alive to block wave progression var vexEnemiesAlive = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].type === 'vex') { vexEnemiesAlive = true; break; } } // Block wave progression if vex enemies are still alive and trying to start wave 2 var blockWaveForVex = vexEnemiesAlive && currentWave === 1 && waveTimer >= nextWaveTime; // Block all wave progression during black screen, including automatic wave 8 start, and when vex enemies are alive if (waveInProgress && !isBlackScreenActive && !preventWave8AutoStart && !(fadeToBlackStarted && currentWave >= 7) && !blockWaveForVex) { if (!waveSpawned && !spawnInProgress) { spawnInProgress = true; // Set spawn in progress flag enemiesSpawnedThisWave = 0; // Reset spawn counter for new wave // Clear any remaining enemies from previous waves to prevent confusion for (var oldEnemyIndex = enemies.length - 1; oldEnemyIndex >= 0; oldEnemyIndex--) { var oldEnemy = enemies[oldEnemyIndex]; if (oldEnemy.waveNumber < currentWave) { // Remove old enemy that shouldn't still be spawning if (oldEnemy.parent) { if (oldEnemy.isFlying) { enemyLayerTop.removeChild(oldEnemy); } else { enemyLayerBottom.removeChild(oldEnemy); } } enemies.splice(oldEnemyIndex, 1); } } 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(); // Play miss sound 5 seconds after og sound tween({}, {}, { duration: 5000, onFinish: function onFinish() { LK.getSound('miss').play(); missSoundStarted = true; // Set flag when miss sound starts // guys sound is now played when vex enemy appears on screen } }); } }); } }); } }); } }); } 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(); } // Play df sound only once when Wave 8 starts spawning if (currentWave === 8 && !window.dfSoundPlayed) { window.dfSoundPlayed = true; LK.getSound('df').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; } // Ensure wave 8+ enemy count is strictly limited to 3 (or 1 for boss waves) if (currentWave >= 8) { if (waveType === 'big' || currentWave % 10 === 0) { enemyCount = 1; // Boss waves always have 1 enemy } else { enemyCount = Math.min(3, enemyCount); // All other waves limited to 3 enemies } } // 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 xr sound flag for new wave window.xrSoundPlayed = false; // Reset yarış sound flag for new wave window.yarışSoundPlayed = false; // Reset guys sound flag for new wave window.guysSoundPlayed = false; // 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 if (typeof window.sniperImmuneHitCounter !== 'undefined') { window.sniperImmuneHitCounter = 0; } // Reset splash immune hit counter for new wave window.splashImmuneHitCounter = 0; // Spawn the appropriate number of enemies with stricter counter protection // Double-check that we don't exceed the enemy count for this wave var actualEnemyCount = Math.min(enemyCount, currentWave >= 8 ? 3 : enemyCount); for (var i = 0; i < actualEnemyCount && enemiesSpawnedThisWave < actualEnemyCount; i++) { var enemyType = waveType; // Special handling for Wave 1: spawn only blue enemies initially if (currentWave === 1) { // All enemies in Wave 1 are blue - vex will spawn later enemyType = 'blue'; } else if (enemyType === 'blue') { // Blue enemies can only be used in wave 1, replace with normal for other waves enemyType = 'normal'; } var enemy = new Enemy(enemyType); // 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 = 225; break; case 2: fixedHealth = 350; break; case 3: fixedHealth = 350; break; case 4: fixedHealth = 365; break; case 5: fixedHealth = 300; break; case 6: fixedHealth = 555; break; case 7: fixedHealth = 1900; break; default: // For waves 8+, start at 600 health and increase by 20 each wave if (currentWave >= 8) { fixedHealth = 600 + (currentWave - 8) * 20; // 600 for wave 8, +20 each wave } else { // Fallback for any unexpected cases fixedHealth = 600; } // 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; // Handle vex enemy spawn location specially var spawnX; if (enemyType === 'vex') { // Vex enemy always spawns from column 14 spawnX = 14; } else { // All other enemy types 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 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); enemiesSpawnedThisWave++; // Increment spawn counter } // Complete spawn process - only set waveSpawned to true after ALL enemies are spawned spawnInProgress = false; // Clear spawn in progress flag // Final validation: ensure we haven't exceeded the enemy limit for wave 8+ if (currentWave >= 8) { var currentWaveEnemyCount = 0; for (var checkIndex = 0; checkIndex < enemies.length; checkIndex++) { if (enemies[checkIndex].waveNumber === currentWave) { currentWaveEnemyCount++; } } // If we somehow spawned too many enemies, remove the excess if (currentWaveEnemyCount > 3) { for (var excessIndex = enemies.length - 1; excessIndex >= 0 && currentWaveEnemyCount > 3; excessIndex--) { var excessEnemy = enemies[excessIndex]; if (excessEnemy.waveNumber === currentWave) { if (excessEnemy.parent) { if (excessEnemy.isFlying) { enemyLayerTop.removeChild(excessEnemy); } else { enemyLayerBottom.removeChild(excessEnemy); } } enemies.splice(excessIndex, 1); currentWaveEnemyCount--; } } } } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } // Special handling for Wave 1: spawn vex enemy 3 seconds after miss sound starts playing // The miss sound is triggered after og sound, so we need to detect when it starts + 3 seconds if (currentWave === 1 && waveSpawned && !wave1VexSpawned && !spawnInProgress) { // Check if miss sound has started and spawn vex enemy 3 seconds after if (missSoundStarted && !vexSpawnTimerStarted) { vexSpawnTimerStarted = true; vexSpawnTimer = LK.ticks; } if (vexSpawnTimerStarted && LK.ticks - vexSpawnTimer >= 660) { // 660 frames = 11 seconds at 60 FPS wave1VexSpawned = true; var vexEnemy = new Enemy('vex'); // Add vex enemy to the appropriate layer if (vexEnemy.isFlying) { enemyLayerTop.addChild(vexEnemy); } else { enemyLayerBottom.addChild(vexEnemy); } // Set vex enemy health for wave 1 vexEnemy.maxHealth = 195; // Same as other wave 1 enemies vexEnemy.health = vexEnemy.maxHealth; // Vex enemy spawns from column 14 var spawnX = 14; var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading vexEnemy.cellX = spawnX; vexEnemy.cellY = 5; // Position after entry vexEnemy.currentCellX = spawnX; vexEnemy.currentCellY = spawnY; vexEnemy.waveNumber = currentWave; enemies.push(vexEnemy); enemiesSpawnedThisWave++; // Count vex enemy in spawn total } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; spawnInProgress = false; // Reset spawn protection flag enemiesSpawnedThisWave = 0; // Reset spawn counter // Reset vex spawn flag for next game if (currentWave === 1) { wave1VexSpawned = false; } // Set timer to automatically start next wave after 7 seconds if not at final wave // But only if we're not in black screen state if (currentWave < totalWaves && !isBlackScreenActive) { 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 (excluding blue enemies and Wave 7 big enemy) if (enemiesInSlowArea.length === 0 && !sunSoundPlayed && currentWave !== 5) { // Exclude sun sound for big enemy in wave 7 if (!(currentWave === 7 && enemy.type === 'big') && enemy.type !== 'blue') { sunSoundPlayed = true; LK.getSound('sun').play(); } } } } // Aggressive enemies array cleanup - enforce maximum size limit var maxEnemiesLimit = 50; // Maximum enemies allowed in memory if (enemies.length > maxEnemiesLimit) { // Force cleanup of oldest enemies that are off-screen or invalid for (var cleanupIndex = enemies.length - 1; cleanupIndex >= 0 && enemies.length > maxEnemiesLimit; cleanupIndex--) { var enemyToCleanup = enemies[cleanupIndex]; if (!enemyToCleanup || !enemyToCleanup.parent || enemyToCleanup.y < -200 || enemyToCleanup.y > 3000) { // Force cleanup enemy that's invalid or far off-screen if (enemyToCleanup && enemyToCleanup.parent) { if (enemyToCleanup.isFlying) { enemyLayerTop.removeChild(enemyToCleanup); } else { enemyLayerBottom.removeChild(enemyToCleanup); } } enemies.splice(cleanupIndex, 1); } } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; // Additional safety check for invalid enemies if (!enemy || !enemy.parent) { enemies.splice(a, 1); continue; } if (enemy.health <= 0) { // Aggressive cleanup of bullet references if (enemy.bulletsTargetingThis) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; if (bullet) { bullet.targetEnemy = null; } } // Clear the array completely to prevent memory leaks enemy.bulletsTargetingThis.length = 0; enemy.bulletsTargetingThis = 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.type === 'vex') && !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; } // Enemy death animation removed // 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); } // Force cleanup of enemy references before removal enemy.targetEnemy = null; enemy.currentTarget = null; if (enemy.bulletsTargetingThis) { enemy.bulletsTargetingThis = null; } 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.type === 'vex') && !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(); } } } // Aggressive bullets array cleanup - enforce maximum size limit var maxBulletsLimit = 100; // Maximum bullets allowed in memory if (bullets.length > maxBulletsLimit) { // Force cleanup of oldest bullets or those without targets for (var cleanupIndex = bullets.length - 1; cleanupIndex >= 0 && bullets.length > maxBulletsLimit; cleanupIndex--) { var bulletToCleanup = bullets[cleanupIndex]; if (!bulletToCleanup || !bulletToCleanup.parent || !bulletToCleanup.targetEnemy || bulletToCleanup.targetEnemy.health <= 0) { // Force cleanup bullet references if (bulletToCleanup && bulletToCleanup.targetEnemy && bulletToCleanup.targetEnemy.bulletsTargetingThis) { var bulletIndex = bulletToCleanup.targetEnemy.bulletsTargetingThis.indexOf(bulletToCleanup); if (bulletIndex !== -1) { bulletToCleanup.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } if (bulletToCleanup && bulletToCleanup.parent) { bulletToCleanup.parent.removeChild(bulletToCleanup); } bullets.splice(cleanupIndex, 1); } } } for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; // Additional safety checks for invalid bullets if (!bullet) { bullets.splice(i, 1); continue; } if (!bullet.parent) { // Aggressive cleanup of target enemy references if (bullet.targetEnemy) { var targetEnemy = bullet.targetEnemy; if (targetEnemy && targetEnemy.bulletsTargetingThis) { var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullet); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } } // Clear bullet references to prevent memory leaks bullet.targetEnemy = null; bullet.sourceTower = null; bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } // Smart pathfinding optimization: only trigger when enemies need updated paths var needsPathfindingUpdate = false; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Only trigger pathfinding for enemies that have entered the gameplay area if (enemy.currentCellY >= 4 && !enemy.isFlying && enemy.type !== 'vex') { // Check if enemy has a valid current target with correct pathId if (!enemy.currentTarget || enemy.currentTarget.pathId !== pathId) { needsPathfindingUpdate = true; break; } } } // Trigger pathfinding update only when needed and not too frequently if (needsPathfindingUpdate && (!pathfindingCache || LK.ticks - lastPathfindTime > 180)) { grid.pathFind(); } // 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('notification', { anchorX: 0.5, anchorY: 0.5 }); homeButtonBg.width = 600; homeButtonBg.height = 180; homeButtonBg.tint = 0xFF0000; 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('notification', { anchorX: 0.5, anchorY: 0.5 }); continueButtonBg.width = 600; continueButtonBg.height = 180; continueButtonBg.tint = 0x00FF00; 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 () { // Stop son sound if it's currently playing try { LK.getSound('son').stop(); } catch (e) { // Sound might not support stop method, ignore error } // Continue to next wave (Wave 8) // Hide the overlay and buttons blackOverlay.alpha = 0; centerText.alpha = 0; homeButton.alpha = 0; continueButton.alpha = 0; // IMPORTANT: Reset black screen state FIRST to disable it for waves 8+ bossWaveCompleted = false; fadeToBlackStarted = false; // Reset game state for next wave (only set to 8 if we're actually at wave 7) if (currentWave === 7) { currentWave = 8; // Only advance to wave 8 if we're coming from wave 7 } waveTimer = 0; // Reset timer to start next wave immediately waveInProgress = true; waveSpawned = 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 && currentWave === 7; 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; }; // Periodic array cleanup and maintenance to prevent memory leaks if (LK.ticks - lastArrayCleanupTime >= arrayCleanupInterval) { lastArrayCleanupTime = LK.ticks; // Clean up invalid tower references for (var t = towers.length - 1; t >= 0; t--) { var tower = towers[t]; if (!tower || !tower.parent) { // Force cleanup tower references if (tower) { tower.targetEnemy = null; if (tower.cellsInRange) { // Clean up cell references to this tower for (var c = 0; c < tower.cellsInRange.length; c++) { var cell = tower.cellsInRange[c]; if (cell && cell.towersInRange) { var towerIndex = cell.towersInRange.indexOf(tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } tower.cellsInRange.length = 0; tower.cellsInRange = null; } } towers.splice(t, 1); } } // Force garbage collection hints by nullifying unused references if (enemies.length === 0 && !waveInProgress) { // Clean up any residual enemy references when no enemies present for (var b = bullets.length - 1; b >= 0; b--) { var bulletForCleanup = bullets[b]; if (bulletForCleanup && (!bulletForCleanup.targetEnemy || bulletForCleanup.targetEnemy.health <= 0)) { if (bulletForCleanup.parent) { bulletForCleanup.parent.removeChild(bulletForCleanup); } bulletForCleanup.targetEnemy = null; bulletForCleanup.sourceTower = null; bullets.splice(b, 1); } } } // Enforce strict limits on all arrays var maxEnemies = Math.min(50, currentWave * 5); // Scale with wave but cap at 50 var maxBullets = Math.min(100, towers.length * 10); // Scale with tower count but cap at 100 var maxTowers = 20; // Hard limit on towers // Trim arrays if they exceed limits if (enemies.length > maxEnemies) { enemies.length = maxEnemies; } if (bullets.length > maxBullets) { // Remove oldest bullets first bullets.splice(0, bullets.length - maxBullets); } if (towers.length > maxTowers) { towers.length = maxTowers; } } // Win condition: Only show you win when all 50 waves are completed if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Add more concentrated bullet movement animation
if (!self.animationPhase) {
self.animationPhase = Math.random() * Math.PI * 2;
}
self.animationPhase += 0.3;
// Apply animation reduction for splash bullets to minimize load
var animationFactor = self.animationReductionFactor || 1.0; // Default to full animation
var concentratedBounce = Math.sin(self.animationPhase) * 2 * animationFactor; // Reduced from larger values
var concentratedSway = Math.cos(self.animationPhase * 0.7) * 1.5 * animationFactor; // 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 (skip damage for vex enemies to make them immortal)
if (self.targetEnemy.type !== 'vex') {
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
trackTween({}, {}, {
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
trackTween({}, {}, {
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();
// Play borc sound 4 seconds after tnk sound
tween({}, {}, {
duration: 4000,
onFinish: function onFinish() {
LK.getSound('borc').play();
// Play db sound 4 seconds after borc sound finishes
tween({}, {}, {
duration: 4000,
onFinish: function onFinish() {
LK.getSound('db').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 ? 3 : 4; // Further reduced smoke particles (was 4-6, now 3-4)
for (var smokeIdx = 0; smokeIdx < smokeCount; smokeIdx++) {
var smokeParticle = getSmokeParticle();
var smokeGraphics = smokeParticle.smokeGraphics;
smokeGraphics.width = 15 + Math.random() * 20;
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 = 8 + Math.random() * 12;
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 tower bullets on 2nd hit - only once per wave
if (otherEnemy.isImmune && self.type === 'splash') {
if (typeof window.splashImmuneHitCounter === 'undefined') {
window.splashImmuneHitCounter = 0;
}
window.splashImmuneHitCounter++;
if (window.splashImmuneHitCounter === 2 && !window.krkSoundPlayedThisWave) {
window.krkSoundPlayedThisWave = true;
try {
LK.getSound('krk').play();
} catch (e) {
console.log("Error playing krk sound:", e);
}
}
}
// Krk sound moved to splash tower bullet hit logic - not played here anymore
}
}
}
} 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 (exclude fast enemies from wave 5)
if (poisonBulletHitCount === 1 && !gogogoPoisonSoundPlayed && !(self.targetEnemy.type === 'fast' && self.targetEnemy.waveNumber === 5)) {
gogogoPoisonSoundPlayed = true;
LK.getSound('gogogo').play();
}
// Play whofarted sound only once on second poison bullet hit with delay (exclude fast enemies from wave 5)
if (poisonBulletHitCount === 2 && !whoFartedSoundPlayed && !(self.targetEnemy.type === 'fast' && self.targetEnemy.waveNumber === 5)) {
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();
// }
}
// Shy sound logic moved to sniper tower fire method to ensure it only triggers for sniper towers
}
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 with animation reduction factor
var animationFactor = self.animationReductionFactor || 1.0; // Default to full animation
var concentratedBounceX = Math.sin(self.animationPhase * 1.2) * 0.8 * animationFactor; // Much smaller bounce
var concentratedBounceY = Math.cos(self.animationPhase * 1.5) * 0.6 * animationFactor; // 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 'vex':
self.maxHealth = 200; // Stronger than blue enemies
self.speed *= 2.0; // Much faster movement
break;
case 'rat':
self.maxHealth = 2000; // High health rat enemy
self.speed *= 1.0; // Normal speed movement
break;
case 'big':
self.maxHealth = 300; // Much stronger than normal enemies
self.speed *= 0.7; // Slower movement
break;
case 'kırık':
self.maxHealth = 100; // Normal health like other enemies
self.speed *= 1.5; // Faster movement (increased from 0.8 to 1.5)
// Removed immortal property - now mortal
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 === 'vex') {
assetId = 'enemy_vex';
} else if (self.type === 'rat') {
assetId = 'enemy_rat';
} else if (self.type === 'kırık') {
assetId = 'enemy_kirik';
} else if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.type === 'rat' ? 1.45 : 1.25,
scaleY: self.type === 'rat' ? 1.45 : 1.25
});
// Add walking feet for normal, swarm, blue, and kırık enemies only (no feet for boss enemies)
self.leftFoot = null;
self.rightFoot = null;
if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue' || self.type === 'vex' || self.type === 'rat' || self.type === 'kırık') && !self.isBoss) {
// Adjust foot size and asset based on enemy type
var footWidth, footHeight, footSpacing, footAsset;
if (self.type === 'vex') {
// Vex gets special feet - smaller size
footWidth = 16;
footHeight = 12;
footSpacing = 18;
footAsset = 'vexFeet';
} else if (self.type === 'rat') {
// Rat enemy gets elliptical feet with wider spacing
footWidth = 28;
footHeight = 22;
footSpacing = 25;
footAsset = 'walkingFeet';
} else if (self.type === 'kırık') {
// Kırık enemy gets bigger feet
footWidth = 24;
footHeight = 18;
footSpacing = 20;
footAsset = 'walkingFeet';
} else {
// Regular feet for other enemies
footWidth = 18;
footHeight = 13;
footSpacing = 15;
footAsset = 'walkingFeet';
}
var footYPosition = 35;
// Create walking feet as regular Container objects (not particles)
self.leftFoot = new Container();
var leftFootGraphics = self.leftFoot.attachAsset(footAsset, {
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(footAsset, {
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 = 1.8;
enemyGraphics.scaleY = 1.8;
} else {
enemyGraphics.scaleX = 1.6;
enemyGraphics.scaleY = 1.6;
}
}
// 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 kırık enemy special properties
if (self.type === 'kırık') {
// Health bar will show normally when damaged like other enemies
self.kırıkPhase = 'goingToTabela'; // 'goingToTabela' or 'returningToStart'
// Set tabela position as target (one cell down and one cell left from previous position)
self.tabelaX = (2048 - 360 - 76) / CELL_SIZE; // Convert pixel position to grid
self.tabelaY = (2732 / 2 + 76 - (200 - CELL_SIZE * 4)) / CELL_SIZE; // Convert pixel position to grid
self.startX = self.currentCellX; // Remember starting position
self.startY = self.currentCellY;
}
// 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 () {
// Clean up targeting arrays periodically
if (LK.ticks % 300 === 0) {
// Every 5 seconds
if (self.bulletsTargetingThis) {
self.bulletsTargetingThis = self.bulletsTargetingThis.filter(function (bullet) {
return bullet && bullet.parent && bullet.targetEnemy === self;
});
}
}
// 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, blue, vex, kırık and immune enemies only (no feet for boss enemies)
if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue' || self.type === 'vex' || self.type === 'kırık' || self.type === 'immune') && !self.isBoss && self.leftFoot && self.rightFoot) {
// Keep feet at fixed positions with type-specific spacing
var footYPos = self.type === 'rat' ? 40 : 35;
var footSpacing;
if (self.type === 'vex') {
footSpacing = 18; // Special spacing for vex
} else if (self.type === 'rat') {
footSpacing = 25; // Wider spacing for rat
} else if (self.type === 'kırık') {
footSpacing = 20; // Wider spacing for kırık
} else {
footSpacing = 15; // Default spacing
}
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 - normal, vex, rat, kırık and immune enemies get foot animation, swarm enemies get no foot animation
if (self.type === 'normal' || self.type === 'vex' || self.type === 'rat' || self.type === 'kırık' || self.type === 'immune') {
// Update foot animation phases with type-specific multipliers
var animationMultiplier;
if (self.type === 'vex') {
animationMultiplier = 2.5; // Special animation for vex
} else if (self.type === 'rat') {
animationMultiplier = 2.3; // Fast animation for rat, slightly less than vex
} else if (self.type === 'kırık') {
animationMultiplier = 3; // Faster animation for kırık
} else if (self.type === 'immune') {
animationMultiplier = 1.5; // Simplified animation for immune
} else {
animationMultiplier = 2; // Default animation
}
self.leftFootPhase += self.walkAnimationSpeed * speedMultiplier * animationMultiplier;
self.rightFootPhase += self.walkAnimationSpeed * speedMultiplier * animationMultiplier;
// Use simple scaling animation with type-specific scaling
var scaleMultiplier;
if (self.type === 'vex') {
scaleMultiplier = 0.5; // Special scaling for vex
} else if (self.type === 'rat') {
scaleMultiplier = 1.45; // Scale to 1.45 for rat feet
} else if (self.type === 'kırık') {
scaleMultiplier = 0.6; // Bigger scaling for kırık
} else if (self.type === 'immune') {
scaleMultiplier = 0.3; // Simplified scaling for immune
} else {
scaleMultiplier = 0.4; // Default scaling
}
var leftFootScale = 1 + Math.abs(Math.sin(self.leftFootPhase)) * scaleMultiplier;
var rightFootScale = 1 + Math.abs(Math.sin(self.rightFootPhase)) * scaleMultiplier;
// Apply scaling animation using pooled animation objects for other enemy types
if (self.type === 'immune') {
// Direct scaling for immune enemies for simplicity
self.leftFoot.scaleX = leftFootScale;
self.leftFoot.scaleY = leftFootScale;
self.rightFoot.scaleX = rightFootScale;
self.rightFoot.scaleY = rightFootScale;
} else if (self.type === 'rat') {
// Stabilized foot animation for rat enemies using tween for smooth movement
if (self.leftFoot && !self.leftFoot.isAnimating) {
self.leftFoot.isAnimating = true;
tween(self.leftFoot, {
scaleX: leftFootScale,
scaleY: leftFootScale
}, {
duration: 120,
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: 120,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.rightFoot) {
self.rightFoot.isAnimating = false;
}
}
});
}
} else {
// Apply scaling animation using pooled animation objects for other enemy types
if (self.leftFoot && !self.leftFoot.isAnimating) {
self.leftFoot.isAnimating = true;
createPooledAnimation(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;
createPooledAnimation(self.rightFoot, {
scaleX: rightFootScale,
scaleY: rightFootScale
}, {
duration: 100,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.rightFoot) {
self.rightFoot.isAnimating = false;
}
}
});
}
}
// Simplified rotation synchronization for immune enemies
if (self.leftFoot && self.rightFoot && enemyGraphics.targetRotation !== undefined) {
var targetFootRotation = enemyGraphics.targetRotation;
if (self.type === 'immune') {
// Direct rotation for immune enemies for simplicity
self.leftFoot.rotation = targetFootRotation;
self.rightFoot.rotation = targetFootRotation;
} else {
// Smoothly rotate feet to match enemy direction for other enemy types using pooled animations
if (Math.abs(targetFootRotation - (self.leftFoot.rotation || 0)) > 0.05) {
createPooledAnimation(self.leftFoot, {
rotation: targetFootRotation
}, {
duration: 250,
easing: tween.easeOut
});
}
if (Math.abs(targetFootRotation - (self.rightFoot.rotation || 0)) > 0.05) {
createPooledAnimation(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.2; // Simplified energetic movement (reduced from 1.5)
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 and fast enemies
if (self.type === 'swarm' || self.type === 'fast') {
var targetY = combinedBobOffset * animationIntensity;
var targetX = horizontalSway * animationIntensity * 0.3; // Much less horizontal movement
// Simple direct assignment for swarm and fast 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 pooled animation for smoother animation instead of direct assignment
if (!self.animatingMovement) {
self.animatingMovement = true;
createPooledAnimation(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 or fast)
if (self.type !== 'swarm' && self.type !== 'fast') {
var walkRotation = Math.sin(self.walkAnimationPhase * 1.5) * 0.05; // Very subtle rotation
if (!self.animatingRotation) {
self.animatingRotation = true;
createPooledAnimation(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
}
// Special handling for vex enemies - they should always play walking sound when moving
if (self.type === 'vex' && isCurrentlyWalking) {
shouldPlaySound = true; // Always play sound for vex enemies when they are walking
}
// Special handling for swarm enemies - they should also play walking sound when moving
if (self.type === 'swarm' && isCurrentlyWalking) {
shouldPlaySound = true; // Always play sound for swarm enemies when they are walking
}
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, blue, and kırık enemies only when not walking
if ((self.type === 'normal' || self.type === 'swarm' || self.type === 'blue' || self.type === 'kırık') && !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 - adjust for kırık and rat enemies
var restFootYPos = 35;
var restFootSpacing = self.type === 'kırık' ? 20 : self.type === 'rat' ? 25 : 15; // Wider spacing for kırık and rat enemies
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;
// Greatly increased cache timeout based on activity for better performance
var activityLevel = enemies.length + towers.length;
if (activityLevel < 5) {
dynamicCacheTimeout = 900; // 15 seconds for very low activity
} else if (activityLevel < 10) {
dynamicCacheTimeout = 600; // 10 seconds for low activity
} else if (activityLevel < 20) {
dynamicCacheTimeout = 450; // 7.5 seconds for medium activity
} else {
dynamicCacheTimeout = 150; // 2.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 guys sound when vex enemies first enter the screen
if (enemy.type === 'vex' && !window.guysSoundPlayed) {
window.guysSoundPlayed = true;
LK.getSound('guys').play();
}
// Play wu sound when rat enemies first enter the screen
if (enemy.type === 'rat' && !window.wuSoundPlayed) {
window.wuSoundPlayed = true;
LK.getSound('wu').play();
// Play bolt sound 3 seconds after wu sound
tween({}, {}, {
duration: 3000,
onFinish: function onFinish() {
LK.getSound('bolt').play();
// Play hamburger sound 10 seconds after bolt sound
tween({}, {}, {
duration: 10000,
onFinish: function onFinish() {
LK.getSound('hamburger').play();
// Play kola sound 10 seconds after hamburger sound (reduced from 3 to 10 seconds total delay)
tween({}, {}, {
duration: 10000,
onFinish: function onFinish() {
console.log("Playing kola sound");
LK.getSound('kola').play();
// Play hungry sound 10 seconds after kola sound
tween({}, {}, {
duration: 10000,
onFinish: function onFinish() {
console.log("Playing hungry sound");
LK.getSound('hungry').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();
// Spawn 1 rat enemy when for_the_fallen sound starts playing - ONLY in Wave 5
if (currentWave === 5) {
var ratEnemy = new Enemy('rat');
// Add rat enemy to the appropriate layer
if (ratEnemy.isFlying) {
enemyLayerTop.addChild(ratEnemy);
} else {
enemyLayerBottom.addChild(ratEnemy);
}
// Set rat enemy health for Wave 5
ratEnemy.maxHealth = 1500;
ratEnemy.health = ratEnemy.maxHealth;
// Rat enemy spawns from a random column in the middle area
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
var spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
var spawnY = -1 - Math.random() * 5; // Random distance above the grid
ratEnemy.cellX = spawnX;
ratEnemy.cellY = 5; // Position after entry
ratEnemy.currentCellX = spawnX;
ratEnemy.currentCellY = spawnY;
ratEnemy.waveNumber = currentWave;
enemies.push(ratEnemy);
}
}
});
}
// Play xr sound when flying enemies from Wave 4 first enter the screen
if (enemy.type === 'flying' && enemy.waveNumber === 4 && !window.xrSoundPlayed) {
window.xrSoundPlayed = true;
LK.getSound('xr').play();
// Play yarış sound after xr sound finishes - increased delay to ensure xr sound completes
tween({}, {}, {
duration: 4000,
// Wait longer for xr sound to finish completely
onFinish: function onFinish() {
LK.getSound('yar').play();
// Play upit sound after yar sound finishes
tween({}, {}, {
duration: 5000,
// Wait longer for yar sound to finish completely
onFinish: function onFinish() {
LK.getSound('upit').play();
// Play water sound 3 seconds after upit sound finishes
tween({}, {}, {
duration: 4000,
// Wait for upit sound to finish + 3 second delay
onFinish: function onFinish() {
LK.getSound('water').play();
// Play funny sound 3 seconds after water sound finishes
tween({}, {}, {
duration: 5000,
// Wait for water sound to finish + 3 second delay
onFinish: function onFinish() {
LK.getSound('funny').play();
// Play z sound 5 seconds after funny sound finishes
tween({}, {}, {
duration: 5000,
// Wait 5 seconds after funny sound
onFinish: function onFinish() {
LK.getSound('z').play();
// Play sm sound 5 seconds after z sound finishes
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
LK.getSound('sm').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();
// Play sh sound after tw sound finishes
tween({}, {}, {
duration: 15000,
// Adjust timing based on 'tw' sound duration
onFinish: function onFinish() {
LK.getSound('sh').play();
}
});
}
});
}
});
}
// df sound is now played when Wave 8 starts, not when enemies enter screen
}
return false;
}
// Handle vex enemy - move to y13, then move right 3 cells, then stop
if (enemy.type === 'vex') {
// Initialize vex movement phase if not set
if (!enemy.vexPhase) {
enemy.vexPhase = 'movingDown';
enemy.targetX = enemy.currentCellX + 3; // Target position 3 cells to the right
}
if (enemy.vexPhase === 'movingDown') {
// Check if vex enemy has reached y13
if (enemy.currentCellY >= 13) {
// Switch to moving right phase
enemy.vexPhase = 'movingRight';
enemy.currentCellY = 13; // Ensure we're exactly at y13
// Play sound 'n' when vex enemy turns right and track when it finishes
LK.getSound('n').play();
// Use tween to track when 'n' sound finishes (assume it's about 2-3 seconds)
tween({}, {}, {
duration: 2500,
// Estimated duration of 'n' sound in milliseconds
onFinish: function onFinish() {
nSoundFinished = true; // Mark that 'n' sound has finished
}
});
// Simplified asset switching - remove the problematic rotation logic
if (enemy.children[0] && enemy.children[0].parent) {
enemy.removeChild(enemy.children[0]);
var vxGraphics = enemy.attachAsset('vx', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.25,
scaleY: 1.25
});
vxGraphics.rotation = 0; // Face right immediately
}
} else {
// Continue moving down to y13
enemy.currentCellY += enemy.speed;
// Add walking animation for vex enemy during y13 progression
if (!enemy.isFlying) {
var speedMultiplier = enemy.speed * 100;
enemy.walkAnimationPhase += enemy.walkAnimationSpeed * speedMultiplier;
// Create realistic 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 walking
var horizontalSway = Math.sin(enemy.walkAnimationPhase * 0.7) * (enemy.walkBobAmount * 0.15);
if (enemy.children[0]) {
var animationIntensity = 1;
if (enemy.isBoss) {
animationIntensity *= 0.8;
bobOffset *= 1.1;
}
// Apply animation
var targetY = bobOffset * animationIntensity;
var targetX = horizontalSway * animationIntensity;
if (!enemy.vexDownAnimating) {
enemy.vexDownAnimating = true;
tween(enemy.children[0], {
y: targetY,
x: targetX
}, {
duration: 60,
easing: tween.easeOut,
onFinish: function onFinish() {
enemy.vexDownAnimating = false;
}
});
}
}
// Animate vex enemy feet during downward movement with special vex feet
if (enemy.type === 'vex' && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) {
// Ensure feet remain visible during downward movement - force visibility every frame
enemy.leftFoot.visible = true;
enemy.rightFoot.visible = true;
// Keep feet at proper positions during downward movement
var footSpacing = 18; // Special spacing for vex
var footYPos = 35;
enemy.leftFoot.x = -footSpacing;
enemy.leftFoot.y = footYPos;
enemy.rightFoot.x = footSpacing;
enemy.rightFoot.y = footYPos;
// Update foot animation phases for vex with enhanced animation
var animationMultiplier = 3.0; // Increased animation speed for vex
enemy.leftFootPhase += enemy.walkAnimationSpeed * speedMultiplier * animationMultiplier;
enemy.rightFootPhase += enemy.walkAnimationSpeed * speedMultiplier * animationMultiplier;
// Enhanced scaling for vex special feet with alternating pattern
var scaleMultiplier = 0.7; // Increased scaling for more visible animation
var leftFootScale = 1 + Math.abs(Math.sin(enemy.leftFootPhase)) * scaleMultiplier;
var rightFootScale = 1 + Math.abs(Math.sin(enemy.rightFootPhase)) * scaleMultiplier;
// Add vertical bobbing motion to feet for more realistic walking
var leftFootBob = Math.sin(enemy.leftFootPhase) * 2;
var rightFootBob = Math.sin(enemy.rightFootPhase) * 2;
// Apply scaling and position animation for vex feet with shorter duration for snappier movement
if (enemy.leftFoot && !enemy.leftFoot.isAnimating) {
enemy.leftFoot.isAnimating = true;
tween(enemy.leftFoot, {
scaleX: leftFootScale,
scaleY: leftFootScale,
y: footYPos + leftFootBob
}, {
duration: 60,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (enemy.leftFoot) {
enemy.leftFoot.isAnimating = false;
}
}
});
}
if (enemy.rightFoot && !enemy.rightFoot.isAnimating) {
enemy.rightFoot.isAnimating = true;
tween(enemy.rightFoot, {
scaleX: rightFootScale,
scaleY: rightFootScale,
y: footYPos + rightFootBob
}, {
duration: 60,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (enemy.rightFoot) {
enemy.rightFoot.isAnimating = false;
}
}
});
}
}
}
}
// Rotate enemy graphic to face downward while moving
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
});
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
} else if (enemy.vexPhase === 'movingRight') {
// Check if vex enemy has moved 3 cells to the right
if (enemy.currentCellX >= enemy.targetX) {
// Stop moving - we've reached the target position
enemy.vexPhase = 'waiting';
enemy.currentCellX = enemy.targetX; // Ensure exact position
enemy.waitTimer = 0; // Initialize wait timer
// Play 'wc' sound when starting to wait, but only if 'n' sound has finished
if (nSoundFinished) {
// Add additional delay even when 'n' sound is marked as finished to ensure proper timing
tween({}, {}, {
duration: 2000,
// 2 second additional delay (1 second more than before)
onFinish: function onFinish() {
LK.getSound('wc').play();
}
});
} else {
// Wait for 'n' sound to finish before playing 'wc'
var _checkNSoundFinished = function checkNSoundFinished() {
if (nSoundFinished) {
// Add delay to ensure 'n' sound has completely finished
tween({}, {}, {
duration: 2500,
// 2.5 second delay after 'n' finishes (1 second more than before)
onFinish: function onFinish() {
LK.getSound('wc').play();
}
});
} else {
// Check again in 100ms
tween({}, {}, {
duration: 100,
onFinish: _checkNSoundFinished
});
}
};
_checkNSoundFinished();
}
} else {
// Continue moving right
enemy.currentCellX += enemy.speed;
// DO NOT change foot positions during turning movement - keep feet stable
}
} else if (enemy.vexPhase === 'waiting') {
// Wait for 8 seconds (480 frames at 60 FPS)
if (!enemy.waitTimer) enemy.waitTimer = 0;
enemy.waitTimer++;
if (enemy.waitTimer >= 480) {
// Activate vx asset after 5 second wait
if (enemy.children[0] && enemy.children[0].parent) {
enemy.removeChild(enemy.children[0]);
}
var vxGraphics = enemy.attachAsset('vx', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.25,
scaleY: 1.25
});
// No rotation animation - vx asset appears without rotation
vxGraphics.rotation = 0; // Face right immediately without animation
// Start returning to spawn area
enemy.vexPhase = 'returningToSpawn';
enemy.returnTargetX = 14; // Return to original spawn column
enemy.returnTargetY = -3; // Return above the spawn area
}
} else if (enemy.vexPhase === 'returningToSpawn') {
// Initialize safety timer if not set
if (!enemy.returningSafetyTimer) {
enemy.returningSafetyTimer = 0;
}
enemy.returningSafetyTimer++;
// Safety timeout: force delete after 4 seconds (400 frames at 60fps)
if (enemy.returningSafetyTimer > 400) {
// Force delete vex enemy after timeout
if (enemy.parent) {
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
}
var enemyIndex = enemies.indexOf(enemy);
if (enemyIndex !== -1) {
enemies.splice(enemyIndex, 1);
}
// Once vex enemy exits, allow wave 2 to start
if (currentWave === 1) {
// Automatically start wave 2 after vex enemy exits
currentWave = 2;
waveTimer = 0;
waveInProgress = true;
waveSpawned = false;
}
return true; // Return true to indicate enemy reached goal (exit condition)
}
// Move back to spawn area first
var dx = enemy.returnTargetX - enemy.currentCellX;
var dy = enemy.returnTargetY - enemy.currentCellY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Improved deletion conditions - delete vex enemy when it's completely off-screen
if (enemy.currentCellY <= -3 || enemy.y <= -100) {
// Delete vex enemy immediately when it's completely off-screen
if (enemy.parent) {
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
}
var enemyIndex = enemies.indexOf(enemy);
if (enemyIndex !== -1) {
enemies.splice(enemyIndex, 1);
}
// Once vex enemy exits, allow wave 2 to start
if (currentWave === 1) {
// Automatically start wave 2 after vex enemy exits
currentWave = 2;
waveTimer = 0;
waveInProgress = true;
waveSpawned = false;
}
return true; // Return true to indicate enemy reached goal (exit condition)
} else {
// Move toward spawn area
var angle = Math.atan2(dy, dx);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
// Rotate enemy to face 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
});
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
} else if (enemy.vexPhase === 'exitingFromSpawn') {
// Move upward to exit off the top of the screen
enemy.currentCellY -= enemy.speed;
// Rotate enemy to face upward
var angle = -Math.PI / 2; // Face upward
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
});
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Check if enemy has moved far enough off screen to be removed
if (enemy.currentCellY < -5) {
// Remove vex enemy completely off screen
if (enemy.parent) {
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
}
var enemyIndex = enemies.indexOf(enemy);
if (enemyIndex !== -1) {
enemies.splice(enemyIndex, 1);
}
// Once vex enemy exits, allow wave 2 to start
if (currentWave === 1) {
// Automatically start wave 2 after vex enemy exits
currentWave = 2;
waveTimer = 0;
waveInProgress = true;
waveSpawned = false;
}
return true; // Return true to indicate enemy reached goal (exit condition)
}
// Keep enemy image straight during rightward movement (like a crab) - no rotation
// Vex enemy moves sideways without rotating the image asset
// Add walking animation for vex enemy during rightward movement
if (!enemy.isFlying) {
var speedMultiplier = enemy.speed * 100;
enemy.walkAnimationPhase += enemy.walkAnimationSpeed * speedMultiplier;
// Create realistic 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 walking
var horizontalSway = Math.sin(enemy.walkAnimationPhase * 0.7) * (enemy.walkBobAmount * 0.15);
if (enemy.children[0]) {
var animationIntensity = 1;
if (enemy.isBoss) {
animationIntensity *= 0.8;
bobOffset *= 1.1;
}
// Apply animation
var targetY = bobOffset * animationIntensity;
var targetX = horizontalSway * animationIntensity;
if (!enemy.vexRightAnimating) {
enemy.vexRightAnimating = true;
tween(enemy.children[0], {
y: targetY,
x: targetX
}, {
duration: 60,
easing: tween.easeOut,
onFinish: function onFinish() {
enemy.vexRightAnimating = false;
}
});
}
}
// Animate vex enemy feet during rightward movement with special vex feet
if (enemy.type === 'vex' && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) {
// Ensure feet remain visible during rightward movement
enemy.leftFoot.visible = true;
enemy.rightFoot.visible = true;
// Keep feet at proper positions during sideways movement
var footSpacing = 18; // Special spacing for vex
var footYPos = 35;
enemy.leftFoot.x = -footSpacing;
enemy.leftFoot.y = footYPos;
enemy.rightFoot.x = footSpacing;
enemy.rightFoot.y = footYPos;
// Update foot animation phases for vex with enhanced animation
var animationMultiplier = 3.5; // Increased for more active sideways movement
enemy.leftFootPhase += enemy.walkAnimationSpeed * speedMultiplier * animationMultiplier;
enemy.rightFootPhase += enemy.walkAnimationSpeed * speedMultiplier * animationMultiplier;
// Enhanced scaling for vex special feet with more pronounced movement
var scaleMultiplier = 0.8; // Increased scaling for more visible animation
var leftFootScale = 1 + Math.abs(Math.sin(enemy.leftFootPhase)) * scaleMultiplier;
var rightFootScale = 1 + Math.abs(Math.sin(enemy.rightFootPhase)) * scaleMultiplier;
// Add horizontal stepping motion for sideways movement
var leftFootStep = Math.cos(enemy.leftFootPhase) * 3;
var rightFootStep = Math.cos(enemy.rightFootPhase) * 3;
// Add vertical lifting motion for more realistic stepping
var leftFootLift = Math.abs(Math.sin(enemy.leftFootPhase)) * 4;
var rightFootLift = Math.abs(Math.sin(enemy.rightFootPhase)) * 4;
// Apply scaling and position animation for vex feet with shorter duration for snappier movement
if (enemy.leftFoot && !enemy.leftFoot.isAnimating) {
enemy.leftFoot.isAnimating = true;
tween(enemy.leftFoot, {
scaleX: leftFootScale,
scaleY: leftFootScale,
x: -footSpacing + leftFootStep,
y: footYPos - leftFootLift
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (enemy.leftFoot) {
enemy.leftFoot.isAnimating = false;
}
}
});
}
if (enemy.rightFoot && !enemy.rightFoot.isAnimating) {
enemy.rightFoot.isAnimating = true;
tween(enemy.rightFoot, {
scaleX: rightFootScale,
scaleY: rightFootScale,
x: footSpacing + rightFootStep,
y: footYPos - rightFootLift
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (enemy.rightFoot) {
enemy.rightFoot.isAnimating = false;
}
}
});
}
}
}
} else if (enemy.vexPhase === 'waiting' || enemy.vexPhase === 'returningToSpawn') {
// Preserve vex enemy waiting foot positions during turning movement phases
if (enemy.type === 'vex' && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) {
// Force both feet to be visible during all turning phases with maximum visibility
enemy.leftFoot.visible = true;
enemy.rightFoot.visible = true;
enemy.leftFoot.alpha = 1; // Ensure full opacity
enemy.rightFoot.alpha = 1; // Ensure full opacity
// Ensure feet are properly attached to parent
if (!enemy.leftFoot.parent) {
enemy.addChild(enemy.leftFoot);
}
if (!enemy.rightFoot.parent) {
enemy.addChild(enemy.rightFoot);
}
var footSpacing = 18; // Special spacing for vex
var footYPos = 35;
// Always use standard positioning during turning phases to prevent feet from disappearing
enemy.leftFoot.x = -footSpacing;
enemy.leftFoot.y = footYPos;
enemy.rightFoot.x = footSpacing;
enemy.rightFoot.y = footYPos;
// Force both feet to front of display list to ensure visibility
enemy.setChildIndex(enemy.leftFoot, enemy.children.length - 1);
enemy.setChildIndex(enemy.rightFoot, enemy.children.length - 1);
// Stop any ongoing foot animations to prevent position changes
tween.stop(enemy.leftFoot, {
scaleX: true,
scaleY: true,
x: true,
y: true
});
tween.stop(enemy.rightFoot, {
scaleX: true,
scaleY: true,
x: true,
y: true
});
// Reset foot animation flags and ensure feet are properly scaled
if (enemy.leftFoot) {
enemy.leftFoot.isAnimating = false;
enemy.leftFoot.scaleX = 1;
enemy.leftFoot.scaleY = 1;
}
if (enemy.rightFoot) {
enemy.rightFoot.isAnimating = false;
enemy.rightFoot.scaleX = 1;
enemy.rightFoot.scaleY = 1;
}
}
}
// If stopped, just maintain position (no movement)
// Update enemy's position
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
return false; // Don't use normal pathfinding
}
// 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 40 frames for flying enemies (slightly increased frequency)
// Reduce frequency for waves 8+ to minimize animation load
var exhaustFrequency = currentWave >= 8 ? 80 : 40; // Slightly decreased interval for better visual effect
if (enemy.exhaustTimer % exhaustFrequency === 0) {
// Create exhaust particles behind flying enemy using particle pool
// Slightly increased exhaust particle count for better visual effect
var exhaustCount = enemies.length > 10 ? 2 : 3; // Increased to max 3 particles when few enemies, 2 when many
if (currentWave === 7 || currentWave >= 8) {
exhaustCount = Math.floor(exhaustCount * 0.15); // Slightly larger particles for boss wave and waves 8+
}
for (var exhaustIdx = 0; exhaustIdx < exhaustCount; exhaustIdx++) {
var exhaustParticle = getSmokeParticle();
var exhaustGraphics = exhaustParticle.smokeGraphics;
exhaustGraphics.width = 20 + Math.random() * 28;
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.2; // Narrower spread
var exhaustDistance = 30 + Math.random() * 15; // Smaller distance
exhaustParticle.x = enemy.x + Math.cos(exhaustAngle) * exhaustDistance;
exhaustParticle.y = enemy.y + Math.sin(exhaustAngle) * exhaustDistance;
exhaustParticle.alpha = 0.5 + Math.random() * 0.2;
exhaustParticle.scaleX = 0.2 + Math.random() * 0.2;
exhaustParticle.scaleY = 0.2 + Math.random() * 0.2;
game.addChild(exhaustParticle);
// Animate exhaust particles moving away from enemy and fading in synchronized direction
var targetDistance = exhaustDistance + 20 + Math.random() * 15; // Smaller movement range
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 * 1.5,
scaleY: exhaustParticle.scaleY * 1.5
}, {
duration: 500 + Math.random() * 200,
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 kırık enemy - move in L-shape: straight down, then straight right to tabela, then return
if (enemy.type === 'kırık' && enemy.isImmortal) {
// Initialize kırık phase if not set
if (!enemy.kırıkPhase) {
enemy.kırıkPhase = 'movingDown';
enemy.startX = enemy.currentCellX; // Remember starting position
enemy.startY = enemy.currentCellY;
}
// Calculate tabela position in grid coordinates (where tabela display is positioned)
var tabelaGridX = (2048 - 360 - 76 - grid.x) / CELL_SIZE;
var tabelaGridY = (2732 / 2 + 76 - grid.y) / CELL_SIZE;
if (enemy.kırıkPhase === 'movingDown') {
// Move straight down like blue enemy until reaching bottom of screen
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
});
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Check if reached bottom of screen (around where goals are)
if (enemy.currentCellY >= 34) {
enemy.kırıkPhase = 'goingToTabela';
}
} else if (enemy.kırıkPhase === 'goingToTabela') {
// Move straight horizontally toward tabela (L-shape movement)
enemy.currentCellX += enemy.speed; // Move straight right
// Rotate enemy graphic to face right (0 radians)
var angle = 0; // Face right
if (enemy.children[0]) {
if (enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Check if reached tabela X position
if (enemy.currentCellX >= tabelaGridX) {
enemy.kırıkPhase = 'returningToStart';
}
} else if (enemy.kırıkPhase === 'returningToStart') {
// Move straight left back to starting position
enemy.currentCellX -= enemy.speed; // Move straight left
// Rotate enemy graphic to face left (PI radians)
var angle = Math.PI; // Face left
if (enemy.children[0]) {
if (enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Check if reached start position
if (enemy.currentCellX <= enemy.startX) {
enemy.kırıkPhase = 'movingDown'; // Restart the cycle
}
}
// Update visual position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
return false; // Don't use normal pathfinding
}
// 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 () {
// Next wave button is removed - always keep it hidden and disabled
self.enabled = false;
self.visible = false;
self.alpha = 0;
};
self.down = function () {
if (!self.enabled) {
return;
}
// Don't allow manual wave progression during black screen (wave 7 completion)
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7;
if (isBlackScreenActive) {
return; // Block manual wave progression during black screen
}
// Check if any vex enemies are still alive to block manual wave progression
var vexEnemiesAlive = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].type === 'vex') {
vexEnemiesAlive = true;
break;
}
}
// Block manual progression to wave 2 if vex enemies are still alive
if (vexEnemiesAlive && currentWave === 1) {
var notification = game.addChild(new Notification("Defeat all vex enemies before starting next wave!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
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: 80,
fill: 0xFFFFFF,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 50;
notificationGraphics.alpha = 0; // Make background transparent
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;
// Add pulsing animation for tower icons before game starts
if (!waveIndicator.gameStarted && self.visible && !self.isPulsing) {
var _pulseIcon = function pulseIcon() {
if (!waveIndicator.gameStarted && self.visible) {
// First pulse - grow
tween(self, {
scaleX: self.scaleX * 1.15,
scaleY: self.scaleY * 1.15
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!waveIndicator.gameStarted && self.visible) {
// Second pulse - shrink back
tween(self, {
scaleX: self.scaleX / 1.15,
scaleY: self.scaleY / 1.15
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Wait before next pulse cycle
tween({}, {}, {
duration: 600,
onFinish: function onFinish() {
if (!waveIndicator.gameStarted && self.visible) {
_pulseIcon(); // Continue pulsing
} else {
self.isPulsing = false;
}
}
});
}
});
} else {
self.isPulsing = false;
}
}
});
} else {
self.isPulsing = false;
}
};
self.isPulsing = true;
_pulseIcon();
} else if (waveIndicator.gameStarted && self.isPulsing) {
// Stop pulsing when game starts
tween.stop(self, {
scaleX: true,
scaleY: true
});
self.isPulsing = false;
}
};
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 3.0, +0.3 per level (increased effect area)
return (3.0 + (self.level - 1) * 0.3) * 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,
width: 40,
height: 40
});
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 () {
// Prevent upgrading before game starts
if (!waveIndicator || !waveIndicator.gameStarted) {
var notification = game.addChild(new Notification("Cannot upgrade towers before game starts!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
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, but exclude vex enemies
if (self.targetEnemy && self.targetEnemy.parent && self.targetEnemy.health > 0 && self.targetEnemy.type !== 'vex') {
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;
// Skip vex enemies - towers ignore them completely
if (enemy.type === 'vex') continue;
// Kırık enemies can now be targeted like other enemies
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 () {
// Clean up enemy targeting arrays
if (LK.ticks % 180 === 0) {
// Every 3 seconds
if (self.targetEnemy && (!self.targetEnemy.parent || self.targetEnemy.health <= 0)) {
self.targetEnemy = null;
}
}
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 - only for wave 7
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7;
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 - only for wave 7
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7;
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
// Reduce frequency significantly for waves 8+ to minimize animation load
if (currentWave >= 8) {
exhaustFrequency = 120; // Significantly increased frequency for waves 8+
}
if (splashTowerCount > 2) {
// Increase interval (reduce frequency) when more than 2 splash towers
exhaustFrequency = exhaustFrequency + (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 - only for wave 7
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7;
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 (excluding blue enemies, Wave 5, and Wave 7 big enemy)
if (affectedEnemies.length > 0 && !welcomeToHellSoundPlayed) {
// Filter out blue enemies, Wave 5 enemies, and Wave 7 big enemy from welcometohell sound triggering
var validEnemiesForWelcome = affectedEnemies.filter(function (enemy) {
// Exclude big enemy in wave 7
if (enemy.waveNumber === 7 && enemy.type === 'big') return false;
return enemy.type !== 'blue' && enemy.waveNumber !== 5 && enemy.waveNumber !== 7;
});
if (validEnemiesForWelcome.length > 0) {
welcomeToHellSoundPlayed = true;
LK.getSound('welcometohell').play();
}
}
// Count enemies entering slow tower area damage (bronz sound removed for blue enemies and all Wave 5 enemies)
if (affectedEnemies.length > 0) {
// Filter out blue enemies and all Wave 5 enemies from bronz sound triggering AND counting
var validEnemiesForBronz = affectedEnemies.filter(function (enemy) {
return enemy.type !== 'blue' && enemy.waveNumber !== 5;
});
if (validEnemiesForBronz.length > 0) {
slowAreaEnemyCount += validEnemiesForBronz.length;
}
}
// 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
for (var i = 0; i < affectedEnemies.length; i++) {
var affectedEnemy = affectedEnemies[i];
// Create flame animation for each affected enemy using particle pool
var flameCount = 4; // Reduced from 8 to 4 flame particles per enemy
for (var flameIdx = 0; flameIdx < flameCount; flameIdx++) {
var flameParticle = getFireParticle();
var flameGraphics = flameParticle.fireGraphics;
flameGraphics.width = 12 + Math.random() * 16; // Reduced size from 15+20 to 12+16
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() * 30; // Reduced distance from 40 to 30
flameParticle.x = affectedEnemy.x + Math.cos(angle) * distance;
flameParticle.y = affectedEnemy.y + Math.sin(angle) * distance;
flameParticle.alpha = 0.8; // Reduced from 0.9 to 0.8
flameParticle.scaleX = 0.4 + Math.random() * 0.4; // Reduced from 0.5+0.5 to 0.4+0.4
flameParticle.scaleY = 0.4 + Math.random() * 0.4;
game.addChild(flameParticle);
// Animate flame flickering and burning out
tween(flameParticle, {
alpha: 0,
scaleX: flameParticle.scaleX * 1.6,
// Reduced from 1.8 to 1.6
scaleY: flameParticle.scaleY * 1.6,
y: flameParticle.y - 15 - Math.random() * 20 // Reduced from 20+30 to 15+20
}, {
duration: 500 + Math.random() * 300,
//{tm} // Reduced from 600+400 to 500+300
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 {
// Krk sound moved to splash tower bullet hit logic - not played here anymore
}
}
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
});
// Reduce splash bullet animation complexity to minimize load
// Apply much smaller bullet animation with greatly reduced movement for splash bullets
bullet.animationReductionFactor = 0.1; // Reduce movement animation to 10% for splash bullets (reduced from 30%)
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('gozluk').play();
}
});
}
// Play shy sound for sniper tower bullets targeting bagışıkdüşman on 3rd hit - only once per wave
if (typeof window.sniperImmuneHitCounter === 'undefined') {
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() {
try {
LK.getSound('shy').play();
} catch (e) {
console.log("Error playing shy sound:", e);
}
}
});
}
LK.getSound('tower_shoot').play();
// Play krk sound for splash tower bullets targeting bagışıkdüşman on 2nd hit - only once per wave
} else if (self.id === 'splash' && self.targetEnemy && self.targetEnemy.isImmune) {
if (typeof window.splashImmuneHitCounter === 'undefined') {
window.splashImmuneHitCounter = 0;
}
window.splashImmuneHitCounter++;
if (window.splashImmuneHitCounter === 2 && !window.krkSoundPlayedThisWave) {
window.krkSoundPlayedThisWave = true;
try {
LK.getSound('krk').play();
} catch (e) {
console.log("Error playing krk sound:", e);
}
}
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();
}
} 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;
// Hide the placement square for this position
if (waveIndicator && waveIndicator.placementFrame) {
var placementSquares = waveIndicator.placementFrame.children;
// Define the placement positions to match with squares
var placementPositions = [{
x: 11,
y: 6
},
// First square
{
x: 11,
y: 11
},
// Second square
{
x: 11,
y: 16
},
// Third square
{
x: 11,
y: 21
},
// Fourth square
{
x: 11,
y: 26
},
// Fifth square
{
x: 11,
y: 31
} // Sixth square
];
// Find which placement square corresponds to this position
for (var i = 0; i < placementPositions.length; i++) {
if (placementPositions[i].x === gridX && placementPositions[i].y === gridY) {
// Hide the corresponding placement square
if (placementSquares[i]) {
placementSquares[i].visible = false;
}
break;
}
}
}
// 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();
// Only invalidate pathfinding cache for non-poison towers (since poison towers don't block paths)
if (self.id !== 'poison') {
pathfindingCache = null;
}
// Initialize continuous poison cloud animation for poison towers
if (self.id === 'poison') {
self.poisonCloudTimer = 0;
self.createContinuousPoisonClouds = function () {
// Check if black screen is active - only for wave 7
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7;
if (isBlackScreenActive) {
return; // Hide poison animation during black screen
}
// 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 () {
// Check if black screen is active - only for wave 7
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7;
if (isBlackScreenActive) {
return; // Hide splash animation during black screen
}
// 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 ? 4 : 6; // Further reduced count when many enemies
// Reduce exhaust particle count for boss waves and waves 8+ to minimize animation load
if (currentWave === 7 || currentWave >= 8) {
exhaustCount = Math.floor(exhaustCount * 0.2); // Further reduce particle count for boss wave and waves 8+
}
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 () {
// Check if black screen is active - only for wave 7
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7;
if (isBlackScreenActive) {
return; // Hide slow tower animation during black screen
}
// 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);
}
});
}
// Create black smoke particles along with flames for enhanced visual effect
var smokeCount = enemies.length > 15 ? 4 : 6; // Black smoke particles
for (var smokeIdx = 0; smokeIdx < smokeCount; smokeIdx++) {
var smokeParticle = getSmokeParticle();
var smokeGraphics = smokeParticle.smokeGraphics;
smokeGraphics.width = 20 + Math.random() * 30;
smokeGraphics.height = smokeGraphics.width;
// Dark smoke colors - various shades of black and dark gray
var smokeColors = [0x000000, 0x1a1a1a, 0x2a2a2a, 0x404040, 0x333333];
smokeGraphics.tint = smokeColors[Math.floor(Math.random() * smokeColors.length)];
// Position smoke particles in outer rings around the tower
var smokeRingNumber = Math.floor(smokeIdx / 3); // 2 rings of 3 particles each
var smokeAngleInRing = smokeIdx % 3 * (Math.PI * 2 / 3) + Math.random() * 0.8;
var smokeBaseDistance = 50 + smokeRingNumber * 25; // Outer rings for smoke
var smokeDistance = smokeBaseDistance + Math.random() * 30;
smokeParticle.x = self.x + Math.cos(smokeAngleInRing) * smokeDistance;
smokeParticle.y = self.y + Math.sin(smokeAngleInRing) * smokeDistance;
smokeParticle.alpha = 0.6 + Math.random() * 0.3;
smokeParticle.scaleX = 0.4 + Math.random() * 0.4;
smokeParticle.scaleY = 0.4 + Math.random() * 0.4;
game.addChild(smokeParticle);
// Animate black smoke rising and dispersing
var smokeTargetScale = smokeParticle.scaleX * (2.0 + Math.random() * 1.0);
var smokeDrift = Math.random() > 0.5 ? 1 : -1;
var smokeDriftAngle = smokeAngleInRing + smokeDrift * (Math.PI * 0.4 + Math.random() * Math.PI * 0.3);
var smokeDriftRadius = smokeDistance * (0.9 + Math.random() * 0.2);
var smokeTargetX = self.x + Math.cos(smokeDriftAngle) * smokeDriftRadius + (Math.random() - 0.5) * 35;
var smokeTargetY = smokeParticle.y - (30 + Math.random() * 40); // Rise higher than flames
var smokeRotationSpeed = smokeDrift * (Math.PI * 1.5 + Math.random() * Math.PI * 1.0);
tween(smokeParticle, {
x: smokeTargetX,
y: smokeTargetY,
alpha: 0,
scaleX: smokeTargetScale,
scaleY: smokeTargetScale,
rotation: smokeRotationSpeed
}, {
duration: 1200 + Math.random() * 600,
// Smoke lasts slightly longer than flames
easing: tween.easeOut,
onFinish: function onFinish() {
returnParticle(smokeParticle);
}
});
}
};
}
};
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 AND game has started
var canAffordUpgrade = gold >= upgradeCost && waveIndicator && waveIndicator.gameStarted;
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 = false;
// Define the exact placement squares where towers can be placed
var allowedPlacements = [{
x: 11,
y: 6
},
// First placement square
{
x: 11,
y: 11
},
// Second placement square
{
x: 11,
y: 16
},
// Third placement square
{
x: 11,
y: 21
},
// Fourth placement square
{
x: 11,
y: 26
},
// Fifth placement square
{
x: 11,
y: 31
} // Sixth placement square
];
// Check if current grid position matches any allowed placement
for (var p = 0; p < allowedPlacements.length; p++) {
var placement = allowedPlacements[p];
if (self.gridX === placement.x && self.gridY === placement.y) {
validGridPlacement = true;
break;
}
}
// If placement is valid, check if cells are actually available
if (validGridPlacement) {
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) {
// Prevent tower selling until wave 8
if (currentWave < 8) {
var notification = game.addChild(new Notification("Tower selling is prohibited until wave 8!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
// Decrement tower placement counter
towersPlacedCount--;
//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;
// Show the placement square again for this position
if (waveIndicator && waveIndicator.placementFrame) {
var placementSquares = waveIndicator.placementFrame.children;
// Define the placement positions to match with squares
var placementPositions = [{
x: 11,
y: 6
},
// First square
{
x: 11,
y: 11
},
// Second square
{
x: 11,
y: 16
},
// Third square
{
x: 11,
y: 21
},
// Fourth square
{
x: 11,
y: 26
},
// Fifth square
{
x: 11,
y: 31
} // Sixth square
];
// Find which placement square corresponds to this tower's position
for (var i = 0; i < placementPositions.length; i++) {
if (placementPositions[i].x === gridX && placementPositions[i].y === gridY) {
// Show the corresponding placement square again
if (placementSquares[i]) {
placementSquares[i].visible = true;
}
break;
}
}
}
// 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);
// Only invalidate pathfinding cache and recalculate for non-poison towers
if (self.tower.id !== 'poison') {
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("Place 6 Towers", {
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("Place 6 Towers", {
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) {
// Check if minimum towers have been placed
if (towersPlacedCount < minTowersRequired) {
var notification = game.addChild(new Notification("Place " + minTowersRequired + " towers before starting! (" + towersPlacedCount + "/" + minTowersRequired + ")"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
//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
});
// Hide tower placement frame when game starts
if (self.placementFrame) {
tween(self.placementFrame, {
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (self.placementFrame && self.placementFrame.parent) {
self.placementFrame.parent.removeChild(self.placementFrame);
}
}
});
}
// 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 = 4;
} else if (waveNum === 3) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 5;
} else if (waveNum === 4) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 3;
} else if (waveNum === 5) {
block.tint = 0x00AAFF; // Blue color for fast enemies
waveType = "Fast";
enemyType = "fast";
enemyCount = 3; // 3 fast enemies, rat enemy spawned when for_the_fallen sound plays
} else if (waveNum === 6) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 3;
} 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 = 3; // Changed from 4 to 3 for waves 8+
break;
case 1:
// Fast waves
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 3; // Changed from 4 to 3 for waves 8+
break;
case 2:
// Flying waves
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 3; // Changed from 4 to 3 for waves 8+
break;
case 3:
// Swarm waves
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 3; // Changed from 4 to 3 for waves 8+
break;
case 4:
// Normal waves (blue enemies only allowed in wave 1)
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 3; // Changed from 4 to 3 for waves 8+
break;
case 5:
// Normal waves
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 3; // Changed from 4 to 3 for waves 8+
break;
case 6:
// Big boss waves (every 7th wave)
block.tint = 0x8B4513;
waveType = "Big Boss";
enemyType = "big";
enemyCount = 1; // Boss waves always have 1 enemy
break;
}
// Special boss waves every 10 waves
if (waveNum % 10 === 0) {
block.tint = 0x8B4513;
waveType = "Mega Boss";
enemyType = "big";
enemyCount = 1; // Boss waves always have 1 enemy
}
} else {
// Fallback for any additional waves beyond 50
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 4;
}
// --- 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;
}
var baseCount = self.enemyCounts[waveNumber - 1];
// Apply limits for waves 8 and above
if (waveNumber === 8) {
baseCount = 3; // Ensure exactly 3 enemies for wave 8 to match waves 9+
} else if (waveNumber > 8) {
var waveType = self.waveTypes[waveNumber - 1];
if (waveType === 'big') {
// Boss waves limited to 1
baseCount = 1;
} else {
// All enemy types limited to 3 for waves 9+
baseCount = Math.min(3, baseCount);
}
}
return baseCount;
};
// 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;
// Create tower placement area frame (only visible before game starts)
var placementFrame = new Container();
placementFrame.x = grid.x;
placementFrame.y = grid.y;
// Create 6 placement squares in gridx11 area (column 11)
var placementSquares = [];
var _loop = function _loop() {
square = placementFrame.attachAsset('placement_dot', {
anchorX: 0.5,
anchorY: 0.5
});
square.width = CELL_SIZE * 0.8;
square.height = CELL_SIZE * 0.8;
square.tint = 0x00FF00; // Green color
square.alpha = 0.7; // Semi-transparent with better visibility
square.x = 11 * CELL_SIZE + CELL_SIZE / 2; // Column 11, centered
square.y = (6 + squareIndex * 5) * CELL_SIZE + CELL_SIZE / 2; // Spaced 5 cells apart vertically starting from row 6, centered
// Add pulsating heart-like animation to make placement dots eye-catching
function pulsatePlacementDot(dot, delay) {
// Start with a small delay to stagger the animations
tween({}, {}, {
duration: delay,
onFinish: function onFinish() {
function heartBeat() {
// First pulse - bigger scale
tween(dot, {
scaleX: 1.4,
scaleY: 1.4,
alpha: 0.9
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
// Return to normal size
tween(dot, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.7
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
// Second pulse - smaller scale (like heartbeat)
tween(dot, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
// Return to normal again
tween(dot, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.7
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
// Wait before next heartbeat cycle
tween({}, {}, {
duration: 1200,
onFinish: function onFinish() {
if (dot.parent && dot.visible) {
heartBeat(); // Continue pulsating if still visible
}
}
});
}
});
}
});
}
});
}
});
}
heartBeat(); // Start the pulsating animation
}
});
}
// Start pulsating animation for each placement square with staggered timing
pulsatePlacementDot(square, squareIndex * 200); // 200ms delay between each dot
placementSquares.push(square);
},
square;
for (var squareIndex = 0; squareIndex < 6; squareIndex++) {
_loop();
}
// Add placement frame to game
game.addChild(placementFrame);
// Store reference to hide it later
self.placementFrame = placementFrame;
self.addChild(self.positionIndicator);
self.update = function () {
// Update start button text based on tower count
if (!self.gameStarted) {
// Show different text based on tower count
if (towersPlacedCount >= minTowersRequired) {
startText.setText("start");
startTextShadow.setText("start");
startBlock.tint = 0x00AA00; // Green when ready
} else {
startText.setText("place tower");
startTextShadow.setText("place tower");
startBlock.tint = 0x888888; // Gray when not ready
}
startTextShadow.x = 4;
startTextShadow.y = 4;
}
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;
}
// Check if black screen is active to prevent wave progression
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7;
if (currentWave < totalWaves && !isBlackScreenActive) {
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
****/
// Initialize activeParticles array to prevent undefined error
// Add tween tracking and cleanup functions
var activeTweens = [];
var maxActiveTweens = 200;
function trackTween(target, properties, config) {
// Clean up completed tweens
activeTweens = activeTweens.filter(function (tweenData) {
return tweenData.isActive;
});
// Enforce limit on active tweens
if (activeTweens.length >= maxActiveTweens) {
// Stop oldest tweens
var oldestTweens = activeTweens.splice(0, 10);
oldestTweens.forEach(function (tweenData) {
tween.stop(tweenData.target, tweenData.properties);
});
}
// Create wrapped config with cleanup
var wrappedConfig = Object.assign({}, config);
var originalOnFinish = config.onFinish;
var tweenData = {
target: target,
properties: properties,
isActive: true,
startTime: LK.ticks
};
wrappedConfig.onFinish = function () {
tweenData.isActive = false;
if (originalOnFinish) originalOnFinish();
};
activeTweens.push(tweenData);
return tween(target, properties, wrappedConfig);
}
function cleanupInactiveTweens() {
// Remove completed tweens from tracking
activeTweens = activeTweens.filter(function (tweenData) {
return tweenData.isActive;
});
// Force cleanup very old tweens (over 30 seconds)
var currentTime = LK.ticks;
activeTweens = activeTweens.filter(function (tweenData) {
if (currentTime - tweenData.startTime > 1800) {
// 30 seconds at 60fps
tween.stop(tweenData.target, tweenData.properties);
return false;
}
return true;
});
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
var activeParticles = activeParticles || []; // Ensure activeParticles is defined
activeParticles = activeParticles.filter(function (particle) {
return particle && particle.parent;
});
var activeSounds = activeSounds || []; // Ensure activeSounds is defined
activeSounds = activeSounds.filter(function (sound) {
if (sound && sound.isPlaying) {
return true;
} else {
// Force cleanup of stopped sounds
forceCleanupSound(sound);
return false;
}
});
// Particle lifetime management - check and cleanup expired particles
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 lastArrayCleanupTime = 0; // Track when we last cleaned up arrays
var arrayCleanupInterval = 300; // Clean up arrays every 5 seconds (300 frames at 60fps)
var defenses = [];
var selectedTower = null;
var gold = 183;
var lives = 30;
var score = 0;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var spawnInProgress = false; // Additional flag to prevent double spawning
var enemiesSpawnedThisWave = 0; // Counter to track spawned enemies per wave
var nextWaveTime = 12000 / 2;
var towersPlacedCount = 0; // Counter for placed towers
var minTowersRequired = 6; // Minimum towers required to start game
var placementNotification = null; // Notification for tower placement guidance
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 wuSoundPlayed = false; // Track if wu sound has been played for rat enemies - 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 animationObjectPool = []; // Pool for reusable animation objects
var maxPoolSize = 30; // Increased pool size for all particle types
var maxAnimationPoolSize = 20; // Maximum size for animation object pool
// Particle lifetime management system
var activeParticles = []; // Track all active particles for lifetime management
var maxActiveParticles = 100; // Maximum size limit for activeParticles array
var maxParticleLifetime = 3000; // Maximum particle lifetime in milliseconds (3 seconds)
var particleCleanupInterval = 120; // Clean up expired particles every 2 seconds (120 frames at 60fps)
var lastParticleCleanup = 0;
var wave1VexSpawned = false; // Track if vex enemy has been spawned in wave 1
var missSoundStarted = false; // Track if miss sound has started
var vexSpawnTimerStarted = false; // Track if vex spawn timer has started
var vexSpawnTimer = 0; // Timer for vex spawn after miss sound
var nSoundFinished = false; // Track if 'n' sound has finished playing
// Sound pooling system
var soundPool = {};
var maxSoundInstances = 8; // Maximum instances per sound type
var soundCleanupInterval = 1200; // Clean up idle sounds every 20 seconds (1200 frames at 60fps)
var lastSoundCleanup = 0;
var activeSounds = []; // Track currently playing sounds
var maxActiveSounds = 50; // Maximum size limit for activeSounds array
// 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 getAnimationObject() {
if (animationObjectPool.length > 0) {
var animObj = animationObjectPool.pop();
// Reset animation object properties
animObj.isActive = false;
animObj.target = null;
animObj.properties = {};
animObj.config = {};
animObj.startTime = 0;
return animObj;
}
// Create new animation object if pool is empty
var animationObject = {
isActive: false,
target: null,
properties: {},
config: {},
startTime: 0,
currentValues: {},
initialValues: {}
};
return animationObject;
}
function returnAnimationObject(animObj) {
if (!animObj) return;
// Stop any active tweens
if (animObj.target && animObj.isActive) {
tween.stop(animObj.target, animObj.properties);
}
// Reset object state
animObj.isActive = false;
animObj.target = null;
animObj.properties = {};
animObj.config = {};
animObj.startTime = 0;
animObj.currentValues = {};
animObj.initialValues = {};
// Return to pool if not at capacity
if (animationObjectPool.length < maxAnimationPoolSize) {
animationObjectPool.push(animObj);
}
}
function createPooledAnimation(target, properties, config) {
if (!target || _typeof(properties) !== 'object') return null;
var animObj = getAnimationObject();
animObj.target = target;
animObj.properties = Object.assign({}, properties);
animObj.config = Object.assign({}, config);
animObj.isActive = true;
animObj.startTime = LK.ticks;
// Store initial values
for (var prop in properties) {
animObj.initialValues[prop] = target[prop] || 0;
animObj.currentValues[prop] = animObj.initialValues[prop];
}
// Create the actual tween with cleanup callback
var originalOnFinish = config.onFinish;
config.onFinish = function () {
if (originalOnFinish) originalOnFinish();
returnAnimationObject(animObj);
};
tween(target, properties, config);
return animObj;
}
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();
}
}
// Sound pool management functions with stricter limits
function getPooledSound(soundId) {
// Initialize pool for this sound type if it doesn't exist
if (!soundPool[soundId]) {
soundPool[soundId] = {
available: [],
active: [],
createdCount: 0
};
}
var pool = soundPool[soundId];
var sound;
// Try to reuse an available sound instance
if (pool.available.length > 0) {
sound = pool.available.pop();
// Reset sound properties if needed
if (sound.stop) {
sound.stop();
}
} else if (pool.createdCount < maxSoundInstances) {
// Create new sound instance if under limit
sound = LK.getSound(soundId);
pool.createdCount++;
// Wrap the original play method to track usage
var originalPlay = sound.play;
sound.play = function () {
// Move to active list
var availableIndex = pool.available.indexOf(sound);
if (availableIndex !== -1) {
pool.available.splice(availableIndex, 1);
}
if (pool.active.indexOf(sound) === -1) {
pool.active.push(sound);
}
// Track globally for cleanup
sound.lastPlayTime = LK.ticks;
sound.isPlaying = true;
if (activeSounds.indexOf(sound) === -1) {
activeSounds.push(sound);
}
return originalPlay.call(this);
};
} else {
// Pool limit reached, force cleanup oldest sound and reuse
if (pool.active.length > 0) {
sound = pool.active[0];
// Force stop and clear audio buffer references
forceCleanupSound(sound);
// Remove from active list
pool.active.splice(0, 1);
} else if (pool.available.length > 0) {
sound = pool.available.pop();
// Force cleanup of unused instance
forceCleanupSound(sound);
} else {
// Last resort: create temporary sound (will be cleaned up aggressively)
sound = LK.getSound(soundId);
}
}
return sound;
}
function forceCleanupSound(sound) {
// Force stop the sound
if (sound && sound.stop) {
try {
sound.stop();
} catch (e) {
// Ignore errors if sound is already stopped
}
}
// Clear audio buffer references
if (sound) {
sound.isPlaying = false;
sound.lastPlayTime = 0;
// Clear internal references if available
if (sound._source) {
sound._source = null;
}
if (sound._buffer) {
sound._buffer = null;
}
if (sound.source) {
sound.source = null;
}
// Additional cleanup for audio context
if (sound.context) {
sound.context.close();
sound.context = null;
}
}
}
function returnSoundToPool(sound, soundId) {
if (!soundPool[soundId]) return;
var pool = soundPool[soundId];
// Force cleanup before returning to pool
forceCleanupSound(sound);
// Remove from active list
var activeIndex = pool.active.indexOf(sound);
if (activeIndex !== -1) {
pool.active.splice(activeIndex, 1);
}
// Add to available list with stricter size limit
var maxAvailable = Math.floor(maxSoundInstances / 3); // Reduced from /2 to /3
if (pool.available.indexOf(sound) === -1 && pool.available.length < maxAvailable) {
pool.available.push(sound);
} else {
// Force cleanup if pool is full
forceCleanupSound(sound);
}
// Remove from global tracking
var globalIndex = activeSounds.indexOf(sound);
if (globalIndex !== -1) {
activeSounds.splice(globalIndex, 1);
}
}
function cleanupIdleSounds() {
// More aggressive cleanup with stricter limits
var aggressiveCleanupThreshold = soundCleanupInterval / 4; // Clean up 4x more frequently
// Clean up sounds that haven't been used recently
for (var i = activeSounds.length - 1; i >= 0; i--) {
var sound = activeSounds[i];
if (sound.lastPlayTime && LK.ticks - sound.lastPlayTime > aggressiveCleanupThreshold) {
// Force cleanup idle sound
forceCleanupSound(sound);
// Remove from active sounds immediately
activeSounds.splice(i, 1);
// Return to pool or destroy
for (var soundId in soundPool) {
var pool = soundPool[soundId];
var activeIndex = pool.active.indexOf(sound);
if (activeIndex !== -1) {
pool.active.splice(activeIndex, 1);
break;
}
}
}
}
// Enforce stricter pool size limits
for (var soundId in soundPool) {
var pool = soundPool[soundId];
// Reduce available pool size to 1/4 of max instances
var maxAvailable = Math.floor(maxSoundInstances / 4);
while (pool.available.length > maxAvailable) {
var excessSound = pool.available.pop();
forceCleanupSound(excessSound);
pool.createdCount = Math.max(0, pool.createdCount - 1);
}
// Force cleanup active sounds if too many
var maxActive = Math.floor(maxSoundInstances / 2);
while (pool.active.length > maxActive) {
var excessActiveSound = pool.active.shift();
forceCleanupSound(excessActiveSound);
// Remove from global tracking
var globalIndex = activeSounds.indexOf(excessActiveSound);
if (globalIndex !== -1) {
activeSounds.splice(globalIndex, 1);
}
}
}
// Enforce global activeSounds limit more strictly
if (activeSounds.length > maxActiveSounds) {
// Sort by last play time and remove oldest
activeSounds.sort(function (a, b) {
return (a.lastPlayTime || 0) - (b.lastPlayTime || 0);
});
while (activeSounds.length > maxActiveSounds / 2) {
var oldestSound = activeSounds.shift();
forceCleanupSound(oldestSound);
}
}
}
// Override LK.getSound to use pooled sounds for performance-critical sounds
var originalLKGetSound = LK.getSound;
LK.getSound = function (soundId) {
// List of sounds that benefit from pooling (frequently played sounds)
var pooledSounds = ['walking', 'tower_shoot', 'footstep', 'egzos', 'flame_sound', 'poison_bullet_hit', 'bullet_casing_drop', 'bird_chirp'];
if (pooledSounds.indexOf(soundId) !== -1) {
return getPooledSound(soundId);
} else {
// Use original method for less frequently played sounds
return originalLKGetSound.call(this, soundId);
}
};
// 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: 200,
height: 160
});
// Position mary at bottom of ask asset
maryDisplay.x = askDisplay.x + 70; // Moved slightly more to the right
maryDisplay.y = askDisplay.y + 150; // Moved slightly down
// Initially hide mary - will be shown when wave 8 starts
maryDisplay.visible = false;
// 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 - only for wave 7
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7;
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 tabela display in right middle edge area
var tabelaDisplay = game.attachAsset('tabela', {
anchorX: 0.5,
anchorY: 0.5,
width: 160,
height: 160
});
// Position tabela one cell down and one cell left from previous position
tabelaDisplay.x = 2048 - 360 - 76; // Move one cell left (76px)
tabelaDisplay.y = 2732 / 2 + 76 - 650; // Move one cell down (76px) then 650px up
// Add displays directly to game object instead of LK.gui for better visibility
game.addChild(goldDisplay);
game.addChild(livesDisplay);
game.addChild(scoreDisplay);
game.addChild(tabelaDisplay);
// Add mary display last so it appears on top of other elements
game.addChild(maryDisplay);
// Add update method to mary to check wave progression
maryDisplay.update = function () {
// Show mary starting from wave 8
if (currentWave >= 8) {
maryDisplay.visible = true;
}
};
// 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) {
// Define the exact placement squares where towers can be placed
var allowedPlacements = [{
x: 11,
y: 6
},
// First placement square
{
x: 11,
y: 11
},
// Second placement square
{
x: 11,
y: 16
},
// Third placement square
{
x: 11,
y: 21
},
// Fourth placement square
{
x: 11,
y: 26
},
// Fifth placement square
{
x: 11,
y: 31
} // Sixth placement square
];
// Check if current grid position matches any allowed placement
var isAllowedPlacement = false;
for (var p = 0; p < allowedPlacements.length; p++) {
var placement = allowedPlacements[p];
if (gridX === placement.x && gridY === placement.y) {
isAllowedPlacement = true;
break;
}
}
// Block placement if not in allowed positions
if (!isAllowedPlacement) {
return true; // Block placement outside allowed squares
}
// Poison towers never block paths since enemies can pass through them
if (towerType === 'poison') {
return false;
}
// In single column formation, only check vertically adjacent towers
for (var checkY = gridY - 2; checkY <= gridY + 3; checkY++) {
// Skip the cells that would be occupied by the new tower
if (checkY >= gridY && checkY < gridY + 2) {
continue;
}
// Only check within the center column (gridX 11-12)
for (var checkX = gridX; checkX < gridX + 2; checkX++) {
var checkCell = grid.getCell(checkX, checkY);
if (checkCell && checkCell.type === 1) {
// Found a vertically adjacent tower in same column, prevent placement
return true;
}
}
}
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);
// Increment tower placement counter
towersPlacedCount++;
// 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) {
// Clean up any orphaned event listeners
if (LK.ticks % 600 === 0) {
// Every 10 seconds
// Force cleanup of objects without parents
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child && typeof child.down === 'function' && !child.parent) {
// Remove orphaned child with event listeners
game.removeChild(child);
}
}
}
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;
// Snap to nearest valid placement square
var allowedPlacements = [{
x: 11,
y: 6
},
// First placement square
{
x: 11,
y: 11
},
// Second placement square
{
x: 11,
y: 16
},
// Third placement square
{
x: 11,
y: 21
},
// Fourth placement square
{
x: 11,
y: 26
},
// Fifth placement square
{
x: 11,
y: 31
} // Sixth placement square
];
// Find the nearest valid placement square
var nearestSquare = null;
var nearestDistance = Infinity;
var dropX = x;
var dropY = y - CELL_SIZE * 1.5; // Account for the offset used during dragging
for (var p = 0; p < allowedPlacements.length; p++) {
var placement = allowedPlacements[p];
var squareWorldX = grid.x + placement.x * CELL_SIZE + CELL_SIZE;
var squareWorldY = grid.y + placement.y * CELL_SIZE + CELL_SIZE;
var dx = dropX - squareWorldX;
var dy = dropY - squareWorldY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestSquare = placement;
}
}
// Snap to the nearest square if within reasonable range (2 grid cells)
if (nearestSquare && nearestDistance < CELL_SIZE * 2) {
towerPreview.gridX = nearestSquare.x;
towerPreview.gridY = nearestSquare.y;
towerPreview.x = grid.x + nearestSquare.x * CELL_SIZE + CELL_SIZE / 2;
towerPreview.y = grid.y + nearestSquare.y * CELL_SIZE + CELL_SIZE / 2;
towerPreview.checkPlacement();
}
if (towerPreview.canPlace) {
var blockResult = wouldBlockPath(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
if (!blockResult) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
// Check if blocking is due to adjacent towers
var hasAdjacentTower = false;
for (var checkX = towerPreview.gridX - 2; checkX <= towerPreview.gridX + 3; checkX++) {
for (var checkY = towerPreview.gridY - 2; checkY <= towerPreview.gridY + 3; checkY++) {
// Skip the cells that would be occupied by the new tower
if (checkX >= towerPreview.gridX && checkX < towerPreview.gridX + 2 && checkY >= towerPreview.gridY && checkY < towerPreview.gridY + 2) {
continue;
}
var checkCell = grid.getCell(checkX, checkY);
if (checkCell && checkCell.type === 1) {
hasAdjacentTower = true;
break;
}
}
if (hasAdjacentTower) break;
}
var notificationText = hasAdjacentTower ? "Cannot place towers next to each other!" : "Tower would block the path!";
var notification = game.addChild(new Notification(notificationText));
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);
// Show placement notification function
function showPlacementNotification() {
if (!placementNotification) {
placementNotification = new Notification("Place the towers on the green spots.");
placementNotification.x = 2048 / 2;
placementNotification.y = 2732 / 2; // Position at center of screen
game.addChild(placementNotification);
// Make notification persistent by overriding its update method
placementNotification.update = function () {
// Only hide notification after second tower is placed
if (towersPlacedCount >= 2) {
this.destroy();
placementNotification = null;
}
};
}
}
// Automatically place default tower at enemy spawn point nokta1 (first allowed placement position)
var autoPlaceTower = function autoPlaceTower() {
// Use the first placement position which corresponds to nokta1 (enemy spawn area)
var nokta1GridX = 11; // First placement square x coordinate
var nokta1GridY = 6; // First placement square y coordinate
// Check if we can place a default tower here
if (canPlaceTowerType('default') && gold >= getTowerCost('default')) {
// Place the default tower automatically
if (placeTower(nokta1GridX, nokta1GridY, 'default')) {
console.log("Default tower automatically placed at nokta1 spawn point");
// Show placement notification after first tower is placed
showPlacementNotification();
}
}
};
// Place tower immediately when game initializes
autoPlaceTower();
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];
// Remove invalid particles from tracking
if (!particle || !particle.parent || typeof particle.creationTime !== "number") {
if (particle && particle.parent) {
if (particle.destroy) particle.destroy();
}
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
});
// Remove from parent if still attached
if (particle.parent) {
particle.parent.removeChild(particle);
}
// Return to pool or destroy
returnParticle(particle);
}
}
// Enforce maxActiveParticles limit
if (activeParticles.length > maxActiveParticles) {
// Remove oldest particles first
for (var p = 0; p < activeParticles.length - maxActiveParticles; p++) {
var particle = activeParticles[p];
if (particle && particle.parent) {
particle.parent.removeChild(particle);
}
if (particle && particle.destroy) particle.destroy();
}
activeParticles.splice(0, activeParticles.length - maxActiveParticles);
}
}
// Sound pool cleanup - check and cleanup idle sounds
if (LK.ticks - lastSoundCleanup >= soundCleanupInterval) {
lastSoundCleanup = LK.ticks;
cleanupIdleSounds();
}
// Tween cleanup - clean up inactive tweens every 5 seconds
if (LK.ticks % 300 === 0) {
cleanupInactiveTweens();
}
// 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 - only for wave 7
var isBlackScreenActive = fadeToBlackStarted && bossWaveCompleted && currentWave === 7;
// Also prevent wave 8 from starting automatically during black screen
var preventWave8AutoStart = isBlackScreenActive && currentWave === 7;
// Check if any vex enemies are still alive to block wave progression
var vexEnemiesAlive = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].type === 'vex') {
vexEnemiesAlive = true;
break;
}
}
// Block wave progression if vex enemies are still alive and trying to start wave 2
var blockWaveForVex = vexEnemiesAlive && currentWave === 1 && waveTimer >= nextWaveTime;
// Block all wave progression during black screen, including automatic wave 8 start, and when vex enemies are alive
if (waveInProgress && !isBlackScreenActive && !preventWave8AutoStart && !(fadeToBlackStarted && currentWave >= 7) && !blockWaveForVex) {
if (!waveSpawned && !spawnInProgress) {
spawnInProgress = true; // Set spawn in progress flag
enemiesSpawnedThisWave = 0; // Reset spawn counter for new wave
// Clear any remaining enemies from previous waves to prevent confusion
for (var oldEnemyIndex = enemies.length - 1; oldEnemyIndex >= 0; oldEnemyIndex--) {
var oldEnemy = enemies[oldEnemyIndex];
if (oldEnemy.waveNumber < currentWave) {
// Remove old enemy that shouldn't still be spawning
if (oldEnemy.parent) {
if (oldEnemy.isFlying) {
enemyLayerTop.removeChild(oldEnemy);
} else {
enemyLayerBottom.removeChild(oldEnemy);
}
}
enemies.splice(oldEnemyIndex, 1);
}
}
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();
// Play miss sound 5 seconds after og sound
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
LK.getSound('miss').play();
missSoundStarted = true; // Set flag when miss sound starts
// guys sound is now played when vex enemy appears on screen
}
});
}
});
}
});
}
});
}
});
} 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();
}
// Play df sound only once when Wave 8 starts spawning
if (currentWave === 8 && !window.dfSoundPlayed) {
window.dfSoundPlayed = true;
LK.getSound('df').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;
}
// Ensure wave 8+ enemy count is strictly limited to 3 (or 1 for boss waves)
if (currentWave >= 8) {
if (waveType === 'big' || currentWave % 10 === 0) {
enemyCount = 1; // Boss waves always have 1 enemy
} else {
enemyCount = Math.min(3, enemyCount); // All other waves limited to 3 enemies
}
}
// 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 xr sound flag for new wave
window.xrSoundPlayed = false;
// Reset yarış sound flag for new wave
window.yarışSoundPlayed = false;
// Reset guys sound flag for new wave
window.guysSoundPlayed = false;
// 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
if (typeof window.sniperImmuneHitCounter !== 'undefined') {
window.sniperImmuneHitCounter = 0;
}
// Reset splash immune hit counter for new wave
window.splashImmuneHitCounter = 0;
// Spawn the appropriate number of enemies with stricter counter protection
// Double-check that we don't exceed the enemy count for this wave
var actualEnemyCount = Math.min(enemyCount, currentWave >= 8 ? 3 : enemyCount);
for (var i = 0; i < actualEnemyCount && enemiesSpawnedThisWave < actualEnemyCount; i++) {
var enemyType = waveType;
// Special handling for Wave 1: spawn only blue enemies initially
if (currentWave === 1) {
// All enemies in Wave 1 are blue - vex will spawn later
enemyType = 'blue';
} else if (enemyType === 'blue') {
// Blue enemies can only be used in wave 1, replace with normal for other waves
enemyType = 'normal';
}
var enemy = new Enemy(enemyType);
// 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 = 225;
break;
case 2:
fixedHealth = 350;
break;
case 3:
fixedHealth = 350;
break;
case 4:
fixedHealth = 365;
break;
case 5:
fixedHealth = 300;
break;
case 6:
fixedHealth = 555;
break;
case 7:
fixedHealth = 1900;
break;
default:
// For waves 8+, start at 600 health and increase by 20 each wave
if (currentWave >= 8) {
fixedHealth = 600 + (currentWave - 8) * 20; // 600 for wave 8, +20 each wave
} else {
// Fallback for any unexpected cases
fixedHealth = 600;
}
// 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;
// Handle vex enemy spawn location specially
var spawnX;
if (enemyType === 'vex') {
// Vex enemy always spawns from column 14
spawnX = 14;
} else {
// All other enemy types 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
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);
enemiesSpawnedThisWave++; // Increment spawn counter
}
// Complete spawn process - only set waveSpawned to true after ALL enemies are spawned
spawnInProgress = false; // Clear spawn in progress flag
// Final validation: ensure we haven't exceeded the enemy limit for wave 8+
if (currentWave >= 8) {
var currentWaveEnemyCount = 0;
for (var checkIndex = 0; checkIndex < enemies.length; checkIndex++) {
if (enemies[checkIndex].waveNumber === currentWave) {
currentWaveEnemyCount++;
}
}
// If we somehow spawned too many enemies, remove the excess
if (currentWaveEnemyCount > 3) {
for (var excessIndex = enemies.length - 1; excessIndex >= 0 && currentWaveEnemyCount > 3; excessIndex--) {
var excessEnemy = enemies[excessIndex];
if (excessEnemy.waveNumber === currentWave) {
if (excessEnemy.parent) {
if (excessEnemy.isFlying) {
enemyLayerTop.removeChild(excessEnemy);
} else {
enemyLayerBottom.removeChild(excessEnemy);
}
}
enemies.splice(excessIndex, 1);
currentWaveEnemyCount--;
}
}
}
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
// Special handling for Wave 1: spawn vex enemy 3 seconds after miss sound starts playing
// The miss sound is triggered after og sound, so we need to detect when it starts + 3 seconds
if (currentWave === 1 && waveSpawned && !wave1VexSpawned && !spawnInProgress) {
// Check if miss sound has started and spawn vex enemy 3 seconds after
if (missSoundStarted && !vexSpawnTimerStarted) {
vexSpawnTimerStarted = true;
vexSpawnTimer = LK.ticks;
}
if (vexSpawnTimerStarted && LK.ticks - vexSpawnTimer >= 660) {
// 660 frames = 11 seconds at 60 FPS
wave1VexSpawned = true;
var vexEnemy = new Enemy('vex');
// Add vex enemy to the appropriate layer
if (vexEnemy.isFlying) {
enemyLayerTop.addChild(vexEnemy);
} else {
enemyLayerBottom.addChild(vexEnemy);
}
// Set vex enemy health for wave 1
vexEnemy.maxHealth = 195; // Same as other wave 1 enemies
vexEnemy.health = vexEnemy.maxHealth;
// Vex enemy spawns from column 14
var spawnX = 14;
var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading
vexEnemy.cellX = spawnX;
vexEnemy.cellY = 5; // Position after entry
vexEnemy.currentCellX = spawnX;
vexEnemy.currentCellY = spawnY;
vexEnemy.waveNumber = currentWave;
enemies.push(vexEnemy);
enemiesSpawnedThisWave++; // Count vex enemy in spawn total
}
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
spawnInProgress = false; // Reset spawn protection flag
enemiesSpawnedThisWave = 0; // Reset spawn counter
// Reset vex spawn flag for next game
if (currentWave === 1) {
wave1VexSpawned = false;
}
// Set timer to automatically start next wave after 7 seconds if not at final wave
// But only if we're not in black screen state
if (currentWave < totalWaves && !isBlackScreenActive) {
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 (excluding blue enemies and Wave 7 big enemy)
if (enemiesInSlowArea.length === 0 && !sunSoundPlayed && currentWave !== 5) {
// Exclude sun sound for big enemy in wave 7
if (!(currentWave === 7 && enemy.type === 'big') && enemy.type !== 'blue') {
sunSoundPlayed = true;
LK.getSound('sun').play();
}
}
}
}
// Aggressive enemies array cleanup - enforce maximum size limit
var maxEnemiesLimit = 50; // Maximum enemies allowed in memory
if (enemies.length > maxEnemiesLimit) {
// Force cleanup of oldest enemies that are off-screen or invalid
for (var cleanupIndex = enemies.length - 1; cleanupIndex >= 0 && enemies.length > maxEnemiesLimit; cleanupIndex--) {
var enemyToCleanup = enemies[cleanupIndex];
if (!enemyToCleanup || !enemyToCleanup.parent || enemyToCleanup.y < -200 || enemyToCleanup.y > 3000) {
// Force cleanup enemy that's invalid or far off-screen
if (enemyToCleanup && enemyToCleanup.parent) {
if (enemyToCleanup.isFlying) {
enemyLayerTop.removeChild(enemyToCleanup);
} else {
enemyLayerBottom.removeChild(enemyToCleanup);
}
}
enemies.splice(cleanupIndex, 1);
}
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
// Additional safety check for invalid enemies
if (!enemy || !enemy.parent) {
enemies.splice(a, 1);
continue;
}
if (enemy.health <= 0) {
// Aggressive cleanup of bullet references
if (enemy.bulletsTargetingThis) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
if (bullet) {
bullet.targetEnemy = null;
}
}
// Clear the array completely to prevent memory leaks
enemy.bulletsTargetingThis.length = 0;
enemy.bulletsTargetingThis = 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.type === 'vex') && !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;
}
// Enemy death animation removed
// 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);
}
// Force cleanup of enemy references before removal
enemy.targetEnemy = null;
enemy.currentTarget = null;
if (enemy.bulletsTargetingThis) {
enemy.bulletsTargetingThis = null;
}
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.type === 'vex') && !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();
}
}
}
// Aggressive bullets array cleanup - enforce maximum size limit
var maxBulletsLimit = 100; // Maximum bullets allowed in memory
if (bullets.length > maxBulletsLimit) {
// Force cleanup of oldest bullets or those without targets
for (var cleanupIndex = bullets.length - 1; cleanupIndex >= 0 && bullets.length > maxBulletsLimit; cleanupIndex--) {
var bulletToCleanup = bullets[cleanupIndex];
if (!bulletToCleanup || !bulletToCleanup.parent || !bulletToCleanup.targetEnemy || bulletToCleanup.targetEnemy.health <= 0) {
// Force cleanup bullet references
if (bulletToCleanup && bulletToCleanup.targetEnemy && bulletToCleanup.targetEnemy.bulletsTargetingThis) {
var bulletIndex = bulletToCleanup.targetEnemy.bulletsTargetingThis.indexOf(bulletToCleanup);
if (bulletIndex !== -1) {
bulletToCleanup.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
if (bulletToCleanup && bulletToCleanup.parent) {
bulletToCleanup.parent.removeChild(bulletToCleanup);
}
bullets.splice(cleanupIndex, 1);
}
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
// Additional safety checks for invalid bullets
if (!bullet) {
bullets.splice(i, 1);
continue;
}
if (!bullet.parent) {
// Aggressive cleanup of target enemy references
if (bullet.targetEnemy) {
var targetEnemy = bullet.targetEnemy;
if (targetEnemy && targetEnemy.bulletsTargetingThis) {
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullet);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
}
// Clear bullet references to prevent memory leaks
bullet.targetEnemy = null;
bullet.sourceTower = null;
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Smart pathfinding optimization: only trigger when enemies need updated paths
var needsPathfindingUpdate = false;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Only trigger pathfinding for enemies that have entered the gameplay area
if (enemy.currentCellY >= 4 && !enemy.isFlying && enemy.type !== 'vex') {
// Check if enemy has a valid current target with correct pathId
if (!enemy.currentTarget || enemy.currentTarget.pathId !== pathId) {
needsPathfindingUpdate = true;
break;
}
}
}
// Trigger pathfinding update only when needed and not too frequently
if (needsPathfindingUpdate && (!pathfindingCache || LK.ticks - lastPathfindTime > 180)) {
grid.pathFind();
}
// 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('notification', {
anchorX: 0.5,
anchorY: 0.5
});
homeButtonBg.width = 600;
homeButtonBg.height = 180;
homeButtonBg.tint = 0xFF0000;
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('notification', {
anchorX: 0.5,
anchorY: 0.5
});
continueButtonBg.width = 600;
continueButtonBg.height = 180;
continueButtonBg.tint = 0x00FF00;
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 () {
// Stop son sound if it's currently playing
try {
LK.getSound('son').stop();
} catch (e) {
// Sound might not support stop method, ignore error
}
// Continue to next wave (Wave 8)
// Hide the overlay and buttons
blackOverlay.alpha = 0;
centerText.alpha = 0;
homeButton.alpha = 0;
continueButton.alpha = 0;
// IMPORTANT: Reset black screen state FIRST to disable it for waves 8+
bossWaveCompleted = false;
fadeToBlackStarted = false;
// Reset game state for next wave (only set to 8 if we're actually at wave 7)
if (currentWave === 7) {
currentWave = 8; // Only advance to wave 8 if we're coming from wave 7
}
waveTimer = 0; // Reset timer to start next wave immediately
waveInProgress = true;
waveSpawned = 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 && currentWave === 7;
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;
};
// Periodic array cleanup and maintenance to prevent memory leaks
if (LK.ticks - lastArrayCleanupTime >= arrayCleanupInterval) {
lastArrayCleanupTime = LK.ticks;
// Clean up invalid tower references
for (var t = towers.length - 1; t >= 0; t--) {
var tower = towers[t];
if (!tower || !tower.parent) {
// Force cleanup tower references
if (tower) {
tower.targetEnemy = null;
if (tower.cellsInRange) {
// Clean up cell references to this tower
for (var c = 0; c < tower.cellsInRange.length; c++) {
var cell = tower.cellsInRange[c];
if (cell && cell.towersInRange) {
var towerIndex = cell.towersInRange.indexOf(tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
tower.cellsInRange.length = 0;
tower.cellsInRange = null;
}
}
towers.splice(t, 1);
}
}
// Force garbage collection hints by nullifying unused references
if (enemies.length === 0 && !waveInProgress) {
// Clean up any residual enemy references when no enemies present
for (var b = bullets.length - 1; b >= 0; b--) {
var bulletForCleanup = bullets[b];
if (bulletForCleanup && (!bulletForCleanup.targetEnemy || bulletForCleanup.targetEnemy.health <= 0)) {
if (bulletForCleanup.parent) {
bulletForCleanup.parent.removeChild(bulletForCleanup);
}
bulletForCleanup.targetEnemy = null;
bulletForCleanup.sourceTower = null;
bullets.splice(b, 1);
}
}
}
// Enforce strict limits on all arrays
var maxEnemies = Math.min(50, currentWave * 5); // Scale with wave but cap at 50
var maxBullets = Math.min(100, towers.length * 10); // Scale with tower count but cap at 100
var maxTowers = 20; // Hard limit on towers
// Trim arrays if they exceed limits
if (enemies.length > maxEnemies) {
enemies.length = maxEnemies;
}
if (bullets.length > maxBullets) {
// Remove oldest bullets first
bullets.splice(0, bullets.length - maxBullets);
}
if (towers.length > maxTowers) {
towers.length = maxTowers;
}
}
// Win condition: Only show you win when all 50 waves are completed
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
};
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
Taş duvar. In-Game asset. 2d. High contrast. No shadows
Kuş bakışı savaş uçağı. In-Game asset. 2d. High contrast. No shadows
Üstten bakış yere sabitlenmiş silah. In-Game asset. 2d. High contrast. No shadows
Alev. In-Game asset. 2d. High contrast. No shadows
Yandan bakış tank. In-Game asset. 2d. High contrast. No shadows
Sniper soilder. In-Game asset. 2d. High contrast. No shadows
Top atan makina yukardan bakış. In-Game asset. 2d. High contrast. No shadows
Zehir bulutu. In-Game asset. 2d. High contrast. No shadows
Ortadaki $ kaldır düz sarı renk
Kalp. In-Game asset. 2d. High contrast. No shadows
Yeşil bir şişe içinde yeşil sıvı var. In-Game asset. 2d. High contrast. No shadows
Taç. In-Game asset. 2d. High contrast. No shadows
Tepeden bakış tank. In-Game asset. 2d. High contrast. No shadows
Yıldız. In-Game asset. 2d. High contrast. No shadows
Yukarı yükseltme oku yeşil. In-Game asset. 2d. High contrast. No shadows
Alev makinesi. In-Game asset. 2d. High contrast. No shadows
Asker kaskı. Önden bakış kayışı yok In-Game asset. 2d. High contrast. No shadows
Anime
Anime
Kask ve pantolonu siyah renk olsun
Tahta bir tabela üzerinde fort yazıyor. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Farklı varyasyonlarda üret
tower_shoot
Sound effect
footstep
Sound effect
splash_impact
Sound effect
tower_place
Sound effect
button_start
Sound effect
game_music
Music
tower_upgrade
Sound effect
tower_sell
Sound effect
poison_impact
Sound effect
poison_bullet_hit
Sound effect
splash_place
Sound effect
gogogo
Sound effect
whofarted
Sound effect
youarekillingme
Sound effect
itdidnthurt
Sound effect
youcantstopus
Sound effect
wearecomingforyou
Sound effect
youwillnevergiveup
Sound effect
giddayfordie
Sound effect
goddayfordie
Sound effect
wewin
Sound effect
gidiklaniyorum
Sound effect
yessir
Sound effect
becareful
Sound effect
sniperr
Sound effect
keepmoving
Sound effect
walking
Sound effect
gooddayfordie
Sound effect
bullet_casing_drop
Sound effect
goddamit
Sound effect
egzos
Sound effect
fast_enemy_sound
Sound effect
for_the_fallen
Sound effect
slow_bullet_shoot
Sound effect
flame_sound
Sound effect
alanhasar
Sound effect
welcometohell
Sound effect
sun
Sound effect
wifi
Sound effect
taksi
Sound effect
vasiyet
Sound effect
gzlk
Sound effect
gozluk
Sound effect
shy
Sound effect
krk
Sound effect
mask
Sound effect
shoes
Sound effect
browser
Sound effect
tnk
Sound effect
bird_chirp
Sound effect
war
Sound effect
place
Sound effect
sevda
Sound effect
mov
Sound effect
og
Sound effect
mezar
Sound effect
we
Sound effect
ek
Sound effect
tw
Sound effect
son
Sound effect
miss
Sound effect
xr
Sound effect
yar
Sound effect
upit
Sound effect
df
Sound effect
guys
Sound effect
n
Sound effect
wc
Sound effect
water
Sound effect
funny
Sound effect
z
Sound effect
borc
Sound effect
sm
Sound effect
db
Sound effect
wu
Sound effect
bolt
Sound effect
hamburger
Sound effect
kola
Sound effect
hungry
Sound effect
sh
Sound effect