User prompt
Теперь что касается шестой покупке. Это башня холода (ice_tower). По суте это копия огненной башни по механике, только она стреляет ледеными сферами (ice_spher) которые вешают полную заморозку на секунду и у нее над головой вместо огненных сфер, леденые (ice_spher) вокруг которой летают снежинки. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Для ледяной башни используй ice_tower
User prompt
Теперь что касается шестой покупке. Это башня холода (ice_tower). По суте это копия огненной башни по механике, только она стреляет ледеными сферами (ice_spher) которые вешают полную заморозку на секунду и у нее над головой вместо огненных сфер, леденые (ice_spher) вокруг которой летают снежинки. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Убери цыфру 6 на шестой покупке в шопе
User prompt
Убери цифру 6 на шестой покупке в шопе
User prompt
Для звука выстрела огненной башни используй Ognemet
User prompt
Но не сильно быстро вращаться ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Снаряды огненной башни должны вращпться вокруг своей оси ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Огненная башня тоже должна стрелять раз в секунду
User prompt
Пусть тоже стреляет раз в секунду
User prompt
Сама огненная башня во время выстрела должна сжематься точно также как башня молнии ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Сама огненная башня во время выстрела должна себя вести точно также как башня молнии (сжематься) ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Сама огненная башня во время выстрела должна себя вести точно также как башня молнии (сжематься) ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Огоньки поджега не должны быть за пределами цели это не красиво. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Когда я говорил про эффект огоньков на сферах я имел ввиду сферы которые крутятся на вершине огненной башне. А ты изменил эффект огоньков поджега. Верни эффект поджега как был а на сфере башни сделай как я просил. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Когда я говорил про эффект огоньков на сферах я имел ввиду сферы которые крутятся на вершине огненной башне. А ты изменил эффект огоньков поджега. Верни эффект поджега как был а на сфере башни сделай как я просил. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Снаряды не нужно подкрашивать пусть будут как есть. И еще я заметил что не регестрируется попадание хотя снаряд точно задел врага
User prompt
Для снаряжов используй fire_circle
User prompt
Все равно эффект огоньков на сферах не так как надо. Они должны подниматься вверх и уменьшаться исчезая ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Эффект огоньков вокруг сфер должен быть шире они как-то в кучу ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
По скольку снарядов стало меньше нужно чтобы выстрелы через один стреляли со смещением так чтобы последующий летел между двумя предидущими так сказать заполнял пробелы. Сохранив количество снарядов (17) ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
По скольку снарядов стало меньше нужно чтобы выстрелы через один стреляли со смещением так чтобы последующий летел между двумя предидущими так сказать заполнял пробелы ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'Timeout.tick error: Cannot read properties of null (reading '__colorFlash_current_tick')' in or related to this line: 'LK.effects.flashObject(toTarget.graphic, 0xFF0000, 100);' Line Number: 4732
User prompt
И нужно сократить частоту до одного выстрела в секунду и количество снарядов до 17
User prompt
Отлично. Теперь я хочу чтобы снаряды разлетались только на 180 градусов. Так как враги всегда только сверху будут падать то стрелять вниз бессмысленно.
/**** * 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; 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.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 var FallingObject = Container.expand(function (assetId, initialHealth, fallSpeed, objectType) { var self = Container.call(this); self.takeDamage = function (damageAmount) { if (self.isDestroyed) return; // 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 // 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 floating damage number var damageNumber = new Text2(finalDamage.toString(), { size: isCritical ? 80 : 60, fill: isCritical ? 0xFF0000 : 0xFFFFFF, align: 'center', font: 'Impact' }); damageNumber.anchor.set(0.5, 0.5); damageNumber.x = self.x + (Math.random() - 0.5) * 40; // Slight random offset damageNumber.y = self.y - 20; game.addChild(damageNumber); game.damageNumbers.push(damageNumber); // Animate damage number floating up without fading out tween(damageNumber, { y: damageNumber.y - 120, scaleX: 1.2, scaleY: 1.2 }, { duration: 1200, easing: tween.easeOut }); // Remove damage number after timeout LK.setTimeout(function () { if (damageNumber.parent) { game.removeChild(damageNumber); var index = game.damageNumbers.indexOf(damageNumber); if (index > -1) game.damageNumbers.splice(index, 1); } }, 1200); // 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; 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 } // 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; } // 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 = []; } // Limit fragment creation to prevent excessive memory usage var maxFragments = Math.min(3 + Math.floor(Math.random() * 4), 6); // Cap at 6 fragments var 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; // Random scale between 0.4 and 1.2 var kusokFragment = LK.getAsset('kusok', { anchorX: 0.5, anchorY: 0.5, scaleX: randomScale, scaleY: randomScale }); kusokFragment.x = self.x; kusokFragment.y = self.y; game.addChild(kusokFragment); // Track fragment in particles array for cleanup if (!game.particles) game.particles = []; game.particles.push(kusokFragment); // 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; // Random rotation speed and direction for each fragment (slower rotation) 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(kusokFragment, { x: kusokFragment.x + velocityX, y: kusokFragment.y + velocityY, scaleX: 0, scaleY: 0, rotation: kusokFragment.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); } }; }(kusokFragment) }); } // 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) }); } // Play boom sound when object is destroyed LK.getSound('Boom').play(); // Only give money if specified if (giveMoney) { // Points for destroying object var moneyEarned = self.objectType === 'meteor' ? 10 : 15; LK.setScore(LK.getScore() + moneyEarned); updateScoreDisplay(); // Create floating money number display var moneyNumber = new Text2('$' + moneyEarned.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; // 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) return; self.y += self.speedY; // Health display disabled - no position updates needed // 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.3 + Math.random() * 0.4; 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.3, scaleY: 0.3 }); // 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.15, scaleY: 0.15 }, { 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.15 + Math.random() * 0.1, scaleY: 0.15 + Math.random() * 0.1 }, { 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.health -= burnDamage; self.lastBurnDamageTime = currentTime; // Create floating burn damage number var burnDamageNumber = new Text2(burnDamage.toString(), { size: 50, fill: 0xFF8C00, // Orange color for burn damage align: 'center', font: 'Impact' }); burnDamageNumber.anchor.set(0.5, 0.5); burnDamageNumber.x = self.x - 60 + (Math.random() - 0.5) * 20; // Position to the left of regular damage burnDamageNumber.y = self.y - 20; game.addChild(burnDamageNumber); // Animate burn damage number tween(burnDamageNumber, { y: burnDamageNumber.y - 80, scaleX: 1.1, scaleY: 1.1 }, { duration: 1000, easing: tween.easeOut }); // Remove burn damage number after timeout LK.setTimeout(function () { if (burnDamageNumber.parent) { game.removeChild(burnDamageNumber); } }, 1000); // 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.3, scaleY: 0.3 }); // 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.15, scaleY: 0.15 }, { 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.15 + Math.random() * 0.1, scaleY: 0.15 + Math.random() * 0.1 }, { 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; // Create floating fire tower burn damage number var fireTowerBurnDamageNumber = new Text2(fireTowerBurnDamage.toString(), { size: 50, fill: 0xFF2500, // More intense orange-red color for fire tower burn damage align: 'center', font: 'Impact' }); fireTowerBurnDamageNumber.anchor.set(0.5, 0.5); fireTowerBurnDamageNumber.x = self.x + 60 + (Math.random() - 0.5) * 20; // Position to the right of regular damage fireTowerBurnDamageNumber.y = self.y - 20; game.addChild(fireTowerBurnDamageNumber); // Animate fire tower burn damage number tween(fireTowerBurnDamageNumber, { y: fireTowerBurnDamageNumber.y - 80, scaleX: 1.1, scaleY: 1.1 }, { duration: 1000, easing: tween.easeOut }); // Remove fire tower burn damage number after timeout LK.setTimeout(function () { if (fireTowerBurnDamageNumber.parent) { game.removeChild(fireTowerBurnDamageNumber); } }, 1000); // 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.3, scaleY: 0.3 }); // 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.15, scaleY: 0.15 }, { 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.15 + Math.random() * 0.1, scaleY: 0.15 + Math.random() * 0.1 }, { 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; // Create floating fire tower burn damage number var fireTowerBurnDamageNumber = new Text2(fireTowerBurnDamage.toString(), { size: 50, fill: 0xFF2500, // More intense orange-red color for fire tower burn damage align: 'center', font: 'Impact' }); fireTowerBurnDamageNumber.anchor.set(0.5, 0.5); fireTowerBurnDamageNumber.x = self.x + 60 + (Math.random() - 0.5) * 20; // Position to the right of regular damage fireTowerBurnDamageNumber.y = self.y - 20; game.addChild(fireTowerBurnDamageNumber); // Animate fire tower burn damage number tween(fireTowerBurnDamageNumber, { y: fireTowerBurnDamageNumber.y - 80, scaleX: 1.1, scaleY: 1.1 }, { duration: 1000, easing: tween.easeOut }); // Remove fire tower burn damage number after timeout LK.setTimeout(function () { if (fireTowerBurnDamageNumber.parent) { game.removeChild(fireTowerBurnDamageNumber); } }, 1000); // 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 = []; } } }; // Set the update method to call updateParent by default self.update = self.updateParent; // Initialization self.graphic = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); self.health = initialHealth; self.maxHealth = initialHealth; // 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.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 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 5) if (self.freezeStacks < 5) { 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 5 stacks reached - full freeze if (self.freezeStacks >= 5) { // Play ice block creation sound LK.getSound('icetresk').play(); 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; }); var MeteorFast = FallingObject.expand(function () { var self = FallingObject.call(this, 'meteorfast1', 120, METEOR_SPEED * 1.35, 'meteor'); // 60% health, 35% faster speed (reduced from 80% faster) // Add rotation animation to meteor self.rotationSpeed = 0.008 + Math.random() * 0.015; // Faster rotation than regular meteor self.rotationDirection = Math.random() < 0.5 ? 1 : -1; // Random direction (clockwise or counterclockwise) self.update = function () { if (self.isDestroyed) return; // Call parent update method to handle health display and burn effects self.updateParent(); // Add rotation self.graphic.rotation += self.rotationSpeed * self.rotationDirection; }; return self; }); var Meteor = FallingObject.expand(function () { var self = FallingObject.call(this, 'meteor', METEOR_HEALTH, METEOR_SPEED / 2, 'meteor'); // Add rotation animation to meteor self.rotationSpeed = 0.005 + Math.random() * 0.01; // Random rotation speed between 0.005 and 0.015 radians per frame self.rotationDirection = Math.random() < 0.5 ? 1 : -1; // Random direction (clockwise or counterclockwise) self.update = function () { if (self.isDestroyed) return; // Call parent update method to handle health display and burn effects self.updateParent(); // Add rotation self.graphic.rotation += self.rotationSpeed * self.rotationDirection; }; return self; }); var Alien = FallingObject.expand(function () { var self = FallingObject.call(this, 'alien', ALIEN_HEALTH, ALIEN_SPEED / 2, 'alien'); // Alien-specific properties or methods (e.g., different movement pattern) 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 = 1000; 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.health -= self.damage; LK.effects.flashObject(self.target.graphic, 0xFF0000, 100); // Red flash on hit // Create floating damage number without critical hit logic var damageNumber = new Text2(self.damage.toString(), { size: 60, fill: 0xFFFFFF, align: 'center', font: 'Impact' }); damageNumber.anchor.set(0.5, 0.5); damageNumber.x = self.target.x + (Math.random() - 0.5) * 40; // Slight random offset damageNumber.y = self.target.y - 20; game.addChild(damageNumber); if (!game.damageNumbers) game.damageNumbers = []; game.damageNumbers.push(damageNumber); // Animate damage number floating up tween(damageNumber, { y: damageNumber.y - 120, scaleX: 1.2, scaleY: 1.2 }, { duration: 1200, easing: tween.easeOut }); // Remove damage number after timeout LK.setTimeout(function () { if (damageNumber.parent) { game.removeChild(damageNumber); var index = game.damageNumbers.indexOf(damageNumber); if (index > -1) game.damageNumbers.splice(index, 1); } }, 1200); // Add pulsing scale animation when hit tween.stop(self.target.graphic, { scaleX: true, scaleY: true }); // Stop any existing scale tweens tween(self.target.graphic, { scaleX: 1.3, scaleY: 1.3 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(self.target.graphic, { scaleX: 1, scaleY: 1 }, { duration: 150, easing: tween.easeIn }); } }); if (self.target.health <= 0) { self.target.destroySelf(); } // Create explosion effect using Dimraket asset for (var p = 0; p < 8; p++) { var particle = LK.getAsset('Dimraket', { anchorX: 0.5, anchorY: 0.5, scaleX: 3.0 + Math.random() * 2.0, scaleY: 3.0 + Math.random() * 2.0 }); particle.x = self.x; particle.y = self.y; particle.alpha = 1.0; // Fully opaque game.addChild(particle); // Animate explosion particles var angle = Math.random() * Math.PI * 2; var speed = 100 + Math.random() * 100; var velX = Math.cos(angle) * speed; var velY = Math.sin(angle) * speed; // Random rotation speed and direction for each particle (slower rotation) var rotationSpeed = 0.01 + Math.random() * 0.03; // Random rotation speed between 0.01 and 0.04 radians per frame (much slower) var rotationDirection = Math.random() < 0.5 ? 1 : -1; // Random direction (clockwise or counterclockwise) var totalRotation = rotationSpeed * rotationDirection * 45; // Total rotation over first phase (45 frames at 60fps) // No color changes - use original Dimraket asset colors tween(particle, { x: particle.x + velX, y: particle.y + velY, rotation: particle.rotation + totalRotation * 2, // Total rotation over entire animation scaleX: 0, scaleY: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { if (particle.parent) { particle.parent.removeChild(particle); } } }); } // Play explosion sound LK.getSound('raketaudar').play(); } self.destroyRocket(); }; self.destroyRocket = function () { if (self.isDestroyed) return; self.isDestroyed = true; // Stop flame pulsation animation if (self.flameGraphic) { tween.stop(self.flameGraphic); } // Remove from active rockets list var index = activeRockets.indexOf(self); if (index > -1) { activeRockets.splice(index, 1); } // Remove from game if (self.parent) { self.parent.removeChild(self); } }; return self; }); var VirtualJoystick = Container.expand(function () { var self = Container.call(this); self.initialize = function (baseAssetId, knobAssetId, visualRadius, interactionRadius) { // baseGraphic and knobGraphic are children of 'self' (the joystick container) // Their positions (0,0) will be the center of 'self'. self.baseGraphic = self.addChild(LK.getAsset(baseAssetId, { anchorX: 0.5, anchorY: 0.5 })); self.knobGraphic = self.addChild(LK.getAsset(knobAssetId, { anchorX: 0.5, anchorY: 0.5 })); self.visualRadius = visualRadius; // Max distance knob can move visually self.interactionRadius = interactionRadius; // Touchable area around joystick center self.active = false; self.touchId = null; self.deltaX = 0; // Normalized movement vector x (-1 to 1) self.deltaY = 0; // Normalized movement vector y (-1 to 1) self.initialTouchX = 0; // Track initial touch position self.initialTouchY = 0; // Track initial touch position }; self.processDown = function (gameGlobalX, gameGlobalY, eventObj, treatTouchAsLocalOrigin) { var localClickPos; if (treatTouchAsLocalOrigin) { // If the joystick was just repositioned to gameGlobalX, gameGlobalY (the touch location), // then this touch point is effectively at the joystick's new local origin (0,0). localClickPos = { x: 0, y: 0 }; } else { // Standard case: joystick is already positioned. Convert global touch to local. localClickPos = self.toLocal({ x: gameGlobalX, y: gameGlobalY }); } var distSq = localClickPos.x * localClickPos.x + localClickPos.y * localClickPos.y; if (distSq <= self.interactionRadius * self.interactionRadius) { self.active = true; self.touchId = eventObj.identifier; // Use identifier from the event object // Always start knob at center when joystick is activated self.knobGraphic.x = 0; self.knobGraphic.y = 0; self.deltaX = 0; self.deltaY = 0; // Store the initial touch position to avoid coordinate transformation issues self.initialTouchX = gameGlobalX; self.initialTouchY = gameGlobalY; return true; // Consumed event } return false; }; self.processMove = function (gameGlobalX, gameGlobalY, eventObj) { if (!self.active || eventObj.identifier !== undefined && eventObj.identifier !== self.touchId) { return; } // Calculate movement relative to initial touch position var deltaX = gameGlobalX - self.initialTouchX; var deltaY = gameGlobalY - self.initialTouchY; self.updateKnobPosition(deltaX, deltaY); }; self.processUp = function (eventObj) { if (!self.active || eventObj.identifier !== undefined && eventObj.identifier !== self.touchId) { return; } self.active = false; self.touchId = null; self.knobGraphic.x = 0; // Reset to center of joystick container self.knobGraphic.y = 0; self.deltaX = 0; self.deltaY = 0; }; self.updateKnobPosition = function (localX, localY) { var dx = localX; var dy = localY; var distance = Math.sqrt(dx * dx + dy * dy); var clampedX = dx; var clampedY = dy; if (distance > self.visualRadius) { clampedX = dx / distance * self.visualRadius; clampedY = dy / distance * self.visualRadius; } self.knobGraphic.x = clampedX; self.knobGraphic.y = clampedY; if (self.visualRadius === 0) { // Avoid division by zero if radius is 0 self.deltaX = 0; self.deltaY = 0; } else { self.deltaX = clampedX / self.visualRadius; self.deltaY = clampedY / self.visualRadius; } self.deltaX = Math.max(-1, Math.min(1, self.deltaX)); self.deltaY = Math.max(-1, Math.min(1, self.deltaY)); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x0a0a18 // Even darker blue for space }); /**** * Game Code ****/ // --- Game Constants --- // Brownish meteor // Lime green alien (can be simple box for now) // Grey city base // A simple red circle for the crosshair var WORLD_WIDTH = 2048; var WORLD_HEIGHT = 2732; var METEOR_HEALTH = 2000; var METEOR_SPEED = 3; // Pixels per frame var METEOR_DAMAGE_TO_CITY = 75; var ALIEN_HEALTH = 3000; var ALIEN_SPEED = 2.5; 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) var FIRE_DAMAGE = 35; // Damage per shot from crosshair var SPAWN_INTERVAL_MS_MIN = 1200; // Minimum time between spawns (halved) var SPAWN_INTERVAL_MS_MAX = 2500; // Maximum time between spawns (halved) // --- 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 spawnPausedTime = null; // Track when 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 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 = [141000, 117000, 93000]; // music1: 2:21, music2: 1:57, music3: 1:33 (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; if (randomValue < 0.4) { // 40% regular meteors newObject = new Meteor(); } else if (randomValue < 0.65) { // 25% fast meteors newObject = new MeteorFast(); } else { newObject = new Alien(); // 35% aliens } // 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); } 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; } // 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 = []; // 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(10000); // 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); // Create object counter display objectCountText = new Text2('Objects: 0', { size: 50, fill: 0xFFFFFF, align: 'left', font: 'Impact' }); objectCountText.anchor.set(0, 0); objectCountText.x = 10; objectCountText.y = 150; game.addChild(objectCountText); // --- 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 cooldown variables var healthRestoreCooldownActive = false; var healthRestoreCooldownEndTime = 0; var healthRestoreCooldownPausedTime = 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 // 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) { return 10 + level * 10; }, max: 10 }, { key: 'fireRate', name: 'Скорострельность +', desc: 'Уменьшить задержку между выстрелами', tooltip: 'Уменьшает время между выстрелами, позволяя стрелять чаще.', cost: function cost(level) { return 10 + level * 10; }, max: 10 }, { key: 'critChance', name: 'Критический урон +', desc: 'Увеличить шанс двойного урона', tooltip: 'Увеличивает шанс нанести двойной урон выстрелом. Каждый уровень добавляет +5% шанса критического удара.', cost: function cost(level) { return 10 + level * 10; }, max: 10 }, { key: 'lightningChance', name: 'Шанс молнии +', desc: 'Увеличить шанс удара молнии', tooltip: 'Добавляет шанс поражения целей молнией, наносящей урон и перескакивающей на ближайших врагов. Каждый уровень добавляет +5% шанса.', cost: function cost(level) { return 10 + level * 10; }, max: 10 }, { key: 'fire', name: 'Шанс поджога +', desc: 'Урон огнем со временем', tooltip: 'Добавляет шанс поджечь врагов, наносящий урон в течение 10 секунд. Огонь накладывается до 10 раз, увеличивая урон. Каждый уровень добавляет +5% шанса.', cost: function cost(level) { return 10 + level * 10; }, max: 10 }, { key: 'freeze', name: 'Шанс заморозки +', desc: 'Замедление и заморозка врагов', tooltip: 'Добавляет шанс замедлить врагов на 50%. При 5 стаках враги полностью замерзают на 4 секунды. Каждый уровень добавляет +5% шанса.', cost: function cost(level) { return 10 + level * 10; }, 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 // 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 5 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%. При 5 стаках враги полностью замерзают на 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 5 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%. À 5 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 5 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. 5 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 } // 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), and icon 5 (i === 4) if (i !== 0 && i !== 1 && i !== 2 && i !== 3 && i !== 4) { 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 = 10; 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 = 10; 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 = 50; 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 = 150; 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); } // Button interaction iconBg.interactive = true; iconBg.buttonMode = true; iconBg.on('down', function (x, y, obj) { // Animate shop icon on purchase (pulse effect, like upgrades) if (!iconBg.originalScaleX) { iconBg.originalScaleX = iconBg.scaleX; iconBg.originalScaleY = iconBg.scaleY; } tween.stop(iconBg, { scaleX: true, scaleY: true }); tween(iconBg, { scaleX: iconBg.originalScaleX * 1.15, scaleY: iconBg.originalScaleY * 1.15 }, { duration: 120, easing: tween.easeOut, onFinish: function onFinish() { tween(iconBg, { scaleX: iconBg.originalScaleX, scaleY: iconBg.originalScaleY }, { duration: 120, easing: tween.easeIn }); } }); if (i === 0) { // Shop item 1: Gradual city health restore var cityHealPrice = 10; // Check if cooldown is active if (healthRestoreCooldownActive) { return; } if (LK.getScore() < cityHealPrice) { LK.effects.flashObject(priceText, 0xFF0000, 300); 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 + 60000; // 60 seconds healthRestoreCooldownPausedTime = null; LK.setScore(LK.getScore() - cityHealPrice); updateScoreDisplay(); LK.getSound('Pokupka').play(); // Gradually restore 70 health per second for 12 seconds (total 840 health max) var healPerSecond = 70; var healDuration = 12000; var healInterval = 1000; // 1 second per tick var healTicks = Math.floor(healDuration / healInterval); var ticksDone = 0; // Track pause time for healing when menus are open cityInstance._healingPausedTime = null; // Prevent multiple concurrent heals if (cityInstance._healingInterval) { LK.clearInterval(cityInstance._healingInterval); cityInstance._healingInterval = null; } cityInstance._healingInterval = LK.setInterval(function () { // Pause healing when any menu is open if (upgradesPopup || controlsPopup || shopPopup) { if (!cityInstance._healingPausedTime) { cityInstance._healingPausedTime = LK.ticks * (1000 / 60); } return; // Skip healing this tick } // Resume healing after menu closes - adjust tick count if (cityInstance._healingPausedTime) { // Don't adjust anything, just clear the pause time cityInstance._healingPausedTime = null; } if (cityInstance.health < CITY_MAX_HEALTH) { var healAmount = healPerSecond; // Clamp to max health if (cityInstance.health + healAmount > CITY_MAX_HEALTH) { healAmount = CITY_MAX_HEALTH - cityInstance.health; } var prevHealth = cityInstance.health; cityInstance.health += healAmount; if (cityInstance.health > CITY_MAX_HEALTH) cityInstance.health = CITY_MAX_HEALTH; ticksDone++; // 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 cityInstance.healthBarFill.width = newWidth; } }); } // Animate green bar for healing (flash effect) - but don't use LK.effects.flashObject // Instead, do a gentle green tint that doesn't interfere with width tween(cityInstance.healthBarFill, { tint: 0x00FF88 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(cityInstance.healthBarFill, { tint: 0xFFFFFF }, { duration: 100, easing: tween.easeIn }); } }); if (cityInstance.health >= CITY_MAX_HEALTH || ticksDone >= healTicks) { cityInstance.health = Math.min(cityInstance.health, CITY_MAX_HEALTH); // Clear the healing interval without additional visual updates LK.clearInterval(cityInstance._healingInterval); cityInstance._healingInterval = null; } } else { LK.clearInterval(cityInstance._healingInterval); cityInstance._healingInterval = null; } }, healInterval); } else if (i === 1) { // Shop item 2: City Shield var shieldPrice = 10; // Check if player has enough money if (LK.getScore() < shieldPrice) { LK.effects.flashObject(priceText, 0xFF0000, 300); 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; } // 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) + 30000; // 30 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; } else if (i === 2) { // Shop item 3: Rocket Barrage - fires homing rockets every second var rocketBarragePrice = 50; // Check if player has enough money if (LK.getScore() < rocketBarragePrice) { LK.effects.flashObject(priceText, 0xFF0000, 300); 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; } // 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; } 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.effects.flashObject(priceText, 0xFF0000, 300); 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; } // 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. } else if (i === 4) { // Shop item 5: Fire Tower - creates powerful fire tower that shoots fire rings var fireTowerPrice = 200; // Check if player has enough money if (LK.getScore() < fireTowerPrice) { LK.effects.flashObject(priceText, 0xFF0000, 300); 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; } // 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 } 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) 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') { FIRE_RATE_MS = 1000 - 176 * upgrades.fireRate; if (FIRE_RATE_MS < 120) FIRE_RATE_MS = 120; } 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 = []; } // 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 () { // Block all game updates while upgrades, controls, or shop popup is open if (upgradesPopup || controlsPopup || shopPopup || currentTooltip) { // Pause spawn timer when popup is open if (!spawnPausedTime) { spawnPausedTime = LK.ticks * (1000 / 60); } return; } var currentTime = LK.ticks * (1000 / 60); // Approximate current time in ms // Resume spawn timer when popup is closed if (spawnPausedTime) { var pauseDuration = currentTime - spawnPausedTime; nextSpawnTime += pauseDuration; // Extend spawn time by pause duration spawnPausedTime = null; } // 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 health restore cooldown if (healthRestoreCooldownActive) { if (currentTime >= healthRestoreCooldownEndTime) { // Cooldown finished healthRestoreCooldownActive = false; healthRestoreCooldownEndTime = 0; } } // 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); // 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.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); } // 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.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; // 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 movement and collision checking fireProjectile.update = function () { if (!this.parent) return; // Move projectile this.x += this.velocityX; this.y += this.velocityY; // 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(); // Create fire projectile damage number var projectileDamageNumber = new Text2(projectileDamage.toString(), { size: 65, fill: 0xFF4500, align: 'center', font: 'Impact' }); projectileDamageNumber.anchor.set(0.5, 0.5); projectileDamageNumber.x = enemy.x + (Math.random() - 0.5) * 40; projectileDamageNumber.y = enemy.y - 25; game.addChild(projectileDamageNumber); // Animate projectile damage number tween(projectileDamageNumber, { y: projectileDamageNumber.y - 120, scaleX: 1.2, scaleY: 1.2 }, { duration: 1200, easing: tween.easeOut }); // Remove projectile damage number after timeout LK.setTimeout(function () { if (projectileDamageNumber.parent) { game.removeChild(projectileDamageNumber); } }, 1200); // 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 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(); } } } // 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 // 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(); } // Create floating lightning damage number 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.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 // 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(); } // Create floating lightning damage number for chain targets var chainLightningDamageNumber = new Text2(chainLightningDamage.toString(), { size: 60, fill: 0x00FFFF, // Blue color for lightning damage align: 'center', font: 'Impact' }); 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(); nextSpawnTime = currentTime + SPAWN_INTERVAL_MS_MIN + Math.random() * (SPAWN_INTERVAL_MS_MAX - SPAWN_INTERVAL_MS_MIN); } // 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.parent === game) { game.removeChild(objectCountText); game.addChild(objectCountText); } };
===================================================================
--- original.js
+++ change.js
@@ -1758,13 +1758,13 @@
/****
* Game Code
****/
-// A simple red circle for the crosshair
-// Grey city base
-// Lime green alien (can be simple box for now)
-// Brownish meteor
// --- Game Constants ---
+// Brownish meteor
+// Lime green alien (can be simple box for now)
+// Grey city base
+// A simple red circle for the crosshair
var WORLD_WIDTH = 2048;
var WORLD_HEIGHT = 2732;
var METEOR_HEALTH = 2000;
var METEOR_SPEED = 3; // Pixels per frame
@@ -4758,32 +4758,32 @@
}(fireParticle2)
});
}
}
- // Fire tower releases 17 fire projectiles in 180 degree arc upward every 800ms
- if (currentTime >= lastFireStrikeTime + 800) {
+ // Fire tower releases 17 fire projectiles in 180 degree arc upward every 1000ms
+ if (currentTime >= lastFireStrikeTime + 1000) {
lastFireStrikeTime = currentTime;
// 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 animation before shooting - compress down like Tesla tower
+ // Fire tower compression before shooting - compress down like Tesla tower
if (fireTowerGraphic && fireTowerGraphic.parent) {
- var originalTowerScaleX = fireTowerGraphic.scaleX;
- var originalTowerScaleY = fireTowerGraphic.scaleY;
+ var originalFireTowerScaleX = fireTowerGraphic.scaleX;
+ var originalFireTowerScaleY = fireTowerGraphic.scaleY;
tween.stop(fireTowerGraphic, {
scaleX: true,
scaleY: true
});
tween(fireTowerGraphic, {
- scaleY: originalTowerScaleY * 0.85 // Compress downward only
+ scaleY: originalFireTowerScaleY * 0.85 // Compress downward only
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
if (fireTowerGraphic) {
tween(fireTowerGraphic, {
- scaleX: originalTowerScaleX,
- scaleY: originalTowerScaleY
+ scaleX: originalFireTowerScaleX,
+ scaleY: originalFireTowerScaleY
}, {
duration: 100,
easing: tween.easeIn
});
Метеорит без огня пастельные цвета 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