User prompt
ŃŠ±ŠµŃŠø наГпиŃŃ ŃŃŠµŃ Šø ŃŠ“ŠµŠ»Š°ŠøĢ ŠµŠ³Š¾ менŃŃŠµ
User prompt
ŃŠµŠŗŃŃ ŃŃŠµŃа ŃŃŃŠ°Š½Š¾Š²Šø ГоГ ŃŠµŠŗŃŃŠ¾Š¼ Š±Š°Š»Š°Š³ŃŠ° Šø ŃŠ“ŠµŠ»Š°ŠøĢ ŠµŠ³Š¾ Š±ŠµŠ»ŃŠ¼
User prompt
ŃŠ“ŠµŠ»Š°ŠøĢ Š¾ŃŠ“ŠµŠ»ŃŠ½ŃŃ ŃŠøŃŃŠµŠ¼Ń поГŃŃŠµŃа Šø ŃŃŠµŃ в Š½ŠµŠøĢ Š±ŃŠ“ŠµŃ ŃŠ°ŠŗŠ¶Šµ ŃŠ²ŠµŠ»ŠøŃиваŃŃŃŃ Š·Š° ŃŃŠµŃ ГобŃŃŠø Генег 1 Šŗ 1. ŠŃоŃŃŠ¾ Ń Š½ŠµŠ³Š¾ не Š±ŃŠ“ŠµŃ ŃŠæŠøŃŃŠ²Š°ŃŃŃŃ Š½ŠøŃŠµŠ³Š¾ он Š½Ńжен Š“Š»Ń ŃŠøŠ½Š°Š»Ńного поГŃŃŠµŃа. Š ŃŠ¾Ń Š±ŃŠ“ŠµŃ ŠŗŠ°Šŗ баланŃ
User prompt
ŃŃŃŠ°Š½Š¾Š²Šø Š±Š°Š»Š°Š½Ń Š¾ŃŠŗŠ¾Š² на 100000
User prompt
ŃŃŃŠ°Š½Š¾Š²Šø Š±Š°Š»Š°Š½Ń Š¾ŃŠŗŠ¾Š² на 100000
User prompt
во Š²ŃŠµŠ¼Ń ŠŗŃŠøŃа Š¾Ń ŃŠµŠ»Šø ŃŠ°Š·Š»ŠµŃаŃŃŃŃ Š² ŃŠ°Š·Š½Ńе ŃŃŠ¾ŃŠ¾Š½Ń ŃŠ°ŃŃŠøŃŃ zvezdakrit (Š·Š²ŠµŠ·Š“Š¾ŃŠŗŠø) 3 ŃŃŃŠŗŠø ŃŠ¼ŠµŠ½ŃŃŠ°ŃŃŃŃ Š² ŃŠµŃении ŃŠµŠŗŃŠ½Š“Ń Šø ŃŠ“алŃŃŃŃŃ
User prompt
Please fix the bug: 'self.updateHealthBarVisual is not a function' in or related to this line: 'self.updateHealthBarVisual(); // Initialize health bar visual' Line Number: 319
User prompt
Please fix the bug: 'self.updateHealthBarVisual is not a function' in or related to this line: 'self.updateHealthBarVisual(); // Initialize health bar visual' Line Number: 318
User prompt
Please fix the bug: 'TypeError: self.updateHealthBarVisual is not a function' in or related to this line: 'self.updateHealthBarVisual();' Line Number: 2432
User prompt
ŃŠ¼Š½Š¾Š¶ Š·Š“Š¾ŃŠ¾Š²Ńе Š¼ŠµŃŠµŠ¾ŃŠøŃов в 10 ŃŠ°Š·
User prompt
ŃŠ¼Š½Š¾Š¶ Š·Š“Š¾ŃŠ¾Š²Ńе Š²Ńагов в 10 ŃŠ°Š· Šø ŠæŃŠøŠ±Š°Š²Ń 100000Š¾ŃŠŗŠ¾Š²
User prompt
мне не Š½ŃавиŃŃŃ ŠŗŠ°Šŗ ŃŠ°Š±Š¾ŃŠ°ŠµŃ ŃŠ¼ŠµŠ½ŃŃŠµŠ½ŠøŠµ Š·Š“Š¾ŃŠ¾Š²ŃŃ Ń Š²ŃŠ°Š³Š¾Š². ŠŠ¾Š³Š“а бŃŃŃŃŠ¾ Š±ŃŠµŃŃ Š¶ŠµŠ»ŃŠ°Ń ŠæŠ¾Š»Š¾ŃŠŗŠ° ŠµŃŠµ не Š·Š°ŠŗŠ¾Š½Ńила Š°Š½ŠøŠ¼Š°ŃŠøŃ, а ŃŠ¶Šµ Голжна Š½Š°ŃаŃŃŃŃ Š½Š¾Š²Š°Ń Š¶ŠµŠ»ŃŠ°Ń ŠæŠ¾Š»Š¾ŃŠŗŠ° а ее неŃ. ŠŃŠ¼Š°Ń ŠµŃŠ»Šø бŃŃŃŃŠ¾ биŃŃ ŃŠ¾ ŠæŃŠµŠ“ŠøŠ“ŃŃŃŃ Š¶ŠµŠ»ŃŃŃ ŠæŠ¾Š»Š¾ŃŠŗŃ Š½ŃŠ¶Š½Š¾ ŃŠ“алŃŃŃ ŃŃŠ°Š·Ń или Š¾Š±ŃеГинŃŃŃ ŃŠ¾ ŃŠ»ŠµŠ“ŃŃŃŠµŠøĢ āŖš” Consider importing and using the following plugins: @upit/tween.v1
User prompt
во Š²ŃŠµŠ¼Ń ŠøŠ²ŠµŠ½ŃŠ° Š²ŃŠµŠ¼Ń Š²ŃŠ“еленное на Š¼ŃŠ·Ńко Го ŠøŠ²ŠµŠ½Ńа не Голжно ŃŠ°Š±Š¾ŃаŃŃ. Š ŃŠ¾ полŃŃŠ°ŠµŃŃŃ Š¼ŃŠ·Ńка ŠøŠ²ŠµŠ½Ńа ŃŠ¾Š»Ńко Š½Š°ŃŠøŠ½Š°ŠµŃ ŠøŠ³ŃŠ°ŃŃ ŠŗŠ°Šŗ поŃŃŠø ŃŃŠ°Š·Ń ŠæŠµŃŠµŠŗŠ»ŃŃŠ°ŠµŃŃŃ ŠæŃŠµŠ“ŠøŠ“ŃŃŠøŠ¼ Š¾ŠŗŠ¾Š½ŃŠ°Š½ŠøŠµŠ¼ Š¼ŃŠ·ŃŠŗŠø
User prompt
наГпиŃŃ Š±ŃŠ“ŠµŃ Š½Š° ŃŠ¾Š¼ ŃŠ·Ńке ŠŗŠ¾ŃоŃŃŠøĢ вŃŃŃŠ°Š²Š»ŠµŠ½ ŠæŠ¾ŃŠ»ŠµŠ“ним в ŠæŠ¾Š“ŃŠŗŠ°Š·ŠŗŠ°Ń
User prompt
ŠæŃŠø повŃŃŠµŠ½ŠøŠø ŃŃŠ¾Š²Š½Ń ŃŠ»Š¾Š¶Š½Š¾ŃŃŠø ŠæŃŃŃŃ Š½Šµ на Голго вŃŃŠŗŠ°ŠŗŠøŠ²Š°ŠµŃ наГпиŃŃ Š² ŠŗŠ¾ŃŠ¾ŃŠ¾ŠøĢ Š³Š¾Š²Š¾ŃŠøŃŃŃ Š½Š° ŠŗŠ°ŠŗŠ¾ŠøĢ ŃŃŠ¾Š²ŠµŠ½Ń ŃŠ»Š¾Š¶Š½Š¾ŃŃŠø ŠøŠ³ŃŠ¾Šŗ ŠæŠµŃŠµŃел āŖš” Consider importing and using the following plugins: @upit/tween.v1
User prompt
ŃŠ±ŠµŃŠø Š²ŃŠæŃŃŠŗŃ ŠæŃŠø повŃŃŠµŠ½ŠøŠø ŃŃŠ¾Š²Š½Ń ŃŠ»Š¾Š¶Š½Š¾ŃŃŠø
User prompt
Š£Š±ŠµŃŠø ŠŗŃŠ°ŃнŃŃ Š²ŃŠæŃŃŠŗŃ ŠæŃŠø повŃŃŠµŠ½ŠøŠø ŃŃŠ¾Š²Š½Ń ŃŠ»Š¾Š¶Š½Š¾ ŃŃŠø Šø ŠæŃŠ¾ŃŃŠ¾ Š“Š¾Š±Š°Š²Ń Š½Š°Š“ŠæŠøŃŃ Š³Š“Šµ ŃŠŗŠ°Š·Š°Š½Š¾ на каком ŃŃŠ¾Š²Š½Šµ ŃŠ»Š¾Š¶Š½Š¾ŃŃŠø ŃŠµŠøĢŃŠ°Ń
User prompt
ŃŠ“ŠµŠ»Š°ŠøĢ ŠŗŠ½Š¾ŠæŠŗŃ Š“Š¾Š±Š°Š²Š»ŠµŠ½ŠøŃ Š¾ŃŠŗŠ¾Š² не Š°ŠŗŃивноиĢ
User prompt
ŃŠ“ŠµŠ»Š°ŠøĢ ŠŗŠ½Š¾ŠæŠŗŃ Š“Š¾Š±Š°Š²Š»ŠµŠ½ŠøŃ Š¾ŃŠŗŠ¾Š² невиГимоиĢ
User prompt
Š¾ŠæŠøŃŠ°Š½ŠøŠµ ŃŠ°ŠŗŠµŃ не на Š²ŃŠµŃ ŃŠ·ŃŠŗŠ°Ń ŠæŃŠ°Š²ŠøŠ»Ńное
User prompt
ŠæŠµŃŠµŠæŠøŃŠø Š¾ŠæŠøŃŠ°Š½ŠøŠµ в ŠæŠ¾Š“ŃŠŗŠ°Š·ŠŗŠ°Ń ŃŠ¾ŠæŠ° ŃŠ¾Š³Š»Š°Ńно ŃŠ¾Š¼Ń как оно еŃŃŃ
User prompt
Please fix the bug: 'addPointsButton is not defined' in or related to this line: 'addPointsHitArea.y = addPointsButton.y;' Line Number: 6650
User prompt
Please fix the bug: 'addPointsButton is not defined' in or related to this line: 'addPointsHitArea.x = addPointsButton.x;' Line Number: 6650
User prompt
ŃŠ“али
Code edit (1 edits merged)
Please save this source code
/**** * 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); 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 }); } }); self.updateHealthBarVisual(); // Update the visual health bar if (self.health <= 0) { self.health = 0; // Cap at 0 LK.showGameOver(); // Trigger game over } }; // Removed setHealthDisplay method as city health text is no longer shown self.updateHealthBarVisual = function () { if (!self.healthBarFill || !self.healthBarBg || !self.damagePreviewFill) { return; } 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; 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 (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(); } 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; // Flash screen to indicate health increase LK.effects.flashScreen(0xFF8000, 500); // Orange flash for health increase } // Give money reward if (giveMoney) { var scoreIncrement = Math.min(Math.floor(totalEnemiesKilled / 50) * 2, 20); // Cap at 20 (10 applications * 2) LK.setScore(LK.getScore() + self.moneyReward + scoreIncrement); 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 } 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 (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; // Flash screen to indicate health increase LK.effects.flashScreen(0xFF8000, 500); // Orange flash for health increase } // 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 moneyEarned = self.moneyReward + scoreIncrement; LK.setScore(LK.getScore() + moneyEarned); 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 ****/ // 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 --- 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 // 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; } 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 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); // --- 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 = 'ru'; // Default to Russian 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.', shop2: 'Creates a protective shield around the city for 15 seconds. Shield blocks all incoming damage until destroyed.', shop3: 'Launches homing rockets for 15 seconds. Rockets target enemies automatically and deal 1000 damage each.', 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.', 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).', 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.' }, ru: { shop1: 'ŠŠ¾ŃŃŠµŠæŠµŠ½Š½Š¾ воŃŃŃŠ°Š½Š°Š²Š»ŠøŠ²Š°ŠµŃ Š·Š“Š¾ŃŠ¾Š²Ńе Š³Š¾ŃоГа в ŃŠµŃение 12 ŃŠµŠŗŃнГ. ŠŠµŃŠøŃ 70 ŠŠ кажГŃŃ ŃŠµŠŗŃнГŃ, Š²ŃŠµŠ³Š¾ 840 ŠŠ.', shop2: 'Š”Š¾Š·Š“Š°ŠµŃ Š·Š°ŃŠøŃŠ½ŃŠ¹ ŃŠøŃ вокŃŃŠ³ Š³Š¾ŃŠ¾Š“а на 15 ŃŠµŠŗŃнГ. Š©ŠøŃ Š±Š»Š¾ŠŗŠøŃŃŠµŃ веŃŃ Š²Ń Š¾Š“ŃŃŠøŠ¹ ŃŃŠ¾Š½ Го ŃŠ½ŠøŃŃŠ¾Š¶ŠµŠ½ŠøŃ.', shop3: 'ŠŠ°ŠæŃŃŠŗŠ°ŠµŃ ŃŠ°Š¼Š¾Š½Š°Š²Š¾Š“ŃŃŠøŠµŃŃ ŃŠ°ŠŗŠµŃŃ Š² ŃŠµŃение 15 ŃŠµŠŗŃнГ. РакеŃŃ Š°Š²ŃŠ¾Š¼Š°ŃŠøŃŠµŃŠŗŠø Š½Š°Ń Š¾Š“ŃŃ Š²ŃŠ°Š³Š¾Š² Šø наноŃŃŃ 1000 ŃŃŠ¾Š½Š° кажГаŃ.', shop4: 'Š Š°Š·Š¼ŠµŃŠ°ŠµŃ Š±Š°ŃŠ½Ń Š¢ŠµŃŠ»Š° на 15 ŃŠµŠŗŃнГ. Š”ŃŃŠµŠ»ŃŠµŃ Š¼Š¾Š»Š½ŠøŃŠ¼Šø кажГŃŃ ŃŠµŠŗŃнГŃ, наноŃŃŃŠøŠ¼Šø 250 ŃŃŠ¾Š½Š° Šø ŠæŠµŃŠµŃкакиваŃŃŠøŠ¼Šø на Го 4 Š±Š»ŠøŠ¶Š°Š¹ŃŠøŃ Š²ŃŠ°Š³Š¾Š² Ń 250 ŃŃŠ¾Š½Š¾Š¼ кажГомŃ.', shop5: 'ŠŃŠøŠ·ŃŠ²Š°ŠµŃ огненнŃŃ Š±Š°ŃŠ½Ń на 15 ŃŠµŠŗŃнГ. Š”ŃŃŠµŠ»ŃŠµŃ 17 гоŃŃŃŠøŠ¼Šø ŃŠ½Š°ŃŃŠ“ами кажГŃŃ ŃŠµŠŗŃнГŃ, наноŃŃŃŠøŠ¼Šø 100 ŃŃŠ¾Š½Š° ŠŗŠ°Š¶Š“ŃŠ¹ Šø Š½Š°ŠŗŠ»Š°Š“ŃŠ²Š°ŃŃŠøŠ¼Šø Š¾Š³Š½ŠµŠ½Š½ŃŠ¹ ожог Š±Š°Ńни (50 ŃŃŠ¾Š½Š° в ŃŠµŠŗŃŠ½Š“Ń Š·Š° ŃŃŠ°Šŗ).', shop6: 'Š”Š¾Š·Š“Š°ŠµŃ Š»ŠµŠ“ŃŠ½ŃŃ Š±Š°ŃŠ½Ń на 15 ŃŠµŠŗŃнГ. Š”ŃŃŠµŠ»ŃŠµŃ 15 Š·Š°Š¼Š¾ŃŠ°Š¶ŠøŠ²Š°ŃŃŠøŠ¼Šø ŃŃŠµŃами ŠŗŠ°Š¶Š“ŃŠµ 1,5 ŃŠµŠŗŃнГŃ, наноŃŃŃŠøŠ¼Šø 50 ŃŃŠ¾Š½Š° ŠŗŠ°Š¶Š“Š°Ń Šø полноŃŃŃŃ Š·Š°Š¼Š¾ŃŠ°Š¶ŠøŠ²Š°ŃŃŠøŠ¼Šø Š²ŃŠ°Š³Š¾Š² на 2 ŃŠµŠŗŃнГŃ.' }, 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 1000 Schaden.', 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 1000 dĆ©gĆ¢ts chacune.', 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 1000 daƱo cada uno.', 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 1000 hasar verir.', 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; 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; 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) { // 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; 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; 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) { // 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(); } }; 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); } ; ;
/****
* 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);
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
});
}
});
self.updateHealthBarVisual(); // Update the visual health bar
if (self.health <= 0) {
self.health = 0; // Cap at 0
LK.showGameOver(); // Trigger game over
}
};
// Removed setHealthDisplay method as city health text is no longer shown
self.updateHealthBarVisual = function () {
if (!self.healthBarFill || !self.healthBarBg || !self.damagePreviewFill) {
return;
}
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;
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 (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();
}
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;
// Flash screen to indicate health increase
LK.effects.flashScreen(0xFF8000, 500); // Orange flash for health increase
}
// Give money reward
if (giveMoney) {
var scoreIncrement = Math.min(Math.floor(totalEnemiesKilled / 50) * 2, 20); // Cap at 20 (10 applications * 2)
LK.setScore(LK.getScore() + self.moneyReward + scoreIncrement);
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
}
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 (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;
// Flash screen to indicate health increase
LK.effects.flashScreen(0xFF8000, 500); // Orange flash for health increase
}
// 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 moneyEarned = self.moneyReward + scoreIncrement;
LK.setScore(LK.getScore() + moneyEarned);
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
****/
// 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 ---
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
// 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;
}
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
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);
// --- 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 = 'ru'; // Default to Russian
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.',
shop2: 'Creates a protective shield around the city for 15 seconds. Shield blocks all incoming damage until destroyed.',
shop3: 'Launches homing rockets for 15 seconds. Rockets target enemies automatically and deal 1000 damage each.',
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.',
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).',
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.'
},
ru: {
shop1: 'ŠŠ¾ŃŃŠµŠæŠµŠ½Š½Š¾ воŃŃŃŠ°Š½Š°Š²Š»ŠøŠ²Š°ŠµŃ Š·Š“Š¾ŃŠ¾Š²Ńе Š³Š¾ŃоГа в ŃŠµŃение 12 ŃŠµŠŗŃнГ. ŠŠµŃŠøŃ 70 ŠŠ кажГŃŃ ŃŠµŠŗŃнГŃ, Š²ŃŠµŠ³Š¾ 840 ŠŠ.',
shop2: 'Š”Š¾Š·Š“Š°ŠµŃ Š·Š°ŃŠøŃŠ½ŃŠ¹ ŃŠøŃ вокŃŃŠ³ Š³Š¾ŃŠ¾Š“а на 15 ŃŠµŠŗŃнГ. Š©ŠøŃ Š±Š»Š¾ŠŗŠøŃŃŠµŃ веŃŃ Š²Ń
оГŃŃŠøŠ¹ ŃŃŠ¾Š½ Го ŃŠ½ŠøŃŃŠ¾Š¶ŠµŠ½ŠøŃ.',
shop3: 'ŠŠ°ŠæŃŃŠŗŠ°ŠµŃ ŃŠ°Š¼Š¾Š½Š°Š²Š¾Š“ŃŃŠøŠµŃŃ ŃŠ°ŠŗŠµŃŃ Š² ŃŠµŃение 15 ŃŠµŠŗŃнГ. РакеŃŃ Š°Š²ŃŠ¾Š¼Š°ŃŠøŃŠµŃŠŗŠø наŃ
оГŃŃ Š²ŃŠ°Š³Š¾Š² Šø наноŃŃŃ 1000 ŃŃŠ¾Š½Š° кажГаŃ.',
shop4: 'Š Š°Š·Š¼ŠµŃŠ°ŠµŃ Š±Š°ŃŠ½Ń Š¢ŠµŃŠ»Š° на 15 ŃŠµŠŗŃнГ. Š”ŃŃŠµŠ»ŃŠµŃ Š¼Š¾Š»Š½ŠøŃŠ¼Šø кажГŃŃ ŃŠµŠŗŃнГŃ, наноŃŃŃŠøŠ¼Šø 250 ŃŃŠ¾Š½Š° Šø ŠæŠµŃŠµŃкакиваŃŃŠøŠ¼Šø на Го 4 Š±Š»ŠøŠ¶Š°Š¹ŃŠøŃ
Š²ŃŠ°Š³Š¾Š² Ń 250 ŃŃŠ¾Š½Š¾Š¼ кажГомŃ.',
shop5: 'ŠŃŠøŠ·ŃŠ²Š°ŠµŃ огненнŃŃ Š±Š°ŃŠ½Ń на 15 ŃŠµŠŗŃнГ. Š”ŃŃŠµŠ»ŃŠµŃ 17 гоŃŃŃŠøŠ¼Šø ŃŠ½Š°ŃŃŠ“ами кажГŃŃ ŃŠµŠŗŃнГŃ, наноŃŃŃŠøŠ¼Šø 100 ŃŃŠ¾Š½Š° ŠŗŠ°Š¶Š“ŃŠ¹ Šø Š½Š°ŠŗŠ»Š°Š“ŃŠ²Š°ŃŃŠøŠ¼Šø Š¾Š³Š½ŠµŠ½Š½ŃŠ¹ ожог Š±Š°Ńни (50 ŃŃŠ¾Š½Š° в ŃŠµŠŗŃŠ½Š“Ń Š·Š° ŃŃŠ°Šŗ).',
shop6: 'Š”Š¾Š·Š“Š°ŠµŃ Š»ŠµŠ“ŃŠ½ŃŃ Š±Š°ŃŠ½Ń на 15 ŃŠµŠŗŃнГ. Š”ŃŃŠµŠ»ŃŠµŃ 15 Š·Š°Š¼Š¾ŃŠ°Š¶ŠøŠ²Š°ŃŃŠøŠ¼Šø ŃŃŠµŃами ŠŗŠ°Š¶Š“ŃŠµ 1,5 ŃŠµŠŗŃнГŃ, наноŃŃŃŠøŠ¼Šø 50 ŃŃŠ¾Š½Š° ŠŗŠ°Š¶Š“Š°Ń Šø полноŃŃŃŃ Š·Š°Š¼Š¾ŃŠ°Š¶ŠøŠ²Š°ŃŃŠøŠ¼Šø Š²ŃŠ°Š³Š¾Š² на 2 ŃŠµŠŗŃнГŃ.'
},
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 1000 Schaden.',
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 1000 dégâts chacune.',
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 1000 daƱo cada uno.',
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 1000 hasar verir.',
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;
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;
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) {
// 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;
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;
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) {
// 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();
}
};
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);
}
;
;
ŠŠµŃŠµŠ¾ŃŠøŃ без Š¾Š³Š½Ń паŃŃŠµŠ»ŃŠ½ŃŠµ ŃŠ²ŠµŃа 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