User prompt
максимальный бонус для gold (не meteorgold) будет ограничен не 20 а 40
User prompt
подними все флаги вверх на 80
User prompt
я все равно могу нажать
User prompt
во время открытой подсказки все кнопки подсказок должны быть не активны пока я не закрою окно подсказок
User prompt
я только что случайно нажал на подсказку во время открытой другой подсказки. Хотя я ее еще не закрывал
User prompt
во время открытого меню подсказок. Ничего нельзя нажать кроме крестика закрыть подсказку
User prompt
вытяни фон для подсказок вверх на 80 пикселей
User prompt
еще на 80
User prompt
опусти все флаги ниже на 75
User prompt
флажок чей язык выбран в настоящий момент должен слегка окрашиваться в белый ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
сделай пульсацию на флажках языках при нажатии а то не понятно нажал или нет ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
можешь по умолчанию язык поставить английский
User prompt
звездочки от крита должны всегда разлетаться в случайном направлении
User prompt
сделай баланс 0
User prompt
для тестов пусть спавнится пока просто каждые 5 секунд со старта. Особенности: он не падает до конца, а зависает на одном уровне (примерно чуть выше середины). После чего просто летает от края до края боковых стен и каждые 4 секунды надувает под собой шар в течении секунды, который и будет падать на город со скоростью среднего метеора и здоровьем малого метеора
User prompt
все равно показывает боланс а не очки
User prompt
все равно показывает боланс а не очки
User prompt
исправь
User prompt
вконце показало не общий счет а баланс, а должны были счет
User prompt
Please fix the bug: 'RangeError: Maximum call stack size exceeded' in or related to this line: 'self.updateHealthBarVisual(); // Update the visual health bar' Line Number: 228
User prompt
почему-то при полном уменьшении здоровья города игра не закончилась
User prompt
звездочки от крита не должны уходить в прозрачность. Она должна просто уменьшатья
User prompt
звездочки от крита не должны быть прозрачными.
User prompt
сделай баланс 100000
User prompt
пусть в счет идет просто количество убитых врагов
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var City = Container.expand(function () { var self = Container.call(this); self.takeDamage = function (damageAmount) { if (self.health <= 0) { return; } // Already destroyed if (self.shielded) { // Shield hit - flash to 90% opacity for 1 second if (self.shieldGraphic && self.shieldGraphic.parent) { // Stop current flickering animation tween.stop(self.shieldGraphic); // Flash to 90% opacity (highly visible) self.shieldGraphic.alpha = 0.9; // Add compression animation - squeeze down 10% like cannon recoil tween.stop(self.shieldGraphic, { scaleX: true, scaleY: true }); // Store original scale if not set if (!self.shieldGraphic.originalScaleX) { self.shieldGraphic.originalScaleX = self.shieldGraphic.scaleX || 1; self.shieldGraphic.originalScaleY = self.shieldGraphic.scaleY || 1; } // Compress shield downward by 5% - check shield still exists before tweening if (self.shieldGraphic && self.shieldGraphic.parent) { tween(self.shieldGraphic, { scaleX: self.shieldGraphic.originalScaleX, scaleY: self.shieldGraphic.originalScaleY * 0.95 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { // Restore to original scale - check shield still exists if (self.shieldGraphic && self.shieldGraphic.parent) { tween(self.shieldGraphic, { scaleX: self.shieldGraphic.originalScaleX, scaleY: self.shieldGraphic.originalScaleY }, { duration: 200, easing: tween.easeInOut }); } } }); } // Wait 1 second then resume flickering LK.setTimeout(function () { if (self.shieldGraphic && self.shieldGraphic.parent) { self.shieldGraphic.isAnimating = false; // Reset animation flag to restart flickering } }, 1000); } return; // Shield active, ignore damage } self.health -= damageAmount; lastCityDamageTime = LK.ticks * (1000 / 60); // --- Ava: Trigger game over immediately if health drops to zero or below --- if (self.health <= 0) { self.health = 0; self.updateHealthBarVisual && self.updateHealthBarVisual(); LK.setScore(getFinalScore()); // Ensure finalScore is shown, not balance LK.showGameOver(); return; } LK.effects.flashObject(self.graphic, 0xFF0000, 300); // Flash city red // Flash both city images red when damage is taken if (leftCityImage) { tween(leftCityImage, { tint: 0xFF0000 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(leftCityImage, { tint: 0xFFFFFF }, { duration: 150, easing: tween.easeIn }); } }); } if (rightCityImage) { tween(rightCityImage, { tint: 0xFF0000 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(rightCityImage, { tint: 0xFFFFFF }, { duration: 150, easing: tween.easeIn }); } }); } // Flash main city graphic red when damage is taken tween(self.graphic, { tint: 0xFF0000 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(self.graphic, { tint: 0xFFFFFF }, { duration: 150, easing: tween.easeIn }); } }); // Removed setHealthDisplay method as city health text is no longer shown self.updateHealthBarVisual = function () { if (!self.healthBarFill || !self.healthBarBg || !self.damagePreviewFill) { return; } // Removed recursive call to self.updateHealthBarVisual(); if (self.health <= 0) { self.health = 0; // Cap at 0 LK.showGameOver(); // Trigger game over } }; var healthPercentage = Math.max(0, self.health) / CITY_MAX_HEALTH; var targetFillWidth = self.healthBarBg.width * healthPercentage; var currentGreenBarWidth = self.healthBarFill.width; // The green bar's width before this animation frame var damageEffectVisualWidth = currentGreenBarWidth - targetFillWidth; // Stop any ongoing animations on these elements tween.stop(self.healthBarFill); tween.stop(self.damagePreviewFill); if (damageEffectVisualWidth <= 0) { // No damage or health increased, just set the width directly self.healthBarFill.width = targetFillWidth; self.damagePreviewFill.alpha = 0; self.damagePreviewFill.width = 0; return; } // Setup the yellow damage preview bar self.damagePreviewFill.alpha = 1; // self.damagePreviewFill.tint = 0xFFFF00; // Asset is already yellow, tinting might not be needed unless asset color changes self.damagePreviewFill.width = damageEffectVisualWidth; // Position the yellow bar to cover the part of the health bar that is "lost" // It starts where the new green bar will end. self.damagePreviewFill.x = self.healthBarFill.x + targetFillWidth; // Animate the green health bar shrinking tween(self.healthBarFill, { width: targetFillWidth }, { duration: 500, easing: tween.easeOut }); // Animate the yellow bar shrinking from right to left tween(self.damagePreviewFill, { width: 0 // x position remains constant, causing the bar to shrink from its right edge. // This makes the yellow bar appear to be "eaten" from the right. }, { duration: 500, // Same duration as green bar for synchronization easing: tween.easeOut, onFinish: function onFinish() { self.damagePreviewFill.alpha = 0; // Hide it after animation self.damagePreviewFill.width = 0; // Reset width } }); }; // Initialization self.graphic = self.attachAsset('city', { anchorX: 0.5, anchorY: 1.0 }); // Anchor bottom-center // Health Bar Properties self.HEALTH_BAR_WIDTH = 2048; self.HEALTH_BAR_HEIGHT = 80; // doubled from 40 to 80 self.healthBarPadding = 0; // No padding, bar at the very bottom // Health bar background, stretched full width, anchored left self.healthBarBg = self.attachAsset('city_health_bar_bg', { anchorX: 0.0, // left anchorY: 1.0 // bottom }); self.healthBarBg.width = self.HEALTH_BAR_WIDTH; self.healthBarBg.height = self.HEALTH_BAR_HEIGHT; self.healthBarBg.x = -WORLD_WIDTH / 2; // City's x is centered, so shift left by half width self.healthBarBg.y = 0; // At bottom of city container // Health bar fill, anchored left self.healthBarFill = self.attachAsset('city_health_bar_fill', { anchorX: 0.0, anchorY: 1.0 }); self.healthBarFill.width = self.HEALTH_BAR_WIDTH; self.healthBarFill.height = self.HEALTH_BAR_HEIGHT; self.healthBarFill.x = -WORLD_WIDTH / 2; self.healthBarFill.y = 0; // Damage Preview Fill (Yellow bar for damage animation), anchored left self.damagePreviewFill = self.attachAsset('city_health_bar_damage_preview', { anchorX: 0.0, anchorY: 1.0 }); self.damagePreviewFill.height = self.HEALTH_BAR_HEIGHT; self.damagePreviewFill.y = 0; self.damagePreviewFill.x = -WORLD_WIDTH / 2; self.damagePreviewFill.width = 0; self.damagePreviewFill.alpha = 0; self.health = CITY_MAX_HEALTH; self.healthDisplay = null; // To be linked to a Text2 object // Shield system properties self.shielded = false; self.shieldStrength = 0; self.maxShieldStrength = 0; self.shieldEndTime = 0; self.shieldPausedTime = null; self.shieldGraphic = null; self.shieldCooldownIndicator = null; self.shieldCooldownTimerText = null; // Define updateHealthBarVisual before calling it to avoid TypeError self.updateHealthBarVisual = function () { if (!self.healthBarFill || !self.healthBarBg || !self.damagePreviewFill) { return; } if (self.health <= 0) { self.health = 0; // Cap at 0 LK.showGameOver(); // Trigger game over } var healthPercentage = Math.max(0, self.health) / CITY_MAX_HEALTH; var targetFillWidth = self.healthBarBg.width * healthPercentage; var currentGreenBarWidth = self.healthBarFill.width; // The green bar's width before this animation frame var damageEffectVisualWidth = currentGreenBarWidth - targetFillWidth; // Stop any ongoing animations on these elements tween.stop(self.healthBarFill); tween.stop(self.damagePreviewFill); if (damageEffectVisualWidth <= 0) { // No damage or health increased, just set the width directly self.healthBarFill.width = targetFillWidth; self.damagePreviewFill.alpha = 0; self.damagePreviewFill.width = 0; return; } // Setup the yellow damage preview bar self.damagePreviewFill.alpha = 1; // self.damagePreviewFill.tint = 0xFFFF00; // Asset is already yellow, tinting might not be needed unless asset color changes self.damagePreviewFill.width = damageEffectVisualWidth; // Position the yellow bar to cover the part of the health bar that is "lost" // It starts where the new green bar will end. self.damagePreviewFill.x = self.healthBarFill.x + targetFillWidth; // Animate the green health bar shrinking tween(self.healthBarFill, { width: targetFillWidth }, { duration: 500, easing: tween.easeOut }); // Animate the yellow bar shrinking from right to left tween(self.damagePreviewFill, { width: 0 // x position remains constant, causing the bar to shrink from its right edge. // This makes the yellow bar appear to be "eaten" from the right. }, { duration: 500, // Same duration as green bar for synchronization easing: tween.easeOut, onFinish: function onFinish() { self.damagePreviewFill.alpha = 0; // Hide it after animation self.damagePreviewFill.width = 0; // Reset width } }); }; self.updateHealthBarVisual(); // Initialize health bar visual return self; }); var Crosshair = Container.expand(function () { var self = Container.call(this); self.isOver = function (target) { if (!target || !target.graphic || !self.graphic || !target.graphic.width || !target.graphic.height) { return false; } // Basic bounding box collision var selfHalfWidth = self.graphic.width / 2; var selfHalfHeight = self.graphic.height / 2; var targetHalfWidth = target.graphic.width * (target.graphic.scaleX || 1) / 2; var targetHalfHeight = target.graphic.height * (target.graphic.scaleY || 1) / 2; var selfLeft = self.x - selfHalfWidth; var selfRight = self.x + selfHalfWidth; var selfTop = self.y - selfHalfHeight; var selfBottom = self.y + selfHalfHeight; var targetLeft = target.x - targetHalfWidth; var targetRight = target.x + targetHalfWidth; var targetTop = target.y - targetHalfHeight; var targetBottom = target.y + targetHalfHeight; return !(selfRight < targetLeft || selfLeft > targetRight || selfBottom < targetTop || selfTop > targetBottom); }; // Initialization code, graphic will be attached in Game Code self.graphic = null; self.speed = 0; // Will be set from game constant return self; }); // Base class for falling objects // Base Enemy class - foundation for all enemy types var Enemy = Container.expand(function (config) { var self = Container.call(this); // Enemy configuration with defaults config = config || {}; self.assetId = config.assetId || 'meteor'; // Apply health scaling based on enemies killed var baseHealth = config.health || 1000; self.health = Math.floor(baseHealth * currentHealthMultiplier); self.maxHealth = self.health; self.speedY = config.speedY || 2; self.damageToCity = config.damageToCity || 50; self.moneyReward = config.moneyReward || 10; self.enemyCategory = config.enemyCategory || 'meteor'; // meteor, insect, robot self.weightClass = config.weightClass || 'medium'; // light, medium, heavy, elite, boss // Calculate rotation speed based on falling speed for more realistic physics var baseRotationSpeed = 0.003; // Increased base minimum rotation speed var speedMultiplier = Math.max(0.5, self.speedY / 2.0); // Scale rotation based on speed with faster multiplier self.rotationSpeed = config.rotationSpeed || baseRotationSpeed * speedMultiplier + Math.random() * 0.003; // Speed-based rotation with faster base self.rotationDirection = config.rotationDirection || (Math.random() < 0.5 ? 1 : -1); self.specialAbilities = config.specialAbilities || []; self.bossPhases = config.bossPhases || []; self.currentBossPhase = 0; self.bossPartsHealth = config.bossPartsHealth || {}; self.bossPartsDestroyed = {}; // Initialize graphics self.graphic = self.attachAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5 }); self.isDestroyed = false; self.originalSpeedY = self.speedY; // Status effect properties self.burnStacks = 0; self.burnStartTime = 0; self.lastBurnDamageTime = 0; self.burnPausedTime = null; self.fireTowerBurnStacks = 0; self.fireTowerBurnStartTime = 0; self.lastFireTowerBurnDamageTime = 0; self.fireTowerBurnPausedTime = null; self.freezeStacks = 0; self.freezeStartTime = 0; self.freezePausedTime = null; self.isFullyFrozen = false; self.freezeBlockEndTime = 0; self.iceBlock = null; self.burnFlames = []; self.fireTowerBurnFlames = []; self.snowflakeParticles = []; self.healthDisplay = null; // Health bar properties self.healthBarBg = null; self.healthBarFill = null; self.healthBarDamagePreview = null; // Health bar visual update method self.updateHealthBarVisual = function () { if (!self.healthBarFill || !self.healthBarBg || !self.healthBarDamagePreview) { return; } var healthPercentage = Math.max(0, self.health) / self.maxHealth; var targetFillWidth = self.healthBarBg.width * healthPercentage; var currentRedBarWidth = self.healthBarFill.width; var damageEffectVisualWidth = currentRedBarWidth - targetFillWidth; // Stop any ongoing animations on these elements tween.stop(self.healthBarFill); tween.stop(self.healthBarDamagePreview); // If a previous yellow bar is still animating, remove it immediately (merge with new damage) self.healthBarDamagePreview.alpha = 0; self.healthBarDamagePreview.width = 0; if (damageEffectVisualWidth <= 0) { // No damage or health increased, just set the width directly self.healthBarFill.width = targetFillWidth; self.healthBarDamagePreview.alpha = 0; self.healthBarDamagePreview.width = 0; return; } // Setup the yellow damage preview bar self.healthBarDamagePreview.alpha = 1; self.healthBarDamagePreview.width = damageEffectVisualWidth; // Position the yellow bar to cover the part of the health bar that is "lost" self.healthBarDamagePreview.x = self.healthBarFill.x + targetFillWidth; // Animate the red health bar shrinking tween(self.healthBarFill, { width: targetFillWidth }, { duration: 500, easing: tween.easeOut }); // Animate the yellow bar shrinking from right to left tween(self.healthBarDamagePreview, { width: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { self.healthBarDamagePreview.alpha = 0; self.healthBarDamagePreview.width = 0; } }); }; // Boss-specific properties self.isInvulnerable = false; self.invulnerabilityReason = ""; // Base enemy methods that all enemies share self.takeDamage = function (damageAmount) { if (self.isDestroyed) { return; } if (damageAmount <= 0) { return; } // Ignore zero or negative damage if (self.isInvulnerable) { return; } // Boss invulnerability // Boss part system - check if any parts need to be destroyed first if (self.weightClass === 'boss' && self.hasVulnerableParts()) { return; // Can't damage main boss while parts exist } // Check for critical hit var critChance = upgrades.critChance * 5; var isCritical = Math.random() * 100 < critChance; var finalDamage = damageAmount; if (isCritical) { finalDamage = damageAmount * 2; LK.getSound('krit').play(); // --- Ava: Spawn 3 zvezdakrit (star) particles flying out and shrinking on crit --- if (!game.particles) { game.particles = []; } for (var i = 0; i < 3; i++) { (function () { var angle = Math.random() * Math.PI * 2; // Fully random direction for each star var speed = 180 + Math.random() * 60; // px to move outward var scale = 0.7 + Math.random() * 0.4; var star = LK.getAsset('zvezdakrit', { anchorX: 0.5, anchorY: 0.5, scaleX: scale, scaleY: scale }); star.x = self.x; star.y = self.y; star.alpha = 1; game.addChild(star); game.particles.push(star); var dx = Math.cos(angle) * speed; var dy = Math.sin(angle) * speed; tween(star, { x: star.x + dx, y: star.y + dy, scaleX: 0, scaleY: 0 // No alpha change, only scale shrinks }, { duration: 1000, easing: tween.easeOut, onFinish: function (starRef) { return function () { if (starRef.parent) { starRef.parent.removeChild(starRef); } if (game.particles) { var idx = game.particles.indexOf(starRef); if (idx > -1) { game.particles.splice(idx, 1); } } }; }(star) }); })(); } } self.health -= finalDamage; LK.effects.flashObject(self.graphic, 0xFF0000, 100); if (self.fireTowerBurnStacks > 0) { LK.effects.flashObject(self.graphic, 0xFF4500, 100); // Flash orange-red for direct hit } else { LK.effects.flashObject(self.graphic, 0xFF4500, 100); // Flash orange-red for direct hit even if no burn stacks } // Create health bar if it doesn't exist if (!self.healthBarBg) { // Health bar background self.healthBarBg = self.attachAsset('enemy_health_bar_bg', { anchorX: 0.5, anchorY: 0.5 }); self.healthBarBg.y = -self.graphic.height * (self.graphic.scaleY || 1) / 2 - 20; // Health bar fill (red) self.healthBarFill = self.attachAsset('enemy_health_bar_fill', { anchorX: 0.0, anchorY: 0.5 }); self.healthBarFill.x = -self.healthBarBg.width / 2; self.healthBarFill.y = self.healthBarBg.y; // Damage preview fill (yellow) self.healthBarDamagePreview = self.attachAsset('enemy_health_bar_damage_preview', { anchorX: 0.0, anchorY: 0.5 }); self.healthBarDamagePreview.x = -self.healthBarBg.width / 2; self.healthBarDamagePreview.y = self.healthBarBg.y; self.healthBarDamagePreview.width = 0; self.healthBarDamagePreview.alpha = 0; } // Update health bar visual if (self.healthBarBg) { self.updateHealthBarVisual(); } // Pulse animation on hit tween.stop(self.graphic, { scaleX: true, scaleY: true }); tween(self.graphic, { scaleX: 1.3, scaleY: 1.3 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(self.graphic, { scaleX: 1, scaleY: 1 }, { duration: 150, easing: tween.easeIn }); } }); // Check for boss phase transition if (self.weightClass === 'boss' && self.health <= 0) { self.handleBossDefeat(); } else if (self.health <= 0) { self.destroySelf(); } }; // Check if boss has vulnerable parts that must be destroyed first self.hasVulnerableParts = function () { if (self.weightClass !== 'boss') { return false; } for (var partName in self.bossPartsHealth) { if (self.bossPartsHealth[partName] > 0 && !self.bossPartsDestroyed[partName]) { return true; } } return false; }; // Handle boss defeat and phase transitions self.handleBossDefeat = function () { if (self.currentBossPhase < self.bossPhases.length - 1) { // Move to next phase self.currentBossPhase++; var nextPhase = self.bossPhases[self.currentBossPhase]; self.health = nextPhase.health || self.maxHealth; self.speedY = nextPhase.speedY || self.originalSpeedY; self.isInvulnerable = nextPhase.invulnerable || false; self.invulnerabilityReason = nextPhase.invulnerabilityReason || ""; // Reset boss parts for new phase if (nextPhase.parts) { self.bossPartsHealth = Object.assign({}, nextPhase.parts); self.bossPartsDestroyed = {}; } } else { // Final defeat self.destroySelf(); } }; // Define update method on prototype so it can be called by subclasses self.updateParent = function () { if (self.isDestroyed) { return; } // Pause all movement and effects when upgrades or shop popup is open if (upgradesPopup || controlsPopup || shopPopup) { // Store the time when the game was paused if (!self._pauseTime) { self._pauseTime = LK.ticks * (1000 / 60); } return; } // If the game was paused and is now resumed, adjust the effect timers if (self._pauseTime) { var pauseDuration = LK.ticks * (1000 / 60) - self._pauseTime; // Adjust relevant timers by the pause duration self.burnStartTime += pauseDuration; self.lastBurnDamageTime += pauseDuration; self.freezeStartTime += pauseDuration; self.freezeBlockEndTime += pauseDuration; self.fireTowerBurnStartTime += pauseDuration; self.lastFireTowerBurnDamageTime += pauseDuration; self._pauseTime = null; // Reset pause time } self.y += self.speedY * 1.04; // 0.65 * 1.6 = 1.04 // Rotate meteor around its own axis (pause rotation if frozen) if (self.graphic && !self.isFullyFrozen) { self.graphic.rotation += self.rotationSpeed * self.rotationDirection; } // Process status effects and movement (same logic as FallingObject) // ... (status effect processing would go here, but keeping it simple for now) }; // Enhanced destroy method with all cleanup from original FallingObject self.destroySelf = function (giveMoney) { if (giveMoney === undefined) { giveMoney = true; } if (self.isDestroyed) { return; } var wasFullyFrozen = self.isFullyFrozen === true; var hasIceBlock = self.iceBlock && self.iceBlock.parent; if (wasFullyFrozen || hasIceBlock) { LK.getSound('Tresklda').play(); } else { LK.getSound('Boom').play(); } self.isDestroyed = true; // Comprehensive tween cleanup tween.stop(self); if (self.graphic) { tween.stop(self.graphic); tween.stop(self.graphic, { tint: true, scaleX: true, scaleY: true, alpha: true, rotation: true }); } // Clean up all status effects and particles (same as original) if (self.healthDisplay) { tween.stop(self.healthDisplay); tween.stop(self.healthDisplay, { x: true, y: true, scaleX: true, scaleY: true, alpha: true }); if (self.healthDisplay.parent) { self.healthDisplay.parent.removeChild(self.healthDisplay); } self.healthDisplay = null; } // Clean up health bar components if (self.healthBarBg) { tween.stop(self.healthBarBg); if (self.healthBarBg.parent) { self.healthBarBg.parent.removeChild(self.healthBarBg); } self.healthBarBg = null; } if (self.healthBarFill) { tween.stop(self.healthBarFill); if (self.healthBarFill.parent) { self.healthBarFill.parent.removeChild(self.healthBarFill); } self.healthBarFill = null; } if (self.healthBarDamagePreview) { tween.stop(self.healthBarDamagePreview); if (self.healthBarDamagePreview.parent) { self.healthBarDamagePreview.parent.removeChild(self.healthBarDamagePreview); } self.healthBarDamagePreview = null; } // Burn flames cleanup if (self.burnFlames) { for (var f = 0; f < self.burnFlames.length; f++) { var flame = self.burnFlames[f]; if (flame) { tween.stop(flame); tween.stop(flame, { alpha: true, scaleX: true, scaleY: true, x: true, y: true }); if (flame.parent) { flame.parent.removeChild(flame); } flame.offsetX = undefined; flame.offsetY = undefined; } } self.burnFlames = []; } // Fire tower burn flames cleanup if (self.fireTowerBurnFlames) { for (var ftf = 0; ftf < self.fireTowerBurnFlames.length; ftf++) { var fireTowerFlame = self.fireTowerBurnFlames[ftf]; if (fireTowerFlame) { tween.stop(fireTowerFlame); tween.stop(fireTowerFlame, { alpha: true, scaleX: true, scaleY: true, x: true, y: true }); if (fireTowerFlame.parent) { fireTowerFlame.parent.removeChild(fireTowerFlame); } fireTowerFlame.offsetX = undefined; fireTowerFlame.offsetY = undefined; } } self.fireTowerBurnFlames = []; } // Snowflake cleanup if (self.snowflakeParticles) { for (var sf = 0; sf < self.snowflakeParticles.length; sf++) { var snowflake = self.snowflakeParticles[sf]; if (snowflake) { tween.stop(snowflake); if (snowflake.parent) { snowflake.parent.removeChild(snowflake); } } } self.snowflakeParticles = []; } // Ice block cleanup if (self.iceBlock) { tween.stop(self.iceBlock); if (self.iceBlock.parent) { self.iceBlock.parent.removeChild(self.iceBlock); } self.iceBlock = null; } // Remove ice block from game if it still exists (defensive for persistent visuals) if (game && game.children && game.children.length) { for (var i = game.children.length - 1; i >= 0; i--) { var child = game.children[i]; if (child && child === self.iceBlock) { game.removeChild(child); } } } // Create destruction particles if (wasFullyFrozen || hasIceBlock) { var maxIceFragments = Math.min(3 + Math.floor(Math.random() * 4), 6); var iceFragmentCount = game.particles ? Math.min(maxIceFragments, Math.max(0, 50 - game.particles.length)) : maxIceFragments; for (var ip = 0; ip < iceFragmentCount; ip++) { var randomIceScale = 0.4 + Math.random() * 0.8; var iceFragment = LK.getAsset('ldinki', { anchorX: 0.5, anchorY: 0.5, scaleX: randomIceScale, scaleY: randomIceScale }); iceFragment.x = self.x; iceFragment.y = self.y; iceFragment.alpha = 0.7; game.addChild(iceFragment); if (!game.particles) { game.particles = []; } game.particles.push(iceFragment); var iceAngle = Math.random() * Math.PI * 2; var iceSpeed = 80 + Math.random() * 120; var iceVelocityX = Math.cos(iceAngle) * iceSpeed; var iceVelocityY = Math.sin(iceAngle) * iceSpeed; var iceRotationSpeed = 0.01 + Math.random() * 0.03; var iceRotationDirection = Math.random() < 0.5 ? 1 : -1; var iceTotalRotation = iceRotationSpeed * iceRotationDirection * 120; tween(iceFragment, { x: iceFragment.x + iceVelocityX, y: iceFragment.y + iceVelocityY, scaleX: 0, scaleY: 0, rotation: iceFragment.rotation + iceTotalRotation }, { duration: 2000, easing: tween.easeOut, onFinish: function (iceFragmentRef) { return function onFinish() { if (iceFragmentRef.parent) { iceFragmentRef.parent.removeChild(iceFragmentRef); } if (game.particles) { var iceFragIndex = game.particles.indexOf(iceFragmentRef); if (iceFragIndex > -1) { game.particles.splice(iceFragIndex, 1); } } }; }(iceFragment) }); } } else { var maxFragments, fragmentCount; if (self.assetId === 'Alien_1') { // Special handling for alien_1 - maximum 3 kaplya fragments maxFragments = Math.min(Math.floor(Math.random() * 3) + 1, 3); // 1-3 fragments, strictly capped at 3 } else { maxFragments = Math.min(3 + Math.floor(Math.random() * 4), 6); } fragmentCount = game.particles ? Math.min(maxFragments, Math.max(0, 50 - game.particles.length)) : maxFragments; for (var p = 0; p < fragmentCount; p++) { var randomScale = 0.4 + Math.random() * 0.8; // Use kaplya (green insect blood) for alien_1, kusok for others var fragmentAssetId = self.assetId === 'Alien_1' ? 'kaplya' : 'kusok'; var fragment = LK.getAsset(fragmentAssetId, { anchorX: 0.5, anchorY: 0.5, scaleX: randomScale, scaleY: randomScale }); fragment.x = self.x; fragment.y = self.y; game.addChild(fragment); if (!game.particles) { game.particles = []; } game.particles.push(fragment); var angle = Math.random() * Math.PI * 2; var speed = 80 + Math.random() * 120; var velocityX = Math.cos(angle) * speed; var velocityY = Math.sin(angle) * speed; // Different behavior for kaplya (alien blood splashes) vs other fragments if (self.assetId === 'Alien_1') { // Kaplya behaves like splashes - scatter then stop and shrink smoothly, total 1 second duration var scatterDistance = speed * 0.4; // How far they scatter (reduced from 0.8 to 0.4) var scatterX = fragment.x + Math.cos(angle) * scatterDistance; var scatterY = fragment.y + Math.sin(angle) * scatterDistance; // First phase: scatter to final position (no rotation for splashes) tween(fragment, { x: scatterX, y: scatterY }, { duration: 200, // Faster scatter phase (200ms) easing: tween.easeOut, onFinish: function (fragmentRef) { return function onFinish() { // Second phase: pause briefly then smoothly shrink LK.setTimeout(function () { if (fragmentRef.parent) { tween(fragmentRef, { scaleX: 0, scaleY: 0 }, { duration: 600, // 600ms shrinking to total 1000ms (1 second) easing: tween.easeIn, onFinish: function onFinish() { if (fragmentRef.parent) { fragmentRef.parent.removeChild(fragmentRef); } // Remove from particles tracking array if (game.particles) { var fragIndex = game.particles.indexOf(fragmentRef); if (fragIndex > -1) { game.particles.splice(fragIndex, 1); } } } }); } }, 200); // Brief pause before shrinking (200ms) for total 1000ms }; }(fragment) }); } else { // Other fragments (kusok) keep original rotating behavior var rotationSpeed = 0.01 + Math.random() * 0.03; var rotationDirection = Math.random() < 0.5 ? 1 : -1; var totalRotation = rotationSpeed * rotationDirection * 120; tween(fragment, { x: fragment.x + velocityX, y: fragment.y + velocityY, scaleX: 0, scaleY: 0, rotation: fragment.rotation + totalRotation }, { duration: 2000, easing: tween.easeOut, onFinish: function (fragmentRef) { return function onFinish() { if (fragmentRef.parent) { fragmentRef.parent.removeChild(fragmentRef); } if (game.particles) { var fragIndex = game.particles.indexOf(fragmentRef); if (fragIndex > -1) { game.particles.splice(fragIndex, 1); } } }; }(fragment) }); } } } // Shrink and fade main graphic if (tween && self.graphic) { tween.stop(self.graphic); tween.stop(self.graphic, { scaleX: true, scaleY: true, alpha: true, tint: true, rotation: true }); tween(self.graphic, { scaleX: 0.1, scaleY: 0.1, alpha: 0 }, { duration: 200, onFinish: function (selfRef) { return function onFinish() { if (selfRef.graphic) { tween.stop(selfRef.graphic); selfRef.graphic = null; } }; }(self) }); } // Track enemy kill for health scaling totalEnemiesKilled++; onEnemyKilledForMeteorEvent(); // New scaling: every 75 kills increases increment by +0.1 every 150 kills var increases = Math.floor(totalEnemiesKilled / 75); var increment = 0.3 + Math.floor(totalEnemiesKilled / 150) * 0.1; var newMultiplier = 1.0 + increases * increment; if (newMultiplier > currentHealthMultiplier) { currentHealthMultiplier = newMultiplier; // Show difficulty level notification var difficultyLevel = increases + 1; // Level starts at 1 // Use current language for difficulty notification var difficultyTextContent = 'Difficulty Level ' + difficultyLevel; if (currentLanguage === 'ru') { difficultyTextContent = 'Уровень сложности ' + difficultyLevel; } else if (currentLanguage === 'de') { difficultyTextContent = 'Schwierigkeitsgrad ' + difficultyLevel; } else if (currentLanguage === 'fr') { difficultyTextContent = 'Niveau de difficulté ' + difficultyLevel; } else if (currentLanguage === 'es') { difficultyTextContent = 'Nivel de dificultad ' + difficultyLevel; } else if (currentLanguage === 'tr') { difficultyTextContent = 'Zorluk seviyesi ' + difficultyLevel; } var difficultyText = new Text2(difficultyTextContent, { size: 120, fill: 0xFFFF00, align: 'center', font: 'Impact' }); difficultyText.anchor.set(0.5, 0.5); difficultyText.x = WORLD_WIDTH / 2; difficultyText.y = WORLD_HEIGHT / 2 - 200; difficultyText.alpha = 0; game.addChild(difficultyText); // Animate difficulty notification tween(difficultyText, { alpha: 1, scaleX: 1.2, scaleY: 1.2 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { // Hold for 2 seconds then fade out LK.setTimeout(function () { tween(difficultyText, { alpha: 0, scaleX: 0.8, scaleY: 0.8 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (difficultyText.parent) { difficultyText.parent.removeChild(difficultyText); } } }); }, 2000); } }); } // Give money reward if (giveMoney) { var scoreIncrement = Math.min(Math.floor(totalEnemiesKilled / 50) * 2, 20); // Cap at 20 (10 applications * 2) var moneyEarned = self.moneyReward + scoreIncrement; LK.setScore(LK.getScore() + moneyEarned); // --- Ava: Also increment finalScore by 1 for each enemy killed (not spent) --- finalScore += 1; updateFinalScoreDisplay && updateFinalScoreDisplay(); updateScoreDisplay(); var moneyNumber = new Text2('$' + self.moneyReward.toString(), { size: 110, fill: 0x00FF00, align: 'center', font: 'Impact' }); moneyNumber.anchor.set(0.5, 0.5); moneyNumber.x = self.x + (Math.random() - 0.5) * 50; moneyNumber.y = self.y + 40; game.addChild(moneyNumber); tween(moneyNumber, { y: moneyNumber.y + 150, scaleX: 1.4, scaleY: 1.4 }, { duration: 1500, easing: tween.easeOut }); LK.setTimeout(function () { if (moneyNumber.parent) { game.removeChild(moneyNumber); } }, 1500); } }; // Status effect methods self.applyBurn = function () { if (self.burnStacks < 10) { self.burnStacks++; } var currentTime = LK.ticks * (1000 / 60); self.burnStartTime = currentTime; self.burnPausedTime = null; tween(self.graphic, { tint: 0xFF4500 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self.graphic, { tint: 0xFFFFFF }, { duration: 200, easing: tween.easeIn }); } }); }; self.applyFireTowerBurn = function () { if (self.fireTowerBurnStacks < 10) { self.fireTowerBurnStacks++; } var currentTime = LK.ticks * (1000 / 60); self.fireTowerBurnStartTime = currentTime; self.fireTowerBurnPausedTime = null; tween(self.graphic, { tint: 0xFF2500 // More intense orange-red tint for fire tower burn }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self.graphic, { tint: 0xFFFFFF // Back to white }, { duration: 200, easing: tween.easeIn }); } }); // Add visual effect for direct damage LK.effects.flashObject(self.graphic, 0xFF4500, 100); // Flash orange-red for direct hit }; self.applyFreeze = function () { if (self.isFullyFrozen) { return; } if (self.freezeStacks < 3) { self.freezeStacks++; } var currentTime = LK.ticks * (1000 / 60); self.freezeStartTime = currentTime; self.speedY = self.originalSpeedY * 0.5; tween(self.graphic, { tint: 0x87CEEB }, { duration: 200, easing: tween.easeOut }); if (self.freezeStacks >= 3) { var currentTime = LK.ticks * (1000 / 60); if (!freezeSoundCooldownActive || currentTime >= freezeSoundCooldownEndTime) { LK.getSound('icetresk').play(); freezeSoundCooldownActive = true; freezeSoundCooldownEndTime = currentTime + 500; } self.isFullyFrozen = true; self.freezeStacks = 0; self.speedY = 0; self.freezeBlockEndTime = currentTime + 4000; self.iceBlock = LK.getAsset('Ice', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); self.iceBlock.x = self.x; self.iceBlock.y = self.y; self.iceBlock.alpha = 0.6; game.addChild(self.iceBlock); tween(self.graphic, { tint: 0x4169E1 }, { duration: 300, easing: tween.easeOut }); } }; return self; }); // Legacy classes for backward compatibility // Legacy FallingObject - now inherits from Enemy for backward compatibility var FallingObject = Container.expand(function (assetId, initialHealth, fallSpeed, objectType) { var self = Container.call(this, config); var config = { assetId: assetId, health: initialHealth, speedY: fallSpeed, enemyCategory: objectType === 'meteor' ? 'meteor' : 'alien', weightClass: 'medium', moneyReward: objectType === 'meteor' ? 10 : 15, damageToCity: objectType === 'meteor' ? METEOR_DAMAGE_TO_CITY : ALIEN_DAMAGE_TO_CITY }; self.takeDamage = function (damageAmount) { if (self.isDestroyed) { return; } if (damageAmount <= 0) { return; } // Ignore zero or negative damage // Check for critical hit var critChance = upgrades.critChance * 5; // 5% per level var isCritical = Math.random() * 100 < critChance; var finalDamage = damageAmount; if (isCritical) { finalDamage = damageAmount * 2; // Double damage LK.getSound('krit').play(); // Play critical hit sound // --- Ava: Spawn 3 zvezdakrit (star) particles flying out and shrinking on crit --- if (!game.particles) { game.particles = []; } for (var i = 0; i < 3; i++) { (function () { var angle = Math.random() * Math.PI * 2; // Fully random direction for each star var speed = 180 + Math.random() * 60; // px to move outward var scale = 0.7 + Math.random() * 0.4; var star = LK.getAsset('zvezdakrit', { anchorX: 0.5, anchorY: 0.5, scaleX: scale, scaleY: scale }); star.x = self.x; star.y = self.y; star.alpha = 1; game.addChild(star); game.particles.push(star); var dx = Math.cos(angle) * speed; var dy = Math.sin(angle) * speed; tween(star, { x: star.x + dx, y: star.y + dy, scaleX: 0, scaleY: 0 // No alpha change, only scale shrinks }, { duration: 1000, easing: tween.easeOut, onFinish: function (starRef) { return function () { if (starRef.parent) { starRef.parent.removeChild(starRef); } if (game.particles) { var idx = game.particles.indexOf(starRef); if (idx > -1) { game.particles.splice(idx, 1); } } }; }(star) }); })(); } } self.health -= finalDamage; LK.effects.flashObject(self.graphic, 0xFF0000, 100); // Red flash on hit if (self.fireTowerBurnStacks > 0) { LK.effects.flashObject(self.graphic, 0xFF4500, 100); // Flash orange-red for direct hit } else { LK.effects.flashObject(self.graphic, 0xFF4500, 100); // Flash orange-red for direct hit even if no burn stacks } // Limit damage numbers to prevent accumulation if (!game.damageNumbers) { game.damageNumbers = []; } if (game.damageNumbers.length >= 20) { var oldDamageNumber = game.damageNumbers.shift(); if (oldDamageNumber.parent) { game.removeChild(oldDamageNumber); } } // Create health bar if it doesn't exist if (!self.healthBarBg) { // Health bar background self.healthBarBg = self.attachAsset('enemy_health_bar_bg', { anchorX: 0.5, anchorY: 0.5 }); self.healthBarBg.y = -self.graphic.height * (self.graphic.scaleY || 1) / 2 - 20; // Health bar fill (red) self.healthBarFill = self.attachAsset('enemy_health_bar_fill', { anchorX: 0.0, anchorY: 0.5 }); self.healthBarFill.x = -self.healthBarBg.width / 2; self.healthBarFill.y = self.healthBarBg.y; // Damage preview fill (yellow) self.healthBarDamagePreview = self.attachAsset('enemy_health_bar_damage_preview', { anchorX: 0.0, anchorY: 0.5 }); self.healthBarDamagePreview.x = -self.healthBarBg.width / 2; self.healthBarDamagePreview.y = self.healthBarBg.y; self.healthBarDamagePreview.width = 0; self.healthBarDamagePreview.alpha = 0; } // Health bar visual update method - same as Enemy class self.updateHealthBarVisual = function () { if (!self.healthBarFill || !self.healthBarBg || !self.healthBarDamagePreview) { return; } var healthPercentage = Math.max(0, self.health) / self.maxHealth; var targetFillWidth = self.healthBarBg.width * healthPercentage; var currentRedBarWidth = self.healthBarFill.width; var damageEffectVisualWidth = currentRedBarWidth - targetFillWidth; // Stop any ongoing animations on these elements tween.stop(self.healthBarFill); tween.stop(self.healthBarDamagePreview); // If a previous yellow bar is still animating, remove it immediately (merge with new damage) self.healthBarDamagePreview.alpha = 0; self.healthBarDamagePreview.width = 0; if (damageEffectVisualWidth <= 0) { // No damage or health increased, just set the width directly self.healthBarFill.width = targetFillWidth; self.healthBarDamagePreview.alpha = 0; self.healthBarDamagePreview.width = 0; return; } // Setup the yellow damage preview bar self.healthBarDamagePreview.alpha = 1; self.healthBarDamagePreview.width = damageEffectVisualWidth; // Position the yellow bar to cover the part of the health bar that is "lost" self.healthBarDamagePreview.x = self.healthBarFill.x + targetFillWidth; // Animate the red health bar shrinking tween(self.healthBarFill, { width: targetFillWidth }, { duration: 500, easing: tween.easeOut }); // Animate the yellow bar shrinking from right to left tween(self.healthBarDamagePreview, { width: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { self.healthBarDamagePreview.alpha = 0; self.healthBarDamagePreview.width = 0; } }); }; // Update health bar visual using same method as Enemy class if (self.healthBarBg) { self.updateHealthBarVisual(); } // Add pulsing scale animation when hit tween.stop(self.graphic, { scaleX: true, scaleY: true }); // Stop any existing scale tweens tween(self.graphic, { scaleX: 1.3, scaleY: 1.3 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(self.graphic, { scaleX: 1, scaleY: 1 }, { duration: 150, easing: tween.easeIn }); } }); if (self.health <= 0) { self.destroySelf(); } }; self.destroySelf = function (giveMoney) { if (giveMoney === undefined) { giveMoney = true; } // Default to giving money if (self.isDestroyed) { return; } // Store freeze state immediately for sound decision before any cleanup var wasFullyFrozen = self.isFullyFrozen === true; var hasIceBlock = self.iceBlock && self.iceBlock.parent; // Play different sound based on freeze state and asset type (check before any cleanup) if (wasFullyFrozen || hasIceBlock) { LK.getSound('Tresklda').play(); // Play ice crack sound when frozen } else if (self.assetId === 'gold') { LK.getSound('goldsound').play(); // Play gold sound for gold meteors } else if (self.assetId === 'Alien_1') { LK.getSound('alien_bum').play(); // Play alien death sound for alien_1 } else { LK.getSound('Boom').play(); // Play normal explosion sound } self.isDestroyed = true; // Comprehensive tween cleanup - stop all tweens on this object and ALL its properties tween.stop(self); if (self.graphic) { tween.stop(self.graphic); // Stop specific property tweens that might be running tween.stop(self.graphic, { tint: true, scaleX: true, scaleY: true, alpha: true, rotation: true }); } // Clean up health display completely if (self.healthDisplay) { tween.stop(self.healthDisplay); tween.stop(self.healthDisplay, { x: true, y: true, scaleX: true, scaleY: true, alpha: true }); if (self.healthDisplay.parent) { self.healthDisplay.parent.removeChild(self.healthDisplay); } self.healthDisplay = null; // Remove reference } // Clean up health bar components if (self.healthBarBg) { tween.stop(self.healthBarBg); if (self.healthBarBg.parent) { self.healthBarBg.parent.removeChild(self.healthBarBg); } self.healthBarBg = null; } if (self.healthBarFill) { tween.stop(self.healthBarFill); if (self.healthBarFill.parent) { self.healthBarFill.parent.removeChild(self.healthBarFill); } self.healthBarFill = null; } if (self.healthBarDamagePreview) { tween.stop(self.healthBarDamagePreview); if (self.healthBarDamagePreview.parent) { self.healthBarDamagePreview.parent.removeChild(self.healthBarDamagePreview); } self.healthBarDamagePreview = null; } // Enhanced burn flames cleanup if (self.burnFlames) { for (var f = 0; f < self.burnFlames.length; f++) { var flame = self.burnFlames[f]; if (flame) { // Stop all tweens on flame tween.stop(flame); tween.stop(flame, { alpha: true, scaleX: true, scaleY: true, x: true, y: true }); if (flame.parent) { flame.parent.removeChild(flame); } // Clear flame references flame.offsetX = undefined; flame.offsetY = undefined; } } self.burnFlames = []; } // Clear burn-related properties to prevent memory leaks self.burnStacks = 0; self.burnStartTime = 0; self.lastBurnDamageTime = 0; self.burnPausedTime = null; // Enhanced fire tower burn flames cleanup if (self.fireTowerBurnFlames) { for (var ftf = 0; ftf < self.fireTowerBurnFlames.length; ftf++) { var fireTowerFlame = self.fireTowerBurnFlames[ftf]; if (fireTowerFlame) { // Stop all tweens on fire tower flame tween.stop(fireTowerFlame); tween.stop(fireTowerFlame, { alpha: true, scaleX: true, scaleY: true, x: true, y: true }); if (fireTowerFlame.parent) { fireTowerFlame.parent.removeChild(fireTowerFlame); } // Clear fire tower flame references fireTowerFlame.offsetX = undefined; fireTowerFlame.offsetY = undefined; } } self.fireTowerBurnFlames = []; } // Clear fire tower burn-related properties to prevent memory leaks self.fireTowerBurnStacks = 0; self.fireTowerBurnStartTime = 0; self.lastFireTowerBurnDamageTime = 0; self.fireTowerBurnPausedTime = null; // Clear freeze-related properties and cleanup self.freezeStacks = 0; self.freezeStartTime = 0; self.freezePausedTime = null; self.isFullyFrozen = false; self.freezeBlockEndTime = 0; if (self.iceBlock) { tween.stop(self.iceBlock); if (self.iceBlock.parent) { self.iceBlock.parent.removeChild(self.iceBlock); } self.iceBlock = null; } // Remove ice block from game if it still exists (defensive for persistent visuals) if (game && game.children && game.children.length) { for (var i = game.children.length - 1; i >= 0; i--) { var child = game.children[i]; if (child && child === self.iceBlock) { game.removeChild(child); } } } // Clean up object's snowflake particles if (self.snowflakeParticles) { for (var sf = 0; sf < self.snowflakeParticles.length; sf++) { var snowflake = self.snowflakeParticles[sf]; if (snowflake) { tween.stop(snowflake); if (snowflake.parent) { snowflake.parent.removeChild(snowflake); } } } self.snowflakeParticles = []; } // Clean up wings for alien_1 if (self.wings) { tween.stop(self.wings); if (self.wings.parent) { self.wings.parent.removeChild(self.wings); } self.wings = null; } // Create ice particles if object was frozen if (wasFullyFrozen || hasIceBlock) { // Limit ice fragment creation to prevent excessive memory usage var maxIceFragments = Math.min(3 + Math.floor(Math.random() * 4), 6); // Cap at 6 ice fragments var iceFragmentCount = game.particles ? Math.min(maxIceFragments, Math.max(0, 50 - game.particles.length)) : maxIceFragments; for (var ip = 0; ip < iceFragmentCount; ip++) { var randomIceScale = 0.4 + Math.random() * 0.8; // Random scale between 0.4 and 1.2 var iceFragment = LK.getAsset('ldinki', { anchorX: 0.5, anchorY: 0.5, scaleX: randomIceScale, scaleY: randomIceScale }); iceFragment.x = self.x; iceFragment.y = self.y; iceFragment.alpha = 0.7; // Make ice particles semi-transparent game.addChild(iceFragment); // Track ice fragment in particles array for cleanup if (!game.particles) { game.particles = []; } game.particles.push(iceFragment); // Random direction and speed for each ice fragment var iceAngle = Math.random() * Math.PI * 2; var iceSpeed = 80 + Math.random() * 120; // Random speed between 80-200 var iceVelocityX = Math.cos(iceAngle) * iceSpeed; var iceVelocityY = Math.sin(iceAngle) * iceSpeed; // Random rotation speed and direction for each ice fragment (slower rotation) var iceRotationSpeed = 0.01 + Math.random() * 0.03; // Random rotation speed between 0.01 and 0.04 radians per frame var iceRotationDirection = Math.random() < 0.5 ? 1 : -1; // Random direction (clockwise or counterclockwise) var iceTotalRotation = iceRotationSpeed * iceRotationDirection * 120; // Total rotation over 2 seconds (120 frames at 60fps) // Animate ice fragment movement, rotation, and scale down over 2 seconds tween(iceFragment, { x: iceFragment.x + iceVelocityX, y: iceFragment.y + iceVelocityY, scaleX: 0, scaleY: 0, rotation: iceFragment.rotation + iceTotalRotation }, { duration: 2000, easing: tween.easeOut, onFinish: function (iceFragmentRef) { return function onFinish() { if (iceFragmentRef.parent) { iceFragmentRef.parent.removeChild(iceFragmentRef); } // Remove from particles tracking array if (game.particles) { var iceFragIndex = game.particles.indexOf(iceFragmentRef); if (iceFragIndex > -1) { game.particles.splice(iceFragIndex, 1); } } }; }(iceFragment) }); } } else { // Create normal fragments for non-frozen objects // Limit fragment creation to prevent excessive memory usage var maxFragments, fragmentCount; if (self.assetId === 'gold') { // Special handling for gold meteors - maximum 3 particles maxFragments = Math.min(1 + Math.floor(Math.random() * 3), 3); // 1-3 fragments, cap at 3 } else if (self.assetId === 'Alien_1') { // Special handling for alien_1 - maximum 3 kaplya fragments maxFragments = Math.min(Math.floor(Math.random() * 3) + 1, 3); // 1-3 fragments, strictly capped at 3 } else { maxFragments = Math.min(3 + Math.floor(Math.random() * 4), 6); // Cap at 6 fragments for others } fragmentCount = game.particles ? Math.min(maxFragments, Math.max(0, 50 - game.particles.length)) : maxFragments; for (var p = 0; p < fragmentCount; p++) { var randomScale; if (self.assetId === 'gold') { // Smaller scale for gold meteor particles randomScale = 0.2 + Math.random() * 0.4; // Random scale between 0.2 and 0.6 (smaller) } else { randomScale = 0.4 + Math.random() * 0.8; // Random scale between 0.4 and 1.2 for others } // Use gold asset for gold meteors, kaplya for alien_1, kusok for others var fragmentAssetId; if (self.assetId === 'gold') { fragmentAssetId = 'gold'; } else if (self.assetId === 'Alien_1') { fragmentAssetId = 'kaplya'; } else { fragmentAssetId = 'kusok'; } var fragmentParticle = LK.getAsset(fragmentAssetId, { anchorX: 0.5, anchorY: 0.5, scaleX: randomScale, scaleY: randomScale }); fragmentParticle.x = self.x; fragmentParticle.y = self.y; game.addChild(fragmentParticle); // Track fragment in particles array for cleanup if (!game.particles) { game.particles = []; } game.particles.push(fragmentParticle); // Random direction and speed for each fragment var angle = Math.random() * Math.PI * 2; var speed = 80 + Math.random() * 120; // Random speed between 80-200 var velocityX = Math.cos(angle) * speed; var velocityY = Math.sin(angle) * speed; // Different behavior for kaplya (alien blood splashes) vs other fragments if (self.assetId === 'Alien_1') { // Kaplya behaves like splashes - scatter then stop and shrink smoothly, total 1 second duration var scatterDistance = speed * 0.4; // How far they scatter (reduced from 0.8 to 0.4) var scatterX = fragmentParticle.x + Math.cos(angle) * scatterDistance; var scatterY = fragmentParticle.y + Math.sin(angle) * scatterDistance; // First phase: scatter to final position (no rotation for splashes) tween(fragmentParticle, { x: scatterX, y: scatterY }, { duration: 200, easing: tween.easeOut, onFinish: function (fragmentRef) { return function onFinish() { // Second phase: pause briefly then smoothly shrink LK.setTimeout(function () { if (fragmentRef.parent) { tween(fragmentRef, { scaleX: 0, scaleY: 0 }, { duration: 600, easing: tween.easeIn, onFinish: function onFinish() { if (fragmentRef.parent) { fragmentRef.parent.removeChild(fragmentRef); } // Remove from particles tracking array if (game.particles) { var fragIndex = game.particles.indexOf(fragmentRef); if (fragIndex > -1) { game.particles.splice(fragIndex, 1); } } } }); } }, 200); // Brief pause before shrinking (200ms) for total 1000ms }; }(fragmentParticle) }); } else { // Other fragments (kusok, gold) keep original rotating behavior var rotationSpeed = 0.01 + Math.random() * 0.03; // Random rotation speed between 0.01 and 0.04 radians per frame var rotationDirection = Math.random() < 0.5 ? 1 : -1; // Random direction (clockwise or counterclockwise) var totalRotation = rotationSpeed * rotationDirection * 120; // Total rotation over 2 seconds (120 frames at 60fps) // Animate fragment movement, rotation, and scale down over 2 seconds tween(fragmentParticle, { x: fragmentParticle.x + velocityX, y: fragmentParticle.y + velocityY, scaleX: 0, scaleY: 0, rotation: fragmentParticle.rotation + totalRotation }, { duration: 2000, easing: tween.easeOut, onFinish: function (fragmentRef) { return function onFinish() { if (fragmentRef.parent) { fragmentRef.parent.removeChild(fragmentRef); } // Remove from particles tracking array if (game.particles) { var fragIndex = game.particles.indexOf(fragmentRef); if (fragIndex > -1) { game.particles.splice(fragIndex, 1); } } }; }(fragmentParticle) }); } } } // Add a small particle explosion or shrink effect with proper cleanup if (tween && self.graphic) { // Stop ALL ongoing tweens on this graphic specifically tween.stop(self.graphic); tween.stop(self.graphic, { scaleX: true, scaleY: true, alpha: true, tint: true, rotation: true }); tween(self.graphic, { scaleX: 0.1, scaleY: 0.1, alpha: 0 }, { duration: 200, onFinish: function (selfRef) { return function onFinish() { // Final cleanup - remove any remaining references if (selfRef.graphic) { tween.stop(selfRef.graphic); selfRef.graphic = null; } // Actual removal from game will be handled in game.update }; }(self) }); } // Sound already played at beginning of method // Special behavior for meteorgold - spawn 3 gold meteors // Check if this object uses the meteorgold asset by checking the stored assetId var isMeteorGold = self.assetId === 'meteorgold'; if (giveMoney && isMeteorGold) { // Store position before any destruction occurs var spawnX = self.x; var spawnY = self.y; // Spawn 3 gold meteors at this location with spreading movement for (var gm = 0; gm < 3; gm++) { (function (goldIndex, posX, posY) { LK.setTimeout(function () { // Create gold meteor directly here instead of calling spawnGoldMeteor var goldMeteor = new FallingObject('gold', ALIEN_HEALTH, METEOR_SPEED, 'meteor'); goldMeteor.moneyReward = 8; // Gold meteor gives 8$ // Position all gold meteors at the same spawn location initially goldMeteor.x = posX; goldMeteor.y = posY; // Generate random spread direction for each meteor var spreadAngle = Math.random() * Math.PI * 2; // Random angle in full circle var spreadDistance = 150 + Math.random() * 100; // Distance to spread (150-250 pixels) var targetSpreadX = posX + Math.cos(spreadAngle) * spreadDistance; var targetSpreadY = posY + Math.sin(spreadAngle) * spreadDistance; // Clamp spread target to screen bounds targetSpreadX = Math.max(50, Math.min(WORLD_WIDTH - 50, targetSpreadX)); targetSpreadY = Math.max(-50, Math.min(posY + 100, targetSpreadY)); // Don't spread too far down // Stop normal falling movement initially goldMeteor.speedY = 0; goldMeteor.isSpreadingPhase = true; // Flag to track spreading phase goldMeteor.normalFallSpeed = METEOR_SPEED; // Store normal fall speed fallingObjects.push(goldMeteor); game.addChild(goldMeteor); // Start spreading movement for 1 second tween(goldMeteor, { x: targetSpreadX, y: targetSpreadY }, { duration: 1000, // 1 second spreading phase easing: tween.easeOut, onFinish: function onFinish() { // After spreading, start normal falling goldMeteor.isSpreadingPhase = false; goldMeteor.speedY = goldMeteor.normalFallSpeed; } }); }, goldIndex * 100); // Slight delay between spawns })(gm, spawnX, spawnY); } } // Track enemy kill for health scaling totalEnemiesKilled++; onEnemyKilledForMeteorEvent(); // New scaling: every 75 kills increases increment by +0.1 every 150 kills var increases = Math.floor(totalEnemiesKilled / 75); var increment = 0.3 + Math.floor(totalEnemiesKilled / 150) * 0.1; var newMultiplier = 1.0 + increases * increment; if (newMultiplier > currentHealthMultiplier) { currentHealthMultiplier = newMultiplier; // Show difficulty level notification var difficultyLevel = increases + 1; // Level starts at 1 // Use current language for difficulty notification var difficultyTextContent = 'Difficulty Level ' + difficultyLevel; if (currentLanguage === 'ru') { difficultyTextContent = 'Уровень сложности ' + difficultyLevel; } else if (currentLanguage === 'de') { difficultyTextContent = 'Schwierigkeitsgrad ' + difficultyLevel; } else if (currentLanguage === 'fr') { difficultyTextContent = 'Niveau de difficulté ' + difficultyLevel; } else if (currentLanguage === 'es') { difficultyTextContent = 'Nivel de dificultad ' + difficultyLevel; } else if (currentLanguage === 'tr') { difficultyTextContent = 'Zorluk seviyesi ' + difficultyLevel; } var difficultyText = new Text2(difficultyTextContent, { size: 120, fill: 0xFFFF00, align: 'center', font: 'Impact' }); difficultyText.anchor.set(0.5, 0.5); difficultyText.x = WORLD_WIDTH / 2; difficultyText.y = WORLD_HEIGHT / 2 - 200; difficultyText.alpha = 0; game.addChild(difficultyText); // Animate difficulty notification tween(difficultyText, { alpha: 1, scaleX: 1.2, scaleY: 1.2 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { // Hold for 2 seconds then fade out LK.setTimeout(function () { tween(difficultyText, { alpha: 0, scaleX: 0.8, scaleY: 0.8 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (difficultyText.parent) { difficultyText.parent.removeChild(difficultyText); } } }); }, 2000); } }); } // Give money reward if (giveMoney) { // Use the moneyReward property set in the constructor var scoreIncrement; if (self.assetId === 'gold') { scoreIncrement = Math.min(Math.floor(totalEnemiesKilled / 50) * 2, 40); // Cap at 40 for gold meteors } else { scoreIncrement = Math.min(Math.floor(totalEnemiesKilled / 50) * 2, 20); // Cap at 20 for others } var moneyEarned = self.moneyReward + scoreIncrement; LK.setScore(LK.getScore() + moneyEarned); // --- Ava: Also increment finalScore by 1 for each enemy killed (not spent) --- finalScore += 1; updateFinalScoreDisplay && updateFinalScoreDisplay(); updateScoreDisplay(); // Create floating money number display var moneyNumber = new Text2('$' + moneyEarned.toString(), { size: 80, fill: 0x00FF00, align: 'center', font: 'Impact' }); moneyNumber.anchor.set(0.5, 0.5); moneyNumber.x = self.x + (Math.random() - 0.5) * 50; // Slight random offset moneyNumber.y = self.y + 40; game.addChild(moneyNumber); // Animate money number floating down without fading out tween(moneyNumber, { y: moneyNumber.y + 150, scaleX: 1.4, scaleY: 1.4 }, { duration: 1500, easing: tween.easeOut }); // Remove money number after timeout LK.setTimeout(function () { if (moneyNumber.parent) { game.removeChild(moneyNumber); } }, 1500); } }; // Define update method on prototype so it can be called by subclasses self.updateParent = function () { if (self.isDestroyed) { return; } // Pause all movement and effects when upgrades or shop popup is open if (upgradesPopup || controlsPopup || shopPopup) { // Store the time when the game was paused if (!self._pauseTime) { self._pauseTime = LK.ticks * (1000 / 60); } return; } // If the game was paused and is now resumed, adjust the effect timers if (self._pauseTime) { var pauseDuration = LK.ticks * (1000 / 60) - self._pauseTime; // Adjust relevant timers by the pause duration self.burnStartTime += pauseDuration; self.lastBurnDamageTime += pauseDuration; self.freezeStartTime += pauseDuration; self.freezeBlockEndTime += pauseDuration; self.fireTowerBurnStartTime += pauseDuration; self.lastFireTowerBurnDamageTime += pauseDuration; // Adjust alien_1 zigzag timer if present if (self.lastZigzagTime !== undefined) { self.lastZigzagTime += pauseDuration; } self._pauseTime = null; // Reset pause time } // Only apply normal falling movement if not in spreading phase if (!self.isSpreadingPhase) { self.y += self.speedY * 1.04; // 0.65 * 1.6 = 1.04 } // Add zigzag movement for alien_1 if (self.assetId === 'Alien_1') { // Initialize zigzag properties if not set if (self.zigzagDirection === undefined) { self.zigzagDirection = Math.random() < 0.5 ? -1 : 1; // Random initial direction self.zigzagDistance = 200 + Math.random() * 100; // Distance to move (200-300 pixels) - much larger self.zigzagInterval = 1000; // Change direction every 1000ms (1 second) self.lastZigzagTime = LK.ticks * (1000 / 60); self.isZigzagging = false; self.zigzagStartX = self.x; } // Update zigzag movement - but only if not fully frozen in ice block var currentTime = LK.ticks * (1000 / 60); if (currentTime - self.lastZigzagTime >= self.zigzagInterval && !self.isZigzagging && !self.isFullyFrozen) { // Start new zigzag movement self.isZigzagging = true; self.zigzagStartX = self.x; self.zigzagDirection *= -1; // Change direction self.lastZigzagTime = currentTime; // Add small random variation to interval and distance for more organic movement var baseInterval = 800 + Math.random() * 400; // 0.8-1.2 seconds - slower than before var baseDistance = 600 + Math.random() * 300; // 600-900 pixels - even larger zigzag // Apply freeze slowdown if partially frozen if (self.freezeStacks > 0) { self.zigzagInterval = baseInterval * 2; // Double the time (slower zigzag) self.zigzagDistance = baseDistance * 0.5; // Half the distance (smaller zigzag) } else { self.zigzagInterval = baseInterval; self.zigzagDistance = baseDistance; } // Calculate target position var targetX = self.zigzagStartX + self.zigzagDirection * self.zigzagDistance; // Keep target within screen bounds targetX = Math.max(50, Math.min(WORLD_WIDTH - 50, targetX)); // If target would be out of bounds, reverse direction if (targetX <= 50 || targetX >= WORLD_WIDTH - 50) { self.zigzagDirection *= -1; targetX = self.zigzagStartX + self.zigzagDirection * self.zigzagDistance; targetX = Math.max(50, Math.min(WORLD_WIDTH - 50, targetX)); } // Animate smooth zigzag movement using tween tween(self, { x: targetX }, { duration: self.zigzagInterval, easing: tween.easeInOut, onFinish: function onFinish() { self.isZigzagging = false; } }); } } // Rotate meteor around its own axis (pause rotation if frozen) // Don't rotate alien_1 enemies if (self.graphic && !self.isFullyFrozen && self.assetId !== 'Alien_1') { self.graphic.rotation += self.rotationSpeed * self.rotationDirection; } // Create health bar if it doesn't exist if (!self.healthBarBg) { // Health bar background self.healthBarBg = self.attachAsset('enemy_health_bar_bg', { anchorX: 0.5, anchorY: 0.5 }); self.healthBarBg.y = -self.graphic.height * (self.graphic.scaleY || 1) / 2 - 20; // Health bar fill (red) self.healthBarFill = self.attachAsset('enemy_health_bar_fill', { anchorX: 0.0, anchorY: 0.5 }); self.healthBarFill.x = -self.healthBarBg.width / 2; self.healthBarFill.y = self.healthBarBg.y; // Damage preview fill (yellow) self.healthBarDamagePreview = self.attachAsset('enemy_health_bar_damage_preview', { anchorX: 0.0, anchorY: 0.5 }); self.healthBarDamagePreview.x = -self.healthBarBg.width / 2; self.healthBarDamagePreview.y = self.healthBarBg.y; self.healthBarDamagePreview.width = 0; self.healthBarDamagePreview.alpha = 0; } // Update health bar position and visibility if (self.healthBarBg) { self.healthBarBg.y = -self.graphic.height * (self.graphic.scaleY || 1) / 2 - 20; self.healthBarFill.y = self.healthBarBg.y; self.healthBarDamagePreview.y = self.healthBarBg.y; // Hide health bar if at full health if (self.health >= self.maxHealth) { self.healthBarBg.visible = false; self.healthBarFill.visible = false; self.healthBarDamagePreview.visible = false; } else { self.healthBarBg.visible = true; self.healthBarFill.visible = true; self.healthBarDamagePreview.visible = true; } } // Process freeze effect var currentTime = LK.ticks * (1000 / 60); // Handle freeze mechanics if (self.isFullyFrozen) { // If upgrades popup is open, pause the freeze timer by extending the block end time if (upgradesPopup && !self.freezePausedTime) { self.freezePausedTime = currentTime; } else if (!upgradesPopup && self.freezePausedTime) { // Resume freeze timer by adjusting block end time var pauseDuration = currentTime - self.freezePausedTime; self.freezeBlockEndTime += pauseDuration; self.freezePausedTime = null; } // Update ice block position if (self.iceBlock) { self.iceBlock.x = self.x; self.iceBlock.y = self.y; } // Check if freeze block duration expired (only if upgrades popup is not open) if (!upgradesPopup && currentTime >= self.freezeBlockEndTime) { self.isFullyFrozen = false; self.speedY = self.originalSpeedY; // Restore normal speed // Remove ice block if (self.iceBlock && self.iceBlock.parent) { game.removeChild(self.iceBlock); self.iceBlock = null; } // Restore normal tint tween(self.graphic, { tint: 0xFFFFFF }, { duration: 300, easing: tween.easeOut }); } } else if (self.freezeStacks > 0) { // If upgrades popup is open, pause the freeze timer by extending the start time if (upgradesPopup && !self.freezePausedTime) { self.freezePausedTime = currentTime; } else if (!upgradesPopup && self.freezePausedTime) { // Resume freeze timer by adjusting start time var pauseDuration = currentTime - self.freezePausedTime; self.freezeStartTime += pauseDuration; self.freezePausedTime = null; } // Handle partial freeze (50% slowdown and blue tint) - only if upgrades popup is not open if (!upgradesPopup && currentTime - self.freezeStartTime >= 4000) { // Freeze stacks expired self.freezeStacks = 0; self.speedY = self.originalSpeedY; // Restore normal speed // Restore normal tint when freeze expires tween(self.graphic, { tint: 0xFFFFFF }, { duration: 300, easing: tween.easeOut }); } } // Create and manage snowflake particles for frozen targets (both partially frozen and fully frozen) if (self.freezeStacks > 0 || self.isFullyFrozen) { // Initialize snowflake particles array on the object if not exists if (!self.snowflakeParticles) { self.snowflakeParticles = []; } // Create new snowflakes periodically if (LK.ticks % 20 === 0 && self.snowflakeParticles.length < 8) { var snowflake = LK.getAsset('Sneg', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.3 + Math.random() * 0.4, // Random scale between 0.3 and 0.7 scaleY: 0.3 + Math.random() * 0.4 }); // Position snowflake around the frozen target and store relative offset var targetWidth = self.graphic.width * (self.graphic.scaleX || 1); var targetHeight = self.graphic.height * (self.graphic.scaleY || 1); // Store relative offset from object center snowflake.offsetX = (Math.random() - 0.5) * targetWidth * 1.2; snowflake.offsetY = (Math.random() - 0.5) * targetHeight * 1.2; // Set initial position snowflake.x = self.x + snowflake.offsetX; snowflake.y = self.y + snowflake.offsetY; snowflake.alpha = 1.0; // Non-transparent // Add gentle floating motion within small radius snowflake.floatAngle = Math.random() * Math.PI * 2; snowflake.floatSpeed = 2 + Math.random() * 3; // Very slow float speed game.addChild(snowflake); self.snowflakeParticles.push(snowflake); // Add melting/fading cycle animation var meltDelay = Math.random() * 1000; LK.setTimeout(function (flakeRef) { return function () { var _meltCycle = function _meltCycle() { if (flakeRef.parent) { // Start melting/fading tween(flakeRef, { alpha: 0, scaleX: flakeRef.scaleX * 0.3, scaleY: flakeRef.scaleY * 0.3 }, { duration: 800 + Math.random() * 600, easing: tween.easeOut, onFinish: function onFinish() { // Immediately reappear at full opacity flakeRef.alpha = 1.0; // Non-transparent flakeRef.scaleX = 0.8 + Math.random() * 0.6; flakeRef.scaleY = flakeRef.scaleX; // Wait before next melt cycle LK.setTimeout(_meltCycle, 200 + Math.random() * 400); } }); } }; _meltCycle(); }; }(snowflake), meltDelay); } // Update positions of existing snowflakes to follow the object with gentle floating for (var sf = 0; sf < self.snowflakeParticles.length; sf++) { var snowflake = self.snowflakeParticles[sf]; if (snowflake.parent && snowflake.offsetX !== undefined && snowflake.offsetY !== undefined) { // Update floating motion snowflake.floatAngle += 0.02 + Math.random() * 0.01; // Slow rotation of float pattern var floatX = Math.cos(snowflake.floatAngle) * snowflake.floatSpeed; var floatY = Math.sin(snowflake.floatAngle) * snowflake.floatSpeed; // Update position to follow object with gentle floating motion snowflake.x = self.x + snowflake.offsetX + floatX; snowflake.y = self.y + snowflake.offsetY + floatY; } } } else { // Clean up snowflakes when freeze ends if (self.snowflakeParticles) { for (var sf = 0; sf < self.snowflakeParticles.length; sf++) { var snowflake = self.snowflakeParticles[sf]; if (snowflake.parent) { // Fade out and remove tween(snowflake, { alpha: 0, scaleX: 0, scaleY: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function (flakeRef) { return function () { if (flakeRef.parent) { flakeRef.parent.removeChild(flakeRef); } }; }(snowflake) }); } } self.snowflakeParticles = []; } } // Process burning effect if (self.burnStacks > 0) { // If upgrades popup is open, pause the burn timer by extending the start time if (upgradesPopup && !self.burnPausedTime) { self.burnPausedTime = currentTime; } else if (!upgradesPopup && self.burnPausedTime) { // Resume burn timer by adjusting start time var pauseDuration = currentTime - self.burnPausedTime; self.burnStartTime += pauseDuration; self.burnPausedTime = null; } // Update burn flame visuals based on stack count var requiredFlames = 0; if (self.burnStacks >= 1 && self.burnStacks <= 3) { requiredFlames = 1; } else if (self.burnStacks >= 4 && self.burnStacks <= 7) { requiredFlames = 2; } else if (self.burnStacks >= 8 && self.burnStacks <= 10) { requiredFlames = 3; } // Initialize burn flames array if not exists if (!self.burnFlames) { self.burnFlames = []; } // Add flames if we need more while (self.burnFlames.length < requiredFlames) { var flame = LK.getAsset('ogonek', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.15, scaleY: 0.15 }); // Position flame randomly on the target and store relative offset var targetWidth = self.graphic.width * (self.graphic.scaleX || 1); var targetHeight = self.graphic.height * (self.graphic.scaleY || 1); flame.offsetX = (Math.random() - 0.5) * targetWidth * 0.8; flame.offsetY = (Math.random() - 0.5) * targetHeight * 0.8; flame.x = self.x + flame.offsetX; flame.y = self.y + flame.offsetY; game.addChild(flame); self.burnFlames.push(flame); // Add initial scale-up effect to show fire ignition tween(flame, { scaleX: 0.1, scaleY: 0.1 }, { duration: 2000, easing: tween.easeOut }); // Add pulsating animation to flame (original behavior) var pulseDelay = Math.random() * 500; LK.setTimeout(function (flameRef) { return function () { var _pulseFlame = function pulseFlame() { if (flameRef.parent) { // Animate flame pulsating scale tween(flameRef, { scaleX: 0.1 + Math.random() * 0.05, scaleY: 0.1 + Math.random() * 0.05 }, { duration: 800 + Math.random() * 400, easing: tween.easeInOut, onFinish: function onFinish() { // Continue the pulsing cycle if (flameRef.parent) { LK.setTimeout(_pulseFlame, 300 + Math.random() * 500); } } }); } }; _pulseFlame(); }; }(flame), pulseDelay); } // Update positions of existing flames to follow the object for (var f = 0; f < self.burnFlames.length; f++) { var flame = self.burnFlames[f]; if (flame.parent && flame.offsetX !== undefined && flame.offsetY !== undefined) { flame.x = self.x + flame.offsetX; flame.y = self.y + flame.offsetY; } } // Remove excess flames if we have too many while (self.burnFlames.length > requiredFlames) { var excessFlame = self.burnFlames.pop(); if (excessFlame.parent) { game.removeChild(excessFlame); } } // Check if burn duration has expired (10 seconds = 10000ms) - only if upgrades popup is not open if (!upgradesPopup && currentTime - self.burnStartTime >= 10000) { self.burnStacks = 0; // Remove all burn flames when burn expires for (var f = 0; f < self.burnFlames.length; f++) { if (self.burnFlames[f].parent) { game.removeChild(self.burnFlames[f]); } } self.burnFlames = []; } // Apply burn damage every 0.5 seconds (only if there are active burn stacks and upgrades popup is not open) if (currentTime - self.lastBurnDamageTime >= 500 && self.burnStacks > 0 && !upgradesPopup) { var burnDamage = Math.floor(FIRE_DAMAGE * 0.2 * self.burnStacks); // 20% of actual fire damage per stack self.takeDamage(burnDamage); self.lastBurnDamageTime = currentTime; // Update health bar visual for burn damage if (self.healthBarBg) { self.updateHealthBarVisual(); } // Check if object is destroyed by burn damage if (self.health <= 0) { self.destroySelf(); return; } } // Process fire tower burning effect (independent from regular burn) if (self.fireTowerBurnStacks > 0) { // If upgrades popup is open, pause the fire tower burn timer by extending the start time if (upgradesPopup && !self.fireTowerBurnPausedTime) { self.fireTowerBurnPausedTime = currentTime; } else if (!upgradesPopup && self.fireTowerBurnPausedTime) { // Resume fire tower burn timer by adjusting start time var pauseDuration = currentTime - self.fireTowerBurnPausedTime; self.fireTowerBurnStartTime += pauseDuration; self.fireTowerBurnPausedTime = null; } // Update fire tower burn flame visuals based on stack count var requiredFireTowerFlames = 0; if (self.fireTowerBurnStacks >= 1 && self.fireTowerBurnStacks <= 3) { requiredFireTowerFlames = 1; } else if (self.fireTowerBurnStacks >= 4 && self.fireTowerBurnStacks <= 7) { requiredFireTowerFlames = 2; } else if (self.fireTowerBurnStacks >= 8 && self.fireTowerBurnStacks <= 10) { requiredFireTowerFlames = 3; } // Initialize fire tower burn flames array if not exists if (!self.fireTowerBurnFlames) { self.fireTowerBurnFlames = []; } // Add fire tower flames if we need more while (self.fireTowerBurnFlames.length < requiredFireTowerFlames) { var fireTowerFlame = LK.getAsset('ogonek', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.15, scaleY: 0.15 }); // Position flame randomly on the target and store relative offset var targetWidth = self.graphic.width * (self.graphic.scaleX || 1); var targetHeight = self.graphic.height * (self.graphic.scaleY || 1); fireTowerFlame.offsetX = (Math.random() - 0.5) * targetWidth * 0.8; fireTowerFlame.offsetY = (Math.random() - 0.5) * targetHeight * 0.8; fireTowerFlame.x = self.x + fireTowerFlame.offsetX; fireTowerFlame.y = self.y + fireTowerFlame.offsetY; fireTowerFlame.tint = 0xFF2500; // More intense orange-red for fire tower burn game.addChild(fireTowerFlame); self.fireTowerBurnFlames.push(fireTowerFlame); // Add initial scale-up effect to show fire ignition tween(fireTowerFlame, { scaleX: 0.1, scaleY: 0.1 }, { duration: 2000, easing: tween.easeOut }); // Add pulsating animation to fire tower flame (original behavior) var pulseDelay = Math.random() * 500; LK.setTimeout(function (flameRef) { return function () { var _pulseFireTowerFlame = function pulseFireTowerFlame() { if (flameRef.parent) { // Animate flame pulsating scale tween(flameRef, { scaleX: 0.1 + Math.random() * 0.05, scaleY: 0.1 + Math.random() * 0.05 }, { duration: 800 + Math.random() * 400, easing: tween.easeInOut, onFinish: function onFinish() { // Continue the pulsing cycle if (flameRef.parent) { LK.setTimeout(_pulseFireTowerFlame, 300 + Math.random() * 500); } } }); } }; _pulseFireTowerFlame(); }; }(fireTowerFlame), pulseDelay); } // Update positions of existing fire tower flames to follow the object for (var ftf = 0; ftf < self.fireTowerBurnFlames.length; ftf++) { var fireTowerFlame = self.fireTowerBurnFlames[ftf]; if (fireTowerFlame.parent && fireTowerFlame.offsetX !== undefined && fireTowerFlame.offsetY !== undefined) { fireTowerFlame.x = self.x + fireTowerFlame.offsetX; fireTowerFlame.y = self.y + fireTowerFlame.offsetY; } } // Remove excess fire tower flames if we have too many while (self.fireTowerBurnFlames.length > requiredFireTowerFlames) { var excessFireTowerFlame = self.fireTowerBurnFlames.pop(); if (excessFireTowerFlame.parent) { game.removeChild(excessFireTowerFlame); } } // Check if fire tower burn duration has expired (10 seconds = 10000ms) - only if upgrades popup is not open if (!upgradesPopup && currentTime - self.fireTowerBurnStartTime >= 10000) { self.fireTowerBurnStacks = 0; // Remove all fire tower burn flames when burn expires for (var ftf = 0; ftf < self.fireTowerBurnFlames.length; ftf++) { if (self.fireTowerBurnFlames[ftf].parent) { game.removeChild(self.fireTowerBurnFlames[ftf]); } } self.fireTowerBurnFlames = []; } // Apply fire tower burn damage every 1000ms (1 second) instead of 500ms if (currentTime - self.lastFireTowerBurnDamageTime >= 1000 && self.fireTowerBurnStacks > 0 && !upgradesPopup) { var fireTowerBurnDamage = Math.floor(50 * self.fireTowerBurnStacks); // 50 damage per stack (50% of 100 projectile damage) self.takeDamage(fireTowerBurnDamage); self.lastFireTowerBurnDamageTime = currentTime; // Update health bar visual for fire tower burn damage if (self.healthBarBg) { self.updateHealthBarVisual(); } // Removed floating fire tower burn damage number text // Check if object is destroyed by fire tower burn damage if (self.health <= 0) { self.destroySelf(); return; } } } } else { // Clean up regular burn flames when burn ends if (self.burnFlames) { for (var f = 0; f < self.burnFlames.length; f++) { if (self.burnFlames[f].parent) { game.removeChild(self.burnFlames[f]); } } self.burnFlames = []; } } // Process fire tower burning effect (independent from regular burn) if (self.fireTowerBurnStacks > 0) { // If upgrades popup is open, pause the fire tower burn timer by extending the start time if (upgradesPopup && !self.fireTowerBurnPausedTime) { self.fireTowerBurnPausedTime = currentTime; } else if (!upgradesPopup && self.fireTowerBurnPausedTime) { // Resume fire tower burn timer by adjusting start time var pauseDuration = currentTime - self.fireTowerBurnPausedTime; self.fireTowerBurnStartTime += pauseDuration; self.fireTowerBurnPausedTime = null; } // Update fire tower burn flame visuals based on stack count var requiredFireTowerFlames = 0; if (self.fireTowerBurnStacks >= 1 && self.fireTowerBurnStacks <= 3) { requiredFireTowerFlames = 1; } else if (self.fireTowerBurnStacks >= 4 && self.fireTowerBurnStacks <= 7) { requiredFireTowerFlames = 2; } else if (self.fireTowerBurnStacks >= 8 && self.fireTowerBurnStacks <= 10) { requiredFireTowerFlames = 3; } // Initialize fire tower burn flames array if not exists if (!self.fireTowerBurnFlames) { self.fireTowerBurnFlames = []; } // Add fire tower flames if we need more while (self.fireTowerBurnFlames.length < requiredFireTowerFlames) { var fireTowerFlame = LK.getAsset('ogonek', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.15, scaleY: 0.15 }); // Position flame randomly on the target and store relative offset var targetWidth = self.graphic.width * (self.graphic.scaleX || 1); var targetHeight = self.graphic.height * (self.graphic.scaleY || 1); fireTowerFlame.offsetX = (Math.random() - 0.5) * targetWidth * 0.8; fireTowerFlame.offsetY = (Math.random() - 0.5) * targetHeight * 0.8; fireTowerFlame.x = self.x + fireTowerFlame.offsetX; fireTowerFlame.y = self.y + fireTowerFlame.offsetY; fireTowerFlame.tint = 0xFF2500; // More intense orange-red for fire tower burn game.addChild(fireTowerFlame); self.fireTowerBurnFlames.push(fireTowerFlame); // Add initial scale-up effect to show fire ignition tween(fireTowerFlame, { scaleX: 0.1, scaleY: 0.1 }, { duration: 2000, easing: tween.easeOut }); // Add pulsating animation to fire tower flame (original behavior) var pulseDelay = Math.random() * 500; LK.setTimeout(function (flameRef) { return function () { var _pulseFireTowerFlame = function pulseFireTowerFlame() { if (flameRef.parent) { // Animate flame pulsating scale tween(flameRef, { scaleX: 0.1 + Math.random() * 0.05, scaleY: 0.1 + Math.random() * 0.05 }, { duration: 800 + Math.random() * 400, easing: tween.easeInOut, onFinish: function onFinish() { // Continue the pulsing cycle if (flameRef.parent) { LK.setTimeout(_pulseFireTowerFlame, 300 + Math.random() * 500); } } }); } }; _pulseFireTowerFlame(); }; }(fireTowerFlame), pulseDelay); } // Update positions of existing fire tower flames to follow the object for (var ftf = 0; ftf < self.fireTowerBurnFlames.length; ftf++) { var fireTowerFlame = self.fireTowerBurnFlames[ftf]; if (fireTowerFlame.parent && fireTowerFlame.offsetX !== undefined && fireTowerFlame.offsetY !== undefined) { fireTowerFlame.x = self.x + fireTowerFlame.offsetX; fireTowerFlame.y = self.y + fireTowerFlame.offsetY; } } // Remove excess fire tower flames if we have too many while (self.fireTowerBurnFlames.length > requiredFireTowerFlames) { var excessFireTowerFlame = self.fireTowerBurnFlames.pop(); if (excessFireTowerFlame.parent) { game.removeChild(excessFireTowerFlame); } } // Check if fire tower burn duration has expired (10 seconds = 10000ms) - only if upgrades popup is not open if (!upgradesPopup && currentTime - self.fireTowerBurnStartTime >= 10000) { self.fireTowerBurnStacks = 0; // Remove all fire tower burn flames when burn expires for (var ftf = 0; ftf < self.fireTowerBurnFlames.length; ftf++) { if (self.fireTowerBurnFlames[ftf].parent) { game.removeChild(self.fireTowerBurnFlames[ftf]); } } self.fireTowerBurnFlames = []; } // Apply fire tower burn damage every 1000ms (1 second) instead of 500ms if (currentTime - self.lastFireTowerBurnDamageTime >= 1000 && self.fireTowerBurnStacks > 0 && !upgradesPopup) { var fireTowerBurnDamage = Math.floor(50 * self.fireTowerBurnStacks); // 50 damage per stack (50% of 100 projectile damage) self.health -= fireTowerBurnDamage; self.lastFireTowerBurnDamageTime = currentTime; // Update health bar visual for fire tower burn damage if (self.healthBarBg) { self.updateHealthBarVisual(); } // Check if object is destroyed by fire tower burn damage if (self.health <= 0) { self.destroySelf(); return; } } } else { // Clean up fire tower burn flames when burn ends if (self.fireTowerBurnFlames) { for (var ftf = 0; ftf < self.fireTowerBurnFlames.length; ftf++) { if (self.fireTowerBurnFlames[ftf].parent) { game.removeChild(self.fireTowerBurnFlames[ftf]); } } self.fireTowerBurnFlames = []; } } // Health bar visual update method - same as Enemy class self.updateHealthBarVisual = function () { if (!self.healthBarFill || !self.healthBarBg || !self.healthBarDamagePreview) { return; } var healthPercentage = Math.max(0, self.health) / self.maxHealth; var targetFillWidth = self.healthBarBg.width * healthPercentage; var currentRedBarWidth = self.healthBarFill.width; var damageEffectVisualWidth = currentRedBarWidth - targetFillWidth; // Stop any ongoing animations on these elements tween.stop(self.healthBarFill); tween.stop(self.healthBarDamagePreview); if (damageEffectVisualWidth <= 0) { // No damage or health increased, just set the width directly self.healthBarFill.width = targetFillWidth; self.healthBarDamagePreview.alpha = 0; self.healthBarDamagePreview.width = 0; return; } // Setup the yellow damage preview bar self.healthBarDamagePreview.alpha = 1; self.healthBarDamagePreview.width = damageEffectVisualWidth; // Position the yellow bar to cover the part of the health bar that is "lost" self.healthBarDamagePreview.x = self.healthBarFill.x + targetFillWidth; // Animate the red health bar shrinking tween(self.healthBarFill, { width: targetFillWidth }, { duration: 500, easing: tween.easeOut }); // Animate the yellow bar shrinking from right to left tween(self.healthBarDamagePreview, { width: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { self.healthBarDamagePreview.alpha = 0; self.healthBarDamagePreview.width = 0; } }); }; }; // Set the update method to call updateParent by default self.update = self.updateParent; // Add wings for alien_1 BEFORE the main graphic so they appear behind if (assetId === 'Alien_1') { self.wings = self.attachAsset('krilo', { anchorX: 0.5, anchorY: 0.5 }); self.wings.x = 0; self.wings.y = 0; self.wings.originalScaleX = 1.0; self.wings.originalScaleY = 1.0; // Start constant flapping animation var _flapWings2 = function _flapWings() { if (!self.wings || !self.wings.parent || self.isDestroyed) { return; } // Compress wings toward center (squeeze from left and right) tween(self.wings, { scaleX: 0.4, // Compress horizontally toward center (stronger compression) scaleY: self.wings.originalScaleY }, { duration: 150, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.wings || !self.wings.parent || self.isDestroyed) { return; } // Expand wings back out tween(self.wings, { scaleX: self.wings.originalScaleX, scaleY: self.wings.originalScaleY }, { duration: 150, easing: tween.easeInOut, onFinish: function onFinish() { // Continue flapping cycle _flapWings2(); } }); } }); }; _flapWings2(); // Start the flapping animation } // Initialization - main graphic added AFTER wings so it appears in front self.graphic = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Apply health scaling based on enemies killed self.health = Math.floor(initialHealth * currentHealthMultiplier); self.maxHealth = self.health; // Store max health for potential future use (e.g. health bars on objects) self.speedY = fallSpeed; self.isDestroyed = false; self.objectType = objectType; // 'meteor' or 'alien' self.assetId = assetId; // Store the asset ID for later identification self.burnStacks = 0; // Number of burn stacks (max 5) self.burnStartTime = 0; // Time when burn effect started self.lastBurnDamageTime = 0; // Track when burn damage was last applied self.freezeStacks = 0; // Number of freeze stacks (max 5) self.freezeStartTime = 0; // Time when freeze effect started self.isFullyFrozen = false; // When 10 stacks reached self.freezeBlockEndTime = 0; // When ice block expires self.freezePausedTime = null; // Track freeze pause time for upgrades menu self.originalSpeedY = self.speedY; // Store original speed for freeze slowdown // Calculate rotation speed based on falling speed for realistic physics var baseRotationSpeed = 0.003; // Increased base minimum rotation speed var speedMultiplier = Math.max(0.5, self.speedY / 2.0); // Scale rotation based on speed with faster multiplier self.rotationSpeed = baseRotationSpeed * speedMultiplier + Math.random() * 0.003; // Speed-based rotation with faster base self.rotationDirection = Math.random() < 0.5 ? 1 : -1; // Random direction (clockwise or counterclockwise) self.iceBlock = null; // Reference to ice block visual // Health display disabled - no longer creating or displaying enemy health self.healthDisplay = null; self.applyBurn = function () { // Add burn stack (max 10) if (self.burnStacks < 10) { self.burnStacks++; } // Reset burn start time to current time for 10 second duration var currentTime = LK.ticks * (1000 / 60); self.burnStartTime = currentTime; self.burnPausedTime = null; // Reset pause time when burn is applied // Visual effect for applying burn tween(self.graphic, { tint: 0xFF4500 // Orange-red tint for burn }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self.graphic, { tint: 0xFFFFFF // Back to white }, { duration: 200, easing: tween.easeIn }); } }); }; // Initialize fire tower burn properties in constructor self.fireTowerBurnStacks = 0; self.fireTowerBurnStartTime = 0; self.lastFireTowerBurnDamageTime = 0; self.fireTowerBurnPausedTime = null; self.applyFireTowerBurn = function () { // Add fire tower burn stack (max 10, independent from regular burn) if (self.fireTowerBurnStacks < 10) { self.fireTowerBurnStacks++; // Add 1 stack per hit } // Reset fire tower burn start time to current time for 10 second duration var currentTime = LK.ticks * (1000 / 60); self.fireTowerBurnStartTime = currentTime; self.fireTowerBurnPausedTime = null; // Reset pause time when burn is applied // Visual effect for applying fire tower burn (more intense orange) tween(self.graphic, { tint: 0xFF2500 // More intense orange-red tint for fire tower burn }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self.graphic, { tint: 0xFFFFFF // Back to white }, { duration: 200, easing: tween.easeIn }); } }); }; self.applyFreeze = function () { // Cannot apply freeze if fully frozen (ice block active) if (self.isFullyFrozen) { return; } // Add freeze stack (max 3) if (self.freezeStacks < 3) { self.freezeStacks++; } // Reset freeze start time to current time for 4 second duration var currentTime = LK.ticks * (1000 / 60); self.freezeStartTime = currentTime; // Apply 50% speed reduction self.speedY = self.originalSpeedY * 0.5; // Visual effect for applying freeze - blue tint that stays while frozen tween(self.graphic, { tint: 0x87CEEB // Light blue tint for freeze }, { duration: 200, easing: tween.easeOut }); // Check if 3 stacks reached - full freeze if (self.freezeStacks >= 3) { // Play ice block creation sound only if cooldown is not active var currentTime = LK.ticks * (1000 / 60); if (!freezeSoundCooldownActive || currentTime >= freezeSoundCooldownEndTime) { LK.getSound('icetresk').play(); freezeSoundCooldownActive = true; freezeSoundCooldownEndTime = currentTime + 500; // 500ms cooldown } self.isFullyFrozen = true; self.freezeStacks = 0; // Clear stacks self.speedY = 0; // Stop completely self.freezeBlockEndTime = currentTime + 4000; // 4 seconds // Create ice block visual self.iceBlock = LK.getAsset('Ice', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); self.iceBlock.x = self.x; self.iceBlock.y = self.y; self.iceBlock.alpha = 0.6; game.addChild(self.iceBlock); // Keep object blue tinted while frozen tween(self.graphic, { tint: 0x4169E1 // Dark blue for full freeze }, { duration: 300, easing: tween.easeOut }); } }; return self; }); // Redirect to new medium insect var IceTower = Container.expand(function () { var self = Container.call(this); // Create main ice tower graphic self.towerGraphic = self.attachAsset('ice_tower', { anchorX: 0.5, anchorY: 1.0, scaleX: 0.15, scaleY: 0.15 }); // Create ice spheres self.iceSphere1 = self.attachAsset('Ice_spher', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.7, scaleY: 0.7 }); self.iceSphere2 = self.attachAsset('Ice_spher', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.7, scaleY: 0.7 }); // Position spheres at top of tower var towerHeight = self.towerGraphic.height * self.towerGraphic.scaleY; self.iceSphere1.y = -towerHeight - 70; self.iceSphere2.y = -towerHeight - 70; self.iceSphere1.alpha = 0.8; self.iceSphere2.alpha = 0.8; // Rotation properties self.rotation1 = 0; self.rotation2 = 0; self.rotationSpeed = 0.03; // Initialize snowflake particles array self.snowflakeParticles = []; self.update = function () { if (!self.parent) { return; } // Update ice sphere rotations self.rotation1 += self.rotationSpeed; self.rotation2 -= self.rotationSpeed; self.iceSphere1.rotation = self.rotation1; self.iceSphere2.rotation = self.rotation2; // Create snowflake particles around ice spheres if (LK.ticks % 12 === 0) { self.createSnowflakeParticles(); } // Update existing snowflake positions self.updateSnowflakeParticles(); }; self.createSnowflakeParticles = function () { if (self.snowflakeParticles.length >= 16) { return; } // Increased limit for more snowflakes var sphere = Math.random() < 0.5 ? self.iceSphere1 : self.iceSphere2; var worldX = self.x + sphere.x; var worldY = self.y + sphere.y; // Create snowflakes around the sphere in a circular pattern var snowflake = LK.getAsset('Sneg', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.5 + Math.random() * 0.4, // Half size snowflakes (was 1.0 + Math.random() * 0.8) scaleY: 0.5 + Math.random() * 0.4 }); // Position snowflakes in a larger radius around the sphere var angle = Math.random() * Math.PI * 2; var radius = 60 + Math.random() * 40; // 60-100 pixel radius around sphere snowflake.x = worldX + Math.cos(angle) * radius; snowflake.y = worldY + Math.sin(angle) * radius; snowflake.alpha = 1.0; // Always fully visible // Add snowflake at a lower z-index to ensure it appears behind menus var leftCityIndex = -1; for (var childIdx = 0; childIdx < game.children.length; childIdx++) { if (game.children[childIdx] === leftCityImage) { leftCityIndex = childIdx; break; } } if (leftCityIndex !== -1) { game.addChildAt(snowflake, leftCityIndex + 1); // Add right after left city image } else { game.addChild(snowflake); // Fallback to normal add if left city not found } self.snowflakeParticles.push(snowflake); // Animate snowflake with gentle floating motion, then shrinking without transparency var floatX = snowflake.x + (Math.random() - 0.5) * 80; var floatY = snowflake.y - 40 - Math.random() * 60; // Float upward tween(snowflake, { x: floatX, y: floatY }, { duration: 1500 + Math.random() * 1000, // Gentle floating phase easing: tween.easeOut, onFinish: function (flakeRef) { return function () { // After floating, shrink without becoming transparent if (flakeRef.parent) { tween(flakeRef, { scaleX: 0, scaleY: 0 // No alpha change - snowflakes remain fully visible while shrinking }, { duration: 800 + Math.random() * 400, // Shrinking phase easing: tween.easeIn, onFinish: function onFinish() { if (flakeRef.parent) { flakeRef.parent.removeChild(flakeRef); } var index = self.snowflakeParticles.indexOf(flakeRef); if (index > -1) { self.snowflakeParticles.splice(index, 1); } } }); } }; }(snowflake) }); }; self.updateSnowflakeParticles = function () { // Clean up destroyed snowflakes for (var i = self.snowflakeParticles.length - 1; i >= 0; i--) { if (!self.snowflakeParticles[i].parent) { self.snowflakeParticles.splice(i, 1); } } }; self.shootIceProjectiles = function () { // Ice tower compression before shooting var originalScale = self.towerGraphic.scaleY; tween.stop(self.towerGraphic, { scaleY: true }); tween(self.towerGraphic, { scaleY: originalScale * 0.85 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { if (self.towerGraphic) { tween(self.towerGraphic, { scaleY: originalScale }, { duration: 100, easing: tween.easeIn }); } } }); // Pulsate ice spheres [self.iceSphere1, self.iceSphere2].forEach(function (sphere) { if (!sphere.isPulsing) { sphere.isPulsing = true; var originalSphereScale = sphere.scaleX; tween.stop(sphere, { scaleX: true, scaleY: true }); tween(sphere, { scaleX: originalSphereScale * 1.4, scaleY: originalSphereScale * 1.4 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { if (sphere) { tween(sphere, { scaleX: originalSphereScale, scaleY: originalSphereScale }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { if (sphere) { sphere.isPulsing = false; } } }); } } }); } }); // Create ice projectiles in 180 degree arc var projectileCount = 15; var launchX = self.x + self.iceSphere1.x; var launchY = self.y + self.iceSphere1.y; for (var p = 0; p < projectileCount; p++) { var angle = Math.PI + p / (projectileCount - 1) * Math.PI; var iceProjectile = LK.getAsset('Ice_spher', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.5, scaleY: 0.5 }); iceProjectile.x = launchX; iceProjectile.y = launchY; iceProjectile.alpha = 0.9; iceProjectile.rotation = angle; // Movement properties iceProjectile.velocityX = Math.cos(angle) * 7; iceProjectile.velocityY = Math.sin(angle) * 7; iceProjectile.hitEnemies = []; iceProjectile.rotationSpeed = 0.02 + Math.random() * 0.02; iceProjectile.rotationDirection = Math.random() < 0.5 ? 1 : -1; game.addChild(iceProjectile); if (!game.iceProjectiles) { game.iceProjectiles = []; } game.iceProjectiles.push(iceProjectile); iceProjectile.update = function () { if (!this.parent) { return; } // Pause movement when popups are open if (upgradesPopup || controlsPopup || shopPopup) { return; } this.x += this.velocityX; this.y += this.velocityY; this.rotation += this.rotationSpeed * this.rotationDirection; // Check collision with enemies for (var enemyIndex = 0; enemyIndex < fallingObjects.length; enemyIndex++) { var enemy = fallingObjects[enemyIndex]; if (enemy.isDestroyed) { continue; } if (this.hitEnemies.indexOf(enemy) !== -1) { continue; } var dx = enemy.x - this.x; var dy = enemy.y - this.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= 80) { this.hitEnemies.push(enemy); // Apply ice projectile damage var projectileDamage = 50; enemy.takeDamage(projectileDamage); LK.effects.flashObject(enemy.graphic, 0x87CEEB, 200); // Apply freeze effect - full freeze for 2 seconds (only if not already frozen) if (!enemy.isFullyFrozen) { enemy.isFullyFrozen = true; enemy.freezeBlockEndTime = LK.ticks * (1000 / 60) + 2000; enemy.speedY = 0; } // Create ice block visual if (!enemy.iceBlock) { enemy.iceBlock = LK.getAsset('Ice', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); enemy.iceBlock.x = enemy.x; enemy.iceBlock.y = enemy.y; enemy.iceBlock.alpha = 0.7; game.addChild(enemy.iceBlock); } // Play freeze sound only if cooldown is not active var currentTime = LK.ticks * (1000 / 60); if (!freezeSoundCooldownActive || currentTime >= freezeSoundCooldownEndTime) { LK.getSound('icetresk').play(); freezeSoundCooldownActive = true; freezeSoundCooldownEndTime = currentTime + 500; // 500ms cooldown } if (enemy.health <= 0) { // Remove ice block visual if present if (enemy.iceBlock && enemy.iceBlock.parent) { game.removeChild(enemy.iceBlock); enemy.iceBlock = null; } enemy.destroySelf(); } } } // Remove projectile when off screen if (this.x < -100 || this.x > WORLD_WIDTH + 100 || this.y < -100 || this.y > WORLD_HEIGHT + 100) { if (this.parent) { this.parent.removeChild(this); } if (game.iceProjectiles) { var index = game.iceProjectiles.indexOf(this); if (index > -1) { game.iceProjectiles.splice(index, 1); } } } }; } }; self.destroy = function () { // Clean up snowflakes for (var i = 0; i < self.snowflakeParticles.length; i++) { var snowflake = self.snowflakeParticles[i]; if (snowflake.parent) { snowflake.parent.removeChild(snowflake); } } self.snowflakeParticles = []; // Animate tower disappearing tween(self, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (self.parent) { self.parent.removeChild(self); } } }); }; return self; }); var Rocket = Container.expand(function (targetEnemy) { var self = Container.call(this); // Initialize rocket properties self.graphic = self.attachAsset('raketa', { anchorX: 0.5, anchorY: 0.5 }); // Add rocket flame self.flameGraphic = self.attachAsset('raketplam', { anchorX: 0.5, anchorY: 0.5 }); self.flameGraphic.x = 0; self.flameGraphic.y = 0; self.target = targetEnemy; self.speed = 8; // Rocket speed self.damage = 500; self.turnSpeed = 0.15; // How quickly rocket can turn (smoother turning) self.isDestroyed = false; // Start from random position along city width at city level self.x = Math.random() * WORLD_WIDTH; self.y = WORLD_HEIGHT - 100; // Just above city level // Initial velocity toward target var dx = self.target.x - self.x; var dy = self.target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); self.velocityX = dx / distance * self.speed; self.velocityY = dy / distance * self.speed; // Set initial rotation to match velocity direction self.graphic.rotation = Math.atan2(self.velocityY, self.velocityX); // Start flame pulsation animation self.startFlamePulsation = function () { if (!self.flameGraphic || !self.flameGraphic.parent) { return; } var _flamePulse2 = function _flamePulse() { if (!self.flameGraphic || !self.flameGraphic.parent || self.isDestroyed) { return; } var targetScaleX = 0.6 + Math.random() * 0.6; // Scale between 0.6 and 1.2 (more compression) var targetScaleY = 0.5 + Math.random() * 1.0; // Scale between 0.5 and 1.5 (more stretching downward) tween(self.flameGraphic, { scaleX: targetScaleX, scaleY: targetScaleY }, { duration: 200 + Math.random() * 300, // Duration between 200-500ms easing: tween.easeInOut, onFinish: function onFinish() { // Continue pulsing LK.setTimeout(_flamePulse2, 50 + Math.random() * 100); } }); }; _flamePulse2(); }; // Start the pulsation self.startFlamePulsation(); self.update = function () { if (self.isDestroyed) { return; } // Pause movement when popups are open if (upgradesPopup || controlsPopup || shopPopup) { return; } // Check if target still exists and is not destroyed if (!self.target || self.target.isDestroyed || fallingObjects.indexOf(self.target) === -1) { // Find new closest target self.target = self.findClosestTarget(); } // If no target available, circle around center of map if (!self.target) { // Calculate center of map var centerX = WORLD_WIDTH / 2; var centerY = WORLD_HEIGHT / 2; var circleRadius = 200; // Radius of circling pattern // Calculate current distance from center var toCenterX = centerX - self.x; var toCenterY = centerY - self.y; var distanceToCenter = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY); // If too far from center, move towards it first if (distanceToCenter > circleRadius + 50) { var desiredVelX = toCenterX / distanceToCenter * self.speed; var desiredVelY = toCenterY / distanceToCenter * self.speed; // Smoothly adjust velocity towards center self.velocityX += (desiredVelX - self.velocityX) * self.turnSpeed; self.velocityY += (desiredVelY - self.velocityY) * self.turnSpeed; } else { // Circle around center // Calculate angle from center to rocket var currentAngle = Math.atan2(self.y - centerY, self.x - centerX); // Add circular motion - rotate clockwise var targetAngle = currentAngle + 0.05; // Adjust this value to change circling speed // Calculate target position on circle var targetX = centerX + Math.cos(targetAngle) * circleRadius; var targetY = centerY + Math.sin(targetAngle) * circleRadius; // Calculate desired velocity for circling var toTargetX = targetX - self.x; var toTargetY = targetY - self.y; var targetDistance = Math.sqrt(toTargetX * toTargetX + toTargetY * toTargetY); if (targetDistance > 0) { var desiredVelX = toTargetX / targetDistance * self.speed; var desiredVelY = toTargetY / targetDistance * self.speed; // Smoothly adjust velocity for smooth circling self.velocityX += (desiredVelX - self.velocityX) * self.turnSpeed; self.velocityY += (desiredVelY - self.velocityY) * self.turnSpeed; } } } else { // Target available - normal homing behavior // Calculate desired direction to target var dx = self.target.x - self.x; var dy = self.target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 50) { // Close enough to hit target self.hitTarget(); return; } // Desired velocity direction var desiredVelX = dx / distance * self.speed; var desiredVelY = dy / distance * self.speed; // Smoothly adjust current velocity toward desired velocity self.velocityX += (desiredVelX - self.velocityX) * self.turnSpeed; self.velocityY += (desiredVelY - self.velocityY) * self.turnSpeed; } // Normalize velocity to maintain constant speed var currentSpeed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY); if (currentSpeed > 0) { self.velocityX = self.velocityX / currentSpeed * self.speed; self.velocityY = self.velocityY / currentSpeed * self.speed; } // Update position self.x += self.velocityX; self.y += self.velocityY; // Update rotation to match velocity self.graphic.rotation = Math.atan2(self.velocityY, self.velocityX); // Update flame position and rotation to match rocket if (self.flameGraphic) { // Position flame behind rocket based on rocket's rotation var flameDistance = 65; // Distance behind rocket self.flameGraphic.x = -Math.cos(self.graphic.rotation) * flameDistance; self.flameGraphic.y = -Math.sin(self.graphic.rotation) * flameDistance; self.flameGraphic.rotation = self.graphic.rotation; } // Create white trail particles every few frames if (LK.ticks % 3 === 0) { // Create trail particle every 3 frames var trailParticle = LK.getAsset('particle', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8 + Math.random() * 0.4, // Random scale between 0.8 and 1.2 scaleY: 0.8 + Math.random() * 0.4 }); // Position trail from flame end instead of rocket center, moved further away var flameEndX = self.x + self.flameGraphic.x; var flameEndY = self.y + self.flameGraphic.y; // Move trail particles further behind the flame var trailOffsetDistance = 40; // Distance behind the flame var trailOffsetX = -Math.cos(self.graphic.rotation) * trailOffsetDistance; var trailOffsetY = -Math.sin(self.graphic.rotation) * trailOffsetDistance; trailParticle.x = flameEndX + trailOffsetX + (Math.random() - 0.5) * 10; // Small random offset from flame, positioned further away trailParticle.y = flameEndY + trailOffsetY + (Math.random() - 0.5) * 10; trailParticle.tint = 0xFFFFFF; // White color trailParticle.alpha = 0.6 + Math.random() * 0.3; // Random transparency // Find the index where puhaBarrelInstance is positioned and add trail particle right after it var puhaBarrelIndex = -1; for (var childIdx = 0; childIdx < game.children.length; childIdx++) { if (game.children[childIdx] === puhaBarrelInstance) { puhaBarrelIndex = childIdx; break; } } if (puhaBarrelIndex !== -1) { game.addChildAt(trailParticle, puhaBarrelIndex + 1); // Add trail particle right after puha barrel } else { game.addChild(trailParticle); // Fallback to normal add if puha not found } // Animate trail particle fading and shrinking with halved duration tween(trailParticle, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 400 + Math.random() * 200, // Duration between 400-600ms (halved from 800-1200ms) easing: tween.easeOut, onFinish: function (particleRef) { return function () { if (particleRef.parent) { particleRef.parent.removeChild(particleRef); } }; }(trailParticle) }); } // Don't destroy rocket for going off screen when circling - only if very far away if (self.x < -500 || self.x > WORLD_WIDTH + 500 || self.y < -500 || self.y > WORLD_HEIGHT + 500) { self.destroyRocket(); } }; self.findClosestTarget = function () { var closestTarget = null; var closestDistance = Infinity; for (var i = 0; i < fallingObjects.length; i++) { var target = fallingObjects[i]; if (!target.isDestroyed) { var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestTarget = target; } } } return closestTarget; }; self.hitTarget = function () { if (self.target && !self.target.isDestroyed) { // Deal damage to target directly without upgrade effects if (self.target.isDestroyed) { return; } // Direct damage application without going through takeDamage to avoid upgrade effects self.target.takeDamage(self.damage); LK.effects.flashObject(self.target.graphic, 0xFF0000, 100); // Red flash on hit // (Removed duplicate display: damage number is already shown by target.takeDamage) // Add pulsing scale animation when hit tween.stop(self.target.graphic, { scaleX: true, scaleY: true }); // Stop any existing scale tweens tween(self.target.graphic, { scaleX: 1.3, scaleY: 1.3 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(self.target.graphic, { scaleX: 1, scaleY: 1 }, { duration: 150, easing: tween.easeIn }); } }); if (self.target.health <= 0) { self.target.destroySelf(); } // Create explosion effect using Dimraket asset for (var p = 0; p < 8; p++) { var particle = LK.getAsset('Dimraket', { anchorX: 0.5, anchorY: 0.5, scaleX: 3.0 + Math.random() * 2.0, scaleY: 3.0 + Math.random() * 2.0 }); particle.x = self.x; particle.y = self.y; particle.alpha = 1.0; // Fully opaque game.addChild(particle); // Animate explosion particles var angle = Math.random() * Math.PI * 2; var speed = 100 + Math.random() * 100; var velX = Math.cos(angle) * speed; var velY = Math.sin(angle) * speed; // Random rotation speed and direction for each particle (slower rotation) var rotationSpeed = 0.01 + Math.random() * 0.03; // Random rotation speed between 0.01 and 0.04 radians per frame (much slower) var rotationDirection = Math.random() < 0.5 ? 1 : -1; // Random direction (clockwise or counterclockwise) var totalRotation = rotationSpeed * rotationDirection * 45; // Total rotation over first phase (45 frames at 60fps) // No color changes - use original Dimraket asset colors tween(particle, { x: particle.x + velX, y: particle.y + velY, rotation: particle.rotation + totalRotation * 2, // Total rotation over entire animation scaleX: 0, scaleY: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { if (particle.parent) { particle.parent.removeChild(particle); } } }); } // Play explosion sound LK.getSound('raketaudar').play(); } self.destroyRocket(); }; self.destroyRocket = function () { if (self.isDestroyed) { return; } self.isDestroyed = true; // Stop flame pulsation animation if (self.flameGraphic) { tween.stop(self.flameGraphic); } // Remove from active rockets list var index = activeRockets.indexOf(self); if (index > -1) { activeRockets.splice(index, 1); } // Remove from game if (self.parent) { self.parent.removeChild(self); } }; return self; }); var VirtualJoystick = Container.expand(function () { var self = Container.call(this); self.initialize = function (baseAssetId, knobAssetId, visualRadius, interactionRadius) { // baseGraphic and knobGraphic are children of 'self' (the joystick container) // Their positions (0,0) will be the center of 'self'. self.baseGraphic = self.addChild(LK.getAsset(baseAssetId, { anchorX: 0.5, anchorY: 0.5 })); self.knobGraphic = self.addChild(LK.getAsset(knobAssetId, { anchorX: 0.5, anchorY: 0.5 })); self.visualRadius = visualRadius; // Max distance knob can move visually self.interactionRadius = interactionRadius; // Touchable area around joystick center self.active = false; self.touchId = null; self.deltaX = 0; // Normalized movement vector x (-1 to 1) self.deltaY = 0; // Normalized movement vector y (-1 to 1) self.initialTouchX = 0; // Track initial touch position self.initialTouchY = 0; // Track initial touch position }; self.processDown = function (gameGlobalX, gameGlobalY, eventObj, treatTouchAsLocalOrigin) { var localClickPos; if (treatTouchAsLocalOrigin) { // If the joystick was just repositioned to gameGlobalX, gameGlobalY (the touch location), // then this touch point is effectively at the joystick's new local origin (0,0). localClickPos = { x: 0, y: 0 }; } else { // Standard case: joystick is already positioned. Convert global touch to local. localClickPos = self.toLocal({ x: gameGlobalX, y: gameGlobalY }); } var distSq = localClickPos.x * localClickPos.x + localClickPos.y * localClickPos.y; if (distSq <= self.interactionRadius * self.interactionRadius) { self.active = true; self.touchId = eventObj.identifier; // Use identifier from the event object // Always start knob at center when joystick is activated self.knobGraphic.x = 0; self.knobGraphic.y = 0; self.deltaX = 0; self.deltaY = 0; // Store the initial touch position to avoid coordinate transformation issues self.initialTouchX = gameGlobalX; self.initialTouchY = gameGlobalY; return true; // Consumed event } return false; }; self.processMove = function (gameGlobalX, gameGlobalY, eventObj) { if (!self.active || eventObj.identifier !== undefined && eventObj.identifier !== self.touchId) { return; } // Calculate movement relative to initial touch position var deltaX = gameGlobalX - self.initialTouchX; var deltaY = gameGlobalY - self.initialTouchY; self.updateKnobPosition(deltaX, deltaY); }; self.processUp = function (eventObj) { if (!self.active || eventObj.identifier !== undefined && eventObj.identifier !== self.touchId) { return; } self.active = false; self.touchId = null; self.knobGraphic.x = 0; // Reset to center of joystick container self.knobGraphic.y = 0; self.deltaX = 0; self.deltaY = 0; }; self.updateKnobPosition = function (localX, localY) { var dx = localX; var dy = localY; var distance = Math.sqrt(dx * dx + dy * dy); var clampedX = dx; var clampedY = dy; if (distance > self.visualRadius) { clampedX = dx / distance * self.visualRadius; clampedY = dy / distance * self.visualRadius; } self.knobGraphic.x = clampedX; self.knobGraphic.y = clampedY; if (self.visualRadius === 0) { // Avoid division by zero if radius is 0 self.deltaX = 0; self.deltaY = 0; } else { self.deltaX = clampedX / self.visualRadius; self.deltaY = clampedY / self.visualRadius; } self.deltaX = Math.max(-1, Math.min(1, self.deltaX)); self.deltaY = Math.max(-1, Math.min(1, self.deltaY)); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x0a0a18 // Even darker blue for space }); /**** * Game Code ****/ // --- Game Constants --- // Brownish meteor // Lime green alien (can be simple box for now) // Grey city base // A simple red circle for the crosshair // Legacy classes for backward compatibility var WORLD_WIDTH = 2048; var WORLD_HEIGHT = 2732; var METEOR_HEALTH = 100; var METEOR_SPEED = 2.4; // Pixels per frame (1.5 * 1.6) var METEOR_DAMAGE_TO_CITY = 75; var ALIEN_HEALTH = 70; var ALIEN_SPEED = 2.0; // (1.25 * 1.6) var ALIEN_DAMAGE_TO_CITY = 100; var CITY_MAX_HEALTH = 1000; // var CLICK_DAMAGE = 35; // Replaced by FIRE_DAMAGE // var HOLD_DAMAGE_PER_INTERVAL = 20; // Replaced by FIRE_DAMAGE // var HOLD_DAMAGE_INTERVAL_MS = 150; // Replaced by FIRE_RATE_MS var CROSSHAIR_SPEED = 10; var JOYSTICK_VISUAL_RADIUS = 180; // Increased max knob center travel distance for larger joystick var JOYSTICK_INTERACTION_RADIUS = 260; // Increased touchable area radius for larger joystick var FIRE_RATE_MS = 1000; // How often the player can fire (1 shot per second initially) var FIRE_DAMAGE = 35; // Damage per shot from crosshair var SPAWN_INTERVAL_MS_MIN = 4000; // Minimum time between spawns (4 seconds) var SPAWN_INTERVAL_MS_MAX = 4000; // Maximum time between spawns (4 seconds) // --- Global Game Variables --- var cityInstance; var fallingObjects = []; var cityHealthText; // For GUI var scoreText; // For player score // --- Ava: Separate finalScore system for end-of-game scoring (now: number of enemies killed) --- var finalScore = 0; function addFinalScore(amount) { // No longer used for money, but kept for compatibility } function getFinalScore() { return finalScore; } function resetFinalScore() { finalScore = 0; updateFinalScoreDisplay && updateFinalScoreDisplay(); } // --- Ava: Show finalScore (enemies killed) at end of game, not balance --- // Listen for game over and you win events to show finalScore instead of balance LK.on('gameover', function () { // Show finalScore as the main score in the end screen LK.setScore(getFinalScore()); }); LK.on('youwin', function () { LK.setScore(getFinalScore()); }); // Defensive: also set LK.setScore(finalScore) right before game over in City and Enemy destroySelf // var playerHoldingTarget = null; // Removed // var isPlayerHolding = false; // Replaced by isFiring // var holdDamageInterval = null; // Removed var crosshairInstance; var joystickInstance; var isFiring = false; var lastFireTime = 0; var joystickTouchId = null; // To track the touch interacting with the joystick var nextSpawnTime = 0; var nextAlienSpawnTime = 0; // Separate timer for alien_1 var spawnPausedTime = null; // Track when spawn timer was paused var alienSpawnPausedTime = null; // Track when alien spawn timer was paused var stars = []; var STAR_COUNT = 50; // Device detection variables var isMobileDevice = false; var isMouseControlling = false; var mouseX = WORLD_WIDTH / 2; var mouseY = WORLD_HEIGHT / 2; var mouseCoordinatesText = null; // Text display for crosshair speed var objectCountText = null; // Text display for object count var spawnRateText = null; // Text display for spawn rate var crosshairLastX = WORLD_WIDTH / 2; var crosshairLastY = WORLD_HEIGHT / 2; var crosshairSpeed = 0; // Music system variables var currentMusicIndex = 0; var musicTracks = ['music1', 'music2', 'music3']; var trackDurations = [139360, 240000, 67800]; // music1: 2:19, music2: 4:00, music3: 1:08 (in milliseconds) var musicPlaying = false; var musicStartTime = 0; // --- Helper Functions --- function detectDevice() { // Check screen size - mobile devices typically have smaller screens var screenWidth = 2048; // LK engine resolution var screenHeight = 2732; // Check for touch capability var isTouchDevice = 'ontouchstart' in window || navigator && navigator.maxTouchPoints > 0; // Simple heuristic: if touch is available and we're in a typical mobile viewport, assume mobile // In LK engine, we can use the fact that mobile games are the primary target isMobileDevice = isTouchDevice; console.log("Device detected as:", isMobileDevice ? "Mobile" : "Desktop"); } function spawnFallingObject() { var randomValue = Math.random(); var newObject; // Distribute spawn chances: small meteor 37.5%, medium 37.5%, big 20%, meteorgold 5% if (randomValue < 0.375) { // 37.5% - Small meteor newObject = new FallingObject('smallmeteor', 70, METEOR_SPEED, 'meteor'); newObject.moneyReward = 2; // Small meteor gives 2$ } else if (randomValue < 0.75) { // 37.5% - Medium meteor newObject = new FallingObject('meteor', 100, METEOR_SPEED, 'meteor'); newObject.moneyReward = 3; // Medium meteor gives 3$ } else if (randomValue < 0.95) { // 20% - Big meteor newObject = new FallingObject('bigmeteor', 140, METEOR_SPEED * 0.8, 'meteor'); newObject.moneyReward = 4; // Big meteor gives 4$ } else { // 5% - Gold meteor newObject = new FallingObject('meteorgold', 100, METEOR_SPEED / 1.5, 'meteor'); newObject.moneyReward = 8; // Gold meteor gives 8$ } // Position just above the screen, at a random horizontal position newObject.x = Math.random() * (WORLD_WIDTH - newObject.graphic.width) + newObject.graphic.width / 2; newObject.y = -newObject.graphic.height / 2; // Start just off-screen top fallingObjects.push(newObject); game.addChild(newObject); } // spawnGoldMeteor function removed - gold meteor spawning is now handled directly in FallingObject.destroySelf function updateScoreDisplay() { if (scoreText) { scoreText.setText('$' + LK.getScore()); } } function getTotalObjectCount() { var total = 0; total += fallingObjects.length; total += activeRockets.length; total += game.particles ? game.particles.length : 0; total += game.damageNumbers ? game.damageNumbers.length : 0; total += game.muzzleParticles ? game.muzzleParticles.length : 0; total += game.snowflakeParticles ? game.snowflakeParticles.length : 0; total += game.fireProjectiles ? game.fireProjectiles.length : 0; total += stars.length; // Count burn flames on all falling objects for (var i = 0; i < fallingObjects.length; i++) { var obj = fallingObjects[i]; if (obj.burnFlames && obj.burnFlames.length > 0) { total += obj.burnFlames.length; } if (obj.snowflakeParticles && obj.snowflakeParticles.length > 0) { total += obj.snowflakeParticles.length; } // Health display disabled - not counted in total objects if (obj.iceBlock) { total += 1; // Ice block when frozen } } // Count tower graphics if active if (teslaTowerActive) { if (teslaTowerGraphic) { total += 1; } if (teslaSphereGraphic) { total += 1; } if (teslaSphereGraphic2) { total += 1; } } if (fireTowerActive) { if (fireTowerGraphic) { total += 1; } if (fireFlameGraphic) { total += 1; } if (fireFlameGraphic2) { total += 1; } } if (iceTowerActive) { if (iceTowerInstance) { total += 1; } } // Count shield if active if (cityInstance.shielded && cityInstance.shieldGraphic) { total += 1; } // Count any active popups if (upgradesPopup) { total += 1; } if (controlsPopup) { total += 1; } if (shopPopup) { total += 1; } if (currentTooltip) { total += 1; } // Count language flags in tooltip if (languageFlags && languageFlags.length > 0) { total += languageFlags.length; } // Count created trail particles from rockets for (var r = 0; r < activeRockets.length; r++) { if (activeRockets[r] && activeRockets[r].flameGraphic) { total += 1; // Count rocket flame graphics } } return total; } function findRocketTarget() { // Find closest enemy var closestTarget = null; var closestDistance = Infinity; // Use cannon position as reference point var refX = puhaBarrelInstance ? puhaBarrelInstance.x : WORLD_WIDTH / 2; var refY = puhaBarrelInstance ? puhaBarrelInstance.y : WORLD_HEIGHT - 200; for (var i = 0; i < fallingObjects.length; i++) { var target = fallingObjects[i]; if (!target.isDestroyed) { var dx = target.x - refX; var dy = target.y - refY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestTarget = target; } } } return closestTarget; } function createStars() { for (var i = 0; i < STAR_COUNT; i++) { // Use star asset with random scale for different sizes var randomScale = 0.5 + Math.random() * 1.5; // Scale between 0.5x and 2.0x var star = LK.getAsset('star', { anchorX: 0.5, anchorY: 0.5, scaleX: randomScale, scaleY: randomScale }); // Generate star position with exclusion zones to prevent overlap with UI elements var starX, starY; var validPosition = false; var attempts = 0; var maxAttempts = 50; while (!validPosition && attempts < maxAttempts) { starX = Math.random() * WORLD_WIDTH; starY = Math.random() * WORLD_HEIGHT; attempts++; // Check if star is not in cannon area (bottom left) var cannonAreaLeft = 0; var cannonAreaRight = 800; var cannonAreaTop = WORLD_HEIGHT - 600; var cannonAreaBottom = WORLD_HEIGHT; var inCannonArea = starX >= cannonAreaLeft && starX <= cannonAreaRight && starY >= cannonAreaTop && starY <= cannonAreaBottom; // Check if star is not in joystick area (will be positioned dynamically, so reserve bottom area) var joystickAreaLeft = 0; var joystickAreaRight = 600; var joystickAreaTop = WORLD_HEIGHT - 800; var joystickAreaBottom = WORLD_HEIGHT; var inJoystickArea = starX >= joystickAreaLeft && starX <= joystickAreaRight && starY >= joystickAreaTop && starY <= joystickAreaBottom; // Check if star is not in city area (bottom center and sides) var cityAreaLeft = 0; var cityAreaRight = WORLD_WIDTH; var cityAreaTop = WORLD_HEIGHT - 448; // City image height var cityAreaBottom = WORLD_HEIGHT; var inCityArea = starX >= cityAreaLeft && starX <= cityAreaRight && starY >= cityAreaTop && starY <= cityAreaBottom; if (!inCannonArea && !inJoystickArea && !inCityArea) { validPosition = true; } } // If no valid position found after max attempts, place randomly (fallback) if (!validPosition) { starX = Math.random() * WORLD_WIDTH; starY = Math.random() * (WORLD_HEIGHT - 600); // At least avoid bottom area } star.x = starX; star.y = starY; star.alpha = 0.3 + Math.random() * 0.7; // Random initial brightness star.twinkleDelay = Math.random() * 3000; // Random delay before first twinkle star.nextTwinkleTime = LK.ticks * (1000 / 60) + star.twinkleDelay; star.isTwinkling = false; // Track if star is currently twinkling star.lastColorChangeTime = LK.ticks * (1000 / 60); // Initialize last color change time stars.push(star); // Add star right after background to ensure it stays behind all other elements game.addChildAt(star, 1); // Position 1 is right after the background (position 0) } } function updateStars() { var currentTime = LK.ticks * (1000 / 60); var colors = [0x4169E1, 0x00FFFF, 0xFFFFFF, 0xFFFF00]; // Blue, Cyan, White, Yellow for (var i = 0; i < stars.length; i++) { var star = stars[i]; // Check if star should start glowing (either due to regular twinkle time or 30-second limit) var shouldGlow = currentTime >= star.nextTwinkleTime; var timeSinceLastChange = currentTime - (star.lastColorChangeTime || 0); var forcedChange = timeSinceLastChange >= 30000; // 30 seconds (increased from 15) if ((shouldGlow || forcedChange) && !star.isTwinkling) { // Start lightning-like glow and pulsing animation star.isTwinkling = true; // Stop any existing tween on this star first tween.stop(star); // Pick a new color different from the current one var currentColor = star.tint; var availableColors = []; for (var c = 0; c < colors.length; c++) { if (colors[c] !== currentColor) { availableColors.push(colors[c]); } } // Fallback in case all colors are the same (shouldn't happen) if (availableColors.length === 0) { availableColors = colors.slice(); } var targetColor = availableColors[Math.floor(Math.random() * availableColors.length)]; var targetAlpha = 0.8 + Math.random() * 0.2; // Brighter for glow effect // Store original scale if not set if (!star.originalScale) { star.originalScale = star.scaleX; } // Lightning-like pulsing: subtle expansion and contraction tween(star, { scaleX: star.originalScale * (1.05 + Math.random() * 0.1), // Expand 1.05-1.15x (much smaller expansion) scaleY: star.originalScale * (1.05 + Math.random() * 0.1), alpha: targetAlpha, tint: targetColor }, { duration: 400 + Math.random() * 200, // Slower expansion (400-600ms) easing: tween.easeOut, onFinish: function (starRef, colorRef) { return function onFinish() { // Contract back with pulsing effect tween(starRef, { scaleX: starRef.originalScale * (0.95 + Math.random() * 0.05), // Contract to 0.95-1.0x (very subtle) scaleY: starRef.originalScale * (0.95 + Math.random() * 0.05), alpha: 0.3 + Math.random() * 0.5 }, { duration: 500 + Math.random() * 300, // Slower contraction (500-800ms) easing: tween.easeInOut, onFinish: function onFinish() { // Final pulse back to normal with minimal variation tween(starRef, { scaleX: starRef.originalScale * (0.98 + Math.random() * 0.04), // Near original size (0.98-1.02x) scaleY: starRef.originalScale * (0.98 + Math.random() * 0.04), alpha: 0.3 + Math.random() * 0.7 }, { duration: 600 + Math.random() * 400, // Even slower final pulse (600-1000ms) easing: tween.easeOut, onFinish: function onFinish() { // Set next glow time (8-20 seconds from now, but no more than 30 seconds total) var now = LK.ticks * (1000 / 60); starRef.lastColorChangeTime = now; var nextInterval = Math.min(8000 + Math.random() * 12000, 30000); // 8-20 seconds starRef.nextTwinkleTime = now + Math.max(nextInterval, 2000); // Ensure at least 2 seconds starRef.isTwinkling = false; // Set the color to the final value to avoid rounding issues starRef.tint = colorRef; } }); } }); }; }(star, targetColor) }); } } } // Music management functions function startMusicRotation() { if (musicTracks.length === 0) { return; } currentMusicIndex = 0; musicPlaying = true; musicStartTime = LK.ticks * (1000 / 60); LK.playMusic(musicTracks[currentMusicIndex]); } function updateMusicRotation() { if (!musicPlaying || !musicEnabled || musicTracks.length === 0) { return; } // Don't update music countdown during meteor event if (meteorEventActive) { return; } var currentTime = LK.ticks * (1000 / 60); // Check if enough time has passed to switch tracks using custom durations var trackDuration = trackDurations[currentMusicIndex] || 180000; // Use current track's duration or fallback to 3 minutes if (currentTime - musicStartTime >= trackDuration) { switchToNextTrack(); } } function switchToNextTrack() { if (musicTracks.length === 0) { return; } currentMusicIndex = (currentMusicIndex + 1) % musicTracks.length; musicStartTime = LK.ticks * (1000 / 60); LK.playMusic(musicTracks[currentMusicIndex]); } // --- Game Initialization --- // Initialize tracking arrays for cleanup game.damageNumbers = []; game.particles = []; game.muzzleParticles = []; game.snowflakeParticles = []; game.fireProjectiles = []; game.iceProjectiles = []; // Reset upgrades for (var k in upgrades) { upgrades[k] = 0; } // Add background var background = LK.getAsset('fon', { anchorX: 0, anchorY: 0 }); background.x = 0; background.y = 0; game.addChild(background); // Initialize Puha character in bottom left corner - split into base and barrel var puhaBaseInstance = LK.getAsset('Puha2', { anchorX: 0.0, anchorY: 0.5, scaleX: 0.15, scaleY: 0.15 }); puhaBaseInstance.x = 450; // Left edge positioned correctly from left edge, moved right by 400 puhaBaseInstance.y = WORLD_HEIGHT - 50 - 2000 * 0.15 / 2 + 200; // Center positioned correctly from bottom edge, moved down by 200 // Rotating barrel part var puhaBarrelInstance = LK.getAsset('Puha', { anchorX: 0.0, anchorY: 0.5, scaleX: 0.3, scaleY: 0.3 }); puhaBarrelInstance.x = 450; // Same position as base puhaBarrelInstance.y = WORLD_HEIGHT - 50 - 2056 * 0.3 / 2 + 200; // Same position as base // Add puha parts before city images to hide them behind cities game.addChild(puhaBaseInstance); game.addChild(puhaBarrelInstance); // Add left city image at bottom left var leftCityImage = LK.getAsset('city', { anchorX: 0, anchorY: 1 }); leftCityImage.x = 0; leftCityImage.y = WORLD_HEIGHT; game.addChild(leftCityImage); // Add right city image at bottom right var rightCityImage = LK.getAsset('city', { anchorX: 1, anchorY: 1 }); rightCityImage.x = WORLD_WIDTH; rightCityImage.y = WORLD_HEIGHT; game.addChild(rightCityImage); // Create and position the city cityInstance = new City(); cityInstance.x = WORLD_WIDTH / 2; cityInstance.y = WORLD_HEIGHT; // Bottom edge of city at bottom of screen game.addChild(cityInstance); // Setup City Health Display // Removed city health text and numbers as requested // Setup Score Display LK.setScore(0); // Initialize score to 0 resetFinalScore(); // --- Ava: Reset finalScore at game start --- scoreText = new Text2('$0', { size: 90, fill: 0x00FF00, align: 'center', font: 'Impact' }); scoreText.anchor.set(0.5, 0); // Anchor top-center // Position score text slightly below the top edge to avoid any engine icons, if necessary // LK.gui.top has y=0. We'll add a small offset. scoreText.y = 60; // Moved higher by 20 pixels (was 80, now 60). LK.gui.top.addChild(scoreText); // --- Ava: Add finalScore text below the balance (score) text, make it white and smaller, and remove the label --- var finalScoreText = new Text2('0', { size: 48, fill: 0xFFFFFF, align: 'center', font: 'Impact' }); finalScoreText.anchor.set(0.5, 0); finalScoreText.y = scoreText.y + scoreText.height + 8; // 8px below balance LK.gui.top.addChild(finalScoreText); // Update function for finalScoreText function updateFinalScoreDisplay() { if (finalScoreText) { finalScoreText.setText(getFinalScore().toString()); } } // Call once at start updateFinalScoreDisplay(); // --- Event Handlers --- // --- Upgrades System --- // Upgrade state variables var upgrades = { fireDamage: 0, // Level fireRate: 0, // Level freeze: 0, // Level lightningChance: 0, // Level fire: 0, // Level critChance: 0 // Level }; var upgradesPopup = null; var upgradesButton = null; var upgradesButtonActive = false; var upgradesButtonWidth = 180; var upgradesButtonHeight = 90; var upgradesButtonPadding = 30; var upgradesButtonY = 20; // Offset from top var upgradesButtonX = WORLD_WIDTH - upgradesButtonWidth - upgradesButtonPadding; // Shop system variables var shopPopup = null; var shopButton = null; var shopButtonActive = false; var shopButtonWidth = 180; var shopButtonHeight = 90; var shopButtonPadding = 30; var shopButtonY = 220; // Position below upgrades button var shopButtonX = WORLD_WIDTH - shopButtonWidth - shopButtonPadding; // Shop item 1: Gradual healing effect variables var gradualHealActive = false; var gradualHealEndTime = 0; var gradualHealLastTick = 0; var gradualHealPausedTime = null; // Shop item 1 cooldown variables var healthRestoreCooldownActive = false; var healthRestoreCooldownEndTime = 0; var healthRestoreCooldownPausedTime = null; // Shop item 2 cooldown variables (shield) var shieldCooldownActive = false; var shieldCooldownEndTime = 0; var shieldCooldownPausedTime = null; // Shop item 3 cooldown variables (rocket barrage) var rocketBarrageCooldownActive = false; var rocketBarrageCooldownEndTime = 0; var rocketBarrageCooldownPausedTime = null; // Shop item 4 cooldown variables (Tesla tower) var teslaTowerCooldownActive = false; var teslaTowerCooldownEndTime = 0; var teslaTowerCooldownPausedTime = null; // Shop item 5 cooldown variables (fire tower) var fireTowerCooldownActive = false; var fireTowerCooldownEndTime = 0; var fireTowerCooldownPausedTime = null; // Shop item 6 cooldown variables (ice tower) var iceTowerCooldownActive = false; var iceTowerCooldownEndTime = 0; var iceTowerCooldownPausedTime = null; // Shop item 3 rocket barrage variables var rocketBarrageActive = false; var rocketBarrageEndTime = 0; var rocketBarragePausedTime = null; var lastRocketLaunchTime = 0; var rocketTargets = []; // Track which targets have been selected var activeRockets = []; // Track active rockets // Shop item 4 Tesla tower variables var teslaTowerActive = false; var teslaTowerEndTime = 0; var teslaTowerPausedTime = null; var lastTeslaStrikeTime = 0; var teslaTowerGraphic = null; var teslaSphereGraphic = null; var teslaSphereGraphic2 = null; // Second Tesla sphere var teslaSphereRotationDirection = 1; // Always clockwise var teslaSphereRotationDirection2 = -1; // Always counterclockwise (opposite) var TESLA_SPHERE_ROTATION_SPEED = 0.05; // Radians per frame // Shop item 5 Fire tower variables var fireTowerActive = false; var fireTowerEndTime = 0; var fireTowerPausedTime = null; var lastFireStrikeTime = 0; var fireTowerGraphic = null; var fireFlameGraphic = null; var fireFlameGraphic2 = null; // Second fire sphere // Shop item 6 Ice tower variables var iceTowerActive = false; var iceTowerEndTime = 0; var iceTowerPausedTime = null; var lastIceStrikeTime = 0; var iceTowerInstance = null; // Freeze sound system - using timer-based cooldown instead of playing state check var freezeSoundCooldownActive = false; var freezeSoundCooldownEndTime = 0; // Fire tower damage removed - projectiles now use their own damage for burn calculation // Upgrade definitions var UPGRADE_DEFS = [{ key: 'fireDamage', name: 'Урон +', desc: 'Увеличить урон выстрела', tooltip: 'Увеличивает урон от каждого выстрела. Каждый уровень добавляет +20 урона к вашим атакам.', cost: function cost(level) { if (level === 0) { return 75; } if (level === 1) { return 150; } if (level === 2) { return 300; } if (level === 3) { return 600; } if (level === 4) { return 900; } if (level === 5) { return 1500; } if (level >= 6 && level <= 9) { return 3000; } return 3000; }, max: 10 }, { key: 'fireRate', name: 'Скорострельность +', desc: 'Уменьшить задержку между выстрелами', tooltip: 'Уменьшает время между выстрелами, позволяя стрелять чаще.', cost: function cost(level) { if (level === 0) { return 75; } if (level === 1) { return 150; } if (level === 2) { return 300; } if (level === 3) { return 600; } if (level === 4) { return 900; } if (level === 5) { return 1500; } if (level >= 6 && level <= 9) { return 3000; } return 3000; }, max: 10 }, { key: 'critChance', name: 'Критический урон +', desc: 'Увеличить шанс двойного урона', tooltip: 'Увеличивает шанс нанести двойной урон выстрелом. Каждый уровень добавляет +5% шанса критического удара.', cost: function cost(level) { if (level === 0) { return 75; } if (level === 1) { return 150; } if (level === 2) { return 300; } if (level === 3) { return 600; } if (level === 4) { return 900; } if (level === 5) { return 1500; } if (level >= 6 && level <= 9) { return 3000; } return 3000; }, max: 10 }, { key: 'lightningChance', name: 'Шанс молнии +', desc: 'Увеличить шанс удара молнии', tooltip: 'Добавляет шанс поражения целей молнией, наносящей урон и перескакивающей на ближайших врагов. Каждый уровень добавляет +5% шанса.', cost: function cost(level) { if (level === 0) { return 75; } if (level === 1) { return 150; } if (level === 2) { return 300; } if (level === 3) { return 600; } if (level === 4) { return 900; } if (level === 5) { return 1500; } if (level >= 6 && level <= 9) { return 3000; } return 3000; }, max: 10 }, { key: 'fire', name: 'Шанс поджога +', desc: 'Урон огнем со временем', tooltip: 'Добавляет шанс поджечь врагов, наносящий урон в течение 10 секунд. Огонь накладывается до 10 раз, увеличивая урон. Каждый уровень добавляет +5% шанса.', cost: function cost(level) { if (level === 0) { return 75; } if (level === 1) { return 150; } if (level === 2) { return 300; } if (level === 3) { return 600; } if (level === 4) { return 900; } if (level === 5) { return 1500; } if (level >= 6 && level <= 9) { return 3000; } return 3000; }, max: 10 }, { key: 'freeze', name: 'Шанс заморозки +', desc: 'Замедление и заморозка врагов', tooltip: 'Добавляет шанс замедлить врагов на 50%. При 5 стаках враги полностью замерзают на 4 секунды. Каждый уровень добавляет +5% шанса.', cost: function cost(level) { if (level === 0) { return 75; } if (level === 1) { return 150; } if (level === 2) { return 300; } if (level === 3) { return 600; } if (level === 4) { return 900; } if (level === 5) { return 1500; } if (level >= 6 && level <= 9) { return 3000; } return 3000; }, max: 10 }]; // Global variable for upgrade icon size var iconSize = 360; // Tooltip system variables var currentTooltip = null; // Language system variables var currentLanguage = 'en'; // Default to English var languageFlags = []; var languages = ['en', 'ru', 'de', 'fr', 'es', 'tr']; var flagAssets = ['Eng', 'Rus', 'Germ', 'Fran', 'Esp', 'Turkish']; // English, Russian, German, French, Spanish, Turkish // Shop item data for tooltips and titles var shopTitleData = { en: { shop1: 'Health Restore', shop2: 'Shield Protection', shop3: 'Rocket Barrage', shop4: 'Tesla Tower', shop5: 'Fire Tower', shop6: 'Ice Tower' }, ru: { shop1: 'Восстановление здоровья', shop2: 'Щит защиты', shop3: 'Ракетный залп', shop4: 'Башня Тесла', shop5: 'Огненная башня', shop6: 'Ледяная башня' }, de: { shop1: 'Gesundheit wiederherstellen', shop2: 'Schildschutz', shop3: 'Raketensperrfeuer', shop4: 'Tesla-Turm', shop5: 'Feuerturm', shop6: 'Eisturm' }, fr: { shop1: 'Restauration de santé', shop2: 'Protection bouclier', shop3: 'Barrage de roquettes', shop4: 'Tour Tesla', shop5: 'Tour de feu', shop6: 'Tour de glace' }, es: { shop1: 'Restaurar salud', shop2: 'Protección escudo', shop3: 'Barrera de cohetes', shop4: 'Torre Tesla', shop5: 'Torre de fuego', shop6: 'Torre de hielo' }, tr: { shop1: 'Sağlık yenileme', shop2: 'Kalkan koruması', shop3: 'Roket barajı', shop4: 'Tesla kulesi', shop5: 'Ateş kulesi', shop6: 'Buz kulesi' } }; var shopTooltipData = { en: { shop1: 'Gradually restores city health over 12 seconds. Heals 70 HP every second for a total of 840 HP restoration. Cooldown: 3 minutes.', shop2: 'Creates a protective shield around the city for 15 seconds. Shield blocks all incoming damage until destroyed. Cooldown: 3 minutes.', shop3: 'Launches homing rockets for 15 seconds. Rockets target enemies automatically and deal 500 damage each. Cooldown: 3 minutes.', shop4: 'Deploys a Tesla tower for 15 seconds. Shoots lightning every second that deals 250 damage and chains to up to 4 nearby enemies with 250 damage each. Cooldown: 3 minutes.', shop5: 'Summons a fire tower for 15 seconds. Shoots 17 burning projectiles every second that deal 100 damage each and apply fire tower burn (50 damage per second per stack). Cooldown: 3 minutes.', shop6: 'Creates an ice tower for 15 seconds. Fires 15 freezing spheres every 1.5 seconds that deal 50 damage each and completely freeze enemies for 2 seconds. Cooldown: 3 minutes.' }, ru: { shop1: 'Постепенно восстанавливает здоровье города в течение 12 секунд. Лечит 70 ОЗ каждую секунду, всего 840 ОЗ. Перезарядка: 3 минуты.', shop2: 'Создает защитный щит вокруг города на 15 секунд. Щит блокирует весь входящий урон до уничтожения. Перезарядка: 3 минуты.', shop3: 'Запускает самонаводящиеся ракеты в течение 15 секунд. Ракеты автоматически находят врагов и наносят 500 урона каждая. Перезарядка: 3 минуты.', shop4: 'Размещает башню Тесла на 15 секунд. Стреляет молниями каждую секунду, наносящими 250 урона и перескакивающими на до 4 ближайших врагов с 250 уроном каждому. Перезарядка: 3 минуты.', shop5: 'Призывает огненную башню на 15 секунд. Стреляет 17 горящими снарядами каждую секунду, наносящими 100 урона каждый и накладывающими огненный ожог башни (50 урона в секунду за стак). Перезарядка: 3 минуты.', shop6: 'Создает ледяную башню на 15 секунд. Стреляет 15 замораживающими сферами каждые 1,5 секунды, наносящими 50 урона каждая и полностью замораживающими врагов на 2 секунды. Перезарядка: 3 минуты.' }, de: { shop1: 'Stellt die Stadtgesundheit über 12 Sekunden allmählich wieder her. Heilt jede Sekunde 70 HP für insgesamt 840 HP.', shop2: 'Erstellt einen Schutzschild um die Stadt für 15 Sekunden. Schild blockiert allen eingehenden Schaden bis zur Zerstörung.', shop3: 'Startet suchende Raketen für 15 Sekunden. Raketen zielen automatisch auf Feinde und verursachen je 500 Schaden. Abklingzeit: 3 Minuten.', shop4: 'Stellt einen Tesla-Turm für 15 Sekunden auf. Schießt jede Sekunde Blitze, die 250 Schaden verursachen und auf bis zu 4 nahe Feinde mit je 250 Schaden überspringen.', shop5: 'Beschwört einen Feuerturm für 15 Sekunden. Schießt jede Sekunde 17 brennende Geschosse, die je 100 Schaden verursachen und Feuerturm-Brand auftragen (50 Schaden pro Sekunde pro Stapel).', shop6: 'Erstellt einen Eisturm für 15 Sekunden. Feuert alle 1,5 Sekunden 15 einfrierende Kugeln, die je 50 Schaden verursachen und Feinde 2 Sekunden lang vollständig einfrieren.' }, fr: { shop1: 'Restaure progressivement la santé de la ville pendant 12 secondes. Soigne 70 PV chaque seconde pour un total de 840 PV.', shop2: 'Crée un bouclier protecteur autour de la ville pendant 15 secondes. Le bouclier bloque tous les dégâts entrants jusqu\'à destruction.', shop3: 'Lance des roquettes guidées pendant 15 secondes. Les roquettes ciblent automatiquement les ennemis et infligent 500 dégâts chacune. Temps de recharge: 3 minutes.', shop4: 'Déploie une tour Tesla pendant 15 secondes. Tire des éclairs chaque seconde qui infligent 250 dégâts et sautent vers jusqu\'à 4 ennemis proches avec 250 dégâts chacun.', shop5: 'Invoque une tour de feu pendant 15 secondes. Tire 17 projectiles brûlants chaque seconde qui infligent 100 dégâts chacun et appliquent la brûlure de tour de feu (50 dégâts par seconde par pile).', shop6: 'Crée une tour de glace pendant 15 secondes. Tire 15 sphères gelantes toutes les 1,5 secondes qui infligent 50 dégâts chacune et gèlent complètement les ennemis pendant 2 secondes.' }, es: { shop1: 'Restaura gradualmente la salud de la ciudad durante 12 segundos. Cura 70 PS cada segundo para un total de 840 PS.', shop2: 'Crea un escudo protector alrededor de la ciudad durante 15 segundos. El escudo bloquea todo el daño entrante hasta ser destruido.', shop3: 'Lanza cohetes teledirigidos durante 15 segundos. Los cohetes apuntan automáticamente a enemigos y causan 500 daño cada uno. Tiempo de recarga: 3 minutos.', shop4: 'Despliega una torre Tesla durante 15 segundos. Dispara rayos cada segundo que causan 250 daño y saltan a hasta 4 enemigos cercanos con 250 daño cada uno.', shop5: 'Invoca una torre de fuego durante 15 segundos. Dispara 17 proyectiles ardientes cada segundo que causan 100 daño cada uno y aplican quemadura de torre de fuego (50 daño por segundo por acumulación).', shop6: 'Crea una torre de hielo durante 15 segundos. Dispara 15 esferas congelantes cada 1,5 segundos que causan 50 daño cada una y congelan completamente a los enemigos por 2 segundos.' }, tr: { shop1: '12 saniye boyunca şehir sağlığını kademeli olarak yeniler. Her saniye 70 Can yeniler, toplamda 840 Can.', shop2: '15 saniye boyunca şehrin etrafında koruyucu kalkan oluşturur. Kalkan yok edilene kadar tüm gelen hasarı engeller.', shop3: '15 saniye boyunca güdümlü roketler fırlatır. Roketler otomatik olarak düşmanları hedefler ve her biri 500 hasar verir. Bekleme süresi: 3 dakika.', shop4: '15 saniye boyunca Tesla kulesi yerleştirir. Her saniye 250 hasar veren ve yakındaki 4 düşmana 250\'şer hasar ile zıplayan şimşekler atar.', shop5: '15 saniye boyunca ateş kulesi çağırır. Her saniye 100\'er hasar veren ve ateş kulesi yanığı uygulayan (yığın başına saniyede 50 hasar) 17 yanan mermi atar.', shop6: '15 saniye boyunca buz kulesi oluşturur. Her 1,5 saniyede 50\'şer hasar veren ve düşmanları 2 saniye boyunca tamamen donduran 15 dondurucu küre atar.' } }; // Language data for tooltips var languageData = { en: { fireDamage: 'Increases damage from each shot. Each level adds +20 damage to your attacks.', fireRate: 'Reduces time between shots, allowing you to shoot more frequently.', critChance: 'Increases chance to deal double damage with shots. Each level adds +5% critical hit chance.', lightningChance: 'Adds chance to strike targets with lightning that damages and jumps to nearby enemies. Each level adds +5% chance and +5% damage.', fire: 'Adds chance to ignite enemies, dealing damage over 10 seconds. Fire stacks up to 10 times, increasing damage. Each level adds +5% chance.', freeze: 'Adds chance to slow enemies by 50%. At 3 stacks enemies freeze completely for 4 seconds. Each level adds +5% chance.' }, ru: { fireDamage: 'Увеличивает урон от каждого выстрела. Каждый уровень добавляет +20 урона к вашим атакам.', fireRate: 'Уменьшает время между выстрелами, позволяя стрелять чаще.', critChance: 'Увеличивает шанс нанести двойной урон выстрелом. Каждый уровень добавляет +5% шанса критического удара.', lightningChance: 'Добавляет шанс поражения целей молнией, наносящей урон и перескакивающей на ближайших врагов. Каждый уровень добавляет +5% шанса и +5% урона.', fire: 'Добавляет шанс поджечь врагов, наносящий урон в течение 10 секунд. Огонь накладывается до 10 раз, увеличивая урон. Каждый уровень добавляет +5% шанса.', freeze: 'Добавляет шанс замедлить врагов на 50%. При 3 стаках враги полностью замерзают на 4 секунды. Каждый уровень добавляет +5% шанса.' }, de: { fireDamage: 'Erhöht den Schaden jedes Schusses. Jede Stufe fügt +20 Schaden zu Ihren Angriffen hinzu.', fireRate: 'Reduziert die Zeit zwischen Schüssen und ermöglicht häufigeres Schießen.', critChance: 'Erhöht die Chance, doppelten Schaden mit Schüssen zu verursachen. Jede Stufe fügt +5% kritische Trefferchance hinzu.', lightningChance: 'Fügt die Chance hinzu, Ziele mit Blitzen zu treffen, die Schaden verursachen und zu nahegelegenen Feinden springen. Jede Stufe fügt +5% Chance und +5% Schaden hinzu.', fire: 'Fügt die Chance hinzu, Feinde zu entzünden und 10 Sekunden lang Schaden zu verursachen. Feuer stapelt sich bis zu 10 Mal und erhöht den Schaden. Jede Stufe fügt +5% Chance hinzu.', freeze: 'Fügt die Chance hinzu, Feinde um 50% zu verlangsamen. Bei 3 Stapeln frieren Feinde 4 Sekunden lang komplett ein. Jede Stufe fügt +5% Chance hinzu.' }, fr: { fireDamage: 'Augmente les dégâts de chaque tir. Chaque niveau ajoute +20 dégâts à vos attaques.', fireRate: 'Réduit le temps entre les tirs, permettant de tirer plus fréquemment.', critChance: 'Augmente les chances d\'infliger des dégâts doubles avec les tirs. Chaque niveau ajoute +5% de chance de coup critique.', lightningChance: 'Ajoute une chance de frapper les cibles avec la foudre qui inflige des dégâts et saute vers les ennemis proches. Chaque niveau ajoute +5% de chance et +5% de dégâts.', fire: 'Ajoute une chance d\'enflammer les ennemis, infligeant des dégâts pendant 10 secondes. Le feu se cumule jusqu\'à 10 fois, augmentant les dégâts. Chaque niveau ajoute +5% de chance.', freeze: 'Ajoute une chance de ralentir les ennemis de 50%. À 3 cumuls, les ennemis gèlent complètement pendant 4 secondes. Chaque niveau ajoute +5% de chance.' }, es: { fireDamage: 'Aumenta el daño de cada disparo. Cada nivel añade +20 daño a tus ataques.', fireRate: 'Reduce el tiempo entre disparos, permitiendo disparar más frecuentemente.', critChance: 'Aumenta la posibilidad de infligir daño doble con los disparos. Cada nivel añade +5% de posibilidad de golpe crítico.', lightningChance: 'Añade posibilidad de golpear objetivos con rayos que infligen daño y saltan a enemigos cercanos. Cada nivel añade +5% de posibilidad y +5% de daño.', fire: 'Añade posibilidad de incendiar enemigos, infligiendo daño durante 10 segundos. El fuego se acumula hasta 10 veces, aumentando el daño. Cada nivel añade +5% de posibilidad.', freeze: 'Añade posibilidad de ralentizar enemigos en 50%. Con 3 acumulaciones los enemigos se congelan completamente durante 4 segundos. Cada nivel añade +5% de posibilidad.' }, tr: { fireDamage: 'Her atışın hasarını artırır. Her seviye saldırılarınıza +20 hasar ekler.', fireRate: 'Atışlar arasındaki süreyi azaltır, daha sık ateş etmenizi sağlar.', critChance: 'Atışlarla çifte hasar verme şansını artırır. Her seviye +%5 kritik vuruş şansı ekler.', lightningChance: 'Hedefleri şimşekle vurma şansı ekler, hasar verir ve yakındaki düşmanlara zıplar. Her seviye +%5 şans ve +%5 hasar ekler.', fire: 'Düşmanları tutuşturma şansı ekler, 10 saniye boyunca hasar verir. Ateş 10 kata kadar birikir, hasarı artırır. Her seviye +%5 şans ekler.', freeze: 'Düşmanları %50 yavaşlatma şansı ekler. 3 birikimde düşmanlar 4 saniye boyunca tamamen donar. Her seviye +%5 şans ekler.' } }; // Tooltip functions function showUpgradeTooltip(upgradeKey, iconX, iconY) { if (currentTooltip) { hideUpgradeTooltip(); } var upgradeDef = null; for (var i = 0; i < UPGRADE_DEFS.length; i++) { if (UPGRADE_DEFS[i].key === upgradeKey) { upgradeDef = UPGRADE_DEFS[i]; break; } } if (!upgradeDef) { return; } currentTooltip = new Container(); // Large tooltip background positioned in center of screen var tooltipWidth = 1000; var tooltipHeight = 600; var tooltipBg = LK.getAsset('Fon2', { anchorX: 0.5, anchorY: 0.5, scaleX: tooltipWidth / 1000, scaleY: tooltipHeight / 1000 }); tooltipBg.alpha = 1.0; tooltipBg.x = WORLD_WIDTH / 2; tooltipBg.y = WORLD_HEIGHT / 2 - 80; tooltipBg.interactive = false; tooltipBg.buttonMode = false; currentTooltip.addChild(tooltipBg); // Language flags above tooltip var flagSize = 120; var flagSpacing = 140; var totalFlagsWidth = languages.length * flagSpacing - (flagSpacing - flagSize); var flagStartX = WORLD_WIDTH / 2 - totalFlagsWidth / 2 + 60; var flagY = WORLD_HEIGHT / 2 - tooltipHeight / 2 - flagSize - 120 + 75; for (var f = 0; f < languages.length; f++) { (function (langIndex) { var flag = LK.getAsset(flagAssets[langIndex], { anchorX: 0.5, anchorY: 0.5, scaleX: flagSize / 100, scaleY: flagSize / 100 }); flag.x = flagStartX + langIndex * flagSpacing; flag.y = flagY; flag.alpha = currentLanguage === languages[langIndex] ? 1.0 : 1.0; flag.interactive = true; flag.buttonMode = true; flag.down = function (x, y, obj) { // Add pulsation animation for feedback tween.stop(flag, { scaleX: true, scaleY: true }); var originalScaleX = flag.scaleX || flagSize / 100; var originalScaleY = flag.scaleY || flagSize / 100; tween(flag, { scaleX: originalScaleX * 1.18, scaleY: originalScaleY * 1.18 }, { duration: 90, easing: tween.easeOut, onFinish: function onFinish() { tween(flag, { scaleX: originalScaleX, scaleY: originalScaleY }, { duration: 90, easing: tween.easeIn }); } }); // Switch language currentLanguage = languages[langIndex]; // Update all flag alphas for (var ff = 0; ff < languageFlags.length; ff++) { languageFlags[ff].alpha = currentLanguage === languages[ff] ? 1.0 : 1.0; } // Update tooltip text var newTooltipText = languageData[currentLanguage][upgradeKey] || upgradeDef.tooltip; // Update title text with new language var newTitleText = titleData[currentLanguage][upgradeKey] || upgradeDef.name; titleText.setText(newTitleText); // Update tooltip text with new language var maxWidth = tooltipWidth - 160; var words = newTooltipText.split(' '); var lines = []; var currentLine = ''; var charactersPerLine = Math.floor(maxWidth / 32); for (var w = 0; w < words.length; w++) { var testLine = currentLine + (currentLine ? ' ' : '') + words[w]; if (testLine.length > charactersPerLine && currentLine) { lines.push(currentLine); currentLine = words[w]; } else { currentLine = testLine; } } if (currentLine) { lines.push(currentLine); } var finalLines = []; for (var l = 0; l < lines.length; l++) { if (lines[l].length > charactersPerLine) { var longLine = lines[l]; while (longLine.length > charactersPerLine) { var breakPoint = charactersPerLine; for (var bp = charactersPerLine - 1; bp > charactersPerLine * 0.7; bp--) { if (longLine[bp] === ' ') { breakPoint = bp; break; } } finalLines.push(longLine.substring(0, breakPoint)); longLine = longLine.substring(breakPoint + (longLine[breakPoint] === ' ' ? 1 : 0)); } if (longLine.length > 0) { finalLines.push(longLine); } } else { finalLines.push(lines[l]); } } tooltipText.setText(finalLines.join('\n')); }; currentTooltip.addChild(flag); languageFlags.push(flag); })(f); } // Close button (X) in bottom center of tooltip using krest image var closeButton = LK.getAsset('krest', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, scaleY: 1.2 }); closeButton.x = WORLD_WIDTH / 2; closeButton.y = WORLD_HEIGHT / 2 + tooltipHeight / 2 - 80 + crosshairInstance.graphic.height + crosshairInstance.graphic.height + crosshairInstance.graphic.height - 8; closeButton.interactive = true; closeButton.buttonMode = true; closeButton.down = function (x, y, obj) { hideUpgradeTooltip(); }; currentTooltip.addChild(closeButton); // Language data for upgrade titles var titleData = { en: { fireDamage: 'Damage +', fireRate: 'Fire Rate +', critChance: 'Critical Damage +', lightningChance: 'Lightning Chance +', fire: 'Fire Chance +', freeze: 'Freeze Chance +' }, ru: { fireDamage: 'Урон +', fireRate: 'Скорострельность +', critChance: 'Критический урон +', lightningChance: 'Шанс молнии +', fire: 'Шанс поджога +', freeze: 'Шанс заморозки +' }, de: { fireDamage: 'Schaden +', fireRate: 'Feuerrate +', critChance: 'Kritischer Schaden +', lightningChance: 'Blitz-Chance +', fire: 'Feuer-Chance +', freeze: 'Frost-Chance +' }, fr: { fireDamage: 'Dégâts +', fireRate: 'Cadence de tir +', critChance: 'Dégâts critiques +', lightningChance: 'Chance de foudre +', fire: 'Chance de feu +', freeze: 'Chance de gel +' }, es: { fireDamage: 'Daño +', fireRate: 'Cadencia +', critChance: 'Daño crítico +', lightningChance: 'Oportunidad de rayo +', fire: 'Oportunidad de fuego +', freeze: 'Oportunidad de congelación +' }, tr: { fireDamage: 'Hasar +', fireRate: 'Atış Hızı +', critChance: 'Kritik Hasar +', lightningChance: 'Şimşek Şansı +', fire: 'Ateş Şansı +', freeze: 'Dondurucu Şansı +' } }; // Upgrade title var upgradeTitleText = titleData[currentLanguage][upgradeKey] || upgradeDef.name; var titleText = new Text2(upgradeTitleText, { size: 80, fill: 0xFFFF00, align: 'center', font: 'Impact' }); titleText.anchor.set(0.5, 0); titleText.x = WORLD_WIDTH / 2; titleText.y = WORLD_HEIGHT / 2 - tooltipHeight / 2 + 20 - 50; currentTooltip.addChild(titleText); // Tooltip text with better word wrapping var tooltipText = new Text2('', { size: 60, fill: 0xFFFFFF, align: 'left', font: 'Impact' }); tooltipText.anchor.set(0, 0); tooltipText.x = WORLD_WIDTH / 2 - tooltipWidth / 2 + 80; tooltipText.y = WORLD_HEIGHT / 2 - tooltipHeight / 2 + 90; // Get tooltip text in current language var tooltipTextContent = languageData[currentLanguage][upgradeKey] || upgradeDef.tooltip; // Improved word wrap for long text var maxWidth = tooltipWidth - 160; var words = tooltipTextContent.split(' '); var lines = []; var currentLine = ''; var charactersPerLine = Math.floor(maxWidth / 32); // Approximate characters per line based on font size for (var w = 0; w < words.length; w++) { var testLine = currentLine + (currentLine ? ' ' : '') + words[w]; if (testLine.length > charactersPerLine && currentLine) { lines.push(currentLine); currentLine = words[w]; } else { currentLine = testLine; } } if (currentLine) { lines.push(currentLine); } // Add line breaks if text is still too long var finalLines = []; for (var l = 0; l < lines.length; l++) { if (lines[l].length > charactersPerLine) { // Force break long lines var longLine = lines[l]; while (longLine.length > charactersPerLine) { var breakPoint = charactersPerLine; // Try to break at a space for (var bp = charactersPerLine - 1; bp > charactersPerLine * 0.7; bp--) { if (longLine[bp] === ' ') { breakPoint = bp; break; } } finalLines.push(longLine.substring(0, breakPoint)); longLine = longLine.substring(breakPoint + (longLine[breakPoint] === ' ' ? 1 : 0)); } if (longLine.length > 0) { finalLines.push(longLine); } } else { finalLines.push(lines[l]); } } tooltipText.setText(finalLines.join('\n')); currentTooltip.addChild(tooltipText); // Position tooltip in center currentTooltip.x = 0; currentTooltip.y = 0; upgradesPopup.addChild(currentTooltip); } function hideUpgradeTooltip() { if (currentTooltip && currentTooltip.parent) { currentTooltip.parent.removeChild(currentTooltip); } currentTooltip = null; languageFlags = []; // Clear language flags array } // Shop tooltip functions function showShopTooltip(shopKey, iconX, iconY) { if (currentTooltip) { hideUpgradeTooltip(); } currentTooltip = new Container(); // Large tooltip background positioned in center of screen var tooltipWidth = 1000; var tooltipHeight = 600; var tooltipBg = LK.getAsset('Fon2', { anchorX: 0.5, anchorY: 0.5, scaleX: tooltipWidth / 1000, scaleY: tooltipHeight / 1000 }); tooltipBg.alpha = 1.0; tooltipBg.x = WORLD_WIDTH / 2; tooltipBg.y = WORLD_HEIGHT / 2 - 80; tooltipBg.interactive = false; tooltipBg.buttonMode = false; currentTooltip.addChild(tooltipBg); // Language flags above tooltip var flagSize = 120; var flagSpacing = 140; var totalFlagsWidth = languages.length * flagSpacing - (flagSpacing - flagSize); var flagStartX = WORLD_WIDTH / 2 - totalFlagsWidth / 2 + 60; var flagY = WORLD_HEIGHT / 2 - tooltipHeight / 2 - flagSize - 120 + 75; for (var f = 0; f < languages.length; f++) { (function (langIndex) { var flag = LK.getAsset(flagAssets[langIndex], { anchorX: 0.5, anchorY: 0.5, scaleX: flagSize / 100, scaleY: flagSize / 100 }); flag.x = flagStartX + langIndex * flagSpacing; flag.y = flagY; flag.alpha = currentLanguage === languages[langIndex] ? 1.0 : 1.0; flag.interactive = true; flag.buttonMode = true; flag.down = function (x, y, obj) { // Add pulsation animation for feedback tween.stop(flag, { scaleX: true, scaleY: true }); var originalScaleX = flag.scaleX || flagSize / 100; var originalScaleY = flag.scaleY || flagSize / 100; tween(flag, { scaleX: originalScaleX * 1.18, scaleY: originalScaleY * 1.18 }, { duration: 90, easing: tween.easeOut, onFinish: function onFinish() { tween(flag, { scaleX: originalScaleX, scaleY: originalScaleY }, { duration: 90, easing: tween.easeIn }); } }); // Switch language currentLanguage = languages[langIndex]; // Update all flag alphas for (var ff = 0; ff < languageFlags.length; ff++) { languageFlags[ff].alpha = currentLanguage === languages[ff] ? 1.0 : 1.0; } // Update tooltip text var newTooltipText = shopTooltipData[currentLanguage][shopKey] || shopTooltipData['en'][shopKey]; // Update title text with new language var newTitleText = shopTitleData[currentLanguage][shopKey] || shopTitleData['en'][shopKey]; titleText.setText(newTitleText); // Update tooltip text with new language var maxWidth = tooltipWidth - 160; var words = newTooltipText.split(' '); var lines = []; var currentLine = ''; var charactersPerLine = Math.floor(maxWidth / 32); for (var w = 0; w < words.length; w++) { var testLine = currentLine + (currentLine ? ' ' : '') + words[w]; if (testLine.length > charactersPerLine && currentLine) { lines.push(currentLine); currentLine = words[w]; } else { currentLine = testLine; } } if (currentLine) { lines.push(currentLine); } var finalLines = []; for (var l = 0; l < lines.length; l++) { if (lines[l].length > charactersPerLine) { var longLine = lines[l]; while (longLine.length > charactersPerLine) { var breakPoint = charactersPerLine; for (var bp = charactersPerLine - 1; bp > charactersPerLine * 0.7; bp--) { if (longLine[bp] === ' ') { breakPoint = bp; break; } } finalLines.push(longLine.substring(0, breakPoint)); longLine = longLine.substring(breakPoint + (longLine[breakPoint] === ' ' ? 1 : 0)); } if (longLine.length > 0) { finalLines.push(longLine); } } else { finalLines.push(lines[l]); } } tooltipText.setText(finalLines.join('\n')); }; currentTooltip.addChild(flag); languageFlags.push(flag); })(f); } // Close button (X) in bottom center of tooltip using krest image var closeButton = LK.getAsset('krest', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, scaleY: 1.2 }); closeButton.x = WORLD_WIDTH / 2; closeButton.y = WORLD_HEIGHT / 2 + tooltipHeight / 2 - 80 + crosshairInstance.graphic.height + crosshairInstance.graphic.height + crosshairInstance.graphic.height - 8; closeButton.interactive = true; closeButton.buttonMode = true; closeButton.down = function (x, y, obj) { hideUpgradeTooltip(); }; currentTooltip.addChild(closeButton); // Shop title var shopTitleText = shopTitleData[currentLanguage][shopKey] || shopTitleData['en'][shopKey]; var titleText = new Text2(shopTitleText, { size: 80, fill: 0xFFFF00, align: 'center', font: 'Impact' }); titleText.anchor.set(0.5, 0); titleText.x = WORLD_WIDTH / 2; titleText.y = WORLD_HEIGHT / 2 - tooltipHeight / 2 + 20 - 50; currentTooltip.addChild(titleText); // Tooltip text with better word wrapping var tooltipText = new Text2('', { size: 60, fill: 0xFFFFFF, align: 'left', font: 'Impact' }); tooltipText.anchor.set(0, 0); tooltipText.x = WORLD_WIDTH / 2 - tooltipWidth / 2 + 80; tooltipText.y = WORLD_HEIGHT / 2 - tooltipHeight / 2 + 90; // Get tooltip text in current language var tooltipTextContent = shopTooltipData[currentLanguage][shopKey] || shopTooltipData['en'][shopKey]; // Improved word wrap for long text var maxWidth = tooltipWidth - 160; var words = tooltipTextContent.split(' '); var lines = []; var currentLine = ''; var charactersPerLine = Math.floor(maxWidth / 32); // Approximate characters per line based on font size for (var w = 0; w < words.length; w++) { var testLine = currentLine + (currentLine ? ' ' : '') + words[w]; if (testLine.length > charactersPerLine && currentLine) { lines.push(currentLine); currentLine = words[w]; } else { currentLine = testLine; } } if (currentLine) { lines.push(currentLine); } // Add line breaks if text is still too long var finalLines = []; for (var l = 0; l < lines.length; l++) { if (lines[l].length > charactersPerLine) { // Force break long lines var longLine = lines[l]; while (longLine.length > charactersPerLine) { var breakPoint = charactersPerLine; // Try to break at a space for (var bp = charactersPerLine - 1; bp > charactersPerLine * 0.7; bp--) { if (longLine[bp] === ' ') { breakPoint = bp; break; } } finalLines.push(longLine.substring(0, breakPoint)); longLine = longLine.substring(breakPoint + (longLine[breakPoint] === ' ' ? 1 : 0)); } if (longLine.length > 0) { finalLines.push(longLine); } } else { finalLines.push(lines[l]); } } tooltipText.setText(finalLines.join('\n')); currentTooltip.addChild(tooltipText); // Position tooltip in center currentTooltip.x = 0; currentTooltip.y = 0; shopPopup.addChild(currentTooltip); } // Create upgrades button (top right, not in topLeft) upgradesButton = LK.getAsset('upgrade', { anchorX: 1, anchorY: 0, scaleX: upgradesButtonWidth / 800, // Scale to fit button size (upgrade asset is 800px wide) scaleY: upgradesButtonHeight / 400 // Scale to fit button size (upgrade asset is 400px tall) }); // Place button at top right, but not in the top left 100x100 area upgradesButton.x = -upgradesButtonPadding; // Will be positioned by LK.gui.topRight upgradesButton.y = upgradesButtonY; upgradesButton.interactive = true; upgradesButton.buttonMode = true; upgradesButton.visible = true; upgradesButtonActive = false; LK.gui.topRight.addChild(upgradesButton); // Also add a large invisible hit area to make it easier to tap if (!upgradesButton.hitArea) { var hitArea = LK.getAsset('fon', { anchorX: 1, anchorY: 0, scaleX: 220 / 2048, scaleY: 100 / 2732 }); hitArea.x = upgradesButton.x; hitArea.y = upgradesButton.y; hitArea.alpha = 0.01; hitArea.interactive = true; hitArea.buttonMode = true; LK.gui.topRight.addChild(hitArea); hitArea.on('down', function (x, y, obj) { if (!upgradesButtonActive && !controlsPopup && !shopPopup) { showUpgradesPopup(); } }); } // Create shop button (top right, below upgrades button) shopButton = LK.getAsset('shop_button', { anchorX: 1, anchorY: 0, scaleX: 1, scaleY: 1 }); // Place button at top right, below upgrades button shopButton.x = -shopButtonPadding; // Will be positioned by LK.gui.topRight shopButton.y = shopButtonY; shopButton.interactive = true; shopButton.buttonMode = true; shopButton.visible = true; shopButtonActive = false; LK.gui.topRight.addChild(shopButton); // Also add a large invisible hit area to make it easier to tap if (!shopButton.hitArea) { var shopHitArea = LK.getAsset('fon', { anchorX: 1, anchorY: 0, scaleX: 300 / 2048, scaleY: 150 / 2732 }); shopHitArea.x = shopButton.x; shopHitArea.y = shopButton.y; shopHitArea.alpha = 0.01; shopHitArea.interactive = true; shopHitArea.buttonMode = true; LK.gui.topRight.addChild(shopHitArea); shopHitArea.on('down', function (x, y, obj) { if (!shopButtonActive && !upgradesPopup && !controlsPopup) { showShopPopup(); } }); } // Control settings variables var controlsPopup = null; var controlsButton = null; var controlsButtonActive = false; var controlsButtonWidth = 180; var controlsButtonHeight = 90; var useJoystickControl = true; // Default to joystick control // Controls popup container function showControlsPopup() { if (controlsPopup) { return; } // Already open LK.getSound('menu').play(); controlsPopup = new Container(); // Dimmed background var popupBg = LK.getAsset('fon', { anchorX: 0, anchorY: 0, scaleX: 1, scaleY: 1 }); popupBg.tint = 0x1a1a2e; popupBg.alpha = 0.85; popupBg.width = WORLD_WIDTH; popupBg.height = WORLD_HEIGHT; popupBg.interactive = false; popupBg.buttonMode = false; controlsPopup.addChild(popupBg); // Popup window var popupWidth = 1200; var popupHeight = 800; var popupWin = LK.getAsset('fonsettings', { anchorX: 0.5, anchorY: 0.5, scaleX: popupWidth / 3000, scaleY: popupHeight / 4000 }); popupWin.x = WORLD_WIDTH / 2; popupWin.y = WORLD_HEIGHT / 2; popupWin.alpha = 0.98; controlsPopup.addChild(popupWin); // Close button (X) in bottom center of controls popup using krest image var controlsCloseButton = LK.getAsset('krest', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); controlsCloseButton.x = WORLD_WIDTH / 2; controlsCloseButton.y = WORLD_HEIGHT / 2 + popupHeight / 2 - 80 + 300 + 10 + 10 + 10 + 10; controlsCloseButton.interactive = true; controlsCloseButton.buttonMode = true; controlsCloseButton.down = function (x, y, obj) { closeControlsPopup(); }; controlsPopup.addChild(controlsCloseButton); // Title removed as requested // Joystick control button var joystickButtonWidth = 300; var joystickButtonHeight = 300; var joystickControlButton = LK.getAsset('joy', { anchorX: 0.5, anchorY: 0.5, scaleX: joystickButtonWidth / 300, scaleY: joystickButtonHeight / 300 }); joystickControlButton.x = WORLD_WIDTH / 2 - 300; joystickControlButton.y = WORLD_HEIGHT / 2 + 50; joystickControlButton.tint = useJoystickControl ? 0x00FF00 : 0x808080; joystickControlButton.interactive = true; joystickControlButton.buttonMode = true; controlsPopup.addChild(joystickControlButton); // Mouse control button var mouseControlButton = LK.getAsset('mouse', { anchorX: 0.5, anchorY: 0.5, scaleX: joystickButtonWidth / 300, scaleY: joystickButtonHeight / 400 }); mouseControlButton.x = WORLD_WIDTH / 2 + 300; mouseControlButton.y = WORLD_HEIGHT / 2 + 50; mouseControlButton.tint = !useJoystickControl ? 0x00FF00 : 0x808080; mouseControlButton.interactive = true; mouseControlButton.buttonMode = true; controlsPopup.addChild(mouseControlButton); // Button interactions joystickControlButton.on('down', function (x, y, obj) { if (!useJoystickControl) { useJoystickControl = true; isMouseControlling = false; joystickControlButton.tint = 0x00FF00; mouseControlButton.tint = 0x808080; LK.getSound('peretik').play(); } }); mouseControlButton.on('down', function (x, y, obj) { if (useJoystickControl) { useJoystickControl = false; isMouseControlling = true; joystickControlButton.tint = 0x808080; mouseControlButton.tint = 0x00FF00; // Hide joystick when switching to mouse control joystickInstance.visible = false; LK.getSound('peretik').play(); } }); // Add popup to game game.addChild(controlsPopup); controlsButtonActive = true; controlsButton.visible = false; controlsButton.interactive = false; controlsButton.buttonMode = false; // Hide upgrades and shop buttons when controls popup is open upgradesButton.visible = false; upgradesButton.interactive = false; upgradesButton.buttonMode = false; shopButton.visible = false; shopButton.interactive = false; shopButton.buttonMode = false; } function closeControlsPopup() { if (controlsPopup && controlsPopup.parent) { controlsPopup.parent.removeChild(controlsPopup); LK.getSound('menu').play(); } controlsPopup = null; controlsButtonActive = false; controlsButton.visible = true; controlsButton.interactive = true; controlsButton.buttonMode = true; // Show upgrades and shop buttons when controls popup is closed upgradesButton.visible = true; upgradesButton.interactive = true; upgradesButton.buttonMode = true; shopButton.visible = true; shopButton.interactive = true; shopButton.buttonMode = true; } // Shop popup container function showShopPopup() { if (shopPopup) { return; } // Already open LK.getSound('menu').play(); shopPopup = new Container(); // Dimmed background for shop popup var popupBg = LK.getAsset('fon', { anchorX: 0, anchorY: 0, scaleX: 1, scaleY: 1 }); popupBg.tint = 0x2e1a1a; // Dark red-purple color for shop background popupBg.alpha = 0.85; popupBg.width = WORLD_WIDTH; popupBg.height = WORLD_HEIGHT; popupBg.interactive = false; popupBg.buttonMode = false; shopPopup.addChild(popupBg); // Popup window var popupWidth = 1800; var popupHeight = 1650; var popupWin = LK.getAsset('Fonshop', { anchorX: 0.5, anchorY: 0.5, scaleX: popupWidth / 2048, scaleY: popupHeight / 2732 }); popupWin.x = WORLD_WIDTH / 2; popupWin.y = WORLD_HEIGHT / 2; popupWin.alpha = 0.98; shopPopup.addChild(popupWin); // Close button (X) in bottom center of shop popup using krest image var shopCloseButton = LK.getAsset('krest', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); shopCloseButton.x = WORLD_WIDTH / 2; shopCloseButton.y = WORLD_HEIGHT / 2 + popupHeight / 2 - 100 + 200 + 80; shopCloseButton.interactive = true; shopCloseButton.buttonMode = true; shopCloseButton.down = function (x, y, obj) { closeShopPopup(); }; shopPopup.addChild(shopCloseButton); // Shop icons grid (3x2) - same layout as upgrades var iconSize = 360; var availableWidth = popupWidth - 60; var availableHeight = popupHeight - 220; var cols = 3; var rows = 2; var iconSpacingX = availableWidth / (cols - 1); var iconSpacingY = availableHeight / (rows - 1); var gridStartX = WORLD_WIDTH / 2 - availableWidth / 2; var gridStartY = popupWin.y - popupHeight / 2 + 80 + 480; for (var i = 0; i < 6; i++) { (function (i) { var row = Math.floor(i / 3); var col = i % 3; // Move leftmost and rightmost icons closer to center var iconX = gridStartX + col * iconSpacingX; if (col === 0) { iconX += 300; } if (col === 2) { iconX -= 300; } var iconY = gridStartY + row * iconSpacingY; // Move bottom row up if (row === 1) { iconY = iconY - 650; } // Create numbered shop icon var iconBg = LK.getAsset('shop_icon_' + (i + 1), { anchorX: 0.5, anchorY: 0.5, scaleX: iconSize / 360, scaleY: iconSize / 360 }); iconBg.x = iconX; iconBg.y = iconY; shopPopup.addChild(iconBg); // Add number text to icon, except for icon 1 (i === 0), icon 2 (i === 1), icon 3 (i === 2), icon 4 (i === 3), icon 5 (i === 4), and icon 6 (i === 5) if (i !== 0 && i !== 1 && i !== 2 && i !== 3 && i !== 4 && i !== 5) { var numberText = new Text2((i + 1).toString(), { size: 120, fill: 0xFFFFFF, align: 'center', font: 'Impact' }); numberText.anchor.set(0.5, 0.5); numberText.x = iconX; numberText.y = iconY; shopPopup.addChild(numberText); } // --- Shop Item 1: City Health Restore --- // Add price label above icon 1 if (i === 0) { var cityHealPrice = 100; var priceText = new Text2("$" + cityHealPrice, { size: 90, fill: 0x00FF00, align: 'center', font: 'Impact' }); priceText.anchor.set(0.5, 1); priceText.x = iconX; priceText.y = iconY - iconSize / 2 - 10; shopPopup.addChild(priceText); } else if (i === 1) { // Add price label above icon 2 (shield) var shieldPrice = 100; var priceText = new Text2("$" + shieldPrice, { size: 90, fill: 0x00FF00, align: 'center', font: 'Impact' }); priceText.anchor.set(0.5, 1); priceText.x = iconX; priceText.y = iconY - iconSize / 2 - 10; shopPopup.addChild(priceText); } else if (i === 2) { // Add price label above icon 3 (rocket barrage) var rocketBarragePrice = 100; var priceText = new Text2("$" + rocketBarragePrice, { size: 90, fill: 0x00FF00, align: 'center', font: 'Impact' }); priceText.anchor.set(0.5, 1); priceText.x = iconX; priceText.y = iconY - iconSize / 2 - 10; shopPopup.addChild(priceText); } else if (i === 3) { // Add price label above icon 4 (Tesla tower) var teslaTowerPrice = 200; var priceText = new Text2("$" + teslaTowerPrice, { size: 90, fill: 0x00FF00, align: 'center', font: 'Impact' }); priceText.anchor.set(0.5, 1); priceText.x = iconX; priceText.y = iconY - iconSize / 2 - 10; shopPopup.addChild(priceText); } else if (i === 4) { // Add price label above icon 5 (fire tower) var fireTowerPrice = 200; var priceText = new Text2("$" + fireTowerPrice, { size: 90, fill: 0x00FF00, align: 'center', font: 'Impact' }); priceText.anchor.set(0.5, 1); priceText.x = iconX; priceText.y = iconY - iconSize / 2 - 10; shopPopup.addChild(priceText); } else if (i === 5) { // Add price label above icon 6 (ice tower) var iceTowerPrice = 200; var priceText = new Text2("$" + iceTowerPrice, { size: 90, fill: 0x00FF00, align: 'center', font: 'Impact' }); priceText.anchor.set(0.5, 1); priceText.x = iconX; priceText.y = iconY - iconSize / 2 - 10; shopPopup.addChild(priceText); } // Add cooldown timer text for each icon var cooldownText = new Text2('', { size: 70, fill: 0xFFFFFF, align: 'center', font: 'Impact' }); cooldownText.anchor.set(0.5, 0.5); cooldownText.x = iconX; cooldownText.y = iconY; cooldownText.visible = false; shopPopup.addChild(cooldownText); // Store references for cooldown updates iconBg.cooldownText = cooldownText; iconBg.shopItemIndex = i; // --- Ava: Normalize cooldowns to always show 60s and decrement by 1 every second --- if (i === 0 && healthRestoreCooldownActive) { iconBg.alpha = 0.5; cooldownText.visible = true; // Calculate seconds left as integer countdown from 60 var secondsLeft = Math.max(1, 60 - Math.floor((LK.ticks * (1000 / 60) - (healthRestoreCooldownEndTime - 60000)) / 1000)); cooldownText.setText(secondsLeft + 's'); } else if (i === 1 && shieldCooldownActive) { iconBg.alpha = 0.5; cooldownText.visible = true; var secondsLeft = Math.max(1, 60 - Math.floor((LK.ticks * (1000 / 60) - (shieldCooldownEndTime - 60000)) / 1000)); cooldownText.setText(secondsLeft + 's'); } else if (i === 2 && rocketBarrageCooldownActive) { iconBg.alpha = 0.5; cooldownText.visible = true; var secondsLeft = Math.max(1, 60 - Math.floor((LK.ticks * (1000 / 60) - (rocketBarrageCooldownEndTime - 60000)) / 1000)); cooldownText.setText(secondsLeft + 's'); } else if (i === 3 && teslaTowerCooldownActive) { iconBg.alpha = 0.5; cooldownText.visible = true; var secondsLeft = Math.max(1, 60 - Math.floor((LK.ticks * (1000 / 60) - (teslaTowerCooldownEndTime - 60000)) / 1000)); cooldownText.setText(secondsLeft + 's'); } else if (i === 4 && fireTowerCooldownActive) { iconBg.alpha = 0.5; cooldownText.visible = true; var secondsLeft = Math.max(1, 60 - Math.floor((LK.ticks * (1000 / 60) - (fireTowerCooldownEndTime - 60000)) / 1000)); cooldownText.setText(secondsLeft + 's'); } else if (i === 5 && iceTowerCooldownActive) { iconBg.alpha = 0.5; cooldownText.visible = true; var secondsLeft = Math.max(1, 60 - Math.floor((LK.ticks * (1000 / 60) - (iceTowerCooldownEndTime - 60000)) / 1000)); cooldownText.setText(secondsLeft + 's'); } // Create info icon in top-right corner of shop icon using iconI asset var shopInfoIcon = LK.getAsset('iconI', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); shopInfoIcon.alpha = 0.8; shopInfoIcon.x = iconX + iconSize / 2 - 30; shopInfoIcon.y = iconY - iconSize / 2 + 30; shopPopup.addChild(shopInfoIcon); // Info icon interaction for shop items shopInfoIcon.interactive = true; shopInfoIcon.buttonMode = true; shopInfoIcon.down = function (x, y, obj) { showShopTooltip('shop' + (i + 1), iconX, iconY); }; // Button interaction iconBg.interactive = true; iconBg.buttonMode = true; iconBg.on('down', function (x, y, obj) { // Check cooldown state before allowing interaction var currentTime = LK.ticks * (1000 / 60); var isCurrentlyCoolingDown = false; var cooldownEnd = 0; if (i === 0) { isCurrentlyCoolingDown = healthRestoreCooldownActive && currentTime < healthRestoreCooldownEndTime; cooldownEnd = healthRestoreCooldownEndTime; } else if (i === 1) { isCurrentlyCoolingDown = shieldCooldownActive && currentTime < shieldCooldownEndTime; cooldownEnd = shieldCooldownEndTime; } else if (i === 2) { isCurrentlyCoolingDown = rocketBarrageCooldownActive && currentTime < rocketBarrageCooldownEndTime; cooldownEnd = rocketBarrageCooldownEndTime; } else if (i === 3) { isCurrentlyCoolingDown = teslaTowerCooldownActive && currentTime < teslaTowerCooldownEndTime; cooldownEnd = teslaTowerCooldownEndTime; } else if (i === 4) { isCurrentlyCoolingDown = fireTowerCooldownActive && currentTime < fireTowerCooldownEndTime; cooldownEnd = fireTowerCooldownEndTime; } else if (i === 5) { isCurrentlyCoolingDown = iceTowerCooldownActive && currentTime < iceTowerCooldownEndTime; cooldownEnd = iceTowerCooldownEndTime; } if (isCurrentlyCoolingDown) { // Show correct timer immediately, never 0s if (iconBg.cooldownText) { var msLeft = Math.max(0, cooldownEnd - currentTime); var secondsLeft = Math.ceil(msLeft / 1000); if (msLeft > 0 && secondsLeft === 0) { secondsLeft = 1; } iconBg.cooldownText.setText(secondsLeft + 's'); iconBg.cooldownText.visible = true; iconBg.alpha = 0.5; } LK.getSound('stop').play(); // Store original position if not already stored if (iconBg.originalX === undefined) { iconBg.originalX = iconBg.x; } // Stop any existing shake animation tween.stop(iconBg, { x: true }); // Shake animation - quick left-right movement var shakeDistance = 20; var shakeSpeed = 50; tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX }, { duration: shakeSpeed, easing: tween.easeIn }); } }); } }); } }); } }); return; // Don't allow purchase during cooldown } // --- Immediately update visual state after purchase --- // Force immediate visual update after purchase by checking the cooldown flags that were just set var currentTime = LK.ticks * (1000 / 60); var isCooldownNowActive = false; var cooldownEndTime = 0; // Check cooldown state immediately after purchase if (i === 0) { isCooldownNowActive = healthRestoreCooldownActive; cooldownEndTime = healthRestoreCooldownEndTime; } else if (i === 1) { isCooldownNowActive = shieldCooldownActive; cooldownEndTime = shieldCooldownEndTime; } else if (i === 2) { isCooldownNowActive = rocketBarrageCooldownActive; cooldownEndTime = rocketBarrageCooldownEndTime; } else if (i === 3) { isCooldownNowActive = teslaTowerCooldownActive; cooldownEndTime = teslaTowerCooldownEndTime; } else if (i === 4) { isCooldownNowActive = fireTowerCooldownActive; cooldownEndTime = fireTowerCooldownEndTime; } else if (i === 5) { isCooldownNowActive = iceTowerCooldownActive; cooldownEndTime = iceTowerCooldownEndTime; } // Apply immediate visual update - force update the current icon if (isCooldownNowActive && iconBg.cooldownText) { // Apply immediate dimming and timer iconBg.alpha = 0.5; // 50% dimming iconBg.cooldownText.visible = true; // --- Ava: Always show 60s at start, decrement by 1 per second, never 0s --- var cooldownStart = 0; if (i === 0) { cooldownStart = healthRestoreCooldownEndTime - 60000; } else if (i === 1) { cooldownStart = shieldCooldownEndTime - 60000; } else if (i === 2) { cooldownStart = rocketBarrageCooldownEndTime - 60000; } else if (i === 3) { cooldownStart = teslaTowerCooldownEndTime - 60000; } else if (i === 4) { cooldownStart = fireTowerCooldownEndTime - 60000; } else if (i === 5) { cooldownStart = iceTowerCooldownEndTime - 60000; } var secondsLeft = Math.max(1, 60 - Math.floor((currentTime - cooldownStart) / 1000)); iconBg.cooldownText.setText(secondsLeft + 's'); } else if (iconBg.cooldownText) { // No cooldown - restore normal appearance iconBg.alpha = 1.0; iconBg.cooldownText.visible = false; iconBg.cooldownText.setText(''); } if (i === 0) { // Shop item 1: Gradual city health restore var cityHealPrice = 20; // Check if cooldown is active if (healthRestoreCooldownActive) { return; } if (LK.getScore() < cityHealPrice) { LK.getSound('stop').play(); // Store original position if not already stored if (iconBg.originalX === undefined) { iconBg.originalX = iconBg.x; } // Stop any existing shake animation tween.stop(iconBg, { x: true }); // Shake animation - quick left-right movement var shakeDistance = 20; var shakeSpeed = 50; tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX }, { duration: shakeSpeed, easing: tween.easeIn }); } }); } }); } }); } }); return; } // Don't allow if city is already at max health if (cityInstance.health >= CITY_MAX_HEALTH) { LK.effects.flashObject(cityInstance.healthBarFill, 0xFFFF00, 300); return; } // Start cooldown (60 seconds) var currentTime = LK.ticks * (1000 / 60); healthRestoreCooldownActive = true; healthRestoreCooldownEndTime = currentTime + 180000; // 3 minutes healthRestoreCooldownPausedTime = null; LK.setScore(LK.getScore() - cityHealPrice); updateScoreDisplay(); LK.getSound('Pokupka').play(); // Activate gradual healing effect, managed in game.update gradualHealActive = true; gradualHealEndTime = currentTime + 12000; // 12 seconds duration gradualHealLastTick = currentTime - 1000; // Set to 1 second ago so first heal happens immediately on next frame gradualHealPausedTime = null; // Prevent multiple concurrent heals by clearing any old interval if it exists if (cityInstance._healingInterval) { LK.clearInterval(cityInstance._healingInterval); cityInstance._healingInterval = null; } // Force immediate visual update for this specific icon iconBg.alpha = 0.5; iconBg.cooldownText.visible = true; iconBg.cooldownText.setText('180s'); } else if (i === 1) { // Shop item 2: City Shield var shieldPrice = 25; // Check if player has enough money if (LK.getScore() < shieldPrice) { LK.getSound('stop').play(); // Store original position if not already stored if (iconBg.originalX === undefined) { iconBg.originalX = iconBg.x; } // Stop any existing shake animation tween.stop(iconBg, { x: true }); // Shake animation - quick left-right movement var shakeDistance = 20; var shakeSpeed = 50; tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX }, { duration: shakeSpeed, easing: tween.easeIn }); } }); } }); } }); } }); return; } // Check if shield is already active if (cityInstance.shielded) { // Flash shield icon to indicate it's already active LK.effects.flashObject(iconBg, 0xFFFF00, 300); return; } // Start cooldown (60 seconds) var currentTime = LK.ticks * (1000 / 60); shieldCooldownActive = true; shieldCooldownEndTime = currentTime + 180000; // 3 minutes shieldCooldownPausedTime = null; // Purchase shield LK.setScore(LK.getScore() - shieldPrice); updateScoreDisplay(); LK.getSound('Pokupka').play(); // Apply shield cityInstance.shielded = true; cityInstance.shieldStrength = 500; cityInstance.maxShieldStrength = 500; cityInstance.shieldEndTime = LK.ticks * (1000 / 60) + 15000; // 15 seconds // Create shield visual var shield = LK.getAsset('Shield', { anchorX: 0.5, anchorY: 1.0 }); shield.x = 0; shield.y = -80; // Positioned above health bar shield.alpha = 0.6; // 60% opacity (0.6 alpha) shield.tint = 0x00FFFF; // Cyan color for shield cityInstance.addChild(shield); cityInstance.shieldGraphic = shield; // Force immediate visual update for this specific icon iconBg.alpha = 0.5; iconBg.cooldownText.visible = true; iconBg.cooldownText.setText('180s'); } else if (i === 2) { // Shop item 3: Rocket Barrage - fires homing rockets every second var rocketBarragePrice = 80; // Check if player has enough money if (LK.getScore() < rocketBarragePrice) { LK.getSound('stop').play(); // Store original position if not already stored if (iconBg.originalX === undefined) { iconBg.originalX = iconBg.x; } // Stop any existing shake animation tween.stop(iconBg, { x: true }); // Shake animation - quick left-right movement var shakeDistance = 20; var shakeSpeed = 50; tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX }, { duration: shakeSpeed, easing: tween.easeIn }); } }); } }); } }); } }); return; } // Check if rocket barrage is already active if (rocketBarrageActive) { // Flash rocket icon to indicate it's already active LK.effects.flashObject(iconBg, 0xFFFF00, 300); return; } // Start cooldown (60 seconds) var currentTime = LK.ticks * (1000 / 60); rocketBarrageCooldownActive = true; rocketBarrageCooldownEndTime = currentTime + 180000; // 3 minutes rocketBarrageCooldownPausedTime = null; // Purchase rocket barrage LK.setScore(LK.getScore() - rocketBarragePrice); updateScoreDisplay(); LK.getSound('Pokupka').play(); // Activate rocket barrage system rocketBarrageActive = true; rocketBarrageEndTime = LK.ticks * (1000 / 60) + 15000; // 15 seconds duration rocketBarragePausedTime = null; lastRocketLaunchTime = 0; // Force immediate visual update for this specific icon iconBg.alpha = 0.5; iconBg.cooldownText.visible = true; iconBg.cooldownText.setText('180s'); } else if (i === 3) { // Shop item 4: Tesla Tower - creates powerful lightning tower var teslaTowerPrice = 150; // Check if player has enough money if (LK.getScore() < teslaTowerPrice) { LK.getSound('stop').play(); // Store original position if not already stored if (iconBg.originalX === undefined) { iconBg.originalX = iconBg.x; } // Stop any existing shake animation tween.stop(iconBg, { x: true }); // Shake animation - quick left-right movement var shakeDistance = 20; var shakeSpeed = 50; tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX }, { duration: shakeSpeed, easing: tween.easeIn }); } }); } }); } }); } }); return; } // Check if Tesla tower is already active if (teslaTowerActive) { // Flash Tesla icon to indicate it's already active LK.effects.flashObject(iconBg, 0xFFFF00, 300); return; } // Start cooldown (60 seconds) var currentTime = LK.ticks * (1000 / 60); teslaTowerCooldownActive = true; teslaTowerCooldownEndTime = currentTime + 180000; // 3 minutes teslaTowerCooldownPausedTime = null; // Purchase Tesla tower LK.setScore(LK.getScore() - teslaTowerPrice); updateScoreDisplay(); LK.getSound('Pokupka').play(); // Activate Tesla tower system teslaTowerActive = true; teslaTowerEndTime = LK.ticks * (1000 / 60) + 15000; // 15 seconds duration teslaTowerPausedTime = null; lastTeslaStrikeTime = 0; // Create Tesla tower visual in center of city teslaTowerGraphic = LK.getAsset('tesla_tower', { anchorX: 0.5, anchorY: 1.0 }); teslaTowerGraphic.x = 200; // Relative to city center, moved further left teslaTowerGraphic.y = -120; // Above city, positioned above health bar teslaTowerGraphic.scaleX = 0.15; teslaTowerGraphic.scaleY = 0.15; teslaTowerGraphic.alpha = 1.0; // Add Tesla tower graphic at the very back, so it is rendered behind the city cityInstance.addChildAt(teslaTowerGraphic, 0); // Create Tesla sphere immediately after tower is created teslaSphereGraphic = LK.getAsset('teslaspher', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.7, scaleY: 0.7 }); // Position sphere at top of the tower after tower appears var towerVisualHeight = teslaTowerGraphic.height * teslaTowerGraphic.scaleY; teslaSphereGraphic.x = teslaTowerGraphic.x; // Same local X as tower in cityInstance teslaSphereGraphic.y = teslaTowerGraphic.y - towerVisualHeight - 70; // Position at top of tower, 70 pixels higher teslaSphereGraphic.alpha = 0.5; // Set to 50% opacity cityInstance.addChild(teslaSphereGraphic); // Add to cityInstance to render on top of tower teslaSphereRotationDirection = 1; // Always clockwise // Create second Tesla sphere for visual effect teslaSphereGraphic2 = LK.getAsset('teslaspher', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.7, scaleY: 0.7 }); teslaSphereGraphic2.x = teslaTowerGraphic.x; // Same local X as tower in cityInstance teslaSphereGraphic2.y = teslaTowerGraphic.y - towerVisualHeight - 70; // Position at top of tower, 70 pixels higher teslaSphereGraphic2.alpha = 0.5; // Set to 50% opacity cityInstance.addChild(teslaSphereGraphic2); // Add to cityInstance to render on top of tower teslaSphereRotationDirection2 = -1; // Always counterclockwise (opposite) // Removed continuous energy effect animation around tower. // Force immediate visual update for this specific icon iconBg.alpha = 0.5; iconBg.cooldownText.visible = true; iconBg.cooldownText.setText('180s'); } else if (i === 4) { // Shop item 5: Fire Tower - creates powerful fire tower that shoots fire rings var fireTowerPrice = 220; // Check if player has enough money if (LK.getScore() < fireTowerPrice) { LK.getSound('stop').play(); // Store original position if not already stored if (iconBg.originalX === undefined) { iconBg.originalX = iconBg.x; } // Stop any existing shake animation tween.stop(iconBg, { x: true }); // Shake animation - quick left-right movement var shakeDistance = 20; var shakeSpeed = 50; tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX }, { duration: shakeSpeed, easing: tween.easeIn }); } }); } }); } }); } }); return; } // Check if Fire tower is already active if (fireTowerActive) { // Flash Fire icon to indicate it's already active LK.effects.flashObject(iconBg, 0xFFFF00, 300); return; } // Start cooldown (60 seconds) var currentTime = LK.ticks * (1000 / 60); fireTowerCooldownActive = true; fireTowerCooldownEndTime = currentTime + 180000; // 3 minutes fireTowerCooldownPausedTime = null; // Purchase Fire tower LK.setScore(LK.getScore() - fireTowerPrice); updateScoreDisplay(); LK.getSound('Pokupka').play(); // Activate Fire tower system fireTowerActive = true; fireTowerEndTime = LK.ticks * (1000 / 60) + 15000; // 15 seconds duration fireTowerPausedTime = null; lastFireStrikeTime = 0; // Create Fire tower visual in center of city using fire_tower asset fireTowerGraphic = LK.getAsset('fire_tower', { anchorX: 0.5, anchorY: 1.0 }); fireTowerGraphic.x = -200; // Relative to city center, positioned opposite to Tesla tower fireTowerGraphic.y = -120; // Above city, positioned above health bar fireTowerGraphic.scaleX = 0.15; fireTowerGraphic.scaleY = 0.15; fireTowerGraphic.alpha = 1.0; // Keep original tower appearance - no tint applied // Add Fire tower graphic at the very back, so it is rendered behind the city cityInstance.addChildAt(fireTowerGraphic, 0); // Create Fire sphere at top of tower using Firespher asset fireFlameGraphic = LK.getAsset('Firespher', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.7, scaleY: 0.7 }); // Position sphere at top of the tower after tower appears var fireTowerVisualHeight = fireTowerGraphic.height * fireTowerGraphic.scaleY; fireFlameGraphic.x = fireTowerGraphic.x; // Same local X as tower in cityInstance fireFlameGraphic.y = fireTowerGraphic.y - fireTowerVisualHeight - 70; // Position at top of tower, 70 pixels higher fireFlameGraphic.alpha = 0.9; cityInstance.addChild(fireFlameGraphic); // Add to cityInstance to render on top of tower // Create second Fire sphere for visual effect fireFlameGraphic2 = LK.getAsset('Firespher', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.7, scaleY: 0.7 }); fireFlameGraphic2.x = fireTowerGraphic.x; // Same local X as tower in cityInstance fireFlameGraphic2.y = fireTowerGraphic.y - fireTowerVisualHeight - 70; // Position at top of tower, 70 pixels higher fireFlameGraphic2.alpha = 0.9; cityInstance.addChild(fireFlameGraphic2); // Add to cityInstance to render on top of tower // Force immediate visual update for this specific icon iconBg.alpha = 0.5; iconBg.cooldownText.visible = true; iconBg.cooldownText.setText('180s'); } else if (i === 5) { // Shop item 6: Ice Tower - creates ice tower that shoots freezing spheres var iceTowerPrice = 300; // Check if player has enough money if (LK.getScore() < iceTowerPrice) { LK.getSound('stop').play(); // Store original position if not already stored if (iconBg.originalX === undefined) { iconBg.originalX = iconBg.x; } // Stop any existing shake animation tween.stop(iconBg, { x: true }); // Shake animation - quick left-right movement var shakeDistance = 20; var shakeSpeed = 50; tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(iconBg, { x: iconBg.originalX }, { duration: shakeSpeed, easing: tween.easeIn }); } }); } }); } }); } }); return; } // Check if Ice tower is already active if (iceTowerActive) { // Flash Ice icon to indicate it's already active LK.effects.flashObject(iconBg, 0xFFFF00, 300); return; } // Start cooldown (60 seconds) var currentTime = LK.ticks * (1000 / 60); iceTowerCooldownActive = true; iceTowerCooldownEndTime = currentTime + 180000; // 3 minutes iceTowerCooldownPausedTime = null; // Purchase Ice tower LK.setScore(LK.getScore() - iceTowerPrice); updateScoreDisplay(); LK.getSound('Pokupka').play(); // Activate Ice tower system iceTowerActive = true; iceTowerEndTime = LK.ticks * (1000 / 60) + 15000; // 15 seconds duration iceTowerPausedTime = null; lastIceStrikeTime = 0; // Create Ice tower instance iceTowerInstance = new IceTower(); iceTowerInstance.x = cityInstance.x + 600; // Position behind city center for proper layering iceTowerInstance.y = cityInstance.y - 120; // Above city, positioned above health bar iceTowerInstance.alpha = 1.0; // Ensure full visibility // Add ice tower at the very beginning to ensure it renders behind city var leftCityIndex = -1; for (var childIdx = 0; childIdx < game.children.length; childIdx++) { if (game.children[childIdx] === leftCityImage) { leftCityIndex = childIdx; break; } } if (leftCityIndex !== -1) { game.addChildAt(iceTowerInstance, leftCityIndex); // Add ice tower right before left city image } else { game.addChild(iceTowerInstance); // Fallback to normal add if left city not found } // Force immediate visual update for this specific icon iconBg.alpha = 0.5; iconBg.cooldownText.visible = true; iconBg.cooldownText.setText('180s'); } else { // Placeholder - will implement shop item functionality later console.log('Shop item ' + (i + 1) + ' clicked'); } }); })(i); } // Add popup to game game.addChild(shopPopup); shopButtonActive = true; shopButton.visible = false; shopButton.interactive = false; shopButton.buttonMode = false; // Hide upgrades and controls buttons when shop popup is open upgradesButton.visible = false; upgradesButton.interactive = false; upgradesButton.buttonMode = false; controlsButton.visible = false; controlsButton.interactive = false; controlsButton.buttonMode = false; } function closeShopPopup() { if (shopPopup && shopPopup.parent) { shopPopup.parent.removeChild(shopPopup); LK.getSound('menu').play(); } shopPopup = null; shopButtonActive = false; shopButton.visible = true; shopButton.interactive = true; shopButton.buttonMode = true; // Show upgrades and controls buttons when shop popup is closed upgradesButton.visible = true; upgradesButton.interactive = true; upgradesButton.buttonMode = true; controlsButton.visible = true; controlsButton.interactive = true; controlsButton.buttonMode = true; } // Upgrades popup container function showUpgradesPopup() { if (upgradesPopup) { return; } // Already open // No speed manipulation needed - objects will check upgradesPopup flag in their update methods LK.getSound('menu').play(); upgradesPopup = new Container(); // Dimmed background for upgrades popup with its own color var popupBg = LK.getAsset('fon', { anchorX: 0, anchorY: 0, scaleX: 1, scaleY: 1 }); popupBg.tint = 0x1a1a2e; // Dark blue-purple color for upgrades background popupBg.alpha = 0.85; // Make it semi-transparent popupBg.width = WORLD_WIDTH; popupBg.height = WORLD_HEIGHT; popupBg.interactive = false; popupBg.buttonMode = false; upgradesPopup.addChild(popupBg); // Popup window var popupWidth = 1800; var popupHeight = 1650; var popupWin = LK.getAsset('fonmenu', { anchorX: 0.5, anchorY: 0.5, scaleX: popupWidth / 2048, scaleY: popupHeight / 2732 }); popupWin.x = WORLD_WIDTH / 2; popupWin.y = WORLD_HEIGHT / 2; popupWin.alpha = 0.98; upgradesPopup.addChild(popupWin); // Close button (X) in top-right corner of upgrades popup using krest image var upgradesCloseButton = LK.getAsset('krest', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); upgradesCloseButton.x = WORLD_WIDTH / 2; upgradesCloseButton.y = WORLD_HEIGHT / 2 + popupHeight / 2 - 100 + 280; upgradesCloseButton.interactive = true; upgradesCloseButton.buttonMode = true; upgradesCloseButton.down = function (x, y, obj) { closeUpgradesPopup(); }; upgradesPopup.addChild(upgradesCloseButton); // Title removed as per request // Upgrade icons grid (3x3) - icons closer together var iconSize = 360; var availableWidth = popupWidth - 60; // Much smaller margin for even closer icons var availableHeight = popupHeight - 220; // Smaller margin for closer icons var cols = 3; var rows = 2; var iconSpacingX = availableWidth / (cols - 1); var iconSpacingY = availableHeight / (rows - 1); var gridStartX = WORLD_WIDTH / 2 - availableWidth / 2; var gridStartY = popupWin.y - popupHeight / 2 + 80 + 480; // Move top row down by 480 pixels (was 450, now +30) for (var i = 0; i < UPGRADE_DEFS.length; i++) { (function (i) { var upg = UPGRADE_DEFS[i]; var row = Math.floor(i / 3); var col = i % 3; // Move leftmost and rightmost icons closer to center var iconX = gridStartX + col * iconSpacingX; if (col === 0) { iconX += 300; // Move leftmost icon right by 300px (even closer to center) } if (col === 2) { iconX -= 300; // Move rightmost icon left by 300px (even closer to center) } var iconY = gridStartY + row * iconSpacingY; // Move bottom row up by 650 pixels (500 + 150) if (row === 1) { iconY = iconY - 650; } // Icon background/placeholder (will be replaced with actual upgrade icons later) var iconBg; if (upg.key === 'fireDamage') { iconBg = LK.getAsset('uron', { anchorX: 0.5, anchorY: 0.5, scaleX: iconSize / 360, // Assuming uron asset is 360x360 or similar aspect for this scaling scaleY: iconSize / 360 }); } else if (upg.key === 'fireRate') { iconBg = LK.getAsset('Skorost', { anchorX: 0.5, anchorY: 0.5, scaleX: iconSize / 360, // Skorost asset is 360x360 scaleY: iconSize / 360 }); } else if (upg.key === 'critChance') { iconBg = LK.getAsset('krit', { anchorX: 0.5, anchorY: 0.5, scaleX: iconSize / 360, // krit asset is 360x360 scaleY: iconSize / 360 }); } else if (upg.key === 'lightningChance') { iconBg = LK.getAsset('lightning', { anchorX: 0.5, anchorY: 0.5, scaleX: iconSize / 337, // lightning asset is 337x337 scaleY: iconSize / 337 }); } else if (upg.key === 'fire') { iconBg = LK.getAsset('fire', { anchorX: 0.5, anchorY: 0.5, scaleX: iconSize / 317, scaleY: iconSize / 317 }); } else if (upg.key === 'freeze') { iconBg = LK.getAsset('Iceicon', { anchorX: 0.5, anchorY: 0.5, scaleX: iconSize / 316, scaleY: iconSize / 316 }); } else { iconBg = LK.getAsset('test', { anchorX: 0.5, anchorY: 0.5, scaleX: iconSize / 360, scaleY: iconSize / 360 }); } iconBg.x = iconX; iconBg.y = iconY; upgradesPopup.addChild(iconBg); // Price text above icon var priceText = new Text2("$" + upg.cost(upgrades[upg.key]), { size: 108, // Increased by 1.5x (72 * 1.5) fill: 0x00FF00, align: 'center', font: 'Impact' }); priceText.anchor.set(0.5, 1); priceText.x = iconX; priceText.y = iconY - iconSize / 2 - 30; // Increased offset for larger icon upgradesPopup.addChild(priceText); // Level indicator below icon var levelText = new Text2(upgrades[upg.key] + "/" + upg.max, { size: 96, // Increased by 1.5x (64 * 1.5) fill: 0xFFFFFF, align: 'center', font: 'Impact' }); levelText.anchor.set(0.5, 0); levelText.x = iconX; levelText.y = iconY + iconSize / 2 + 30; // Increased offset for larger icon upgradesPopup.addChild(levelText); // Create info icon in top-right corner of upgrade icon using iconI asset var infoIcon = LK.getAsset('iconI', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); infoIcon.alpha = 0.8; infoIcon.x = iconX + iconSize / 2 - 30; infoIcon.y = iconY - iconSize / 2 + 30; upgradesPopup.addChild(infoIcon); // Info icon interaction infoIcon.interactive = true; infoIcon.buttonMode = true; infoIcon.down = function (x, y, obj) { showUpgradeTooltip(upg.key, iconX, iconY); }; // Button interaction for main icon iconBg.interactive = true; iconBg.buttonMode = true; iconBg.upgradeKey = upg.key; iconBg.down = function (x, y, obj) { handleUpgradePurchase(upg, priceText, levelText); }; })(i); } // Add popup to game game.addChild(upgradesPopup); // Pause game logic (handled by LK automatically if popup is modal) upgradesButtonActive = true; upgradesButton.visible = false; upgradesButton.interactive = false; upgradesButton.buttonMode = false; // Hide controls and shop buttons when upgrades popup is open controlsButton.visible = false; controlsButton.interactive = false; controlsButton.buttonMode = false; shopButton.visible = false; shopButton.interactive = false; shopButton.buttonMode = false; } function closeUpgradesPopup() { // Hide any active tooltip hideUpgradeTooltip(); if (upgradesPopup && upgradesPopup.parent) { upgradesPopup.parent.removeChild(upgradesPopup); LK.getSound('menu').play(); } // No speed restoration needed - objects check upgradesPopup flag automatically upgradesPopup = null; upgradesButtonActive = false; upgradesButton.visible = true; upgradesButton.interactive = true; upgradesButton.buttonMode = true; // Show controls and shop buttons when upgrades popup is closed controlsButton.visible = true; controlsButton.interactive = true; controlsButton.buttonMode = true; shopButton.visible = true; shopButton.interactive = true; shopButton.buttonMode = true; } // Handle upgrade purchase function handleUpgradePurchase(upg, priceText, levelText) { // Block upgrade purchase when tooltip is open if (currentTooltip) { return; } var level = upgrades[upg.key]; var cost = upg.cost(level); if (level >= upg.max) { return; } if (LK.getScore() < cost) { // Not enough money - shake the icon LK.getSound('stop').play(); for (var i = 0; i < upgradesPopup.children.length; i++) { var child = upgradesPopup.children[i]; if (child.upgradeKey === upg.key) { // Store original position if not already stored if (child.originalX === undefined) { child.originalX = child.x; } // Stop any existing shake animation tween.stop(child, { x: true }); // Shake animation - quick left-right movement var shakeDistance = 20; var shakeSpeed = 50; tween(child, { x: child.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeOut, onFinish: function onFinish() { tween(child, { x: child.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(child, { x: child.originalX - shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(child, { x: child.originalX + shakeDistance }, { duration: shakeSpeed, easing: tween.easeInOut, onFinish: function onFinish() { tween(child, { x: child.originalX }, { duration: shakeSpeed, easing: tween.easeIn }); } }); } }); } }); } }); break; } } return; } // Apply upgrade LK.setScore(LK.getScore() - cost); upgrades[upg.key]++; // Play purchase sound LK.getSound('Pokupka').play(); // Apply effect applyUpgradeEffect(upg.key); updateScoreDisplay(); // Update price and level text priceText.setText("$" + upg.cost(upgrades[upg.key])); levelText.setText(upgrades[upg.key] + "/" + upg.max); // Optionally, flash or animate the texts LK.effects.flashObject(priceText, 0x00ff00, 200); LK.effects.flashObject(levelText, 0x00ff00, 200); // Find and pulse the upgrade icon that was purchased for (var i = 0; i < upgradesPopup.children.length; i++) { var child = upgradesPopup.children[i]; if (child.upgradeKey === upg.key) { // Store original scale if not already stored if (!child.originalScaleX) { if (upg.key === 'lightningChance') { child.originalScaleX = iconSize / 337; // Lightning asset scale child.originalScaleY = iconSize / 337; } else if (upg.key === 'fire') { child.originalScaleX = iconSize / 317; // Fire asset scale child.originalScaleY = iconSize / 317; } else if (upg.key === 'freeze') { child.originalScaleX = iconSize / 316; // Freeze asset scale child.originalScaleY = iconSize / 316; } else { child.originalScaleX = iconSize / 360; // Other assets scale child.originalScaleY = iconSize / 360; } } // Pulse animation: scale up and then back down using absolute values tween.stop(child, { scaleX: true, scaleY: true }); tween(child, { scaleX: child.originalScaleX * 1.15, scaleY: child.originalScaleY * 1.15 }, { duration: 120, easing: tween.easeOut, onFinish: function onFinish() { tween(child, { scaleX: child.originalScaleX, scaleY: child.originalScaleY }, { duration: 120, easing: tween.easeIn }); } }); break; } } } // Apply upgrade effect function applyUpgradeEffect(key) { if (key === 'fireDamage') { FIRE_DAMAGE = 35 + 20 * upgrades.fireDamage; } else if (key === 'fireRate') { // Progressive increments: Levels 1-6 use +0.5 shots/sec, Levels 7-10 use +1.0 shots/sec // Convert shots per second to milliseconds: 1000ms / shots_per_second if (upgrades.fireRate === 0) { FIRE_RATE_MS = 1000; // 1 shot per second } else { var shotsPerSecond; if (upgrades.fireRate <= 6) { // Levels 1-6: +0.5 shots per second per level shotsPerSecond = 1 + upgrades.fireRate * 0.5; // 1.5, 2.0, 2.5, 3.0, 3.5, 4.0 } else { // Levels 7-10: +1.0 shots per second per level (starting from level 6's value of 4.0) shotsPerSecond = 4.0 + (upgrades.fireRate - 6) * 1.0; // 5.0, 6.0, 7.0, 8.0 } FIRE_RATE_MS = Math.floor(1000 / shotsPerSecond); } // Ensure minimum limit of 100ms for performance if (FIRE_RATE_MS < 100) { FIRE_RATE_MS = 100; } } else if (key === 'freeze') { // Freeze chance upgrade applied - no immediate visual effect needed } else if (key === 'lightningChance') { // Lightning chance upgrade applied - no immediate visual effect needed } else if (key === 'fire') { // Fire upgrade applied - no immediate visual effect needed } else if (key === 'critChance') { // Critical chance upgrade applied - no immediate visual effect needed } } // Button event - using LK event system for image asset upgradesButton.down = function (x, y, obj) { if (!upgradesButtonActive && !controlsPopup && !shopPopup) { showUpgradesPopup(); } }; // Initialize Crosshair crosshairInstance = new Crosshair(); crosshairInstance.graphic = crosshairInstance.attachAsset('crosshair_asset', { anchorX: 0.5, anchorY: 0.5 }); crosshairInstance.speed = CROSSHAIR_SPEED; crosshairInstance.x = WORLD_WIDTH / 2; crosshairInstance.y = WORLD_HEIGHT / 2; game.addChild(crosshairInstance); // Detect device type detectDevice(); // Initialize Virtual Joystick joystickInstance = new VirtualJoystick(); joystickInstance.initialize('joystick_base_asset', 'joystick_knob_asset', JOYSTICK_VISUAL_RADIUS, JOYSTICK_INTERACTION_RADIUS); game.addChild(joystickInstance); joystickInstance.visible = false; // Start hidden, will appear on first touch // Hide joystick completely on desktop if (!isMobileDevice) { joystickInstance.visible = false; isMouseControlling = true; } // --- Event Handlers --- game.down = function (x, y, eventObj) { if (upgradesPopup || controlsPopup || shopPopup || currentTooltip) { // Block all input and pause game when popup is open return; } if (useJoystickControl) { // Joystick controls // Always show joystick at finger, and only allow one joystick at a time joystickInstance.x = x; joystickInstance.y = y; joystickInstance.visible = true; // Always activate joystick at this position, treat as local origin if (joystickInstance.processDown(x, y, eventObj, true)) { joystickTouchId = eventObj.identifier; isFiring = true; // Only allow firing while finger is held } } else { // Mouse controls mouseX = x; mouseY = y; isFiring = true; // Update mouse coordinates display if (mouseCoordinatesText) { mouseCoordinatesText.setText('Mouse: ' + Math.round(mouseX) + ', ' + Math.round(mouseY)); } } }; game.move = function (x, y, eventObj) { if (upgradesPopup || controlsPopup || shopPopup || currentTooltip) { // Block all input and pause game when popup is open return; } if (useJoystickControl) { // Joystick: handle joystick movement if (joystickInstance.active && eventObj.identifier === joystickTouchId) { joystickInstance.processMove(x, y, eventObj); } } else { // Mouse: update mouse position for crosshair tracking mouseX = x; mouseY = y; // Update mouse coordinates display if (mouseCoordinatesText) { mouseCoordinatesText.setText('Mouse: ' + Math.round(mouseX) + ', ' + Math.round(mouseY)); } } // No crosshair update here; handled in game.update }; game.up = function (x, y, eventObj) { if (upgradesPopup || controlsPopup || shopPopup || currentTooltip) { // Block all input and pause game when popup is open return; } if (useJoystickControl) { // Joystick: handle joystick release if (eventObj.identifier === joystickTouchId) { joystickInstance.processUp(eventObj); joystickInstance.visible = false; joystickTouchId = null; isFiring = false; } } else { // Mouse: stop firing on mouse release isFiring = false; } }; // --- Game Loop --- // Set initial spawn delay to 5 seconds (5000ms) nextSpawnTime = 5000; // 5 second delay before first spawn // Enhanced cleanup function to prevent memory leaks function cleanupGame() { // Comprehensive tween cleanup - stop ALL active tweens if (tween) { // Try to stop all tweens globally if method exists if (tween.stop) { try { tween.stop(); // Stop all active tweens } catch (e) { console.log("Error stopping global tweens:", e); } } // Stop tweens on specific objects that might have them if (crosshairInstance && crosshairInstance.graphic) { tween.stop(crosshairInstance); tween.stop(crosshairInstance.graphic); } if (puhaBarrelInstance) { tween.stop(puhaBarrelInstance); } if (leftCityImage) { tween.stop(leftCityImage); } if (rightCityImage) { tween.stop(rightCityImage); } if (cityInstance && cityInstance.graphic) { tween.stop(cityInstance); tween.stop(cityInstance.graphic); } } // Clear any remaining timeouts/intervals for (var i = 0; i < 1000; i++) { LK.clearTimeout(i); LK.clearInterval(i); } // Enhanced falling objects cleanup if (fallingObjects) { for (var j = 0; j < fallingObjects.length; j++) { var obj = fallingObjects[j]; if (obj) { // Stop all tweens on object before destroying if (tween) { tween.stop(obj); if (obj.graphic) { tween.stop(obj.graphic); } if (obj.healthDisplay) { tween.stop(obj.healthDisplay); } } // Destroy object properly if (!obj.isDestroyed) { obj.destroySelf(); } } } fallingObjects = []; } // Enhanced damage numbers cleanup if (game.damageNumbers) { for (var d = 0; d < game.damageNumbers.length; d++) { var damageNum = game.damageNumbers[d]; if (damageNum) { tween.stop(damageNum); // Stop any active tweens if (damageNum.parent) { damageNum.parent.removeChild(damageNum); } } } game.damageNumbers = []; } // Enhanced particles cleanup if (game.particles) { for (var p = 0; p < game.particles.length; p++) { var particle = game.particles[p]; if (particle) { tween.stop(particle); // Stop any active tweens if (particle.parent) { particle.parent.removeChild(particle); } } } game.particles = []; } // Enhanced muzzle particles cleanup if (game.muzzleParticles) { for (var m = 0; m < game.muzzleParticles.length; m++) { var muzzleParticle = game.muzzleParticles[m]; if (muzzleParticle) { tween.stop(muzzleParticle); // Stop any active tweens if (muzzleParticle.parent) { muzzleParticle.parent.removeChild(muzzleParticle); } } } game.muzzleParticles = []; } // Enhanced snowflake particles cleanup if (game.snowflakeParticles) { for (var sf = 0; sf < game.snowflakeParticles.length; sf++) { var snowflakeParticle = game.snowflakeParticles[sf]; if (snowflakeParticle) { tween.stop(snowflakeParticle); // Stop any active tweens if (snowflakeParticle.parent) { snowflakeParticle.parent.removeChild(snowflakeParticle); } } } game.snowflakeParticles = []; } // Enhanced fire projectiles cleanup if (game.fireProjectiles) { for (var fp = 0; fp < game.fireProjectiles.length; fp++) { var fireProjectile = game.fireProjectiles[fp]; if (fireProjectile) { tween.stop(fireProjectile); // Stop any active tweens if (fireProjectile.parent) { fireProjectile.parent.removeChild(fireProjectile); } } } game.fireProjectiles = []; } // Enhanced ice projectiles cleanup if (game.iceProjectiles) { for (var ip = 0; ip < game.iceProjectiles.length; ip++) { var iceProjectile = game.iceProjectiles[ip]; if (iceProjectile) { tween.stop(iceProjectile); // Stop any active tweens if (iceProjectile.parent) { iceProjectile.parent.removeChild(iceProjectile); } } } game.iceProjectiles = []; } // Clean up stars and their tweens if (stars) { for (var s = 0; s < stars.length; s++) { var star = stars[s]; if (star) { tween.stop(star); if (star.parent) { star.parent.removeChild(star); } } } stars = []; } // Force garbage collection hint (if available) if (typeof gc === 'function') { try { gc(); } catch (e) { // Ignore if gc is not available } } } // Call cleanup when game starts cleanupGame(); // --- Game Loop --- // Create twinkling stars (after cleanup to prevent immediate removal) createStars(); // Start background music rotation startMusicRotation(); // Music toggle button variables var musicButton = null; var musicEnabled = true; // Track music state // Create controls button at absolute position 100, 360 - positioned below music button controlsButton = LK.getAsset('Control', { anchorX: 0.5, anchorY: 0.5, scaleX: controlsButtonWidth / 100, scaleY: controlsButtonWidth / 100 }); controlsButton.x = 100; controlsButton.y = 360; controlsButton.interactive = true; controlsButton.buttonMode = true; controlsButton.visible = true; controlsButtonActive = false; game.addChild(controlsButton); // Create music toggle button positioned at left edge musicButton = LK.getAsset('musonn', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.8, scaleY: 1.8 }); musicButton.x = 100; // Position at left edge musicButton.y = 560; // Position above settings button musicButton.interactive = true; musicButton.buttonMode = true; musicButton.visible = true; game.addChild(musicButton); // Add hit area for controls button - make it larger to cover the full button var controlsHitArea = LK.getAsset('fon', { anchorX: 0.5, anchorY: 0.5, scaleX: 300 / 2048, scaleY: 200 / 2732 }); controlsHitArea.x = 100; controlsHitArea.y = 360; controlsHitArea.alpha = 0.01; controlsHitArea.interactive = true; controlsHitArea.buttonMode = true; game.addChild(controlsHitArea); controlsHitArea.on('down', function (x, y, obj) { if (!controlsButtonActive && !upgradesPopup && !shopPopup) { showControlsPopup(); } }); // Add hit area for music button - same size as controls button var musicHitArea = LK.getAsset('fon', { anchorX: 0.5, anchorY: 0.5, scaleX: 300 / 2048, scaleY: 200 / 2732 }); musicHitArea.x = 100; // Same position as music button musicHitArea.y = 560; musicHitArea.alpha = 0.01; musicHitArea.interactive = true; musicHitArea.buttonMode = true; game.addChild(musicHitArea); // Music button interaction function toggleMusic() { musicEnabled = !musicEnabled; if (musicEnabled) { // Switch to music on icon and resume music musicButton.removeChild(musicButton.children[0]); var onIcon = musicButton.attachAsset('musonn', { anchorX: 0.5, anchorY: 0.5 }); if (!musicPlaying) { startMusicRotation(); } } else { // Switch to music off icon and stop music musicButton.removeChild(musicButton.children[0]); var offIcon = musicButton.attachAsset('musoff', { anchorX: 0.5, anchorY: 0.5 }); LK.stopMusic(); musicPlaying = false; } } musicHitArea.on('down', function (x, y, obj) { if (!controlsButtonActive && !upgradesPopup && !shopPopup) { toggleMusic(); } }); musicButton.down = function (x, y, obj) { if (!controlsButtonActive && !upgradesPopup && !shopPopup) { toggleMusic(); } }; // Add button to add 10000 points (for testing) var addPointsButton = LK.getAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.6, scaleY: 0.6 }); addPointsButton.x = 100; // Position on left side addPointsButton.y = 760; // Position below music button addPointsButton.interactive = false; addPointsButton.buttonMode = false; addPointsButton.visible = false; // Make button invisible game.addChild(addPointsButton); var addPointsText = new Text2('+10000', { size: 60, fill: 0xFFFFFF, align: 'center', font: 'Impact' }); addPointsText.anchor.set(0.5, 0.5); addPointsText.x = addPointsButton.x; addPointsText.y = addPointsButton.y; addPointsText.visible = false; // Make text invisible game.addChild(addPointsText); // addPointsButton.on('down', ...) removed to disable add points button // Add a larger hit area for the add points button var addPointsHitArea = LK.getAsset('fon', { anchorX: 0.5, anchorY: 0.5, scaleX: 300 / 2048, scaleY: 200 / 2732 }); addPointsHitArea.x = addPointsButton.x; addPointsHitArea.y = addPointsButton.y; addPointsHitArea.alpha = 0.01; addPointsHitArea.interactive = false; addPointsHitArea.buttonMode = false; game.addChild(addPointsHitArea); // addPointsHitArea.on('down', ...) removed to disable add points hit area game.update = function () { var currentTime = LK.ticks * (1000 / 60); var isPaused = upgradesPopup || controlsPopup || shopPopup || currentTooltip; // --- Master Pause System --- if (isPaused) { // This block executes when the game is paused by a menu. // It sets the 'pause start time' for all active timed effects. if (!spawnPausedTime) { spawnPausedTime = currentTime; } // Master pause trigger if (cityInstance.shielded && !cityInstance.shieldPausedTime) { cityInstance.shieldPausedTime = currentTime; } if (healthRestoreCooldownActive && !healthRestoreCooldownPausedTime) { healthRestoreCooldownPausedTime = currentTime; } if (shieldCooldownActive && !shieldCooldownPausedTime) { shieldCooldownPausedTime = currentTime; } if (rocketBarrageCooldownActive && !rocketBarrageCooldownPausedTime) { rocketBarrageCooldownPausedTime = currentTime; } if (teslaTowerCooldownActive && !teslaTowerCooldownPausedTime) { teslaTowerCooldownPausedTime = currentTime; } if (fireTowerCooldownActive && !fireTowerCooldownPausedTime) { fireTowerCooldownPausedTime = currentTime; } if (iceTowerCooldownActive && !iceTowerCooldownPausedTime) { iceTowerCooldownPausedTime = currentTime; } if (gradualHealActive && !gradualHealPausedTime) { gradualHealPausedTime = currentTime; } if (rocketBarrageActive && !rocketBarragePausedTime) { rocketBarragePausedTime = currentTime; } if (teslaTowerActive && !teslaTowerPausedTime) { teslaTowerPausedTime = currentTime; } if (fireTowerActive && !fireTowerPausedTime) { fireTowerPausedTime = currentTime; } if (iceTowerActive && !iceTowerPausedTime) { iceTowerPausedTime = currentTime; } if (!alienSpawnPausedTime) { alienSpawnPausedTime = currentTime; } // Pause alien spawn timer return; // Halt all further game logic updates. } // --- Master Unpause System --- if (spawnPausedTime) { var pauseDuration = currentTime - spawnPausedTime; nextSpawnTime += pauseDuration; if (cityInstance.shieldPausedTime) { cityInstance.shieldEndTime += pauseDuration; cityInstance.shieldPausedTime = null; } if (healthRestoreCooldownPausedTime) { healthRestoreCooldownEndTime += pauseDuration; healthRestoreCooldownPausedTime = null; } if (shieldCooldownPausedTime) { shieldCooldownEndTime += pauseDuration; shieldCooldownPausedTime = null; } if (rocketBarrageCooldownPausedTime) { rocketBarrageCooldownEndTime += pauseDuration; rocketBarrageCooldownPausedTime = null; } if (teslaTowerCooldownPausedTime) { teslaTowerCooldownEndTime += pauseDuration; teslaTowerCooldownPausedTime = null; } if (fireTowerCooldownPausedTime) { fireTowerCooldownEndTime += pauseDuration; fireTowerCooldownPausedTime = null; } if (iceTowerCooldownPausedTime) { iceTowerCooldownEndTime += pauseDuration; iceTowerCooldownPausedTime = null; } if (gradualHealPausedTime) { gradualHealEndTime += pauseDuration; gradualHealLastTick += pauseDuration; gradualHealPausedTime = null; } if (rocketBarragePausedTime) { rocketBarrageEndTime += pauseDuration; lastRocketLaunchTime += pauseDuration; rocketBarragePausedTime = null; } if (teslaTowerPausedTime) { teslaTowerEndTime += pauseDuration; lastTeslaStrikeTime += pauseDuration; teslaTowerPausedTime = null; } if (fireTowerPausedTime) { fireTowerEndTime += pauseDuration; lastFireStrikeTime += pauseDuration; fireTowerPausedTime = null; } if (iceTowerPausedTime) { iceTowerEndTime += pauseDuration; lastIceStrikeTime += pauseDuration; iceTowerPausedTime = null; } spawnPausedTime = null; // Reset master pause tracker } // Update shield system if (cityInstance.shielded) { // Check if shield duration expired if (currentTime >= cityInstance.shieldEndTime) { // Remove shield cityInstance.shielded = false; cityInstance.shieldStrength = 0; if (cityInstance.shieldGraphic && cityInstance.shieldGraphic.parent) { cityInstance.removeChild(cityInstance.shieldGraphic); cityInstance.shieldGraphic = null; } } else { // Shield visual continuously flickers between 70% and 90% transparency if (cityInstance.shieldGraphic && cityInstance.shieldGraphic.parent) { if (!cityInstance.shieldGraphic.isAnimating) { cityInstance.shieldGraphic.isAnimating = true; // Start continuous flickering loop var _flickerShield = function flickerShield() { if (!cityInstance.shieldGraphic || !cityInstance.shieldGraphic.parent) { if (cityInstance.shieldGraphic) { cityInstance.shieldGraphic.isAnimating = false; } return; // Shield was destroyed } var targetAlpha = cityInstance.shieldGraphic.alpha <= 0.65 ? 0.8 : 0.6; // Switch between 0.6 (60% opacity) and 0.8 (80% opacity) tween(cityInstance.shieldGraphic, { alpha: targetAlpha }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { // Continue the loop immediately _flickerShield(); } }); }; _flickerShield(); // Start the animation loop } } } } // Update gradual health restore if (gradualHealActive) { if (currentTime >= gradualHealEndTime) { gradualHealActive = false; } else { if (currentTime >= gradualHealLastTick + 1000) { // Every 1 second gradualHealLastTick += 1000; // Important to increment, not set to currentTime, to handle lag if (cityInstance.health < CITY_MAX_HEALTH) { var healAmount = 70; var prevHealth = cityInstance.health; cityInstance.health += healAmount; if (cityInstance.health > CITY_MAX_HEALTH) { cityInstance.health = CITY_MAX_HEALTH; } // Create healing particles along city width during restoration with different intervals var particleCount = 4 + Math.floor(Math.random() * 5); // 4-8 particles per healing tick (increased by 1.5x) for (var hp = 0; hp < particleCount; hp++) { (function (particleIndex) { var particleDelay = particleIndex * (100 + Math.random() * 66); // Different intervals between 0-400ms (more frequent) LK.setTimeout(function () { var baseScale = 0.5 + Math.random() * 0.8; // Base scale between 0.5-1.3 var healingParticle = LK.getAsset('kresthil', { anchorX: 0.5, anchorY: 0.5, scaleX: baseScale, // Use same scale for both X and Y to maintain square proportions scaleY: baseScale }); // Position randomly along city width at health bar level, raised by 300 healingParticle.x = Math.random() * WORLD_WIDTH; healingParticle.y = cityInstance.y - 40 - 300; // At health bar level, raised by 300 healingParticle.alpha = 0.8 + Math.random() * 0.2; // Slight transparency variation game.addChild(healingParticle); // Animate particle floating upward and shrinking over 3 seconds (slower movement) var floatDistance = 100 + Math.random() * 50; // Float 100-150 pixels up (reduced by half) var lateralDrift = (Math.random() - 0.5) * 60; // Small side drift tween(healingParticle, { x: healingParticle.x + lateralDrift, y: healingParticle.y - floatDistance, scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 5000, // 5 seconds (extended by 2 seconds) easing: tween.easeOut, onFinish: function (particleRef) { return function () { if (particleRef.parent) { particleRef.parent.removeChild(particleRef); } }; }(healingParticle) }); }, particleDelay); })(hp); } // Animate green bar for healing (smooth width increase) if (cityInstance.healthBarFill && cityInstance.healthBarBg) { var prevPercent = Math.max(0, prevHealth) / CITY_MAX_HEALTH; var newPercent = Math.max(0, cityInstance.health) / CITY_MAX_HEALTH; var prevWidth = cityInstance.healthBarBg.width * prevPercent; var newWidth = cityInstance.healthBarBg.width * newPercent; // Stop any ongoing width tween tween.stop(cityInstance.healthBarFill, { width: true }); // Animate width from prevWidth to newWidth cityInstance.healthBarFill.width = prevWidth; tween(cityInstance.healthBarFill, { width: newWidth }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { // Ensure the width is exactly correct at the end if (cityInstance.healthBarFill) { cityInstance.healthBarFill.width = newWidth; } } }); } // Animate green bar for healing (flash effect) tween(cityInstance.healthBarFill, { tint: 0x00FF88 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { if (cityInstance.healthBarFill) { tween(cityInstance.healthBarFill, { tint: 0xFFFFFF }, { duration: 100, easing: tween.easeIn }); } } }); if (cityInstance.health >= CITY_MAX_HEALTH) { gradualHealActive = false; // Stop early if max health is reached } } else { gradualHealActive = false; // Stop if max health is already reached } } } } // Update all shop cooldowns if (healthRestoreCooldownActive) { if (currentTime >= healthRestoreCooldownEndTime) { // Cooldown finished healthRestoreCooldownActive = false; healthRestoreCooldownEndTime = 0; } } if (shieldCooldownActive) { if (currentTime >= shieldCooldownEndTime) { shieldCooldownActive = false; shieldCooldownEndTime = 0; } } if (rocketBarrageCooldownActive) { if (currentTime >= rocketBarrageCooldownEndTime) { rocketBarrageCooldownActive = false; rocketBarrageCooldownEndTime = 0; } } if (teslaTowerCooldownActive) { if (currentTime >= teslaTowerCooldownEndTime) { teslaTowerCooldownActive = false; teslaTowerCooldownEndTime = 0; } } if (fireTowerCooldownActive) { if (currentTime >= fireTowerCooldownEndTime) { fireTowerCooldownActive = false; fireTowerCooldownEndTime = 0; } } if (iceTowerCooldownActive) { if (currentTime >= iceTowerCooldownEndTime) { iceTowerCooldownActive = false; iceTowerCooldownEndTime = 0; } } // Update shop cooldowns visual state when shop popup is open if (shopPopup) { // Update all shop item cooldown displays for (var shopChild = 0; shopChild < shopPopup.children.length; shopChild++) { var child = shopPopup.children[shopChild]; if (child.cooldownText && child.shopItemIndex !== undefined) { var itemIndex = child.shopItemIndex; var itemCooldownActive = false; var cooldownStartTime = 0; if (itemIndex === 0) { // Health restore item itemCooldownActive = healthRestoreCooldownActive; cooldownStartTime = healthRestoreCooldownEndTime - 60000; } else if (itemIndex === 1) { // Shield item itemCooldownActive = shieldCooldownActive; cooldownStartTime = shieldCooldownEndTime - 60000; } else if (itemIndex === 2) { // Rocket barrage item itemCooldownActive = rocketBarrageCooldownActive; cooldownStartTime = rocketBarrageCooldownEndTime - 60000; } else if (itemIndex === 3) { // Tesla tower item itemCooldownActive = teslaTowerCooldownActive; cooldownStartTime = teslaTowerCooldownEndTime - 60000; } else if (itemIndex === 4) { // Fire tower item itemCooldownActive = fireTowerCooldownActive; cooldownStartTime = fireTowerCooldownEndTime - 60000; } else if (itemIndex === 5) { // Ice tower item itemCooldownActive = iceTowerCooldownActive; cooldownStartTime = iceTowerCooldownEndTime - 60000; } if (itemCooldownActive) { // Item is on cooldown - dim and show timer child.alpha = 0.5; // 50% dimming (50% opacity) child.cooldownText.visible = true; // Always show countdown from 60s decrementing by 1 per second var secondsElapsed = Math.floor((currentTime - cooldownStartTime) / 1000); var secondsLeft = Math.max(1, 60 - secondsElapsed); child.cooldownText.setText(secondsLeft + 's'); } else { // Item is not on cooldown - restore normal appearance child.alpha = 1.0; // Full opacity child.cooldownText.visible = false; child.cooldownText.setText(''); } } } } // --- Immediately update shop icon dimming/timer after purchase, even if popup is open --- if (shopPopup) { for (var shopChild = 0; shopChild < shopPopup.children.length; shopChild++) { var child = shopPopup.children[shopChild]; if (child.cooldownText && child.shopItemIndex !== undefined) { var itemIndex = child.shopItemIndex; var itemCooldownActive = false; var itemCooldownTimeLeft = 0; if (itemIndex === 0) { itemCooldownActive = healthRestoreCooldownActive; if (itemCooldownActive) { itemCooldownTimeLeft = Math.max(0, healthRestoreCooldownEndTime - currentTime); } } else if (itemIndex === 1) { itemCooldownActive = shieldCooldownActive; if (itemCooldownActive) { itemCooldownTimeLeft = Math.max(0, shieldCooldownEndTime - currentTime); } } else if (itemIndex === 2) { itemCooldownActive = rocketBarrageCooldownActive; if (itemCooldownActive) { itemCooldownTimeLeft = Math.max(0, rocketBarrageCooldownEndTime - currentTime); } } else if (itemIndex === 3) { itemCooldownActive = teslaTowerCooldownActive; if (itemCooldownActive) { itemCooldownTimeLeft = Math.max(0, teslaTowerCooldownEndTime - currentTime); } } else if (itemIndex === 4) { itemCooldownActive = fireTowerCooldownActive; if (itemCooldownActive) { itemCooldownTimeLeft = Math.max(0, fireTowerCooldownEndTime - currentTime); } } else if (itemIndex === 5) { itemCooldownActive = iceTowerCooldownActive; if (itemCooldownActive) { itemCooldownTimeLeft = Math.max(0, iceTowerCooldownEndTime - currentTime); } } // If cooldown just started, update immediately if (itemCooldownActive && itemCooldownTimeLeft > 0) { child.alpha = 0.5; child.cooldownText.visible = true; var secondsLeft = Math.ceil(itemCooldownTimeLeft / 1000); if (itemCooldownTimeLeft > 0 && secondsLeft === 0) { secondsLeft = 1; } child.cooldownText.setText(secondsLeft + 's'); } else { child.alpha = 1.0; child.cooldownText.visible = false; child.cooldownText.setText(''); } } } } // Update rocket barrage system if (rocketBarrageActive) { // Check if rocket barrage duration expired if (currentTime >= rocketBarrageEndTime) { // Deactivate rocket barrage rocketBarrageActive = false; rocketBarrageEndTime = 0; } else { // Launch rocket every second (1000ms) if (currentTime >= lastRocketLaunchTime + 1000) { lastRocketLaunchTime = currentTime; // Find target for new rocket var rocketTarget = findRocketTarget(); if (rocketTarget) { // Create and launch rocket var rocket = new Rocket(rocketTarget); activeRockets.push(rocket); // Play rocket launch sound LK.getSound('startraket').play(); // Find the index where puhaBarrelInstance is positioned and add rocket right after it var puhaBarrelIndex = -1; for (var childIdx = 0; childIdx < game.children.length; childIdx++) { if (game.children[childIdx] === puhaBarrelInstance) { puhaBarrelIndex = childIdx; break; } } if (puhaBarrelIndex !== -1) { game.addChildAt(rocket, puhaBarrelIndex + 1); // Add rocket right after puha barrel } else { // Find the index where puhaBarrelInstance is positioned and add rocket right after it var puhaBarrelIndex = -1; for (var childIdx = 0; childIdx < game.children.length; childIdx++) { if (game.children[childIdx] === puhaBarrelInstance) { puhaBarrelIndex = childIdx; break; } } if (puhaBarrelIndex !== -1) { game.addChildAt(rocket, puhaBarrelIndex + 1); // Add rocket right after puha barrel } else { game.addChild(rocket); // Fallback to normal add if puha not found } } } } } } // Update Tesla tower system if (teslaTowerActive) { // Update Tesla sphere self-rotation around their own axis if (teslaSphereGraphic && teslaSphereGraphic.parent) { teslaSphereGraphic.rotation += teslaSphereRotationDirection * TESLA_SPHERE_ROTATION_SPEED; } if (teslaSphereGraphic2 && teslaSphereGraphic2.parent) { teslaSphereGraphic2.rotation += teslaSphereRotationDirection2 * TESLA_SPHERE_ROTATION_SPEED; } // Create lightning particles around Tesla sphere 1 every few frames if (teslaSphereGraphic && teslaSphereGraphic.parent && LK.ticks % 10 === 0) { // Calculate world position of Tesla sphere 1 var sphere1WorldX = cityInstance.x + teslaSphereGraphic.x; var sphere1WorldY = cityInstance.y + teslaSphereGraphic.y; // Spawn 4 lightning particles around the sphere var particleCount = 4; // 4 particles for (var lp = 0; lp < particleCount; lp++) { var lightningParticle = LK.getAsset('molnia2', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.15 + Math.random() * 0.1, // Random scale between 0.15 and 0.25 scaleY: 0.15 + Math.random() * 0.1 }); // Position particle at Tesla sphere center lightningParticle.x = sphere1WorldX; lightningParticle.y = sphere1WorldY; // Random rotation for each particle - they always point upward (0 rotation) lightningParticle.rotation = Math.random() * Math.PI * 2; lightningParticle.alpha = 0.3; // 30% opacity lightningParticle.tint = 0x00FFFF; // Cyan Tesla color game.addChild(lightningParticle); // Animate lightning particle - appear and disappear tween(lightningParticle, { alpha: 0, scaleX: 0, scaleY: 0 }, { duration: 1500 + Math.random() * 1000, // 1.5-2.5 seconds easing: tween.easeOut, onFinish: function (particleRef) { return function () { if (particleRef.parent) { particleRef.parent.removeChild(particleRef); } }; }(lightningParticle) }); } } // Create lightning particles around Tesla sphere 2 every few frames if (teslaSphereGraphic2 && teslaSphereGraphic2.parent && LK.ticks % 12 === 0) { // Calculate world position of Tesla sphere 2 var sphere2WorldX = cityInstance.x + teslaSphereGraphic2.x; var sphere2WorldY = cityInstance.y + teslaSphereGraphic2.y; // Spawn 4 lightning particles around the sphere var particleCount = 4; // 4 particles for (var lp = 0; lp < particleCount; lp++) { var lightningParticle2 = LK.getAsset('molnia2', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.15 + Math.random() * 0.1, // Random scale between 0.15 and 0.25 scaleY: 0.15 + Math.random() * 0.1 }); // Position particle at Tesla sphere center lightningParticle2.x = sphere2WorldX; lightningParticle2.y = sphere2WorldY; // Random rotation for each particle - they always point upward (0 rotation) lightningParticle2.rotation = Math.random() * Math.PI * 2; lightningParticle2.alpha = 0.3; // 30% opacity lightningParticle2.tint = 0x00FFFF; // Cyan Tesla color game.addChild(lightningParticle2); // Animate lightning particle - appear and disappear tween(lightningParticle2, { alpha: 0, scaleX: 0, scaleY: 0 }, { duration: 1500 + Math.random() * 1000, // 1.5-2.5 seconds easing: tween.easeOut, onFinish: function (particleRef) { return function () { if (particleRef.parent) { particleRef.parent.removeChild(particleRef); } }; }(lightningParticle2) }); } } // Check if Tesla tower duration expired if (currentTime >= teslaTowerEndTime) { // Update Tesla sphere rotation only if active (keep it at top) if (teslaTowerActive && teslaSphereGraphic && teslaSphereGraphic.parent && teslaTowerGraphic && teslaTowerGraphic.parent) { // Keep sphere at top of tower var towerVisualHeight = teslaTowerGraphic.height * teslaTowerGraphic.scaleY; teslaSphereGraphic.x = teslaTowerGraphic.x; teslaSphereGraphic.y = teslaTowerGraphic.y - towerVisualHeight - 70; // Stay at top, 70 pixels higher } // Update Tesla sphere 2 rotation only if active (keep it at top) if (teslaTowerActive && teslaSphereGraphic2 && teslaSphereGraphic2.parent && teslaTowerGraphic && teslaTowerGraphic.parent) { // Keep sphere 2 at top of tower var towerVisualHeight = teslaTowerGraphic.height * teslaTowerGraphic.scaleY; teslaSphereGraphic2.x = teslaTowerGraphic.x; teslaSphereGraphic2.y = teslaTowerGraphic.y - towerVisualHeight - 70; // Stay at top, 70 pixels higher } // Deactivate Tesla tower teslaTowerActive = false; teslaTowerEndTime = 0; // Tesla sphere shrink and disappear animation if (teslaSphereGraphic && teslaSphereGraphic.parent) { tween(teslaSphereGraphic, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (teslaSphereGraphic && teslaSphereGraphic.parent) { cityInstance.removeChild(teslaSphereGraphic); teslaSphereGraphic = null; } } }); } // Tesla sphere 2 shrink and disappear animation if (teslaSphereGraphic2 && teslaSphereGraphic2.parent) { tween(teslaSphereGraphic2, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (teslaSphereGraphic2 && teslaSphereGraphic2.parent) { cityInstance.removeChild(teslaSphereGraphic2); teslaSphereGraphic2 = null; } } }); } // Tower disappear animation if (teslaTowerGraphic && teslaTowerGraphic.parent) { tween(teslaTowerGraphic, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (teslaTowerGraphic && teslaTowerGraphic.parent) { cityInstance.removeChild(teslaTowerGraphic); teslaTowerGraphic = null; } } }); } } else { // Tesla strike every 1000ms (once per second) if (currentTime >= lastTeslaStrikeTime + 1000) { lastTeslaStrikeTime = currentTime; // Find closest target to Tesla tower var towerWorldX = cityInstance.x; var towerWorldY = cityInstance.y - 120; var teslaTarget = null; var closestDistance = Infinity; for (var t = 0; t < fallingObjects.length; t++) { var target = fallingObjects[t]; if (!target.isDestroyed) { var dx = target.x - towerWorldX; var dy = target.y - towerWorldY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; teslaTarget = target; } } } if (teslaTarget) { // Tesla tower pulsation before shooting - compress down and sideways if (teslaTowerGraphic && teslaTowerGraphic.parent) { var originalTowerScaleX = teslaTowerGraphic.scaleX; var originalTowerScaleY = teslaTowerGraphic.scaleY; tween.stop(teslaTowerGraphic, { scaleX: true, scaleY: true }); tween(teslaTowerGraphic, { scaleY: originalTowerScaleY * 0.85 // Compress downward only }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { if (teslaTowerGraphic) { tween(teslaTowerGraphic, { scaleX: originalTowerScaleX, scaleY: originalTowerScaleY }, { duration: 100, easing: tween.easeIn }); } } }); } if (teslaSphereGraphic && teslaSphereGraphic.parent) { // Pulsate sphere var originalSphereScale = teslaSphereGraphic.scaleX; // Assuming scaleX and scaleY are same tween.stop(teslaSphereGraphic, { scaleX: true, scaleY: true }); tween(teslaSphereGraphic, { scaleX: originalSphereScale * 1.5, scaleY: originalSphereScale * 1.5 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { if (teslaSphereGraphic) { // Check if sphere still exists tween(teslaSphereGraphic, { scaleX: originalSphereScale, scaleY: originalSphereScale }, { duration: 150, easing: tween.easeIn }); } } }); // Keep rotation direction consistent (always clockwise) } if (teslaSphereGraphic2 && teslaSphereGraphic2.parent) { // Pulsate sphere 2 var originalSphereScale2 = teslaSphereGraphic2.scaleX; // Assuming scaleX and scaleY are same tween.stop(teslaSphereGraphic2, { scaleX: true, scaleY: true }); tween(teslaSphereGraphic2, { scaleX: originalSphereScale2 * 1.5, scaleY: originalSphereScale2 * 1.5 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { if (teslaSphereGraphic2) { // Check if sphere 2 still exists tween(teslaSphereGraphic2, { scaleX: originalSphereScale2, scaleY: originalSphereScale2 }, { duration: 150, easing: tween.easeIn }); } } }); // Keep rotation direction consistent (always counterclockwise) } // Tesla lightning damage (stronger than normal lightning) var teslaDamage = 250; // Fixed 250 damage for Tesla strikes teslaTarget.health -= teslaDamage; LK.effects.flashObject(teslaTarget.graphic, 0xFF0000, 100); // Update health bar visual for Tesla damage if (teslaTarget.healthBarBg) { teslaTarget.updateHealthBarVisual(); } // Play lightning sound LK.getSound('molnia').play(); // Create Tesla lightning damage number var teslaDamageNumber = new Text2(teslaDamage.toString(), { size: 80, fill: 0x00FFFF, // Cyan color for Tesla damage (matching lightning gun) align: 'center', font: 'Impact' }); teslaDamageNumber.alpha = 0; // Make damage number invisible teslaDamageNumber.anchor.set(0.5, 0.5); teslaDamageNumber.x = teslaTarget.x + (Math.random() - 0.5) * 40; teslaDamageNumber.y = teslaTarget.y - 20; game.addChild(teslaDamageNumber); // Animate Tesla damage number tween(teslaDamageNumber, { y: teslaDamageNumber.y - 120, scaleX: 1.3, scaleY: 1.3 }, { duration: 1200, easing: tween.easeOut }); // Remove Tesla damage number after timeout LK.setTimeout(function () { if (teslaDamageNumber.parent) { game.removeChild(teslaDamageNumber); } }, 1200); // Check if target destroyed by Tesla if (teslaTarget.health <= 0) { teslaTarget.destroySelf(); } // Tesla chain lightning - hits up to 5 targets (increased from 2) var teslaChainTargets = []; var chainDistances = []; for (var ct = 0; ct < fallingObjects.length; ct++) { var candidate = fallingObjects[ct]; if (!candidate.isDestroyed && candidate !== teslaTarget) { var distX = candidate.x - teslaTarget.x; var distY = candidate.y - teslaTarget.y; var distance = Math.sqrt(distX * distX + distY * distY); // Remove distance limitation - Tesla chains to any target regardless of distance chainDistances.push({ target: candidate, distance: distance }); } } // Sort by distance and take closest 4 (so 5 total including original target) chainDistances.sort(function (a, b) { return a.distance - b.distance; }); for (var d = 0; d < Math.min(4, chainDistances.length); d++) { teslaChainTargets.push(chainDistances[d].target); } // Calculate Tesla sphere world position var sphereWorldX = cityInstance.x + teslaSphereGraphic.x; var sphereWorldY = cityInstance.y + teslaSphereGraphic.y; // Create Tesla lightning bolt from sphere to target var sphereDx = teslaTarget.x - sphereWorldX; var sphereDy = teslaTarget.y - sphereWorldY; var sphereDistance = Math.sqrt(sphereDx * sphereDx + sphereDy * sphereDy); var boltSegmentCount = Math.max(2, Math.floor(sphereDistance / 80)); var boltAngle = Math.atan2(sphereDy, sphereDx); // Create smaller Tesla lightning segments for (var s = 0; s < boltSegmentCount; s++) { var segmentProgress = (s + 0.1) / boltSegmentCount; var segmentX = sphereWorldX + sphereDx * segmentProgress; var segmentY = sphereWorldY + sphereDy * segmentProgress; // Add Tesla-specific randomization segmentX += (Math.random() - 0.5) * 15; segmentY += (Math.random() - 0.5) * 15; // Tesla glow effect (much larger, bright cyan) var teslaGlow = LK.getAsset('molnia', { anchorX: 0.5, anchorY: 0.5 }); teslaGlow.x = segmentX; teslaGlow.y = segmentY; teslaGlow.rotation = boltAngle + (Math.random() - 0.5) * 0.3; teslaGlow.scaleX = (0.15 + Math.random() * 0.1) * 6.144; // 20% smaller than 7.68 (7.68 * 0.8) teslaGlow.scaleY = (0.15 + Math.random() * 0.1) * 6.144; teslaGlow.alpha = 0.4; teslaGlow.tint = 0x00FFFF; // Bright cyan for glow game.addChild(teslaGlow); // Main Tesla lightning (much larger, bright cyan) var teslaLightning = LK.getAsset('molnia', { anchorX: 0.5, anchorY: 0.5 }); teslaLightning.x = segmentX; teslaLightning.y = segmentY; teslaLightning.rotation = boltAngle + (Math.random() - 0.5) * 0.3; teslaLightning.scaleX = (0.15 + Math.random() * 0.1) * 3.264; // 20% smaller than 4.08 (4.08 * 0.8) teslaLightning.scaleY = (0.15 + Math.random() * 0.1) * 3.264; teslaLightning.alpha = 0.9 + Math.random() * 0.1; teslaLightning.tint = 0x00FFFF; // Bright cyan Tesla color game.addChild(teslaLightning); // Animate Tesla lightning segments tween(teslaGlow, { alpha: 0 }, { duration: 400, easing: tween.easeIn, onFinish: function (segment) { return function () { if (segment.parent) { game.removeChild(segment); } }; }(teslaGlow) }); tween(teslaLightning, { scaleY: 0, alpha: 0 }, { duration: 400, easing: tween.easeIn, onFinish: function (segment) { return function () { if (segment.parent) { game.removeChild(segment); } }; }(teslaLightning) }); } // Tesla chain lightning effect (sequential, faster than normal) var _teslaChainLightning = function teslaChainLightning(fromTarget, targetIndex) { if (targetIndex >= teslaChainTargets.length) { return; } var toTarget = teslaChainTargets[targetIndex]; // Tesla chain damage (same as main Tesla strike) var teslaChainDamage = 250; // Fixed 250 damage for Tesla chain toTarget.health -= teslaChainDamage; if (toTarget && toTarget.graphic && toTarget.graphic.parent) { LK.effects.flashObject(toTarget.graphic, 0xFF0000, 100); } // Update health bar for chain lightning damage if (toTarget.healthBarBg) { toTarget.updateHealthBarVisual(); } // Check for fire/freeze effects if upgrades exist var fireChance = upgrades.fire * 5; if (Math.random() * 100 < fireChance) { toTarget.applyBurn(); } var freezeChance = upgrades.freeze * 5; if (Math.random() * 100 < freezeChance) { toTarget.applyFreeze(); } // Tesla chain damage number var teslaChainDamageNumber = new Text2(teslaChainDamage.toString(), { size: 70, fill: 0x00FFFF, // Cyan for Tesla chain (matching lightning gun) align: 'center', font: 'Impact' }); teslaChainDamageNumber.alpha = 0; // Make damage number invisible teslaChainDamageNumber.anchor.set(0.5, 0.5); teslaChainDamageNumber.x = toTarget.x + 60 + Math.random() * 20; teslaChainDamageNumber.y = toTarget.y - 20; game.addChild(teslaChainDamageNumber); // Animate Tesla chain damage number tween(teslaChainDamageNumber, { y: teslaChainDamageNumber.y - 120, scaleX: 1.2, scaleY: 1.2 }, { duration: 1200, easing: tween.easeOut }); // Remove Tesla chain damage number LK.setTimeout(function () { if (teslaChainDamageNumber.parent) { game.removeChild(teslaChainDamageNumber); } }, 1200); // Create Tesla chain lightning visual from fromTarget to toTarget var chainDx = toTarget.x - fromTarget.x; var chainDy = toTarget.y - fromTarget.y; var chainDistance = Math.sqrt(chainDx * chainDx + chainDy * chainDy); var chainBoltSegmentCount = Math.max(2, Math.floor(chainDistance / 80)); var chainBoltAngle = Math.atan2(chainDy, chainDx); // Create Tesla chain lightning segments for (var cs = 0; cs < chainBoltSegmentCount; cs++) { var chainSegmentProgress = (cs + 0.1) / chainBoltSegmentCount; var chainSegmentX = fromTarget.x + chainDx * chainSegmentProgress; var chainSegmentY = fromTarget.y + chainDy * chainSegmentProgress; // Add Tesla-specific randomization chainSegmentX += (Math.random() - 0.5) * 15; chainSegmentY += (Math.random() - 0.5) * 15; // Tesla chain glow effect (large, bright cyan) var teslaChainGlow = LK.getAsset('molnia', { anchorX: 0.5, anchorY: 0.5 }); teslaChainGlow.x = chainSegmentX; teslaChainGlow.y = chainSegmentY; teslaChainGlow.rotation = chainBoltAngle + (Math.random() - 0.5) * 0.3; teslaChainGlow.scaleX = (0.15 + Math.random() * 0.1) * 6.144; // 20% smaller than 7.68 (7.68 * 0.8) teslaChainGlow.scaleY = (0.15 + Math.random() * 0.1) * 6.144; teslaChainGlow.alpha = 0.4; teslaChainGlow.tint = 0x00FFFF; // Bright cyan for glow game.addChild(teslaChainGlow); // Main Tesla chain lightning (large, bright cyan) var teslaChainLightning = LK.getAsset('molnia', { anchorX: 0.5, anchorY: 0.5 }); teslaChainLightning.x = chainSegmentX; teslaChainLightning.y = chainSegmentY; teslaChainLightning.rotation = chainBoltAngle + (Math.random() - 0.5) * 0.3; teslaChainLightning.scaleX = (0.15 + Math.random() * 0.1) * 3.264; // 20% smaller than 4.08 (4.08 * 0.8) teslaChainLightning.scaleY = (0.15 + Math.random() * 0.1) * 3.264; teslaChainLightning.alpha = 0.9 + Math.random() * 0.1; teslaChainLightning.tint = 0x00FFFF; // Bright cyan Tesla color game.addChild(teslaChainLightning); // Animate Tesla chain lightning segments tween(teslaChainGlow, { alpha: 0 }, { duration: 400, easing: tween.easeIn, onFinish: function (segment) { return function () { if (segment.parent) { game.removeChild(segment); } }; }(teslaChainGlow) }); tween(teslaChainLightning, { scaleY: 0, alpha: 0 }, { duration: 400, easing: tween.easeIn, onFinish: function (segment) { return function () { if (segment.parent) { game.removeChild(segment); } }; }(teslaChainLightning) }); } // Check if target destroyed if (toTarget.health <= 0) { toTarget.destroySelf(); } // Continue chain to next target LK.setTimeout(function () { _teslaChainLightning(toTarget, targetIndex + 1); }, 100); // Faster Tesla chain }; // Start Tesla chain if (teslaChainTargets.length > 0) { _teslaChainLightning(teslaTarget, 0); } } } } } // Update Fire tower system if (fireTowerActive) { // Update fire sphere rotation around its own axis (slower) if (fireFlameGraphic && fireFlameGraphic.parent) { fireFlameGraphic.rotation += 0.02; // Slower continuous rotation around its own axis } // Update second fire sphere rotation around its own axis (opposite direction) if (fireFlameGraphic2 && fireFlameGraphic2.parent) { fireFlameGraphic2.rotation -= 0.02; // Slower continuous rotation in opposite direction } // Create fire particles around fire sphere 1 every few frames if (fireFlameGraphic && fireFlameGraphic.parent && LK.ticks % 8 === 0) { // Calculate world position of fire sphere 1 var fireSphere1WorldX = cityInstance.x + fireFlameGraphic.x; var fireSphere1WorldY = cityInstance.y + fireFlameGraphic.y; // Spawn 3 fire particles around the sphere var fireParticleCount = 3; // 3 particles for (var fp = 0; fp < fireParticleCount; fp++) { var fireParticle = LK.getAsset('ogonek', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.08 + Math.random() * 0.05, // Random scale between 0.08 and 0.13 scaleY: 0.08 + Math.random() * 0.05 }); // Position particle at fire sphere center fireParticle.x = fireSphere1WorldX + (Math.random() - 0.5) * 60; fireParticle.y = fireSphere1WorldY + (Math.random() - 0.5) * 60; // Random rotation for each particle fireParticle.rotation = Math.random() * Math.PI * 2; fireParticle.alpha = 0.7 + Math.random() * 0.3; // 70-100% opacity fireParticle.tint = 0xFF4500; // Orange fire color game.addChild(fireParticle); // Animate fire particle floating upward while shrinking and fading tween(fireParticle, { x: fireParticle.x + (Math.random() - 0.5) * 20, y: fireParticle.y - 80 - Math.random() * 40, scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 1500 + Math.random() * 1000, // 1.5-2.5 seconds easing: tween.easeOut, onFinish: function (particleRef) { return function () { if (particleRef.parent) { particleRef.parent.removeChild(particleRef); } }; }(fireParticle) }); } } // Create fire particles around fire sphere 2 every few frames if (fireFlameGraphic2 && fireFlameGraphic2.parent && LK.ticks % 10 === 0) { // Calculate world position of fire sphere 2 var fireSphere2WorldX = cityInstance.x + fireFlameGraphic2.x; var fireSphere2WorldY = cityInstance.y + fireFlameGraphic2.y; // Spawn 3 fire particles around the sphere var fireParticleCount = 3; // 3 particles for (var fp = 0; fp < fireParticleCount; fp++) { var fireParticle2 = LK.getAsset('ogonek', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.08 + Math.random() * 0.05, // Random scale between 0.08 and 0.13 scaleY: 0.08 + Math.random() * 0.05 }); // Position particle at fire sphere center fireParticle2.x = fireSphere2WorldX + (Math.random() - 0.5) * 60; fireParticle2.y = fireSphere2WorldY + (Math.random() - 0.5) * 60; // Random rotation for each particle fireParticle2.rotation = Math.random() * Math.PI * 2; fireParticle2.alpha = 0.7 + Math.random() * 0.3; // 70-100% opacity fireParticle2.tint = 0xFF4500; // Orange fire color game.addChild(fireParticle2); // Animate fire particle floating upward while shrinking and fading tween(fireParticle2, { x: fireParticle2.x + (Math.random() - 0.5) * 20, y: fireParticle2.y - 80 - Math.random() * 40, scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 1500 + Math.random() * 1000, // 1.5-2.5 seconds easing: tween.easeOut, onFinish: function (particleRef) { return function () { if (particleRef.parent) { particleRef.parent.removeChild(particleRef); } }; }(fireParticle2) }); } } // Fire tower releases 17 fire projectiles in 180 degree arc upward every 1000ms if (currentTime >= lastFireStrikeTime + 1000) { lastFireStrikeTime = currentTime; // Play fire tower shooting sound LK.getSound('Ognemet').play(); // Use fire sphere position as the spawn point for projectiles var towerWorldX = cityInstance.x + fireFlameGraphic.x; var towerWorldY = cityInstance.y + fireFlameGraphic.y; // Fire tower compression before shooting - compress down like Tesla tower if (fireTowerGraphic && fireTowerGraphic.parent) { var originalFireTowerScaleX = fireTowerGraphic.scaleX; var originalFireTowerScaleY = fireTowerGraphic.scaleY; tween.stop(fireTowerGraphic, { scaleX: true, scaleY: true }); tween(fireTowerGraphic, { scaleY: originalFireTowerScaleY * 0.85 // Compress downward only }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { if (fireTowerGraphic) { tween(fireTowerGraphic, { scaleX: originalFireTowerScaleX, scaleY: originalFireTowerScaleY }, { duration: 100, easing: tween.easeIn }); } } }); } // Pulsate fire flame when launching projectiles if (fireFlameGraphic && fireFlameGraphic.parent) { var originalFlameScale = fireFlameGraphic.scaleX; if (!fireFlameGraphic.isPulsing) { fireFlameGraphic.isPulsing = true; tween.stop(fireFlameGraphic, { scaleX: true, scaleY: true }); tween(fireFlameGraphic, { scaleX: originalFlameScale * 1.3, scaleY: originalFlameScale * 1.3 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { if (fireFlameGraphic) { tween(fireFlameGraphic, { scaleX: originalFlameScale, scaleY: originalFlameScale }, { duration: 100, easing: tween.easeIn, onFinish: function onFinish() { if (fireFlameGraphic) { fireFlameGraphic.isPulsing = false; } } }); } } }); } } // Pulsate fire flame 2 when launching projectiles if (fireFlameGraphic2 && fireFlameGraphic2.parent) { var originalFlameScale2 = fireFlameGraphic2.scaleX; if (!fireFlameGraphic2.isPulsing) { fireFlameGraphic2.isPulsing = true; tween.stop(fireFlameGraphic2, { scaleX: true, scaleY: true }); tween(fireFlameGraphic2, { scaleX: originalFlameScale2 * 1.3, scaleY: originalFlameScale2 * 1.3 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { if (fireFlameGraphic2) { tween(fireFlameGraphic2, { scaleX: originalFlameScale2, scaleY: originalFlameScale2 }, { duration: 100, easing: tween.easeIn, onFinish: function onFinish() { if (fireFlameGraphic2) { fireFlameGraphic2.isPulsing = false; } } }); } } }); } } // Initialize fire tower shot counter if not exists if (!fireTowerGraphic.shotCounter) { fireTowerGraphic.shotCounter = 0; } // Create 17 fire projectiles shooting in 180 degree arc upward with alternating pattern var projectileCount = 17; var isAlternateShot = fireTowerGraphic.shotCounter % 2 === 1; // Every other shot is alternate var angleOffset = isAlternateShot ? Math.PI / (projectileCount - 1) / 2 : 0; // Half-step offset for alternate shots for (var p = 0; p < projectileCount; p++) { var angle = Math.PI + p / (projectileCount - 1) * Math.PI + angleOffset; // Distribute evenly in 180 degree arc upward (π to 2π) with offset var fireProjectile = LK.getAsset('fire_circle', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.25, scaleY: 0.25 }); fireProjectile.x = towerWorldX; fireProjectile.y = towerWorldY; fireProjectile.alpha = 0.9; fireProjectile.rotation = angle; // Rotate projectile to face movement direction // Store movement properties fireProjectile.velocityX = Math.cos(angle) * 8; // Speed of 8 pixels per frame fireProjectile.velocityY = Math.sin(angle) * 8; fireProjectile.hitEnemies = []; // Track which enemies this projectile has hit // Add to game and tracking array game.addChild(fireProjectile); if (!game.fireProjectiles) { game.fireProjectiles = []; } game.fireProjectiles.push(fireProjectile); // Add rotation speed for spinning effect fireProjectile.rotationSpeed = 0.02 + Math.random() * 0.02; // Random rotation speed between 0.02 and 0.04 radians per frame (much slower) fireProjectile.rotationDirection = Math.random() < 0.5 ? 1 : -1; // Random direction (clockwise or counterclockwise) // Add movement and collision checking fireProjectile.update = function () { if (!this.parent) { return; } // Pause movement when popups are open if (upgradesPopup || controlsPopup || shopPopup) { return; } // Move projectile this.x += this.velocityX; this.y += this.velocityY; // Rotate projectile around its own axis this.rotation += this.rotationSpeed * this.rotationDirection; // Check collision with enemies for (var enemyIndex = 0; enemyIndex < fallingObjects.length; enemyIndex++) { var enemy = fallingObjects[enemyIndex]; if (enemy.isDestroyed) { continue; } if (this.hitEnemies.indexOf(enemy) !== -1) { continue; } // Already hit this enemy // Calculate distance from projectile to enemy var dx = enemy.x - this.x; var dy = enemy.y - this.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if projectile is within damage range of enemy if (distance <= 80) { this.hitEnemies.push(enemy); // Apply fire projectile damage var projectileDamage = 100; enemy.health -= projectileDamage; LK.effects.flashObject(enemy.graphic, 0xFF4500, 200); // Apply fire tower burn stacks - always call since all enemies have this method enemy.applyFireTowerBurn(); // Removed floating fire projectile damage number text // Check if target destroyed by fire projectile if (enemy.health <= 0) { enemy.destroySelf(); } } } // Remove projectile when it goes off screen if (this.x < -100 || this.x > WORLD_WIDTH + 100 || this.y < -100 || this.y > WORLD_HEIGHT + 100) { if (this.parent) { this.parent.removeChild(this); } // Remove from tracking array if (game.fireProjectiles) { var index = game.fireProjectiles.indexOf(this); if (index > -1) { game.fireProjectiles.splice(index, 1); } } } }; } // Increment shot counter for alternating pattern fireTowerGraphic.shotCounter++; } // Check if Fire tower duration expired if (currentTime >= fireTowerEndTime) { // Deactivate Fire tower fireTowerActive = false; fireTowerEndTime = 0; // Clean up aura visuals if (fireTowerGraphic && fireTowerGraphic.auraVisuals) { for (var av = 0; av < fireTowerGraphic.auraVisuals.length; av++) { var auraVisual = fireTowerGraphic.auraVisuals[av]; if (auraVisual && auraVisual.parent) { tween(auraVisual, { alpha: 0, scaleX: 0, scaleY: 0 }, { duration: 300, easing: tween.easeIn, onFinish: function (visualRef) { return function () { if (visualRef.parent) { visualRef.parent.removeChild(visualRef); } }; }(auraVisual) }); } } fireTowerGraphic.auraVisuals = []; } // Fire flame shrink and disappear animation if (fireFlameGraphic && fireFlameGraphic.parent) { tween(fireFlameGraphic, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (fireFlameGraphic && fireFlameGraphic.parent) { cityInstance.removeChild(fireFlameGraphic); fireFlameGraphic = null; } } }); } // Fire flame 2 shrink and disappear animation if (fireFlameGraphic2 && fireFlameGraphic2.parent) { tween(fireFlameGraphic2, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (fireFlameGraphic2 && fireFlameGraphic2.parent) { cityInstance.removeChild(fireFlameGraphic2); fireFlameGraphic2 = null; } } }); } // Tower disappear animation if (fireTowerGraphic && fireTowerGraphic.parent) { tween(fireTowerGraphic, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (fireTowerGraphic && fireTowerGraphic.parent) { cityInstance.removeChild(fireTowerGraphic); fireTowerGraphic = null; } } }); } } } // Update Ice tower system if (iceTowerActive) { // Update ice tower if (iceTowerInstance && iceTowerInstance.parent) { iceTowerInstance.update(); } // Ice tower shoots every 1500ms (every 1.5 seconds) if (currentTime >= lastIceStrikeTime + 1500) { lastIceStrikeTime = currentTime; if (iceTowerInstance && iceTowerInstance.parent) { iceTowerInstance.shootIceProjectiles(); } } // Check if Ice tower duration expired if (currentTime >= iceTowerEndTime) { // Deactivate Ice tower iceTowerActive = false; iceTowerEndTime = 0; if (iceTowerInstance && iceTowerInstance.parent) { iceTowerInstance.destroy(); iceTowerInstance = null; } } } // Update active rockets for (var r = activeRockets.length - 1; r >= 0; r--) { var rocket = activeRockets[r]; if (rocket.isDestroyed) { activeRockets.splice(r, 1); } else { rocket.update(); } } // Update fire projectiles if (game.fireProjectiles) { for (var fp = game.fireProjectiles.length - 1; fp >= 0; fp--) { var fireProjectile = game.fireProjectiles[fp]; if (fireProjectile && fireProjectile.update) { fireProjectile.update(); } } } // Update ice projectiles if (game.iceProjectiles) { for (var ip = game.iceProjectiles.length - 1; ip >= 0; ip--) { var iceProjectile = game.iceProjectiles[ip]; if (iceProjectile && iceProjectile.update) { iceProjectile.update(); } } } // 0. Update Crosshair Position if (crosshairInstance) { if (useJoystickControl && joystickInstance) { // Joystick: Update crosshair based on joystick input // Calculate how far the knob is from the center (0..1) var knobDistance = Math.sqrt(joystickInstance.deltaX * joystickInstance.deltaX + joystickInstance.deltaY * joystickInstance.deltaY); // Crosshair speed is proportional to knob distance, up to CROSSHAIR_SPEED var effectiveSpeed = CROSSHAIR_SPEED * knobDistance * 2 / 1.5; crosshairInstance.x += joystickInstance.deltaX * effectiveSpeed; crosshairInstance.y += joystickInstance.deltaY * effectiveSpeed; } else if (!useJoystickControl) { // Mouse: Move crosshair directly to mouse position with same speed as joystick var targetX = mouseX; var targetY = mouseY; var deltaX = targetX - crosshairInstance.x; var deltaY = targetY - crosshairInstance.y; var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); if (distance > 1) { // Move crosshair towards mouse at reduced speed to match joystick (13.3 speed) var moveSpeed = CROSSHAIR_SPEED * 1.33; var moveX = deltaX / distance * moveSpeed; var moveY = deltaY / distance * moveSpeed; // Don't overshoot the target if (Math.abs(moveX) > Math.abs(deltaX)) { moveX = deltaX; } if (Math.abs(moveY) > Math.abs(deltaY)) { moveY = deltaY; } crosshairInstance.x += moveX; crosshairInstance.y += moveY; } } // Clamp crosshair to screen bounds (considering anchor is 0.5, 0.5) var chHW = crosshairInstance.graphic.width / 2; var chHH = crosshairInstance.graphic.height / 2; crosshairInstance.x = Math.max(chHW, Math.min(WORLD_WIDTH - chHW, crosshairInstance.x)); crosshairInstance.y = Math.max(chHH, Math.min(WORLD_HEIGHT - chHH, crosshairInstance.y)); // Crosshair speed calculation and display removed as requested } // 1. Check if crosshair is over any target and change color accordingly var crosshairOverTarget = false; for (var k = 0; k < fallingObjects.length; k++) { var target = fallingObjects[k]; if (!target.isDestroyed && crosshairInstance.isOver(target)) { crosshairOverTarget = true; break; } } // Change crosshair color based on targeting if (crosshairOverTarget && crosshairInstance.graphic.tint !== 0xFF0000) { // Turn red when over target tween(crosshairInstance.graphic, { tint: 0xFF0000 }, { duration: 100, easing: tween.easeOut }); } else if (!crosshairOverTarget && crosshairInstance.graphic.tint !== 0xFFFFFF) { // Turn white when not over target tween(crosshairInstance.graphic, { tint: 0xFFFFFF }, { duration: 100, easing: tween.easeOut }); } // 2. Firing Logic // Only fire when crosshair is over target and firing is enabled if (crosshairOverTarget && currentTime >= lastFireTime + FIRE_RATE_MS) { lastFireTime = currentTime; var hitTargets = []; // Track which targets were hit this frame for (var k = 0; k < fallingObjects.length; k++) { var target = fallingObjects[k]; if (!target.isDestroyed && crosshairInstance.isOver(target)) { target.takeDamage(FIRE_DAMAGE); hitTargets.push(target); // Add to hit targets list // Check for fire chance var fireChance = upgrades.fire * 5; // 5% per level if (Math.random() * 100 < fireChance) { target.applyBurn(); } // Check for freeze chance var freezeChance = upgrades.freeze * 5; // 5% per level if (Math.random() * 100 < freezeChance) { target.applyFreeze(); } // Aggressive particle cleanup to prevent accumulation if (!game.particles) { game.particles = []; } if (game.particles.length >= 30) { // Reduced threshold from 50 to 30 // Clean up oldest 15 particles instead of 10 for (var oldP = 0; oldP < 15; oldP++) { var oldParticle = game.particles.shift(); if (oldParticle) { // Stop any active tweens before removal tween.stop(oldParticle); if (oldParticle.parent) { oldParticle.parent.removeChild(oldParticle); } } } } // Create particle explosion at hit location var particleCount = 3 + Math.floor(Math.random() * 3); // 3 to 5 particles for (var p = 0; p < particleCount; p++) { var randomScale = 0.5 + Math.random() * 1.5; // Random scale between 0.5 and 2.0 var particle = LK.getAsset('particle', { anchorX: 0.5, anchorY: 0.5, scaleX: randomScale, scaleY: randomScale }); particle.x = target.x; particle.y = target.y; game.addChild(particle); game.particles.push(particle); // Random direction and speed for each particle var angle = Math.random() * Math.PI * 2; var speed = 100 + Math.random() * 100; // Random speed between 100-200 var velocityX = Math.cos(angle) * speed; var velocityY = Math.sin(angle) * speed; // Animate particle movement and scale down with proper cleanup tween(particle, { x: particle.x + velocityX, y: particle.y + velocityY, scaleX: 0, scaleY: 0 }, { duration: 1000, easing: tween.easeOut, onFinish: function (particleRef) { return function onFinish() { // Stop any remaining tweens on this particle tween.stop(particleRef); if (particleRef.parent) { particleRef.parent.removeChild(particleRef); } // Remove from tracking array if (game.particles) { var pIndex = game.particles.indexOf(particleRef); if (pIndex > -1) { game.particles.splice(pIndex, 1); } } }; }(particle) }); } } } // Play shoot sound once if any targets were hit if (hitTargets.length > 0) { LK.getSound('Shoot').play(); // Lightning strike effect - based on lightningChance upgrade (5% per level) var lightningChance = upgrades.lightningChance * 5; // 5% per level if (Math.random() * 100 < lightningChance && hitTargets.length > 0) { var primaryTarget = hitTargets[0]; // Use first hit target as lightning destination // Calculate cannon muzzle position var puhaWidth = 900 * 0.3; // Original width * scale var muzzleDistance = puhaWidth * 2.2; // Distance from pivot to muzzle end var cannonX = puhaBarrelInstance.x + Math.cos(puhaBarrelInstance.rotation) * muzzleDistance; var cannonY = puhaBarrelInstance.y + Math.sin(puhaBarrelInstance.rotation) * muzzleDistance; // Play lightning sound LK.getSound('molnia').play(); // Lightning also damages the primary target - check for critical hit // Lightning damage: 55% base + 5% per level (55% at level 1, 100% at level 10) var lightningDamagePercent = 0.55 + (upgrades.lightningChance - 1) * 0.05; // 55% + 5% per level var baseLightningDamage = Math.floor(FIRE_DAMAGE * lightningDamagePercent); var critChance = upgrades.critChance * 5; // 5% per level var isLightningCritical = Math.random() * 100 < critChance; var lightningDamage = isLightningCritical ? baseLightningDamage * 2 : baseLightningDamage; if (isLightningCritical) { LK.getSound('krit').play(); // Play critical hit sound for lightning } primaryTarget.health -= lightningDamage; LK.effects.flashObject(primaryTarget.graphic, 0xFF0000, 100); // Red flash on hit // Update health bar visual for lightning damage if (primaryTarget.healthBarBg) { primaryTarget.updateHealthBarVisual(); } // Check for fire chance on lightning var fireChance = upgrades.fire * 5; // 5% per level if (Math.random() * 100 < fireChance) { primaryTarget.applyBurn(); } // Check for freeze chance on lightning var freezeChance = upgrades.freeze * 5; // 5% per level if (Math.random() * 100 < freezeChance) { primaryTarget.applyFreeze(); } var lightningDamageNumber = new Text2(lightningDamage.toString(), { size: isLightningCritical ? 80 : 60, fill: isLightningCritical ? 0xFF0000 : 0x00FFFF, // Red color for critical lightning damage, blue for normal align: 'center', font: 'Impact' }); lightningDamageNumber.alpha = 0; // Make damage number invisible lightningDamageNumber.anchor.set(0.5, 0.5); lightningDamageNumber.x = primaryTarget.x + 60 + Math.random() * 20; // Position to the right of regular damage lightningDamageNumber.y = primaryTarget.y - 20; game.addChild(lightningDamageNumber); // Animate lightning damage number floating up tween(lightningDamageNumber, { y: lightningDamageNumber.y - 120, scaleX: 1.2, scaleY: 1.2 }, { duration: 1200, easing: tween.easeOut }); // Remove lightning damage number after timeout LK.setTimeout(function () { if (lightningDamageNumber.parent) { game.removeChild(lightningDamageNumber); } }, 1200); // Create initial lightning bolt from cannon to primary target var initialBoltDx = primaryTarget.x - cannonX; var initialBoltDy = primaryTarget.y - cannonY; var initialBoltDistance = Math.sqrt(initialBoltDx * initialBoltDx + initialBoltDy * initialBoltDy); var initialSegmentCount = Math.max(2, Math.floor(initialBoltDistance / 180)); var initialBoltAngle = Math.atan2(initialBoltDy, initialBoltDx); // Create molnia segments for initial bolt from cannon to primary target for (var s = 0; s < initialSegmentCount; s++) { var segmentProgress = (s + 0.05) / initialSegmentCount; var segmentX = cannonX + initialBoltDx * segmentProgress; var segmentY = cannonY + initialBoltDy * segmentProgress; // Add slight random offset for more natural lightning look segmentX += (Math.random() - 0.5) * 20; segmentY += (Math.random() - 0.5) * 20; // Create glow effect for initial bolt var glowSegment = LK.getAsset('molnia', { anchorX: 0.5, anchorY: 0.5 }); glowSegment.x = segmentX; glowSegment.y = segmentY; glowSegment.rotation = initialBoltAngle + (Math.random() - 0.5) * 0.3; glowSegment.scaleX = (0.25 + Math.random() * 0.15) * 3.0; glowSegment.scaleY = (0.25 + Math.random() * 0.15) * 3.0; glowSegment.alpha = 0.4; glowSegment.tint = 0x0088BB; game.addChild(glowSegment); // Main lightning segment for initial bolt var lightningSegment = LK.getAsset('molnia', { anchorX: 0.5, anchorY: 0.5 }); lightningSegment.x = segmentX; lightningSegment.y = segmentY; lightningSegment.rotation = initialBoltAngle + (Math.random() - 0.5) * 0.3; lightningSegment.scaleX = (0.25 + Math.random() * 0.15) * 1.3; lightningSegment.scaleY = (0.25 + Math.random() * 0.15) * 1.3; lightningSegment.alpha = 0.7 + Math.random() * 0.3; lightningSegment.tint = 0x66DDFF; game.addChild(lightningSegment); // Animate initial bolt segments tween(glowSegment, { alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function (segment) { return function onFinish() { if (segment.parent) { game.removeChild(segment); } }; }(glowSegment) }); tween(lightningSegment, { scaleY: 0, alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function (segment) { return function onFinish() { if (segment.parent) { game.removeChild(segment); } }; }(lightningSegment) }); } // Check if primary target is destroyed by lightning if (primaryTarget.health <= 0) { primaryTarget.destroySelf(); } var lightningTargets = []; // Find 2 nearest targets to the primary target (excluding already hit targets) var distances = []; for (var lt = 0; lt < fallingObjects.length; lt++) { var candidate = fallingObjects[lt]; if (!candidate.isDestroyed && hitTargets.indexOf(candidate) === -1) { var distX = candidate.x - primaryTarget.x; var distY = candidate.y - primaryTarget.y; var distance = Math.sqrt(distX * distX + distY * distY); distances.push({ target: candidate, distance: distance }); } } // Sort by distance and take closest 2 distances.sort(function (a, b) { return a.distance - b.distance; }); for (var d = 0; d < Math.min(2, distances.length); d++) { lightningTargets.push(distances[d].target); } // Chain lightning effect: jump from target to target sequentially var _chainLightning = function chainLightning(fromTarget, targetIndex) { if (targetIndex >= lightningTargets.length) { return; } var toTarget = lightningTargets[targetIndex]; var chainLightningDamage = baseLightningDamage; // Same as base lightning damage toTarget.health -= chainLightningDamage; LK.effects.flashObject(toTarget.graphic, 0xFF0000, 100); // Red flash on hit // Update health bar visual for chain lightning damage if (toTarget.healthBarBg) { toTarget.updateHealthBarVisual(); } // Check for fire chance on chain lightning var fireChance = upgrades.fire * 5; // 5% per level if (Math.random() * 100 < fireChance) { toTarget.applyBurn(); } // Check for freeze chance on chain lightning var freezeChance = upgrades.freeze * 5; // 5% per level if (Math.random() * 100 < freezeChance) { toTarget.applyFreeze(); } var chainLightningDamageNumber = new Text2(chainLightningDamage.toString(), { size: 60, fill: 0x00FFFF, // Blue color for lightning damage align: 'center', font: 'Impact' }); chainLightningDamageNumber.alpha = 0; // Make damage number invisible chainLightningDamageNumber.anchor.set(0.5, 0.5); chainLightningDamageNumber.x = toTarget.x + 60 + Math.random() * 20; // Position to the right of regular damage chainLightningDamageNumber.y = toTarget.y - 20; game.addChild(chainLightningDamageNumber); // Animate chain lightning damage number floating up tween(chainLightningDamageNumber, { y: chainLightningDamageNumber.y - 120, scaleX: 1.2, scaleY: 1.2 }, { duration: 1200, easing: tween.easeOut }); // Remove chain lightning damage number after timeout LK.setTimeout(function () { if (chainLightningDamageNumber.parent) { game.removeChild(chainLightningDamageNumber); } }, 1200); // Check if chain target is destroyed by lightning if (toTarget.health <= 0) { toTarget.destroySelf(); } // Create lightning using multiple molnia segments var boltDx = toTarget.x - fromTarget.x; var boltDy = toTarget.y - fromTarget.y; var boltDistance = Math.sqrt(boltDx * boltDx + boltDy * boltDy); var segmentCount = Math.max(2, Math.floor(boltDistance / 180)); // Fewer segments with more distance between them var segmentLength = boltDistance / segmentCount; // Calculate angle of the bolt var boltAngle = Math.atan2(boltDy, boltDx); // Create molnia segments along the path with minimal overlap for (var s = 0; s < segmentCount; s++) { // Reduce overlap by using smaller offset var segmentProgress = (s + 0.05) / segmentCount; // Much smaller overlap var segmentX = fromTarget.x + boltDx * segmentProgress; var segmentY = fromTarget.y + boltDy * segmentProgress; // Add slight random offset for more natural lightning look segmentX += (Math.random() - 0.5) * 20; // Reduced random offset segmentY += (Math.random() - 0.5) * 20; // Create glow effect (larger, darker copy behind lightning) var glowSegment = LK.getAsset('molnia', { anchorX: 0.5, anchorY: 0.5 }); glowSegment.x = segmentX; glowSegment.y = segmentY; glowSegment.rotation = boltAngle + (Math.random() - 0.5) * 0.3; glowSegment.scaleX = (0.25 + Math.random() * 0.15) * 3.0; // Much larger for glow effect glowSegment.scaleY = (0.25 + Math.random() * 0.15) * 3.0; glowSegment.alpha = 0.4; // Semi-transparent glow glowSegment.tint = 0x00FFFF; // Darker blue for glow game.addChild(glowSegment); // Main lightning segment (thinner, very light blue) var lightningSegment = LK.getAsset('molnia', { anchorX: 0.5, anchorY: 0.5 }); lightningSegment.x = segmentX; lightningSegment.y = segmentY; lightningSegment.rotation = boltAngle + (Math.random() - 0.5) * 0.3; // Slight rotation variation lightningSegment.scaleX = (0.25 + Math.random() * 0.15) * 1.3; // Thinner lightningSegment.scaleY = (0.25 + Math.random() * 0.15) * 1.3; // Thinner lightningSegment.alpha = 0.7 + Math.random() * 0.3; // Random brightness lightningSegment.tint = 0x00FFFF; // Very light blue color game.addChild(lightningSegment); // Animate glow segment fade out tween(glowSegment, { alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function (segment) { return function onFinish() { if (segment.parent) { game.removeChild(segment); } }; }(glowSegment) }); // Lightning segments shrink from both ends to middle before fade out tween(lightningSegment, { scaleY: 0, // Shrink vertically to create top-bottom to middle effect alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function (segment) { return function onFinish() { if (segment.parent) { game.removeChild(segment); } }; }(lightningSegment) }); } // Create additional impact particles at target for (var lp = 0; lp < 6; lp++) { var impactParticle = LK.getAsset('particle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.8, scaleY: 1.8 }); impactParticle.tint = 0x00FFFF; // Cyan lightning color impactParticle.x = toTarget.x + (Math.random() - 0.5) * 30; impactParticle.y = toTarget.y + (Math.random() - 0.5) * 30; game.addChild(impactParticle); // Animate impact particles bursting outward var lpAngle = Math.random() * Math.PI * 2; var lpSpeed = 80 + Math.random() * 60; var lpVelX = Math.cos(lpAngle) * lpSpeed; var lpVelY = Math.sin(lpAngle) * lpSpeed; // Delay impact particles to start after traveling particles arrive LK.setTimeout(function (particle, velX, velY) { return function () { tween(particle, { x: particle.x + velX, y: particle.y + velY, scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { if (particle.parent) { game.removeChild(particle); } } }); }; }(impactParticle, lpVelX, lpVelY), 100 + lp * 10); // Start after particles arrive } // Chain to next target after delay LK.setTimeout(function () { _chainLightning(toTarget, targetIndex + 1); }, 150); // 150ms delay between jumps for faster lightning }; // Start the chain from primary target to first lightning target if (lightningTargets.length > 0) { _chainLightning(primaryTarget, 0); } } // Add visual recoil effect to barrel (scale-based, not position-based) if (puhaBarrelInstance) { // Stop any existing recoil animations tween.stop(puhaBarrelInstance, { scaleX: true, scaleY: true }); // Store original scale var originalScaleX = 0.3; var originalScaleY = 0.3; // Quick recoil animation using scale tween(puhaBarrelInstance, { scaleX: originalScaleX * 0.85, // Shrink slightly scaleY: originalScaleY * 1.1 // Stretch slightly vertically }, { duration: 80, easing: tween.easeOut, onFinish: function onFinish() { // Return to original scale tween(puhaBarrelInstance, { scaleX: originalScaleX, scaleY: originalScaleY }, { duration: 250, easing: tween.easeInOut }); } }); } // Aggressive muzzle particles cleanup to prevent accumulation if (!game.muzzleParticles) { game.muzzleParticles = []; } if (game.muzzleParticles.length >= 20) { // Reduced threshold from 30 to 20 // Clean up oldest 10 particles instead of 5 for (var oldM = 0; oldM < 10; oldM++) { var oldMuzzleParticle = game.muzzleParticles.shift(); if (oldMuzzleParticle) { // Stop any active tweens before removal tween.stop(oldMuzzleParticle); if (oldMuzzleParticle.parent) { oldMuzzleParticle.parent.removeChild(oldMuzzleParticle); } } } } // Create muzzle flash particles at puha's muzzle end var muzzleParticleCount = 5 + Math.floor(Math.random() * 3); // 5 to 7 particles for (var mp = 0; mp < muzzleParticleCount; mp++) { var muzzleParticle = LK.getAsset('particle', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.0 + Math.random() * 2.0, // Random scale between 2.0 and 4.0 for larger smoke-like particles scaleY: 2.0 + Math.random() * 2.0 }); // Position particles at the muzzle end accounting for rotation // Puha barrel is anchored at left side (0,0.5) and scaled to 0.3 var puhaWidth = 900 * 0.3; // Original width * scale var puhaHeight = 2056 * 0.3; // Original height * scale // Calculate muzzle position relative to puha barrel's pivot point (left side, center) var muzzleDistance = puhaWidth * 2.2; // Distance from pivot to muzzle end - moved closer to barrel for smoke effect var muzzleX = puhaBarrelInstance.x + Math.cos(puhaBarrelInstance.rotation) * muzzleDistance; var muzzleY = puhaBarrelInstance.y + Math.sin(puhaBarrelInstance.rotation) * muzzleDistance; muzzleParticle.x = muzzleX + (Math.random() - 0.5) * 20; // Small random spread muzzleParticle.y = muzzleY + (Math.random() - 0.5) * 20; game.addChild(muzzleParticle); game.muzzleParticles.push(muzzleParticle); // Animate muzzle particles in the direction puha is facing var muzzleAngle = puhaBarrelInstance.rotation + (Math.random() - 0.5) * 0.5; // Small angle variation var muzzleSpeed = 150 + Math.random() * 100; // Speed between 150-250 var muzzleVelX = Math.cos(muzzleAngle) * muzzleSpeed; var muzzleVelY = Math.sin(muzzleAngle) * muzzleSpeed; tween(muzzleParticle, { x: muzzleParticle.x + muzzleVelX, y: muzzleParticle.y + muzzleVelY, scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function (muzzleRef) { return function onFinish() { // Stop any remaining tweens on this muzzle particle tween.stop(muzzleRef); if (muzzleRef.parent) { muzzleRef.parent.removeChild(muzzleRef); } // Remove from tracking array if (game.muzzleParticles) { var mIndex = game.muzzleParticles.indexOf(muzzleRef); if (mIndex > -1) { game.muzzleParticles.splice(mIndex, 1); } } }; }(muzzleParticle) }); } } } // 3. Update twinkling stars updateStars(); // 3.1. Update music rotation updateMusicRotation(); // 4. Spawn new falling objects if (currentTime >= nextSpawnTime) { spawnFallingObject(); // Check if the city has not taken damage for 30 seconds if (currentTime - lastCityDamageTime >= 60000) { // Set spawn interval to 2 seconds nextSpawnTime = currentTime + 2000; } else if (currentTime - lastCityDamageTime >= 30000) { // Set spawn interval to 3 seconds nextSpawnTime = currentTime + 3000; } else { // Set spawn interval to 4 seconds nextSpawnTime = currentTime + 4000; } } // Spawn alien_1 based on its separate timer if (totalEnemiesKilled >= 75 && currentTime >= nextAlienSpawnTime) { var newAlien = new FallingObject('Alien_1', 70, METEOR_SPEED, 'alien'); newAlien.moneyReward = 4; // alien_1 gives 4$ // Position just above the screen, at a random horizontal position newAlien.x = Math.random() * (WORLD_WIDTH - newAlien.graphic.width) + newAlien.graphic.width / 2; newAlien.y = -newAlien.graphic.height / 2; // Start just off-screen top fallingObjects.push(newAlien); game.addChild(newAlien); // Set next alien spawn time to exactly 5 seconds nextAlienSpawnTime = currentTime + 5000; } // 5. Update and check falling objects for (var i = fallingObjects.length - 1; i >= 0; i--) { var obj = fallingObjects[i]; if (obj.isDestroyed) { // Remove from game immediately when destroyed if (obj.parent) { game.removeChild(obj); } fallingObjects.splice(i, 1); continue; } obj.update(); // Check shield collision before city collision if (cityInstance.shielded && cityInstance.shieldGraphic) { // Calculate shield bounds (shield is positioned above health bar) var shieldWorldX = cityInstance.x + cityInstance.shieldGraphic.x; var shieldWorldY = cityInstance.y + cityInstance.shieldGraphic.y; var shieldWidth = cityInstance.shieldGraphic.width * (cityInstance.shieldGraphic.scaleX || 1); var shieldHeight = cityInstance.shieldGraphic.height * (cityInstance.shieldGraphic.scaleY || 1); // Calculate object bounds var objWidth = obj.graphic.width * (obj.graphic.scaleX || 1); var objHeight = obj.graphic.height * (obj.graphic.scaleY || 1); var objLeft = obj.x - objWidth / 2; var objRight = obj.x + objWidth / 2; var objTop = obj.y - objHeight / 2; var objBottom = obj.y + objHeight / 2; // Calculate shield bounds var shieldLeft = shieldWorldX - shieldWidth / 2; var shieldRight = shieldWorldX + shieldWidth / 2; var shieldTop = shieldWorldY - shieldHeight / 2; var shieldBottom = shieldWorldY + shieldHeight / 2; // Check if object intersects with shield if (!(objRight < shieldLeft || objLeft > shieldRight || objBottom < shieldTop || objTop > shieldBottom)) { // Object hit shield - destroy object and trigger shield hit effect cityInstance.takeDamage(0); // Trigger shield flash effect LK.getSound('udarshield').play(); // Play shield hit sound obj.destroySelf(false); // Don't give money for destroying object with shield continue; } } // Calculate the Y coordinate of the "middle" of the city image var cityMiddleY = cityInstance.y - cityInstance.graphic.height / 2; // Check if the falling object has reached the middle of the city if (obj.y + obj.graphic.height * 0.5 * (obj.graphic.scaleY || 1) >= cityMiddleY) { var damageToCity = obj.objectType === 'meteor' ? METEOR_DAMAGE_TO_CITY : ALIEN_DAMAGE_TO_CITY; cityInstance.takeDamage(damageToCity); LK.getSound('Boom').play(); // Play boom sound when object hits city obj.destroySelf(false); // Don't give money when enemy crashes into city continue; } if (obj.y - obj.graphic.height * 0.5 * (obj.graphic.scaleY || 1) > WORLD_HEIGHT) { obj.destroySelf(); continue; } } // Old safety check for playerHoldingTarget is removed as that system is gone. // Update puha barrel to track crosshair if (puhaBarrelInstance && crosshairInstance) { // Calculate angle from puha barrel to crosshair var dx = crosshairInstance.x - puhaBarrelInstance.x; var dy = crosshairInstance.y - puhaBarrelInstance.y; var angle = Math.atan2(dy, dx); // Rotate only the barrel to face crosshair puhaBarrelInstance.rotation = angle; } // Always ensure crosshair is rendered above all falling objects if (crosshairInstance && crosshairInstance.parent === game) { game.removeChild(crosshairInstance); game.addChild(crosshairInstance); } // Always ensure controls button is rendered above all objects (including meteors) if (controlsButton && controlsButton.parent === game) { game.removeChild(controlsButton); game.addChild(controlsButton); } if (controlsHitArea && controlsHitArea.parent === game) { game.removeChild(controlsHitArea); game.addChild(controlsHitArea); } // Always ensure music button is rendered above all objects (including meteors) if (musicButton && musicButton.parent === game) { game.removeChild(musicButton); game.addChild(musicButton); } if (musicHitArea && musicHitArea.parent === game) { game.removeChild(musicHitArea); game.addChild(musicHitArea); } // Update object counter display in real-time if (objectCountText) { objectCountText.setText('Objects: ' + getTotalObjectCount()); } // Always ensure object counter is rendered above all objects if (objectCountText) { objectCountText.setText('Objects: ' + getTotalObjectCount()); } // Always ensure object counter is rendered above all objects if (objectCountText && objectCountText.parent === game) { game.removeChild(objectCountText); game.addChild(objectCountText); } // Update spawn rate display if (spawnRateText) { if (currentTime - lastCityDamageTime >= 30000) { spawnRateText.setText('Spawn Rate: 2s'); } else { spawnRateText.setText('Spawn Rate: 3s'); } } // Always ensure spawn rate text is rendered above all objects if (spawnRateText && spawnRateText.parent === game) { game.removeChild(spawnRateText); game.addChild(spawnRateText); } // Always ensure add points button and text are rendered above all objects if (addPointsButton && addPointsButton.parent === game) { game.removeChild(addPointsButton); game.addChild(addPointsButton); } if (addPointsText && addPointsText.parent === game) { game.removeChild(addPointsText); game.addChild(addPointsText); } if (addPointsHitArea && addPointsHitArea.parent === game) { game.removeChild(addPointsHitArea); game.addChild(addPointsHitArea); } }; var lastCityDamageTime = 0; var totalEnemiesKilled = 0; // Track total enemies destroyed for health scaling var currentHealthMultiplier = 1.0; // Current health multiplier based on kills // --- Meteor Event Variables --- var meteorEventActive = false; var meteorEventKills = 0; var meteorEventTimeout = null; var meteorEventInterval = null; // Call this when an enemy is killed function onEnemyKilledForMeteorEvent() { // Do not count kills during the event if (meteorEventActive) return; meteorEventKills++; if (meteorEventKills >= 200) { startMeteorEvent(); meteorEventKills = 0; // Reset counter for next event } } function startMeteorEvent() { if (meteorEventActive) return; meteorEventActive = true; // Set background slightly red if (game && game.setBackgroundColor) { game.setBackgroundColor(0x3a1010); // Slightly red, not full red } // Stop current music and play musicFight for 55 seconds LK.stopMusic(); LK.playMusic('musicFight', { loop: false }); // Spawn a small meteor every second for 55 seconds meteorEventInterval = LK.setInterval(function () { // Defensive: only spawn if game is not paused if (!upgradesPopup && !controlsPopup && !shopPopup && !currentTooltip) { var newObject = new FallingObject('smallmeteor', 70, METEOR_SPEED, 'meteor'); newObject.moneyReward = 2; newObject.x = Math.random() * (WORLD_WIDTH - newObject.graphic.width) + newObject.graphic.width / 2; newObject.y = -newObject.graphic.height / 2; fallingObjects.push(newObject); game.addChild(newObject); } }, 1000); meteorEventTimeout = LK.setTimeout(function () { meteorEventActive = false; meteorEventKills = 0; if (meteorEventInterval) { LK.clearInterval(meteorEventInterval); meteorEventInterval = null; } meteorEventTimeout = null; // Restore background color if (game && game.setBackgroundColor) { game.setBackgroundColor(0x0a0a18); // Restore to original dark blue } // Resume next music track (not the one that was interrupted) if (musicEnabled && musicTracks.length > 0) { currentMusicIndex = (currentMusicIndex + 1) % musicTracks.length; musicStartTime = LK.ticks * (1000 / 60); LK.playMusic(musicTracks[currentMusicIndex]); musicPlaying = true; } }, 55000); } ; ;
===================================================================
--- original.js
+++ change.js
@@ -1809,9 +1809,14 @@
}
// Give money reward
if (giveMoney) {
// Use the moneyReward property set in the constructor
- var scoreIncrement = Math.min(Math.floor(totalEnemiesKilled / 50) * 2, 20); // Cap at 20 (10 applications * 2)
+ var scoreIncrement;
+ if (self.assetId === 'gold') {
+ scoreIncrement = Math.min(Math.floor(totalEnemiesKilled / 50) * 2, 40); // Cap at 40 for gold meteors
+ } else {
+ scoreIncrement = Math.min(Math.floor(totalEnemiesKilled / 50) * 2, 20); // Cap at 20 for others
+ }
var moneyEarned = self.moneyReward + scoreIncrement;
LK.setScore(LK.getScore() + moneyEarned);
// --- Ava: Also increment finalScore by 1 for each enemy killed (not spent) ---
finalScore += 1;
@@ -3486,14 +3491,14 @@
/****
* Game Code
****/
-// Legacy classes for backward compatibility
-// A simple red circle for the crosshair
-// Grey city base
-// Lime green alien (can be simple box for now)
-// Brownish meteor
// --- Game Constants ---
+// Brownish meteor
+// Lime green alien (can be simple box for now)
+// Grey city base
+// A simple red circle for the crosshair
+// Legacy classes for backward compatibility
var WORLD_WIDTH = 2048;
var WORLD_HEIGHT = 2732;
var METEOR_HEALTH = 100;
var METEOR_SPEED = 2.4; // Pixels per frame (1.5 * 1.6)
Метеорит без огня пастельные цвета In-Game asset. 2d. High contrast. No shadows
Похожий
Иконка повышение урона, сочные цвета. In-Game asset. 2d. High contrast. No shadows. Comix
иконка на скорость атаки
надпись upgrade как красивая кнопка In-Game asset. 2d. High contrast. No shadows. comix
центральный круг желтый а внешний оранжевый
голубой вместо оранжевого
Красно оранжевый
Restyled
Разрешение 2048 на 400
молния должна быть с двух концов одинаковая и ответвления смотреть строго вверх и вниз а не наискосок
иконка шанса двойного урона (x2)
иконка голубой молнии без текста и цыферблата
иконка огня
Вместо молнии синяя снежинка, все остальное без изменений
сделать светлее
Комикс
сделать рамку толще в два раза и немного не правильной формы как в комиксах
сделать рамку тоньше сохранив стиль и цвета сочнее
надпись shop как красивая кнопка In-Game asset. 2d. High contrast. No shadows. comix
Рамка для всплывающей меню подсказки. In-Game asset. 2d. High contrast. No shadows
Крестик для закрытия окна. In-Game asset. 2d. High contrast. No shadows
Иконка английского языка флаг без текста In-Game asset. 2d. High contrast. No shadows
Заменить на российский без текста, рамку сохранить
Удалить желтый фон
Флаг земенить на немецкий рамки сохранить
Заменить на испанский, сохранить рамку.
сделать точно такуюже рамку но надпись заменить на shop. звезду заменить на ракету, а стрелку на щит
все оставить как есть но удалить черноту за рамками
круглая иконка подсказки I. In-Game asset. 2d. High contrast. No shadows
убери все звезды оставь только чистое небо
иконка восстановление здоровья много зеленых крестов в рамке, сочные цвета красивый фон. In-Game asset. 2d. High contrast. No shadows
синий щит на ярко оранжевом фоне
залп ракетного огня
шаровая молния. In-Game asset. 2d. High contrast. No shadows
башня тесла с молниями фон голубой
Огненный шар
перекрасить больше желтого и оранжевого
перекрасить больше голубого, светло-голубого,
турецкий флаг
Вместо огненного кольца, огненные шары разлетающие вверх в разные стороны
Текст убрать. Вместо молний снежинки
Вместо молнии снежинка, и покрасить в синий
Льдинка как стеклышко. In-Game asset. 2d. High contrast. No shadows
убрать дырку
бесформенная амеба
удали крывлья оставь только жука
оставь только крылья, удали жука
перекрась
Shoot
Sound effect
Boom
Sound effect
Pokupka
Sound effect
menu
Sound effect
molnia
Sound effect
krit
Sound effect
icetresk
Sound effect
peretik
Sound effect
music1
Music
music2
Music
music3
Music
musicFight
Music
udarshield
Sound effect
startraket
Sound effect
raketaudar
Sound effect
Ognemet
Sound effect
Tresklda
Sound effect
stop
Sound effect
goldsound
Sound effect
alien_bum
Sound effect