User prompt
Sallanma degil vex son aşamada gridx14 e dönmüyor mu onu gridx13 yapmak istiyorum
User prompt
Yengeç gibi ilerlesin ama sallanarak ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Ana yürüme animasyonu sistemi vex için çalıştırılır - Ama vex'in özel hareket aşamalarına göre ``enemy.children[0]' (ana grafik) animasyonu uygulayın - Ayak animasyonu ile ana vücut animasyonunu birleştirin ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Ayak büyüyüp küçülme oranı cok yüksek azalt ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Ana yürüme ayakları hala cok büyük ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Ana yürümede ayaklar cok büyük biraz küçült ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
4. Uygula ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Ves Uptade metodundaki ana yürüme animasyonu sağa dönüşe başladıgında durdururlsun ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Vex bekleme sırasında ayaklar sabit kalmalı hareket etmemeli ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Ayak büyüyüp küçülme oranı cok az ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Vex oyın alanına girdiginde resim varlıgı yürüme animasyonu çalışmıyor diger düşmanlar gibi sallanmalı ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Bekleme sırasonda vex ayakladı sabit kalsın ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Oyun alanına girmeden önce ayakları çok büyük gözüküyor ↪💡 Consider importing and using the following plugins: @upit/tween.v1
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
/**** * 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 - start smaller before entering game area 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); // Add vex enemy support for walking animation - vex enemies should sway during their movement phases if (self.type === 'vex' && isOnScreen && !self.isFlying) { // Vex enemy is considered walking during movingDown and movingRight phases if (self.vexPhase === 'movingDown' || self.vexPhase === 'movingRight') { isCurrentlyWalking = true; } } // Apply main walking animation to vex enemies during their movement phases if (self.type === 'vex' && isOnScreen && !self.isFlying && (self.vexPhase === 'movingDown' || self.vexPhase === 'movingRight')) { // Use the main walking animation system for vex enemies if (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); // Apply animation to enemy.children[0] (main graphic) instead of separate animation if (self.children[0]) { var animationIntensity = 1.2; // Slightly more intense for vex if (self.isBoss) { animationIntensity *= 0.8; combinedBobOffset *= 1.1; } // Apply main body animation var targetY = combinedBobOffset * animationIntensity; var targetX = horizontalSway * animationIntensity; if (!self.vexMainBodyAnimating) { self.vexMainBodyAnimating = true; tween(self.children[0], { y: targetY, x: targetX }, { duration: 60, easing: tween.easeOut, onFinish: function onFinish() { self.vexMainBodyAnimating = false; } }); } } } } // 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.2; // Reduced scaling for vex (was 0.5) } else if (self.type === 'rat') { scaleMultiplier = 0.4; // Reduced scaling for rat feet (was 1.45) } else if (self.type === 'kırık') { scaleMultiplier = 0.25; // Reduced scaling for kırık (was 0.6) } else if (self.type === 'immune') { scaleMultiplier = 0.15; // Reduced scaling for immune (was 0.3) } else { scaleMultiplier = 0.2; // Reduced default scaling (was 0.4) } 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); // Scale up vex enemy feet to proper size when entering game area if (enemy.type === 'vex' && enemy.leftFoot && enemy.rightFoot) { // Animate feet to proper size tween(enemy.leftFoot.children[0], { width: 24, height: 18 }, { duration: 300, easing: tween.easeOut }); tween(enemy.rightFoot.children[0], { width: 24, height: 18 }, { duration: 300, easing: tween.easeOut }); } // 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 = 1.8; // Significantly increased scaling for much 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) * 3; var rightFootBob = Math.sin(enemy.rightFootPhase) * 3; // Apply continuous scaling animation for vex feet with proper tween cycling if (!enemy.leftFoot.isScaling) { enemy.leftFoot.isScaling = true; var _animateLeftFoot = function animateLeftFoot() { if (enemy.leftFoot && enemy.leftFoot.parent) { tween(enemy.leftFoot, { scaleX: leftFootScale, scaleY: leftFootScale, y: footYPos + leftFootBob }, { duration: 150, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.leftFoot && enemy.leftFoot.parent) { // Reverse the animation for continuous scaling tween(enemy.leftFoot, { scaleX: 1, scaleY: 1, y: footYPos }, { duration: 150, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.leftFoot && enemy.leftFoot.parent) { _animateLeftFoot(); // Continue animation loop } } }); } } }); } }; _animateLeftFoot(); } if (!enemy.rightFoot.isScaling) { enemy.rightFoot.isScaling = true; var _animateRightFoot = function animateRightFoot() { if (enemy.rightFoot && enemy.rightFoot.parent) { tween(enemy.rightFoot, { scaleX: rightFootScale, scaleY: rightFootScale, y: footYPos + rightFootBob }, { duration: 150, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.rightFoot && enemy.rightFoot.parent) { // Reverse the animation for continuous scaling tween(enemy.rightFoot, { scaleX: 1, scaleY: 1, y: footYPos }, { duration: 150, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.rightFoot && enemy.rightFoot.parent) { _animateRightFoot(); // Continue animation loop } } }); } } }); } }; _animateRightFoot(); } } } } // 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 // CRITICAL: Stop main walking animation when turning right if (enemy.leftFoot && enemy.rightFoot) { // Stop any ongoing foot animations 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 animation flags enemy.leftFoot.isAnimating = false; enemy.leftFoot.isRightScaling = false; enemy.rightFoot.isAnimating = false; enemy.rightFoot.isRightScaling = false; // Keep feet at static positions enemy.leftFoot.scaleX = 1; enemy.leftFoot.scaleY = 1; enemy.rightFoot.scaleX = 1; enemy.rightFoot.scaleY = 1; } // 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) { // CRITICAL: Preserve feet during asset switching var leftFootBackup = null; var rightFootBackup = null; // Store current foot state before asset switch if (enemy.leftFoot && enemy.leftFoot.parent) { leftFootBackup = { x: enemy.leftFoot.x, y: enemy.leftFoot.y, scaleX: enemy.leftFoot.scaleX, scaleY: enemy.leftFoot.scaleY, rotation: enemy.leftFoot.rotation, visible: enemy.leftFoot.visible, alpha: enemy.leftFoot.alpha }; } if (enemy.rightFoot && enemy.rightFoot.parent) { rightFootBackup = { x: enemy.rightFoot.x, y: enemy.rightFoot.y, scaleX: enemy.rightFoot.scaleX, scaleY: enemy.rightFoot.scaleY, rotation: enemy.rightFoot.rotation, visible: enemy.rightFoot.visible, alpha: enemy.rightFoot.alpha }; } // Switch to vx asset 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 // CRITICAL: Restore feet after asset switch if (leftFootBackup && enemy.leftFoot) { enemy.leftFoot.x = leftFootBackup.x; enemy.leftFoot.y = leftFootBackup.y; enemy.leftFoot.scaleX = leftFootBackup.scaleX; enemy.leftFoot.scaleY = leftFootBackup.scaleY; enemy.leftFoot.rotation = leftFootBackup.rotation; enemy.leftFoot.visible = true; // Force visible enemy.leftFoot.alpha = 1; // Force full opacity // Ensure left foot is properly attached if (!enemy.leftFoot.parent) { enemy.addChild(enemy.leftFoot); } } if (rightFootBackup && enemy.rightFoot) { enemy.rightFoot.x = rightFootBackup.x; enemy.rightFoot.y = rightFootBackup.y; enemy.rightFoot.scaleX = rightFootBackup.scaleX; enemy.rightFoot.scaleY = rightFootBackup.scaleY; enemy.rightFoot.rotation = rightFootBackup.rotation; enemy.rightFoot.visible = true; // Force visible enemy.rightFoot.alpha = 1; // Force full opacity // Ensure right foot is properly attached if (!enemy.rightFoot.parent) { enemy.addChild(enemy.rightFoot); } } // Emergency foot recreation if backup failed if (!enemy.leftFoot || !enemy.leftFoot.parent) { enemy.leftFoot = new Container(); var leftFootGraphics = enemy.leftFoot.attachAsset('vxFeet', { anchorX: 0.5, anchorY: 0.5 }); leftFootGraphics.width = 24; leftFootGraphics.height = 18; leftFootGraphics.tint = 0x000000; enemy.leftFoot.x = -18; enemy.leftFoot.y = 35; enemy.leftFoot.visible = true; enemy.leftFoot.alpha = 1; enemy.addChild(enemy.leftFoot); } if (!enemy.rightFoot || !enemy.rightFoot.parent) { enemy.rightFoot = new Container(); var rightFootGraphics = enemy.rightFoot.attachAsset('vxFeet', { anchorX: 0.5, anchorY: 0.5 }); rightFootGraphics.width = 24; rightFootGraphics.height = 18; rightFootGraphics.tint = 0x000000; enemy.rightFoot.x = 18; enemy.rightFoot.y = 35; enemy.rightFoot.visible = true; enemy.rightFoot.alpha = 1; enemy.addChild(enemy.rightFoot); } // Start returning to spawn area enemy.vexPhase = 'returningToSpawn'; enemy.returnTargetX = 13; // Return to gridX13 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) } // Apply main walking animation to vex enemy during rightward movement for crab-like swaying if (!enemy.isFlying && shouldAnimate) { 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.2; // Slightly more intense for vex crab-like movement if (enemy.isBoss) { animationIntensity *= 0.8; bobOffset *= 1.1; } // Apply main body animation to enemy.children[0] (main graphic) for crab-like swaying var targetY = bobOffset * animationIntensity; var targetX = horizontalSway * animationIntensity; if (!enemy.vexMainBodyAnimating) { enemy.vexMainBodyAnimating = true; tween(enemy.children[0], { y: targetY, x: targetX }, { duration: 60, easing: tween.easeOut, onFinish: function onFinish() { enemy.vexMainBodyAnimating = 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 = 1.9; // Significantly increased scaling for much 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) * 4; var rightFootStep = Math.cos(enemy.rightFootPhase) * 4; // Add vertical lifting motion for more realistic stepping var leftFootLift = Math.abs(Math.sin(enemy.leftFootPhase)) * 5; var rightFootLift = Math.abs(Math.sin(enemy.rightFootPhase)) * 5; // Apply continuous scaling animation for vex feet during rightward movement if (!enemy.leftFoot.isRightScaling) { enemy.leftFoot.isRightScaling = true; var _animateLeftFootRight = function animateLeftFootRight() { if (enemy.leftFoot && enemy.leftFoot.parent && enemy.vexPhase === 'movingRight') { tween(enemy.leftFoot, { scaleX: leftFootScale, scaleY: leftFootScale, x: -footSpacing + leftFootStep, y: footYPos - leftFootLift }, { duration: 120, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.leftFoot && enemy.leftFoot.parent && enemy.vexPhase === 'movingRight') { // Reverse the animation for continuous scaling tween(enemy.leftFoot, { scaleX: 1, scaleY: 1, x: -footSpacing, y: footYPos }, { duration: 120, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.leftFoot && enemy.leftFoot.parent && enemy.vexPhase === 'movingRight') { _animateLeftFootRight(); // Continue animation loop } else { enemy.leftFoot.isRightScaling = false; } } }); } else { enemy.leftFoot.isRightScaling = false; } } }); } }; _animateLeftFootRight(); } if (!enemy.rightFoot.isRightScaling) { enemy.rightFoot.isRightScaling = true; var _animateRightFootRight = function animateRightFootRight() { if (enemy.rightFoot && enemy.rightFoot.parent && enemy.vexPhase === 'movingRight') { tween(enemy.rightFoot, { scaleX: rightFootScale, scaleY: rightFootScale, x: footSpacing + rightFootStep, y: footYPos - rightFootLift }, { duration: 120, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.rightFoot && enemy.rightFoot.parent && enemy.vexPhase === 'movingRight') { // Reverse the animation for continuous scaling tween(enemy.rightFoot, { scaleX: 1, scaleY: 1, x: footSpacing, y: footYPos }, { duration: 120, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.rightFoot && enemy.rightFoot.parent && enemy.vexPhase === 'movingRight') { _animateRightFootRight(); // Continue animation loop } else { enemy.rightFoot.isRightScaling = false; } } }); } else { enemy.rightFoot.isRightScaling = false; } } }); } }; _animateRightFootRight(); } } } } else if (enemy.vexPhase === 'waiting' || enemy.vexPhase === 'returningToSpawn') { // Enhanced foot preservation and animation for vex enemies during all movement phases - special focus on left foot if (enemy.type === 'vex' && !enemy.isBoss && enemy.leftFoot && enemy.rightFoot) { // CRITICAL: Force both feet to be visible and properly positioned every frame 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 - critical fix 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; // ENHANCED: Force fixed positioning every frame to prevent drift during turns 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 if (enemy.leftFoot.parent === enemy) { enemy.setChildIndex(enemy.leftFoot, enemy.children.length - 1); } if (enemy.rightFoot.parent === enemy) { enemy.setChildIndex(enemy.rightFoot, enemy.children.length - 1); } // Keep vex feet static during waiting phase - no idle animation if (enemy.vexPhase === 'waiting') { // During waiting, keep feet completely static - no animation // Stop any ongoing foot animations 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 animation flags if (enemy.leftFoot) { enemy.leftFoot.isIdleScaling = false; enemy.leftFoot.isRightScaling = false; enemy.leftFoot.isAnimating = false; } if (enemy.rightFoot) { enemy.rightFoot.isIdleScaling = false; enemy.rightFoot.isRightScaling = false; enemy.rightFoot.isAnimating = false; } // Keep feet at static scale and position enemy.leftFoot.scaleX = 1; enemy.leftFoot.scaleY = 1; enemy.rightFoot.scaleX = 1; enemy.rightFoot.scaleY = 1; } else if (enemy.vexPhase === 'returningToSpawn') { // Add subtle idle animation for feet during return phase only // Update foot animation phases for subtle movement var idleAnimationMultiplier = 1.5; // Slower animation for idle state enemy.leftFootPhase += 0.05 * idleAnimationMultiplier; enemy.rightFootPhase += 0.05 * idleAnimationMultiplier; // Create subtle breathing-like animation for feet var idleScaleMultiplier = 1.2; // Significantly increased scale changes for much more visible idle animation var leftFootIdleScale = 1 + Math.abs(Math.sin(enemy.leftFootPhase)) * idleScaleMultiplier; var rightFootIdleScale = 1 + Math.abs(Math.sin(enemy.rightFootPhase)) * idleScaleMultiplier; // Apply continuous idle animation to feet if (!enemy.leftFoot.isIdleScaling) { enemy.leftFoot.isIdleScaling = true; var _animateLeftFootIdle = function animateLeftFootIdle() { if (enemy.leftFoot && enemy.leftFoot.parent && enemy.vexPhase === 'returningToSpawn') { tween(enemy.leftFoot, { scaleX: leftFootIdleScale, scaleY: leftFootIdleScale }, { duration: 200, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.leftFoot && enemy.leftFoot.parent && enemy.vexPhase === 'returningToSpawn') { // Reverse the animation for continuous scaling tween(enemy.leftFoot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.leftFoot && enemy.leftFoot.parent && enemy.vexPhase === 'returningToSpawn') { _animateLeftFootIdle(); // Continue animation loop } else { enemy.leftFoot.isIdleScaling = false; } } }); } else { enemy.leftFoot.isIdleScaling = false; } } }); } }; _animateLeftFootIdle(); } if (!enemy.rightFoot.isIdleScaling) { enemy.rightFoot.isIdleScaling = true; var _animateRightFootIdle = function animateRightFootIdle() { if (enemy.rightFoot && enemy.rightFoot.parent && enemy.vexPhase === 'returningToSpawn') { tween(enemy.rightFoot, { scaleX: rightFootIdleScale, scaleY: rightFootIdleScale }, { duration: 200, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.rightFoot && enemy.rightFoot.parent && enemy.vexPhase === 'returningToSpawn') { // Reverse the animation for continuous scaling tween(enemy.rightFoot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeInOut, onFinish: function onFinish() { if (enemy.rightFoot && enemy.rightFoot.parent && enemy.vexPhase === 'returningToSpawn') { _animateRightFootIdle(); // Continue animation loop } else { enemy.rightFoot.isIdleScaling = false; } } }); } else { enemy.rightFoot.isIdleScaling = false; } } }); } }; _animateRightFootIdle(); } } else { // Stop any ongoing foot animations to prevent position changes during other phases 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) { if (enemy.vexPhase !== 'waiting' && enemy.vexPhase !== 'returningToSpawn') { enemy.leftFoot.isAnimating = false; enemy.leftFoot.scaleX = 1; enemy.leftFoot.scaleY = 1; } // CRITICAL: Force left foot positioning - additional enforcement enemy.leftFoot.x = -footSpacing; enemy.leftFoot.y = footYPos; // EMERGENCY: Force left foot recreation if missing graphics if (!enemy.leftFoot.children || enemy.leftFoot.children.length === 0) { var leftFootGraphics = enemy.leftFoot.attachAsset('vexFeet', { anchorX: 0.5, anchorY: 0.5 }); leftFootGraphics.width = 24; leftFootGraphics.height = 18; leftFootGraphics.tint = 0x000000; } } if (enemy.rightFoot) { if (enemy.vexPhase !== 'waiting' && enemy.vexPhase !== 'returningToSpawn') { enemy.rightFoot.isAnimating = false; enemy.rightFoot.scaleX = 1; enemy.rightFoot.scaleY = 1; } // Ensure right foot positioning enemy.rightFoot.x = footSpacing; enemy.rightFoot.y = footYPos; } // Additional safety check - recreate feet if they've been lost if (!enemy.leftFoot || !enemy.rightFoot) { // Recreate missing feet if (!enemy.leftFoot) { enemy.leftFoot = new Container(); var leftFootGraphics = enemy.leftFoot.attachAsset('vexFeet', { anchorX: 0.5, anchorY: 0.5 }); leftFootGraphics.width = 24; leftFootGraphics.height = 18; leftFootGraphics.tint = 0x000000; enemy.leftFoot.x = -footSpacing; enemy.leftFoot.y = footYPos; enemy.addChild(enemy.leftFoot); } if (!enemy.rightFoot) { enemy.rightFoot = new Container(); var rightFootGraphics = enemy.rightFoot.attachAsset('vxFeet', { anchorX: 0.5, anchorY: 0.5 }); rightFootGraphics.width = 24; rightFootGraphics.height = 18; rightFootGraphics.tint = 0x000000; enemy.rightFoot.x = footSpacing; enemy.rightFoot.y = footYPos; enemy.addChild(enemy.rightFoot); } } // ENHANCED: Multiple visibility and positioning checks per frame // First check - immediate visibility enemy.leftFoot.visible = true; enemy.rightFoot.visible = true; enemy.leftFoot.alpha = 1; enemy.rightFoot.alpha = 1; // Second check - positioning enforcement enemy.leftFoot.x = -footSpacing; enemy.leftFoot.y = footYPos; enemy.rightFoot.x = footSpacing; enemy.rightFoot.y = footYPos; // Third check - parent attachment if (enemy.leftFoot && !enemy.leftFoot.parent) { enemy.addChild(enemy.leftFoot); } if (enemy.rightFoot && !enemy.rightFoot.parent) { enemy.addChild(enemy.rightFoot); } // Fourth check - graphics visibility if (enemy.leftFoot && enemy.leftFoot.children[0]) { enemy.leftFoot.children[0].visible = true; enemy.leftFoot.children[0].alpha = 1; } if (enemy.rightFoot && enemy.rightFoot.children[0]) { enemy.rightFoot.children[0].visible = true; enemy.rightFoot.children[0].alpha = 1; } // FINAL: Emergency left foot preservation - force recreation if still missing if (!enemy.leftFoot || !enemy.leftFoot.visible || enemy.leftFoot.alpha < 1 || !enemy.leftFoot.children || enemy.leftFoot.children.length === 0) { if (enemy.leftFoot && enemy.leftFoot.parent) { enemy.removeChild(enemy.leftFoot); } enemy.leftFoot = new Container(); var leftFootGraphics = enemy.leftFoot.attachAsset('vexFeet', { anchorX: 0.5, anchorY: 0.5 }); leftFootGraphics.width = 24; leftFootGraphics.height = 18; leftFootGraphics.tint = 0x000000; enemy.leftFoot.x = -footSpacing; enemy.leftFoot.y = footYPos; enemy.leftFoot.visible = true; enemy.leftFoot.alpha = 1; enemy.addChild(enemy.leftFoot); } } } // 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); } }); } }; } }; 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(); } };
===================================================================
--- original.js
+++ change.js
@@ -2178,9 +2178,9 @@
enemy.addChild(enemy.rightFoot);
}
// Start returning to spawn area
enemy.vexPhase = 'returningToSpawn';
- enemy.returnTargetX = 14; // Return to original spawn column
+ enemy.returnTargetX = 13; // Return to gridX13 column
enemy.returnTargetY = -3; // Return above the spawn area
}
} else if (enemy.vexPhase === 'returningToSpawn') {
// Initialize safety timer if not set
@@ -5495,10 +5495,10 @@
/****
* Game Code
****/
-// Add tween tracking and cleanup functions
// 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
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