/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var Asteroid = Container.expand(function (x, y) { var self = Container.call(this); self.x = x; self.y = y; self.health = 150; self.maxHealth = 150; self.isDestroyed = false; self.gridX = Math.floor((x - grid.x) / CELL_SIZE); self.gridY = Math.floor((y - grid.y) / CELL_SIZE); var asteroidGraphics = self.attachAsset('asteroid', { anchorX: 0.5, anchorY: 0.5 }); // Add rotation animation tween(asteroidGraphics, { rotation: Math.PI * 2 }, { duration: 8000, easing: tween.linear, onFinish: function onFinish() { if (!self.isDestroyed) { asteroidGraphics.rotation = 0; } } }); // Health bar var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarOutline.y = healthBar.y = -asteroidGraphics.height / 2 - 15; healthBarOutline.x = -healthBarOutline.width / 2; healthBar.x = -healthBar.width / 2; healthBar.tint = 0x8B4513; self.healthBar = healthBar; self.takeDamage = function (damage) { self.health -= damage; if (self.health <= 0) { self.health = 0; self.destroy(); } else { self.healthBar.width = self.health / self.maxHealth * 70; } }; self.down = function () { // Allow manual destruction by tapping self.takeDamage(50); }; self.destroy = function () { if (self.isDestroyed) return; self.isDestroyed = true; // Create debris effect for (var i = 0; i < 5; i++) { var debris = new Container(); debris.x = self.x + (Math.random() - 0.5) * 100; debris.y = self.y + (Math.random() - 0.5) * 100; var debrisGraphics = debris.attachAsset('asteroidDebris', { anchorX: 0.5, anchorY: 0.5 }); game.addChild(debris); tween(debris, { x: debris.x + (Math.random() - 0.5) * 200, y: debris.y + (Math.random() - 0.5) * 200, alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { if (debris.parent) { debris.parent.removeChild(debris); } } }); } // Clear grid cell var cell = grid.getCell(self.gridX, self.gridY); if (cell) { cell.type = 0; } // Remove from asteroids array var asteroidIndex = asteroids.indexOf(self); if (asteroidIndex !== -1) { asteroids.splice(asteroidIndex, 1); } // Recalculate paths grid.pathFind(); grid.renderDebug(); tween.stop(self); if (self.parent) { self.parent.removeChild(self); } Container.prototype.destroy.call(self); }; // Block the grid cell var cell = grid.getCell(self.gridX, self.gridY); if (cell) { cell.type = 1; } return self; }); var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; // Create sci-fi projectile based on bullet type var assetName = 'bullet'; // default var bulletGraphics; if (self.type === 'rapid') { bulletGraphics = self.attachAsset('energyBeam', { anchorX: 0.5, anchorY: 0.5 }); bulletGraphics.tint = 0x00aaff; } else if (self.type === 'sniper') { bulletGraphics = self.attachAsset('laserBurst', { anchorX: 0.5, anchorY: 0.5 }); bulletGraphics.tint = 0xff0044; } else if (self.type === 'splash') { bulletGraphics = self.attachAsset('plasmaBolt', { anchorX: 0.5, anchorY: 0.5 }); bulletGraphics.tint = 0x44ff00; } else { bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); } // Add pulsing glow effect to projectiles tween(bulletGraphics, { scaleX: 1.3, scaleY: 1.3, alpha: 0.7 }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { tween(bulletGraphics, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 300, easing: tween.easeInOut }); } }); self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { returnBulletToPool(self); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Create sci-fi impact flash if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDestroyed) { tween(self.targetEnemy, { tint: 0xffffff }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDestroyed) { tween(self.targetEnemy, { tint: 0xffffff }, { duration: 200, easing: tween.easeIn }); } } }); } // Apply damage to target enemy self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; // Play enemy destroy sound LK.getSound('enemyDestroy').play(); } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; // Play enemy hit sound LK.getSound('enemyHit').play(); } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect with limiting if (canCreateEffect()) { var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); registerEffect(splashEffect); } // Determine splash radius and damage based on specialization var splashRadius = CELL_SIZE * 1.5; var splashDamageMultiplier = 0.5; if (self.specialization === 'nuclear') { splashRadius = CELL_SIZE * 3; splashDamageMultiplier = 0.8; // Create nuclear explosion effect if (canCreateEffect()) { var nuclearEffect = new Container(); nuclearEffect.x = self.targetEnemy.x; nuclearEffect.y = self.targetEnemy.y; var nuclearGraphics = nuclearEffect.attachAsset('nuclearBlast', { anchorX: 0.5, anchorY: 0.5 }); nuclearGraphics.tint = 0xFF0000; nuclearGraphics.alpha = 0.8; game.addChild(nuclearEffect); tween(nuclearEffect, { scaleX: 3, scaleY: 3, alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { if (nuclearEffect.parent) { nuclearEffect.parent.removeChild(nuclearEffect); } } }); } } // Splash damage to nearby enemies for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { var splashDamage = self.damage * splashDamageMultiplier; otherEnemy.health -= splashDamage; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } // Chain reaction effect if (self.specialization === 'chainreaction' && Math.random() < 0.3) { // 30% chance to chain to another enemy for (var j = 0; j < enemies.length; j++) { var chainEnemy = enemies[j]; if (chainEnemy !== otherEnemy && chainEnemy !== self.targetEnemy) { var chainDx = chainEnemy.x - otherEnemy.x; var chainDy = chainEnemy.y - otherEnemy.y; var chainDistance = Math.sqrt(chainDx * chainDx + chainDy * chainDy); if (chainDistance <= CELL_SIZE * 2) { chainEnemy.health -= splashDamage * 0.7; if (chainEnemy.health <= 0) { chainEnemy.health = 0; } else { chainEnemy.healthBar.width = chainEnemy.health / chainEnemy.maxHealth * 70; } break; } } } } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect with limiting if (canCreateEffect()) { var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); registerEffect(slowEffect); } // Apply slow effect // Make slow percentage scale with tower level (default 50%, up to 80% at max level) var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6 var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; // Slow by X% self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { self.targetEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'poison') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual poison effect with limiting if (canCreateEffect()) { var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); registerEffect(poisonEffect); } // Apply poison effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper with limiting if (canCreateEffect()) { var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); registerEffect(sniperEffect); } } returnBulletToPool(self); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; self.destroy = function () { if (self.isDestroyed) { return; } self.isDestroyed = true; // Stop all active tweens on this bullet and its graphics tween.stop(self); if (self.children && self.children[0]) { tween.stop(self.children[0]); } // Remove from target enemy's bullets array if (self.targetEnemy && self.targetEnemy.bulletsTargetingThis) { var bulletIndex = self.targetEnemy.bulletsTargetingThis.indexOf(self); if (bulletIndex !== -1) { self.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } // Remove from bullets array var bulletArrayIndex = bullets.indexOf(self); if (bulletArrayIndex !== -1) { bullets.splice(bulletArrayIndex, 1); } // Remove from parent container if (self.parent) { self.parent.removeChild(self); } // Nullify all object references self.targetEnemy = null; // Call parent destroy Container.prototype.destroy.call(self); }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); cellGraphics.tint = 0x220044; // Deep space color return; } numberLabel.visible = true; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; // Energy field blue } else { // Create starfield effect with bluish tint var starfieldTint = 0x001122 + (tint << 8) + (tint >> 1); cellGraphics.tint = starfieldTint; } while (debugArrows.length > data.targets.length) { self.removeChild(debugArrows.pop()); } for (var a = 0; a < data.targets.length; a++) { var destination = data.targets[a]; var ox = destination.x - data.x; var oy = destination.y - data.y; var angle = Math.atan2(oy, ox); if (!debugArrows[a]) { debugArrows[a] = LK.getAsset('arrow', { anchorX: -.5, anchorY: 0.5 }); debugArrows[a].alpha = .5; self.addChildAt(debugArrows[a], 1); } debugArrows[a].rotation = angle; } break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0x008800; numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; // Create energy particles around main effect var particles = []; for (var i = 0; i < 6; i++) { var particle = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); particle.width = 8; particle.height = 8; var angle = i / 6 * Math.PI * 2; particle.x = Math.cos(angle) * 30; particle.y = Math.sin(angle) * 30; particles.push(particle); } switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; for (var i = 0; i < particles.length; i++) { particles[i].tint = 0x88FF44; } break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; for (var i = 0; i < particles.length; i++) { particles[i].tint = 0xCC44FF; } break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; for (var i = 0; i < particles.length; i++) { particles[i].tint = 0x44FFCC; } break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; for (var i = 0; i < particles.length; i++) { particles[i].tint = 0xFF8844; } break; } effectGraphics.alpha = 0.7; // Animate particles spiraling outward for (var i = 0; i < particles.length; i++) { var particle = particles[i]; var delay = i * 50; tween(particle, { x: particle.x * 2, y: particle.y * 2, scaleX: 0.5, scaleY: 0.5, alpha: 0 }, { duration: 400, delay: delay, easing: tween.easeOut }); } self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); self.destroy = function () { if (self.isDestroyed) { return; } self.isDestroyed = true; // Stop all active tweens on this effect and its children tween.stop(self); for (var i = 0; i < self.children.length; i++) { if (self.children[i]) { tween.stop(self.children[i]); } } // Remove from activeEffects array var effectIndex = activeEffects.indexOf(self); if (effectIndex !== -1) { activeEffects.splice(effectIndex, 1); } // Remove from parent container if (self.parent) { self.parent.removeChild(self); } // Call parent destroy Container.prototype.destroy.call(self); }; return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; self.isDestroyed = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; // Add visual type indicator for better enemy identification if (self.type !== 'normal') { var typeIndicator = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); typeIndicator.width = 16; typeIndicator.height = 16; typeIndicator.y = -enemyGraphics.height / 2 - 35; // Above health bar // Color code the indicator based on enemy type switch (self.type) { case 'fast': typeIndicator.tint = 0x00AAFF; break; case 'immune': typeIndicator.tint = 0xAA0000; break; case 'flying': typeIndicator.tint = 0xFFFF00; break; case 'swarm': typeIndicator.tint = 0xFF00FF; break; } // Make boss indicators larger and more prominent if (self.isBoss) { typeIndicator.width = 24; typeIndicator.height = 24; typeIndicator.tint = 0xFFD700; // Gold for bosses // Add pulsing effect for boss indicator tween(typeIndicator, { scaleX: 1.3, scaleY: 1.3, alpha: 0.7 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(typeIndicator, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 800, easing: tween.easeInOut }); } }); } } self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Improved visual clarity for enemy types with better differentiation var baseTint = 0xFFFFFF; // First set base tint based on enemy type for better visual clarity switch (self.type) { case 'fast': baseTint = 0x4488FF; // Bright blue for fast enemies break; case 'immune': baseTint = 0xFF4444; // Bright red for immune enemies break; case 'flying': baseTint = 0xFFDD44; // Bright yellow for flying enemies break; case 'swarm': baseTint = 0xFF44DD; // Bright magenta for swarm enemies break; case 'normal': default: baseTint = 0xCCCCCC; // Light gray for normal enemies break; } // Apply boss scaling to tint if this is a boss if (self.isBoss) { // Make boss enemies more vibrant and add a red outline effect var r = baseTint >> 16 & 0xFF; var g = baseTint >> 8 & 0xFF; var b = baseTint & 0xFF; // Boost color intensity for bosses r = Math.min(255, Math.floor(r * 1.3)); g = Math.min(255, Math.floor(g * 1.3)); b = Math.min(255, Math.floor(b * 1.3)); baseTint = r << 16 | g << 8 | b; } // Then apply status effect overlays if (self.isImmune) { // Keep immune tint but add pulsing effect for better visibility enemyGraphics.tint = baseTint; // Add shield shimmer effect for immune enemies if (!self.shieldEffect && !self.isDestroyed) { self.shieldEffect = true; tween(enemyGraphics, { alpha: 0.8 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.isDestroyed) { tween(enemyGraphics, { alpha: 1.0 }, { duration: 500, easing: tween.easeInOut }); } } }); } } else if (self.poisoned && self.slowed) { // Blend poison green with slow purple over the base tint enemyGraphics.tint = 0x4C7FD4; // Keep existing blend color } else if (self.poisoned) { // Poison overlay - green tint var r = Math.floor((baseTint >> 16 & 0xFF) * 0.5); var g = Math.min(255, Math.floor((baseTint >> 8 & 0xFF) * 0.5 + 170)); var b = Math.floor((baseTint & 0xFF) * 0.5 + 85); enemyGraphics.tint = r << 16 | g << 8 | b; // Add crackling energy effect for poisoned if (!self.poisonFlicker && !self.isDestroyed) { self.poisonFlicker = true; tween(enemyGraphics, { scaleX: 1.1, scaleY: 1.1 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { if (!self.isDestroyed) { tween(enemyGraphics, { scaleX: 1.0, scaleY: 1.0 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { self.poisonFlicker = false; } }); } } }); } } else if (self.slowed) { // Slow overlay - purple tint var r = Math.floor((baseTint >> 16 & 0xFF) * 0.5 + 76); var g = Math.floor((baseTint >> 8 & 0xFF) * 0.5); var b = Math.min(255, Math.floor((baseTint & 0xFF) * 0.5 + 127)); enemyGraphics.tint = r << 16 | g << 8 | b; } else { enemyGraphics.tint = baseTint; self.shieldEffect = false; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05 && !self.isDestroyed) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; self.destroy = function () { if (self.isDestroyed) { return; } self.isDestroyed = true; // Stop all active tweens on this enemy and its graphics tween.stop(self); if (self.children && self.children[0]) { tween.stop(self.children[0]); } // Clear all bullets targeting this enemy for (var i = 0; i < self.bulletsTargetingThis.length; i++) { var bullet = self.bulletsTargetingThis[i]; if (bullet) { bullet.targetEnemy = null; } } self.bulletsTargetingThis = []; // Remove from enemies array var enemyIndex = enemies.indexOf(self); if (enemyIndex !== -1) { enemies.splice(enemyIndex, 1); } // Remove from parent container if (self.parent) { self.parent.removeChild(self); } // Clean up shadow if it's a flying enemy if (self.isFlying && self.shadow) { if (self.shadow.parent) { self.shadow.parent.removeChild(self.shadow); } self.shadow = null; } // Nullify all object references self.targetEnemy = null; self.currentTarget = null; self.flyingTarget = null; self.healthBar = null; self.energyCore = null; // Call parent destroy Container.prototype.destroy.call(self); }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; if (i > 11 - 3 && i <= 11 + 3) { if (j === 0) { cellType = 2; self.spawns.push(cell); } else if (j <= 4) { cellType = 0; } else if (j === gridHeight - 1) { cellType = 3; self.goals.push(cell); } else if (j >= gridHeight - 4) { cellType = 0; } } cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } // Mark junction cells for branching for (var i = 0; i < pathJunctions.length; i++) { var junction = pathJunctions[i]; var junctionCell = self.getCell(junction.gridX, junction.gridY); if (junctionCell) { junctionCell.isJunction = true; junctionCell.junction = junction; } } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 1 && node.left.type != 1) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.right.type != 1) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.right.type != 1) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.left.type != 1) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed; // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) var angle = Math.PI / 2; if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05 && !enemy.isDestroyed) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update enemy's position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // If enemy has now reached the entry area, update cell coordinates if (enemy.currentCellY >= 4) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05 && !enemy.isDestroyed) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (!enemy.currentTarget) { // Check if enemy is at a junction if (cell && cell.isJunction && !enemy.chosenBranch) { var junction = cell.junction; var chosenBranch = junction.choosePath(enemy); if (chosenBranch) { enemy.chosenBranch = chosenBranch; enemy.currentTarget = grid.getCell(chosenBranch.targetX, chosenBranch.targetY); // Show visual path indicator if (routeVisualizer && Math.random() < 0.3) { // Show for 30% of enemies routeVisualizer.showPaths(junction, enemy); } // Add notification for significant route changes if (Math.random() < 0.1) { // 10% chance var routeType = chosenBranch.isShortPath ? "direct" : "alternate"; var notification = game.addChild(new Notification("Enemy taking " + routeType + " route!")); notification.x = 2048 / 2; notification.y = grid.height - 100; } } else { enemy.currentTarget = cell.targets[0]; } } else { enemy.currentTarget = cell.targets[0]; } } if (enemy.currentTarget) { if (cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var Hero = Container.expand(function (heroType) { var self = Container.call(this); self.heroType = heroType || 'tank'; self.level = storage['hero_' + heroType + '_level'] || 1; self.experience = storage['hero_' + heroType + '_xp'] || 0; self.maxHealth = 200; self.health = self.maxHealth; self.isDestroyed = false; self.movementSpeed = 1.5; self.abilities = []; self.currentTarget = null; self.lastAbilityUse = 0; self.abilityCooldown = 300; // 5 seconds at 60fps // Set hero-specific stats switch (self.heroType) { case 'tank': self.maxHealth = 300 + (self.level - 1) * 50; self.movementSpeed = 1.0; self.abilityCooldown = 600; // 10 seconds break; case 'support': self.maxHealth = 150 + (self.level - 1) * 30; self.movementSpeed = 2.0; self.abilityCooldown = 480; // 8 seconds break; case 'dps': self.maxHealth = 180 + (self.level - 1) * 35; self.movementSpeed = 1.8; self.abilityCooldown = 360; // 6 seconds break; } self.health = self.maxHealth; // Get appropriate asset var assetName = 'hero_' + self.heroType; var heroGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); // Create health bar var healthBarOutline = self.attachAsset('heroHealthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('heroHealthBar', { anchorX: 0, anchorY: 0.5 }); healthBarOutline.y = healthBar.y = -heroGraphics.height / 2 - 20; healthBarOutline.x = -healthBarOutline.width / 2; healthBar.x = -healthBar.width / 2; self.healthBar = healthBar; // Create XP bar var xpBarOutline = self.attachAsset('heroXpBarOutline', { anchorX: 0, anchorY: 0.5 }); var xpBar = self.attachAsset('heroXpBar', { anchorX: 0, anchorY: 0.5 }); xpBarOutline.y = xpBar.y = -heroGraphics.height / 2 - 35; xpBarOutline.x = -xpBarOutline.width / 2; xpBar.x = -xpBar.width / 2; self.xpBar = xpBar; // Create ability icon var abilityIcon = self.attachAsset('heroAbilityIcon', { anchorX: 0.5, anchorY: 0.5 }); abilityIcon.y = heroGraphics.height / 2 + 20; self.abilityIcon = abilityIcon; // Level indicator var levelText = new Text2(self.level.toString(), { size: 40, fill: 0xFFFFFF, weight: 800 }); levelText.anchor.set(0.5, 0.5); levelText.y = heroGraphics.height / 2 + 20; levelText.x = 40; self.addChild(levelText); self.levelText = levelText; self.getExperienceForNextLevel = function () { return self.level * 100; // 100 XP per level }; self.gainExperience = function (amount) { self.experience += amount; var xpNeeded = self.getExperienceForNextLevel(); if (self.experience >= xpNeeded) { self.experience -= xpNeeded; self.level++; self.levelText.setText(self.level.toString()); // Save progression storage['hero_' + self.heroType + '_level'] = self.level; // Level up effects self.onLevelUp(); // Visual level up effect tween(heroGraphics, { scaleX: 1.3, scaleY: 1.3, tint: 0xFFD700 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(heroGraphics, { scaleX: 1.0, scaleY: 1.0, tint: 0xFFFFFF }, { duration: 300, easing: tween.easeIn }); } }); } // Save XP storage['hero_' + self.heroType + '_xp'] = self.experience; self.updateXpBar(); }; self.onLevelUp = function () { // Increase health var healthIncrease = self.heroType === 'tank' ? 50 : self.heroType === 'support' ? 30 : 35; self.maxHealth += healthIncrease; self.health = self.maxHealth; // Full heal on level up self.updateHealthBar(); }; self.updateHealthBar = function () { self.healthBar.width = self.health / self.maxHealth * 80; }; self.updateXpBar = function () { var xpProgress = self.experience / self.getExperienceForNextLevel(); self.xpBar.width = xpProgress * 80; }; self.useAbility = function () { if (LK.ticks - self.lastAbilityUse < self.abilityCooldown) { return false; // Ability on cooldown } self.lastAbilityUse = LK.ticks; // Play hero ability sound LK.getSound('heroAbility').play(); // Ability cooldown visual effect tween(self.abilityIcon, { tint: 0x888888, scaleX: 0.8, scaleY: 0.8 }, { duration: self.abilityCooldown, easing: tween.linear, onFinish: function onFinish() { tween(self.abilityIcon, { tint: 0xFFD700, scaleX: 1.0, scaleY: 1.0 }, { duration: 200, easing: tween.easeOut }); } }); switch (self.heroType) { case 'tank': return self.tankAbility(); case 'support': return self.supportAbility(); case 'dps': return self.dpsAbility(); } return false; }; self.tankAbility = function () { // Taunt - Draw all enemies within range to attack hero var tauntRadius = CELL_SIZE * 4; var taunted = 0; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= tauntRadius) { enemy.heroTarget = self; enemy.heroTargetDuration = 300; // 5 seconds taunted++; } } // Visual effect var effectRadius = tauntRadius; var tauntEffect = new Container(); tauntEffect.x = self.x; tauntEffect.y = self.y; var effectGraphics = tauntEffect.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.width = effectGraphics.height = effectRadius * 2; effectGraphics.tint = 0x4CAF50; effectGraphics.alpha = 0.6; game.addChild(tauntEffect); tween(tauntEffect, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { if (tauntEffect.parent) { tauntEffect.parent.removeChild(tauntEffect); } } }); return taunted > 0; }; self.supportAbility = function () { // Buff nearby towers var buffRadius = CELL_SIZE * 3; var buffed = 0; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.x - self.x; var dy = tower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= buffRadius) { tower.heroBuff = { damage: 1.5, fireRate: 0.7, duration: 600 // 10 seconds }; buffed++; // Visual effect on tower tween(tower.children[0], { tint: 0x2196F3 }, { duration: 600, easing: tween.linear, onFinish: function onFinish() { tower.children[0].tint = 0x445566; } }); } } return buffed > 0; }; self.dpsAbility = function () { // Area damage attack var damageRadius = CELL_SIZE * 2.5; var damaged = 0; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= damageRadius) { var damage = 100 + (self.level - 1) * 20; enemy.health -= damage; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } damaged++; // Visual damage effect tween(enemy, { tint: 0xFF5722 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(enemy, { tint: 0xFFFFFF }, { duration: 200, easing: tween.easeIn }); } }); } } // Visual explosion effect var explosionEffect = new Container(); explosionEffect.x = self.x; explosionEffect.y = self.y; var effectGraphics = explosionEffect.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.width = effectGraphics.height = damageRadius * 2; effectGraphics.tint = 0xFF5722; effectGraphics.alpha = 0.8; game.addChild(explosionEffect); tween(explosionEffect, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { if (explosionEffect.parent) { explosionEffect.parent.removeChild(explosionEffect); } } }); return damaged > 0; }; self.update = function () { if (self.health <= 0) { return; } // Update bars self.updateHealthBar(); self.updateXpBar(); // Move towards enemies or patrol if (!self.currentTarget) { self.findTarget(); } if (self.currentTarget && self.currentTarget.parent && self.currentTarget.health > 0) { var dx = self.currentTarget.x - self.x; var dy = self.currentTarget.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > CELL_SIZE * 0.8) { // Move towards target var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.movementSpeed; self.y += Math.sin(angle) * self.movementSpeed; } else { // Attack target if (self.heroType === 'tank') { self.currentTarget.health -= 15; } else if (self.heroType === 'dps') { self.currentTarget.health -= 25; } else { self.currentTarget.health -= 10; } if (self.currentTarget.health <= 0) { self.currentTarget.health = 0; self.gainExperience(20); self.currentTarget = null; } else { self.currentTarget.healthBar.width = self.currentTarget.health / self.currentTarget.maxHealth * 70; } } } else { self.currentTarget = null; } }; self.findTarget = function () { var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.health <= 0) continue; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance && distance < CELL_SIZE * 5) { closestDistance = distance; closestEnemy = enemy; } } self.currentTarget = closestEnemy; }; self.down = function (x, y, obj) { // Use ability when tapped if (self.useAbility()) { var notification = game.addChild(new Notification("Hero ability activated!")); notification.x = 2048 / 2; notification.y = grid.height - 100; } }; self.destroy = function () { if (self.isDestroyed) { return; } self.isDestroyed = true; tween.stop(self); for (var i = 0; i < self.children.length; i++) { if (self.children[i]) { tween.stop(self.children[i]); } } if (self.parent) { self.parent.removeChild(self); } Container.prototype.destroy.call(self); }; return self; }); var NebulaCloud = Container.expand(function (x, y) { var self = Container.call(this); self.x = x; self.y = y; self.radius = 100; self.isDestroyed = false; var nebulaGraphics = self.attachAsset('nebulaCloud', { anchorX: 0.5, anchorY: 0.5 }); nebulaGraphics.alpha = 0.6; // Pulsing animation tween(nebulaGraphics, { scaleX: 1.2, scaleY: 1.2, alpha: 0.8 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.isDestroyed) { tween(nebulaGraphics, { scaleX: 1.0, scaleY: 1.0, alpha: 0.6 }, { duration: 2000, easing: tween.easeInOut }); } } }); self.update = function () { // Apply effects to units within the nebula for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.radius) { // Slow effect if (!enemy.nebulaSlowed) { enemy.originalSpeed = enemy.originalSpeed || enemy.speed; enemy.speed *= 0.7; // 30% speed reduction enemy.nebulaSlowed = true; } // Stealth effect - harder for towers to target enemy.nebulaStealthed = true; } else { // Remove effects when outside nebula if (enemy.nebulaSlowed) { enemy.speed = enemy.originalSpeed || enemy.speed; enemy.nebulaSlowed = false; } enemy.nebulaStealthed = false; } } // Apply slow effect to heroes for (var i = 0; i < heroes.length; i++) { var hero = heroes[i]; var dx = hero.x - self.x; var dy = hero.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.radius) { if (!hero.nebulaSlowed) { hero.originalMovementSpeed = hero.originalMovementSpeed || hero.movementSpeed; hero.movementSpeed *= 0.7; hero.nebulaSlowed = true; } } else { if (hero.nebulaSlowed) { hero.movementSpeed = hero.originalMovementSpeed || hero.movementSpeed; hero.nebulaSlowed = false; } } } }; self.destroy = function () { if (self.isDestroyed) return; self.isDestroyed = true; // Remove effects from all units for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.nebulaSlowed) { enemy.speed = enemy.originalSpeed || enemy.speed; enemy.nebulaSlowed = false; } enemy.nebulaStealthed = false; } for (var i = 0; i < heroes.length; i++) { var hero = heroes[i]; if (hero.nebulaSlowed) { hero.movementSpeed = hero.originalMovementSpeed || hero.movementSpeed; hero.nebulaSlowed = false; } } var nebulaIndex = nebulaClouds.indexOf(self); if (nebulaIndex !== -1) { nebulaClouds.splice(nebulaIndex, 1); } tween.stop(self); if (self.parent) { self.parent.removeChild(self); } Container.prototype.destroy.call(self); }; return self; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0xFFFFFF, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 60; notificationGraphics.height = 80; notificationGraphics.tint = 0x1A1A1A; notificationGraphics.alpha = 0.95; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var PathJunction = Container.expand(function (x, y, branches) { var self = Container.call(this); self.x = x; self.y = y; self.branches = branches || []; // Array of possible path directions self.gridX = Math.floor((x - grid.x) / CELL_SIZE); self.gridY = Math.floor((y - grid.y) / CELL_SIZE); self.isDestroyed = false; // Visual junction indicator var junctionGraphics = self.attachAsset('teleportEffect', { anchorX: 0.5, anchorY: 0.5 }); junctionGraphics.width = CELL_SIZE * 0.8; junctionGraphics.height = CELL_SIZE * 0.8; junctionGraphics.tint = 0x9c27b0; junctionGraphics.alpha = 0.6; // Pulsing animation to indicate junction tween(junctionGraphics, { scaleX: 1.2, scaleY: 1.2, alpha: 0.8 }, { duration: 1500, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.isDestroyed) { tween(junctionGraphics, { scaleX: 1.0, scaleY: 1.0, alpha: 0.6 }, { duration: 1500, easing: tween.easeInOut }); } } }); // Choose path for enemy based on various factors self.choosePath = function (enemy) { if (self.branches.length === 0) return null; var pathScores = []; // Evaluate each branch for (var i = 0; i < self.branches.length; i++) { var branch = self.branches[i]; var score = 0; // Base score - some randomness for variety score += Math.random() * 100; // Factor in enemy type preferences switch (enemy.type) { case 'fast': // Fast enemies prefer shorter paths score += branch.isShortPath ? 150 : 50; break; case 'immune': // Immune enemies prefer heavily defended paths (they can tank it) score += branch.towerDensity * 80; break; case 'flying': // Flying enemies prefer paths with fewer anti-air towers score += branch.hasAntiAir ? 20 : 120; break; case 'swarm': // Swarm enemies prefer paths other swarm enemies haven't taken recently score += branch.recentSwarmTraffic > 3 ? 30 : 100; break; default: // Normal enemies prefer balanced paths score += branch.difficulty < 50 ? 80 : 60; break; } // Factor in current congestion (avoid clustering) var congestionPenalty = branch.currentEnemyCount * 25; score -= congestionPenalty; // Factor in tower coverage (enemies avoid heavy defenses unless immune) if (!enemy.isImmune) { score -= branch.towerDensity * 40; } // Factor in path length (generally prefer shorter paths) score -= branch.pathLength * 2; pathScores.push({ branch: branch, score: score }); } // Sort by score and add some weighted randomness pathScores.sort(function (a, b) { return b.score - a.score; }); // Weighted selection - higher scores more likely but not guaranteed var totalWeight = 0; for (var i = 0; i < pathScores.length; i++) { var weight = Math.max(1, pathScores[i].score); pathScores[i].weight = weight; totalWeight += weight; } var randomValue = Math.random() * totalWeight; var currentWeight = 0; for (var i = 0; i < pathScores.length; i++) { currentWeight += pathScores[i].weight; if (randomValue <= currentWeight) { return pathScores[i].branch; } } // Fallback to first option return pathScores[0].branch; }; // Update junction statistics self.update = function () { // Update branch statistics for (var i = 0; i < self.branches.length; i++) { var branch = self.branches[i]; // Count enemies currently on this branch branch.currentEnemyCount = 0; branch.recentSwarmTraffic = Math.max(0, branch.recentSwarmTraffic - 0.1); for (var j = 0; j < enemies.length; j++) { var enemy = enemies[j]; if (enemy.chosenBranch === branch) { branch.currentEnemyCount++; if (enemy.type === 'swarm') { branch.recentSwarmTraffic += 0.1; } } } // Calculate tower density in branch area branch.towerDensity = 0; branch.hasAntiAir = false; for (var t = 0; t < towers.length; t++) { var tower = towers[t]; var dx = tower.x - (grid.x + branch.targetX * CELL_SIZE); var dy = tower.y - (grid.y + branch.targetY * CELL_SIZE); var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= CELL_SIZE * 4) { branch.towerDensity++; if (tower.id === 'sniper' || tower.id === 'rapid') { branch.hasAntiAir = true; } } } // Update path difficulty branch.difficulty = branch.towerDensity * 10 + branch.pathLength; } }; self.destroy = function () { if (self.isDestroyed) return; self.isDestroyed = true; var junctionIndex = pathJunctions.indexOf(self); if (junctionIndex !== -1) { pathJunctions.splice(junctionIndex, 1); } tween.stop(self); if (self.parent) { self.parent.removeChild(self); } Container.prototype.destroy.call(self); }; return self; }); var RouteVisualizer = Container.expand(function () { var self = Container.call(this); self.pathLines = []; self.isDestroyed = false; self.showPaths = function (junction, enemy) { // Clear existing path lines self.clearPaths(); // Create visual lines for each possible branch for (var i = 0; i < junction.branches.length; i++) { var branch = junction.branches[i]; var pathLine = new Container(); // Create line segments to show the path var segments = []; var currentX = junction.gridX; var currentY = junction.gridY; // Draw path to target var targetX = branch.targetX; var targetY = branch.targetY; var steps = Math.max(Math.abs(targetX - currentX), Math.abs(targetY - currentY)); for (var step = 0; step <= steps; step++) { var progress = step / steps; var segmentX = Math.round(currentX + (targetX - currentX) * progress); var segmentY = Math.round(currentY + (targetY - currentY) * progress); var segment = pathLine.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); segment.width = 12; segment.height = 12; segment.x = grid.x + segmentX * CELL_SIZE; segment.y = grid.y + segmentY * CELL_SIZE; // Color code based on path difficulty var difficulty = branch.difficulty || 0; if (difficulty < 30) { segment.tint = 0x00FF00; // Green for easy } else if (difficulty < 60) { segment.tint = 0xFFFF00; // Yellow for medium } else { segment.tint = 0xFF0000; // Red for hard } segment.alpha = 0.7; } game.addChild(pathLine); self.pathLines.push(pathLine); // Animate the path appearance tween(pathLine, { alpha: 1.0 }, { duration: 500, easing: tween.easeOut }); } // Auto-hide after a few seconds LK.setTimeout(function () { self.clearPaths(); }, 3000); }; self.clearPaths = function () { for (var i = 0; i < self.pathLines.length; i++) { if (self.pathLines[i].parent) { self.pathLines[i].parent.removeChild(self.pathLines[i]); } } self.pathLines = []; }; self.destroy = function () { if (self.isDestroyed) return; self.isDestroyed = true; self.clearPaths(); tween.stop(self); if (self.parent) { self.parent.removeChild(self); } Container.prototype.destroy.call(self); }; return self; }); var SolarFlare = Container.expand(function () { var self = Container.call(this); self.duration = 600; // 10 seconds at 60fps self.remainingTime = self.duration; self.isActive = true; self.isDestroyed = false; // Position in center of battlefield self.x = grid.x + 12 * CELL_SIZE; self.y = grid.y + 15 * CELL_SIZE; var flareGraphics = self.attachAsset('solarFlare', { anchorX: 0.5, anchorY: 0.5 }); flareGraphics.alpha = 0.8; flareGraphics.blendMode = 1; // Additive blending // Intense pulsing animation tween(flareGraphics, { scaleX: 1.5, scaleY: 1.5, alpha: 1.0 }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.isDestroyed) { tween(flareGraphics, { scaleX: 1.2, scaleY: 1.2, alpha: 0.8 }, { duration: 300, easing: tween.easeInOut }); } } }); // Show notification var notification = game.addChild(new Notification("⚡ Solar Flare! Energy towers boosted! ⚡")); notification.x = 2048 / 2; notification.y = grid.height - 100; self.update = function () { self.remainingTime--; if (self.remainingTime <= 0) { self.destroy(); return; } // Boost energy-based towers (rapid, sniper, default) for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (tower.id === 'rapid' || tower.id === 'sniper' || tower.id === 'default') { if (!tower.solarFlareBoost) { tower.originalFireRate = tower.fireRate; tower.originalDamage = tower.damage; tower.fireRate = Math.floor(tower.fireRate * 0.6); // 40% faster tower.damage = Math.floor(tower.damage * 1.3); // 30% more damage tower.solarFlareBoost = true; // Visual effect on boosted towers tween(tower.energyCore, { tint: 0xFFD700, scaleX: 1.3, scaleY: 1.3 }, { duration: 200, easing: tween.easeOut }); } } } }; self.destroy = function () { if (self.isDestroyed) return; self.isDestroyed = true; // Remove boosts from all towers for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (tower.solarFlareBoost) { tower.fireRate = tower.originalFireRate; tower.damage = tower.originalDamage; tower.solarFlareBoost = false; // Reset visual effects var originalTint = 0x88AACC; switch (tower.id) { case 'rapid': originalTint = 0x00AAFF; break; case 'sniper': originalTint = 0xFF5500; break; case 'default': originalTint = 0x88AACC; break; } tween(tower.energyCore, { tint: originalTint, scaleX: 1.0, scaleY: 1.0 }, { duration: 300, easing: tween.easeIn }); } } var flareIndex = solarFlares.indexOf(self); if (flareIndex !== -1) { solarFlares.splice(flareIndex, 1); } tween.stop(self); if (self.parent) { self.parent.removeChild(self); } Container.prototype.destroy.call(self); }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Increase size of base for easier touch var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); switch (self.towerType) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'sniper': baseGraphics.tint = 0xFF5500; break; case 'splash': baseGraphics.tint = 0x33CC00; break; case 'slow': baseGraphics.tint = 0x9900FF; break; case 'poison': baseGraphics.tint = 0x00FFAA; break; default: baseGraphics.tint = 0xAAAAAA; } var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -20; // Position above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var SpecializationMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 300; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 1800; menuBackground.height = 600; menuBackground.tint = 0x1A1A1A; menuBackground.alpha = 0.98; var titleText = new Text2('Choose Specialization Path', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -200; self.addChild(titleText); var subtitleText = new Text2('Your tower has reached level 3 and can specialize!', { size: 50, fill: 0xCCCCCC, weight: 400 }); subtitleText.anchor.set(0.5, 0.5); subtitleText.y = -140; self.addChild(subtitleText); // Create specialization options based on tower type var options = []; if (self.tower.id === 'sniper') { options = [{ type: 'antiarmor', name: 'Anti-Armor', desc: 'Pierce shields\n+50% damage vs immune', color: 0xFF6600 }, { type: 'longrange', name: 'Long-Range', desc: 'Double firing range\nBetter enemy targeting', color: 0x00AAFF }]; } else if (self.tower.id === 'splash') { options = [{ type: 'nuclear', name: 'Nuclear', desc: 'Huge damage\nLarger splash radius', color: 0xFF0000 }, { type: 'chainreaction', name: 'Chain Reaction', desc: 'Damage spreads\nbetween enemies', color: 0xFFFF00 }]; } // Create option buttons for (var i = 0; i < options.length; i++) { var option = options[i]; var button = new Container(); var buttonBg = button.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBg.width = 700; buttonBg.height = 200; buttonBg.tint = option.color; buttonBg.alpha = 0.8; var nameText = new Text2(option.name, { size: 70, fill: 0xFFFFFF, weight: 800 }); nameText.anchor.set(0.5, 0.5); nameText.y = -40; button.addChild(nameText); var descText = new Text2(option.desc, { size: 45, fill: 0xFFFFFF, weight: 400 }); descText.anchor.set(0.5, 0.5); descText.y = 40; button.addChild(descText); button.x = (i - 0.5) * 800; button.y = 50; button.optionType = option.type; button.down = function () { self.selectSpecialization(this.optionType); }; self.addChild(button); } // Close button var closeButton = new Container(); var closeBg = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBg.width = 100; closeBg.height = 100; closeBg.tint = 0xAA0000; var closeText = new Text2('X', { size: 80, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = 850; closeButton.y = -250; closeButton.down = function () { self.destroy(); }; self.addChild(closeButton); self.selectSpecialization = function (branchType) { // Apply specialization to tower var specialization = new TowerSpecialization(self.tower, branchType); self.tower.specialization = specialization; self.tower.addChild(specialization); specialization.applySpecialization(); var notification = game.addChild(new Notification("Tower specialized: " + branchType)); notification.x = 2048 / 2; notification.y = grid.height - 50; self.destroy(); }; return self; }); var StationCustomizer = Container.expand(function () { var self = Container.call(this); self.modules = []; self.moduleSlots = []; self.selectedSlot = null; self.customizationMode = false; // Create module slots around the station var slotPositions = [{ x: -2, y: -2 }, { x: 0, y: -2 }, { x: 2, y: -2 }, { x: -2, y: 0 }, { x: 2, y: 0 }, { x: -2, y: 2 }, { x: 0, y: 2 }, { x: 2, y: 2 }]; for (var i = 0; i < slotPositions.length; i++) { var slot = new Container(); var slotGraphics = slot.attachAsset('moduleSocket', { anchorX: 0.5, anchorY: 0.5 }); slotGraphics.alpha = 0.5; slotGraphics.tint = 0x556677; slot.gridX = slotPositions[i].x; slot.gridY = slotPositions[i].y; slot.x = grid.x + (12 + slot.gridX) * CELL_SIZE; slot.y = grid.y + (15 + slot.gridY) * CELL_SIZE; slot.module = null; slot.slotIndex = i; slot.down = function () { if (self.customizationMode) { self.selectedSlot = this; self.showModuleMenu(); } }; self.addChild(slot); self.moduleSlots.push(slot); } self.showModuleMenu = function () { if (!self.selectedSlot) return; var menu = new Container(); var menuBg = menu.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBg.width = 600; menuBg.height = 300; menuBg.tint = 0x333333; var moduleTypes = ['research', 'shield', 'teleporter']; for (var i = 0; i < moduleTypes.length; i++) { var button = new Container(); var buttonBg = button.attachAsset('techTreeNode', { anchorX: 0.5, anchorY: 0.5 }); buttonBg.width = 120; buttonBg.height = 80; var cost = getModuleCost(moduleTypes[i], 1); var buttonText = new Text2(moduleTypes[i] + '\n' + cost + 'g', { size: 30, fill: 0xFFFFFF, weight: 600 }); buttonText.anchor.set(0.5, 0.5); button.addChild(buttonText); button.moduleType = moduleTypes[i]; button.x = -180 + i * 180; button.y = 50; button.down = function () { self.buildModule(this.moduleType); menu.destroy(); }; menu.addChild(button); } menu.x = 2048 / 2; menu.y = 1400; game.addChild(menu); LK.setTimeout(function () { if (menu.parent) { menu.parent.removeChild(menu); } }, 5000); }; self.buildModule = function (moduleType) { if (!self.selectedSlot || self.selectedSlot.module) return; var cost = getModuleCost(moduleType, 1); if (gold < cost) { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } var module; switch (moduleType) { case 'research': module = new ResearchLab(); break; case 'shield': module = new ShieldGenerator(); break; case 'teleporter': module = new TeleporterNode(); break; default: module = new StationModule(moduleType); } module.gridX = self.selectedSlot.gridX; module.gridY = self.selectedSlot.gridY; module.x = self.selectedSlot.x; module.y = self.selectedSlot.y; self.selectedSlot.module = module; self.selectedSlot.children[0].alpha = 0; // Hide slot graphic self.addChild(module); self.modules.push(module); setGold(gold - cost); var notification = game.addChild(new Notification(moduleType + " module built!")); notification.x = 2048 / 2; notification.y = grid.height - 50; self.selectedSlot = null; }; self.toggleCustomizationMode = function () { self.customizationMode = !self.customizationMode; for (var i = 0; i < self.moduleSlots.length; i++) { var slot = self.moduleSlots[i]; if (!slot.module) { slot.children[0].alpha = self.customizationMode ? 0.8 : 0.3; } } var modeText = self.customizationMode ? "ON" : "OFF"; var notification = game.addChild(new Notification("Customization mode: " + modeText)); notification.x = 2048 / 2; notification.y = grid.height - 50; }; self.update = function () { for (var i = 0; i < self.modules.length; i++) { if (self.modules[i].update) { self.modules[i].update(); } } }; return self; }); var StationModule = Container.expand(function (moduleType) { var self = Container.call(this); self.moduleType = moduleType || 'basic'; self.level = 1; self.maxLevel = 3; self.isActive = true; self.gridX = 0; self.gridY = 0; self.isDestroyed = false; // Get appropriate asset var assetName = 'stationModule'; switch (moduleType) { case 'research': assetName = 'researchLab'; break; case 'shield': assetName = 'shieldGenerator'; break; case 'teleporter': assetName = 'teleporter'; break; } var moduleGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); // Add energy core var energyCore = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); energyCore.width = 30; energyCore.height = 30; energyCore.tint = 0x00aaff; // Level indicators var levelIndicators = []; for (var i = 0; i < self.maxLevel; i++) { var indicator = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = 12; indicator.height = 12; indicator.x = -30 + i * 30; indicator.y = 60; indicator.tint = i < self.level ? 0xffffff : 0x444444; levelIndicators.push(indicator); } self.updateLevelIndicators = function () { for (var i = 0; i < levelIndicators.length; i++) { levelIndicators[i].tint = i < self.level ? 0xffffff : 0x444444; } }; self.upgrade = function () { if (self.level < self.maxLevel) { var upgradeCost = getModuleCost(self.moduleType, self.level + 1); if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; self.updateLevelIndicators(); self.onUpgrade(); return true; } } return false; }; self.onUpgrade = function () { // Override in specific module types }; self.update = function () { if (!self.isActive) return; // Pulsing energy core if (LK.ticks % 120 === 0) { tween(energyCore, { scaleX: 1.3, scaleY: 1.3, alpha: 0.7 }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { tween(energyCore, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 300, easing: tween.easeInOut }); } }); } }; self.destroy = function () { if (self.isDestroyed) return; self.isDestroyed = true; tween.stop(self); if (self.parent) { self.parent.removeChild(self); } Container.prototype.destroy.call(self); }; return self; }); var TeleporterNode = StationModule.expand(function () { var self = StationModule.call(this, 'teleporter'); self.teleportEnergy = 100; self.maxTeleportEnergy = 100; self.linkedNodes = []; self.onUpgrade = function () { self.maxTeleportEnergy = 100 + (self.level - 1) * 25; self.teleportEnergy = self.maxTeleportEnergy; }; self.teleportHero = function (hero, targetNode) { if (self.teleportEnergy >= 30 && targetNode && targetNode.teleportEnergy >= 30) { // Create teleport effect at source var sourceEffect = new Container(); sourceEffect.x = self.x; sourceEffect.y = self.y; var sourceGraphics = sourceEffect.attachAsset('teleportEffect', { anchorX: 0.5, anchorY: 0.5 }); sourceGraphics.tint = 0x9c27b0; sourceGraphics.alpha = 0.8; game.addChild(sourceEffect); // Create teleport effect at destination var destEffect = new Container(); destEffect.x = targetNode.x; destEffect.y = targetNode.y; var destGraphics = destEffect.attachAsset('teleportEffect', { anchorX: 0.5, anchorY: 0.5 }); destGraphics.tint = 0x9c27b0; destGraphics.alpha = 0.8; game.addChild(destEffect); // Animate effects tween(sourceEffect, { scaleX: 2, scaleY: 2, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { if (sourceEffect.parent) { sourceEffect.parent.removeChild(sourceEffect); } } }); tween(destEffect, { scaleX: 2, scaleY: 2, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { if (destEffect.parent) { destEffect.parent.removeChild(destEffect); } } }); // Teleport hero hero.x = targetNode.x; hero.y = targetNode.y; // Consume energy self.teleportEnergy = Math.max(0, self.teleportEnergy - 30); targetNode.teleportEnergy = Math.max(0, targetNode.teleportEnergy - 30); return true; } return false; }; self.redirectEnemyPath = function () { if (self.teleportEnergy >= 50 && self.linkedNodes.length > 0) { var targetNode = self.linkedNodes[Math.floor(Math.random() * self.linkedNodes.length)]; // Find nearby enemies and redirect one for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (!enemy.isFlying) { var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= CELL_SIZE * 2) { // Teleport enemy enemy.x = targetNode.x; enemy.y = targetNode.y; enemy.currentCellX = targetNode.gridX; enemy.currentCellY = targetNode.gridY; enemy.currentTarget = null; self.teleportEnergy = Math.max(0, self.teleportEnergy - 50); var notification = game.addChild(new Notification("Enemy redirected through teleporter!")); notification.x = 2048 / 2; notification.y = grid.height - 50; break; } } } } }; self.update = function () { StationModule.prototype.update.call(self); // Regenerate teleport energy if (LK.ticks % 120 === 0 && self.teleportEnergy < self.maxTeleportEnergy) { // Every 2 seconds self.teleportEnergy = Math.min(self.maxTeleportEnergy, self.teleportEnergy + self.level * 3); } }; self.down = function () { self.redirectEnemyPath(); }; return self; }); var ShieldGenerator = StationModule.expand(function () { var self = StationModule.call(this, 'shield'); self.shieldEnergy = 100; self.maxShieldEnergy = 100; self.shieldActive = false; self.shieldEffect = null; self.onUpgrade = function () { self.maxShieldEnergy = 100 + (self.level - 1) * 50; self.shieldEnergy = self.maxShieldEnergy; }; self.activateShield = function () { if (self.shieldEnergy >= 50 && !self.shieldActive) { self.shieldActive = true; self.shieldEnergy = Math.max(0, self.shieldEnergy - 50); // Create visual shield effect self.shieldEffect = new Container(); self.shieldEffect.x = self.x; self.shieldEffect.y = self.y; var shieldGraphics = self.shieldEffect.attachAsset('shieldEffect', { anchorX: 0.5, anchorY: 0.5 }); shieldGraphics.alpha = 0.3; shieldGraphics.tint = 0x00ffff; game.addChild(self.shieldEffect); // Shield lasts for 10 seconds LK.setTimeout(function () { self.deactivateShield(); }, 10000); var notification = game.addChild(new Notification("Station shields activated!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; self.deactivateShield = function () { self.shieldActive = false; if (self.shieldEffect && self.shieldEffect.parent) { self.shieldEffect.parent.removeChild(self.shieldEffect); self.shieldEffect = null; } }; self.update = function () { StationModule.prototype.update.call(self); // Regenerate shield energy if (LK.ticks % 180 === 0 && self.shieldEnergy < self.maxShieldEnergy) { // Every 3 seconds self.shieldEnergy = Math.min(self.maxShieldEnergy, self.shieldEnergy + self.level * 5); } // Update shield effect position if (self.shieldEffect) { self.shieldEffect.x = self.x; self.shieldEffect.y = self.y; } }; self.down = function () { self.activateShield(); }; return self; }); var ResearchLab = StationModule.expand(function () { var self = StationModule.call(this, 'research'); self.researchPoints = 0; self.activeResearch = null; self.completedTechs = []; self.onUpgrade = function () { // Higher level labs generate research points faster self.researchPointsPerTick = self.level; }; self.update = function () { StationModule.prototype.update.call(self); // Generate research points if (LK.ticks % 60 === 0) { // Every second self.researchPoints += self.level; storage.researchPoints = self.researchPoints; } // Complete active research if (self.activeResearch && self.researchPoints >= self.activeResearch.cost) { self.completeResearch(); } }; self.startResearch = function (techId) { var tech = getTechnology(techId); if (tech && !self.activeResearch) { self.activeResearch = tech; var notification = game.addChild(new Notification("Research started: " + tech.name)); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; self.completeResearch = function () { if (self.activeResearch) { self.researchPoints -= self.activeResearch.cost; self.completedTechs.push(self.activeResearch.id); storage.completedTechs = self.completedTechs; // Apply research benefits self.activeResearch.onComplete(); var notification = game.addChild(new Notification("Research complete: " + self.activeResearch.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; self.activeResearch = null; } }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level var baseRange; switch (self.id) { case 'sniper': // Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost if (self.level === self.maxLevel) { baseRange = 12 * CELL_SIZE; // Significantly increased range for max level } else { baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE; } break; case 'splash': // Splash: base 2, +0.2 per level (max ~4 blocks at max level) return (2 + (self.level - 1) * 0.2) * CELL_SIZE; case 'rapid': // Rapid: base 2.5, +0.5 per level return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'slow': // Slow: base 3.5, +0.5 per level return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'poison': // Poison: base 3.2, +0.5 per level return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; default: // Default: base 3, +0.5 per level baseRange = (3 + (self.level - 1) * 0.5) * CELL_SIZE; } // Apply specialization range multiplier if (self.specialization && self.specialization.branchType === 'longrange') { baseRange *= 2.0; } return baseRange; }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 35; // Slightly slower to reduce overwhelming power self.damage = 8; // Increased from 5 for better effectiveness self.range = 2.8 * CELL_SIZE; // Slightly better range self.bulletSpeed = 7; break; case 'sniper': self.fireRate = 100; // Slower but more impactful self.damage = 40; // Increased significantly for true sniper feel self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'splash': self.fireRate = 80; // Slightly slower self.damage = 18; // Increased base damage self.range = 2.2 * CELL_SIZE; // Slightly better range self.bulletSpeed = 4; break; case 'slow': self.fireRate = 45; // Faster to apply slow effects more consistently self.damage = 12; // Increased from 8 for better utility self.range = 3.8 * CELL_SIZE; // Better range for support role self.bulletSpeed = 5; break; case 'poison': self.fireRate = 60; // Faster to apply poison more effectively self.damage = 15; // Increased from 12 self.range = 3.5 * CELL_SIZE; // Better range self.bulletSpeed = 5; break; } // Create tower base structure var baseAssetName = 'tower_base_' + (self.id === 'default' ? 'default' : self.id); var baseGraphics = self.attachAsset(baseAssetName, { anchorX: 0.5, anchorY: 0.5 }); // Create support structures for higher levels self.supportStructures = []; // Create energy core based on tower type var energyCore = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); energyCore.width = 40; energyCore.height = 40; switch (self.id) { case 'rapid': energyCore.tint = 0x00AAFF; break; case 'sniper': energyCore.tint = 0xFF5500; break; case 'splash': energyCore.tint = 0x33CC00; break; case 'slow': energyCore.tint = 0x9900FF; break; case 'poison': energyCore.tint = 0x00FFAA; break; default: energyCore.tint = 0x88AACC; } // Add pulsing energy core animation self.energyCore = energyCore; tween(energyCore, { scaleX: 1.2, scaleY: 1.2, alpha: 0.8 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(energyCore, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 1000, easing: tween.easeInOut }); } }); // Method to update tower visual appearance based on level self.updateTowerVisuals = function () { // Ensure gunContainer exists before trying to use it if (!gunContainer) { console.warn("gunContainer not found, skipping visual update"); return; } // Clear existing gun graphics if (self.gunGraphics && self.gunGraphics.parent) { gunContainer.removeChild(self.gunGraphics); } // Clear existing support structures for (var i = 0; i < self.supportStructures.length; i++) { if (self.supportStructures[i].parent) { self.removeChild(self.supportStructures[i]); } } self.supportStructures = []; // Create new gun graphics based on current level var gunAssetName = 'tower_gun_' + (self.id === 'default' ? 'default' : self.id) + '_' + self.level; self.gunGraphics = gunContainer.attachAsset(gunAssetName, { anchorX: 0.5, anchorY: 0.5 }); // Add support structures for higher levels if (self.level >= 2) { // Add support beams for (var i = 0; i < 2; i++) { var beam = self.attachAsset('tower_support_beam', { anchorX: 0.5, anchorY: 1.0 }); beam.x = i === 0 ? -30 : 30; beam.y = baseGraphics.height / 2 - 10; beam.rotation = i === 0 ? -0.3 : 0.3; self.supportStructures.push(beam); } } if (self.level >= 3) { // Add radar dish for better targeting var radar = self.attachAsset('tower_radar_dish', { anchorX: 0.5, anchorY: 0.5 }); radar.x = 0; radar.y = -baseGraphics.height / 2 - 20; radar.alpha = 0.8; self.supportStructures.push(radar); // Rotate radar dish slowly tween(radar, { rotation: Math.PI * 2 }, { duration: 4000, easing: tween.linear, onFinish: function onFinish() { if (!self.isDestroyed && radar.parent) { radar.rotation = 0; } } }); } if (self.level >= 4) { // Add antenna for enhanced range var antenna = self.attachAsset('tower_antenna', { anchorX: 0.5, anchorY: 1.0 }); antenna.x = 0; antenna.y = -baseGraphics.height / 2 - 60; self.supportStructures.push(antenna); // Add armor plates for (var i = 0; i < 4; i++) { var armor = self.attachAsset('tower_armor_plate', { anchorX: 0.5, anchorY: 0.5 }); var angle = i / 4 * Math.PI * 2; armor.x = Math.cos(angle) * 40; armor.y = Math.sin(angle) * 40; armor.rotation = angle; armor.alpha = 0.7; self.supportStructures.push(armor); } } if (self.level >= 5) { // Add energy conduits for (var i = 0; i < 3; i++) { var conduit = self.attachAsset('tower_energy_conduit', { anchorX: 0.5, anchorY: 0.5 }); var angle = i / 3 * Math.PI * 2; conduit.x = Math.cos(angle) * 50; conduit.y = Math.sin(angle) * 50; conduit.rotation = angle + Math.PI / 2; // Pulsing energy effect tween(conduit, { alpha: 0.4 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.isDestroyed && conduit.parent) { tween(conduit, { alpha: 1.0 }, { duration: 800, easing: tween.easeInOut }); } } }); self.supportStructures.push(conduit); } } if (self.level >= 6) { // Max level - add ultimate enhancement visual var enhancement = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); enhancement.width = 80; enhancement.height = 80; enhancement.tint = 0xFFD700; enhancement.alpha = 0.3; enhancement.y = 0; // Rotating golden aura for max level tween(enhancement, { rotation: Math.PI * 2, scaleX: 1.2, scaleY: 1.2 }, { duration: 3000, easing: tween.linear, onFinish: function onFinish() { if (!self.isDestroyed && enhancement.parent) { enhancement.rotation = 0; enhancement.scaleX = 1.0; enhancement.scaleY = 1.0; } } }); self.supportStructures.push(enhancement); } // Update energy core size based on level var coreSize = 40 + (self.level - 1) * 8; self.energyCore.width = coreSize; self.energyCore.height = coreSize; // Add specialization visuals if present if (self.specialization) { self.updateSpecializationVisuals(); } }; // Method to add specialization visual indicators self.updateSpecializationVisuals = function () { if (!self.specialization) return; var specAssetName; switch (self.specialization.branchType) { case 'antiarmor': specAssetName = 'tower_spec_armor_pierce'; break; case 'longrange': specAssetName = 'tower_spec_long_range'; break; case 'nuclear': specAssetName = 'tower_spec_nuclear'; break; case 'chainreaction': specAssetName = 'tower_spec_chain_reaction'; break; default: return; } var specVisual = self.attachAsset(specAssetName, { anchorX: 0.5, anchorY: 0.5 }); specVisual.y = -baseGraphics.height / 2 - 40; specVisual.alpha = 0.9; // Pulsing effect for specialization indicator tween(specVisual, { scaleX: 1.3, scaleY: 1.3, alpha: 0.6 }, { duration: 1200, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.isDestroyed && specVisual.parent) { tween(specVisual, { scaleX: 1.0, scaleY: 1.0, alpha: 0.9 }, { duration: 1200, easing: tween.easeInOut }); } } }); self.supportStructures.push(specVisual); }; // Initialize tower visuals self.updateTowerVisuals(); var gunContainer = new Container(); self.addChild(gunContainer); var gunGraphics = gunContainer.attachAsset('defense', { anchorX: 0.5, anchorY: 0.5 }); var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 6; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { switch (self.id) { case 'rapid': towerLevelIndicator.tint = 0x00AAFF; break; case 'sniper': towerLevelIndicator.tint = 0xFF5500; break; case 'splash': towerLevelIndicator.tint = 0x33CC00; break; case 'slow': towerLevelIndicator.tint = 0x9900FF; break; case 'poison': towerLevelIndicator.tint = 0x00FFAA; break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // More balanced upgrade cost scaling if (self.level === self.maxLevel - 1) { // Final upgrade is expensive but not prohibitive upgradeCost = Math.floor(baseUpgradeCost * Math.pow(1.8, self.level - 1) * 2.5); } else { // Use 1.8x multiplier instead of 2x for more manageable costs upgradeCost = Math.floor(baseUpgradeCost * Math.pow(1.8, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // Play upgrade sound LK.getSound('towerUpgrade').play(); // Update tower visuals for new level self.updateTowerVisuals(); // Check for specialization unlock at level 3 if (self.level === 3 && !self.specialization && (self.id === 'sniper' || self.id === 'splash')) { var specMenu = new SpecializationMenu(self); game.addChild(specMenu); specMenu.x = 2048 / 2; tween(specMenu, { y: 2732 - 300 }, { duration: 300, easing: tween.backOut }); specializationMenus.push(specMenu); return true; } // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type with balanced scaling switch (self.id) { case 'rapid': if (self.level === self.maxLevel) { // Max level gets significant boost self.fireRate = Math.max(8, 35 - self.level * 4.2); self.damage = 8 + self.level * 6; // More modest scaling self.bulletSpeed = 7 + self.level * 1.5; } else { self.fireRate = Math.max(18, 35 - self.level * 2.8); self.damage = 8 + self.level * 3.5; self.bulletSpeed = 7 + self.level * 0.8; } break; case 'sniper': if (self.level === self.maxLevel) { self.fireRate = Math.max(15, 100 - self.level * 12); self.damage = 40 + self.level * 18; // High damage scaling self.bulletSpeed = 25 + self.level * 2; } else { self.fireRate = Math.max(30, 100 - self.level * 8); self.damage = 40 + self.level * 12; self.bulletSpeed = 25 + self.level * 1; } break; case 'splash': if (self.level === self.maxLevel) { self.fireRate = Math.max(12, 80 - self.level * 10); self.damage = 18 + self.level * 12; // Strong splash scaling self.bulletSpeed = 4 + self.level * 1.5; } else { self.fireRate = Math.max(25, 80 - self.level * 6); self.damage = 18 + self.level * 8; self.bulletSpeed = 4 + self.level * 0.8; } break; case 'slow': if (self.level === self.maxLevel) { self.fireRate = Math.max(8, 45 - self.level * 5.5); self.damage = 12 + self.level * 8; // Better damage for utility tower self.bulletSpeed = 5 + self.level * 1.2; } else { self.fireRate = Math.max(20, 45 - self.level * 3.5); self.damage = 12 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.7; } break; case 'poison': if (self.level === self.maxLevel) { self.fireRate = Math.max(10, 60 - self.level * 7.5); self.damage = 15 + self.level * 10; // Strong poison scaling self.bulletSpeed = 5 + self.level * 1.3; } else { self.fireRate = Math.max(22, 60 - self.level * 5); self.damage = 15 + self.level * 6; self.bulletSpeed = 5 + self.level * 0.8; } break; default: if (self.level === self.maxLevel) { self.fireRate = Math.max(10, 60 - self.level * 8); self.damage = 10 + self.level * 12; self.bulletSpeed = 5 + self.level * 1.5; } else { self.fireRate = Math.max(25, 60 - self.level * 5); self.damage = 10 + self.level * 7; self.bulletSpeed = 5 + self.level * 0.8; } } if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; // Use spatial partitioning for better performance var nearbyEnemies = spatialGrid.getNearbyEnemies(self.x, self.y, self.getRange()); for (var i = 0; i < nearbyEnemies.length; i++) { var enemy = nearbyEnemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range if (distance <= self.getRange()) { // Apply nebula stealth effect - reduce targeting chance var targetingChance = enemy.nebulaStealthed ? 0.3 : 1.0; if (Math.random() > targetingChance) { continue; // Skip this enemy due to stealth } // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { // Update ultimate ability if (self.ultimate) { self.ultimate.update(); } self.targetEnemy = self.findTarget(); if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); gunContainer.rotation = angle; var currentFireRate = self.fireRate; if (self.heroBuff && self.heroBuff.duration > 0) { currentFireRate = Math.floor(self.fireRate * self.heroBuff.fireRate); } if (LK.ticks - self.lastFired >= currentFireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { // Tutorial action tracking if (tutorialSystem && tutorialSystem.isActive) { tutorialSystem.checkActionCompleted('selectTower'); } // Check for ultimate ability activation (double tap or long press simulation) if (self.ultimate && self.ultimate.canActivate()) { // Simple activation - tap when ultimate is ready if (self.ultimate.activate()) { return; // Ultimate was activated, don't show menu } } var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; // Apply hero buff if present var finalDamage = self.damage; var finalFireRate = self.fireRate; if (self.heroBuff && self.heroBuff.duration > 0) { finalDamage = Math.floor(self.damage * self.heroBuff.damage); finalFireRate = Math.floor(self.fireRate * self.heroBuff.fireRate); self.heroBuff.duration--; if (self.heroBuff.duration <= 0) { self.heroBuff = null; } } // Apply support bonus from linked towers if (self.supportBonus) { finalDamage = Math.floor(finalDamage * self.supportBonus); finalFireRate = Math.floor(finalFireRate / self.supportBonus); } // Apply specialization effects if (self.specialization) { if (self.specialization.branchType === 'antiarmor' && self.targetEnemy.isImmune) { finalDamage = Math.floor(finalDamage * 1.5); } } var bullet = getBulletFromPool(bulletX, bulletY, self.targetEnemy, finalDamage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; } // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0x00AAFF; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'sniper': bullet.children[0].tint = 0xFF5500; bullet.children[0].width = 15; bullet.children[0].height = 15; break; case 'splash': bullet.children[0].tint = 0x33CC00; bullet.children[0].width = 40; bullet.children[0].height = 40; break; case 'slow': bullet.children[0].tint = 0x9900FF; bullet.children[0].width = 35; bullet.children[0].height = 35; break; case 'poison': bullet.children[0].tint = 0x00FFAA; bullet.children[0].width = 35; bullet.children[0].height = 35; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // Play shooting sound LK.getSound('towerShoot').play(); // --- Sci-fi charge and firing effect --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Add energy charge effect to core if (self.energyCore) { tween.stop(self.energyCore, { scaleX: true, scaleY: true, alpha: true }); tween(self.energyCore, { scaleX: 1.5, scaleY: 1.5, alpha: 1.5 }, { duration: 120, easing: tween.easeOut, onFinish: function onFinish() { tween(self.energyCore, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 300, easing: tween.easeIn }); } }); } // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; } } } self.refreshCellsInRange(); }; self.destroy = function () { if (self.isDestroyed) { return; } self.isDestroyed = true; // Stop all active tweens on this tower and its components tween.stop(self); tween.stop(gunContainer); if (self.energyCore) { tween.stop(self.energyCore); } // Clean up all support structures and their tweens if (self.supportStructures) { for (var i = 0; i < self.supportStructures.length; i++) { if (self.supportStructures[i]) { tween.stop(self.supportStructures[i]); } } self.supportStructures = []; } // Clean up gun graphics if (self.gunGraphics) { tween.stop(self.gunGraphics); self.gunGraphics = null; } // Clear cells in range references for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; // Clear grid cells occupied by this tower for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (cell) { cell.type = 0; } } } // Remove from towers array var towerIndex = towers.indexOf(self); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } // Remove from parent container if (self.parent) { self.parent.removeChild(self); } // Remove any associated range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange && game.children[i].tower === self) { game.removeChild(game.children[i]); } } // Close any upgrade menus for this tower var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu && child.tower === self; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } // Clear selected tower if it's this tower if (selectedTower === self) { selectedTower = null; } // Clean up ultimate ability if (self.ultimate) { self.ultimate.destroy(); self.ultimate = null; } // Nullify all object references self.targetEnemy = null; self.energyCore = null; // Call parent destroy Container.prototype.destroy.call(self); }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; self.hasEnoughGold = true; var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; break; case 'sniper': previewGraphics.tint = 0xFF5500; break; case 'splash': previewGraphics.tint = 0x33CC00; break; case 'slow': previewGraphics.tint = 0x9900FF; break; case 'poison': previewGraphics.tint = 0x00FFAA; break; default: previewGraphics.tint = 0xAAAAAA; } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) { validGridPlacement = false; } else { for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (!cell || cell.type !== 0) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) { continue; } // Only check non-flying enemies, flying enemies can pass over towers if (!enemy.isFlying) { if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) { self.blockedByEnemy = true; break; } } } } } self.canPlace = validGridPlacement && !self.blockedByEnemy; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; self.checkPlacement(); }; return self; }); var TowerSpecialization = Container.expand(function (tower, branchType) { var self = Container.call(this); self.tower = tower; self.branchType = branchType || 'none'; self.linkedTowers = []; self.isDestroyed = false; // Add specialization visual indicator var specIcon = self.attachAsset('specializationIcon', { anchorX: 0.5, anchorY: 0.5 }); specIcon.width = 30; specIcon.height = 30; specIcon.y = -self.tower.children[0].height / 2 - 20; // Set specialization color based on branch switch (self.branchType) { case 'antiarmor': specIcon.tint = 0xFF6600; break; case 'longrange': specIcon.tint = 0x00AAFF; break; case 'nuclear': specIcon.tint = 0xFF0000; break; case 'chainreaction': specIcon.tint = 0xFFFF00; break; default: specIcon.tint = 0xFFD700; } self.applySpecialization = function () { switch (self.branchType) { case 'antiarmor': // Anti-armor: pierce shields, extra damage to immune enemies self.tower.armorPiercing = true; self.tower.damage = Math.floor(self.tower.damage * 1.5); break; case 'longrange': // Long-range: significantly extended range self.tower.rangeMultiplier = 2.0; self.tower.refreshCellsInRange(); break; case 'nuclear': // Nuclear: huge splash damage, slower fire rate self.tower.damage = Math.floor(self.tower.damage * 2.5); self.tower.fireRate = Math.floor(self.tower.fireRate * 1.8); self.tower.splashRadius = CELL_SIZE * 3; break; case 'chainreaction': // Chain reaction: damage spreads between enemies self.tower.chainReaction = true; self.tower.chainRange = CELL_SIZE * 2; self.tower.chainDamage = 0.7; break; } // Update tower visuals to show specialization if (self.tower.updateSpecializationVisuals) { self.tower.updateSpecializationVisuals(); } }; self.findNearbyTowers = function () { var nearby = []; var searchRadius = CELL_SIZE * 4; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (tower !== self.tower && tower.specialization) { var dx = tower.x - self.tower.x; var dy = tower.y - self.tower.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= searchRadius) { nearby.push(tower); } } } return nearby; }; self.createSupportLinks = function () { var nearbyTowers = self.findNearbyTowers(); for (var i = 0; i < nearbyTowers.length; i++) { var tower = nearbyTowers[i]; if (self.linkedTowers.indexOf(tower) === -1) { self.linkedTowers.push(tower); // Create visual link beam var linkBeam = new Container(); linkBeam.x = self.tower.x; linkBeam.y = self.tower.y; var beamGraphics = linkBeam.attachAsset('linkBeam', { anchorX: 0, anchorY: 0.5 }); var dx = tower.x - self.tower.x; var dy = tower.y - self.tower.y; var distance = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx); beamGraphics.width = distance; beamGraphics.rotation = angle; beamGraphics.alpha = 0.5; beamGraphics.tint = 0x00FF88; game.addChild(linkBeam); self.tower.linkBeam = linkBeam; } } }; self.applySupportBonus = function () { // Linked towers get damage and fire rate bonus var bonusMultiplier = 1 + self.linkedTowers.length * 0.15; self.tower.supportBonus = bonusMultiplier; }; self.update = function () { if (self.branchType !== 'none') { self.createSupportLinks(); self.applySupportBonus(); } }; self.destroy = function () { if (self.isDestroyed) return; self.isDestroyed = true; // Remove link beams if (self.tower.linkBeam && self.tower.linkBeam.parent) { self.tower.linkBeam.parent.removeChild(self.tower.linkBeam); } tween.stop(self); if (self.parent) { self.parent.removeChild(self); } Container.prototype.destroy.call(self); }; return self; }); var TowerUltimate = Container.expand(function (tower, ultimateType) { var self = Container.call(this); self.tower = tower; self.ultimateType = ultimateType || 'none'; self.cooldownTime = 1800; // 30 seconds at 60fps self.currentCooldown = 0; self.isDestroyed = false; // Add ultimate indicator var ultimateIcon = self.attachAsset('ultimateIndicator', { anchorX: 0.5, anchorY: 0.5 }); ultimateIcon.y = -self.tower.children[0].height / 2 - 35; ultimateIcon.alpha = 0.3; // Dim when on cooldown // Set ultimate-specific properties switch (self.ultimateType) { case 'orbital': self.cooldownTime = 2400; // 40 seconds ultimateIcon.tint = 0xFF0000; break; case 'nanite': self.cooldownTime = 2100; // 35 seconds ultimateIcon.tint = 0x00FF88; break; case 'temporal': self.cooldownTime = 1800; // 30 seconds ultimateIcon.tint = 0x9966FF; break; default: ultimateIcon.tint = 0xFFD700; } self.canActivate = function () { return self.currentCooldown <= 0 && !self.isDestroyed; }; self.activate = function () { if (!self.canActivate()) return false; self.currentCooldown = self.cooldownTime; // Visual cooldown effect ultimateIcon.alpha = 0.3; tween(ultimateIcon, { alpha: 1.0 }, { duration: self.cooldownTime, easing: tween.linear }); switch (self.ultimateType) { case 'orbital': return self.orbitalStrike(); case 'nanite': return self.naniteSwarm(); case 'temporal': return self.timeDilation(); } return false; }; self.orbitalStrike = function () { // Find target area with most enemies var bestTarget = null; var maxEnemies = 0; var strikeRadius = CELL_SIZE * 3; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var nearbyCount = 0; for (var j = 0; j < enemies.length; j++) { var otherEnemy = enemies[j]; var dx = otherEnemy.x - enemy.x; var dy = otherEnemy.y - enemy.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= strikeRadius) { nearbyCount++; } } if (nearbyCount > maxEnemies) { maxEnemies = nearbyCount; bestTarget = enemy; } } if (bestTarget) { // Create orbital strike effect var strikeEffect = new Container(); strikeEffect.x = bestTarget.x; strikeEffect.y = bestTarget.y; var strikeGraphics = strikeEffect.attachAsset('orbitalStrike', { anchorX: 0.5, anchorY: 0.5 }); strikeGraphics.alpha = 0.0; strikeGraphics.scaleX = 0.1; strikeGraphics.scaleY = 0.1; strikeGraphics.blendMode = 1; // Additive game.addChild(strikeEffect); // Warning phase tween(strikeGraphics, { alpha: 0.7, scaleX: 1.0, scaleY: 1.0 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { // Strike phase - massive damage var strikeDamage = self.tower.damage * 15; for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; var dx = enemy.x - bestTarget.x; var dy = enemy.y - bestTarget.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= strikeRadius) { enemy.health -= strikeDamage; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } // Visual damage effect tween(enemy, { tint: 0xFFFFFF, scaleX: 1.2, scaleY: 1.2 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(enemy, { scaleX: 1.0, scaleY: 1.0 }, { duration: 200, easing: tween.easeIn }); } }); } } // Final explosion effect tween(strikeGraphics, { scaleX: 2.5, scaleY: 2.5, alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { if (strikeEffect.parent) { strikeEffect.parent.removeChild(strikeEffect); } } }); } }); var notification = game.addChild(new Notification("🛰️ ORBITAL STRIKE INCOMING! 🛰️")); notification.x = 2048 / 2; notification.y = grid.height - 50; return true; } return false; }; self.naniteSwarm = function () { // Create nanite swarm that spreads poison and self-replicates var swarmCenter = self.tower; var swarmRadius = CELL_SIZE * 4; var swarmDamage = self.tower.damage * 3; // Create visual swarm effect var swarmEffect = new Container(); swarmEffect.x = swarmCenter.x; swarmEffect.y = swarmCenter.y; var swarmGraphics = swarmEffect.attachAsset('naniteSwarm', { anchorX: 0.5, anchorY: 0.5 }); swarmGraphics.alpha = 0.8; swarmGraphics.tint = 0x00FF88; game.addChild(swarmEffect); // Create multiple nanite particles var particles = []; for (var i = 0; i < 12; i++) { var particle = swarmEffect.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); particle.width = 15; particle.height = 15; particle.tint = 0x44FFAA; var angle = i / 12 * Math.PI * 2; particle.x = Math.cos(angle) * 50; particle.y = Math.sin(angle) * 50; particles.push(particle); } // Animate swarm expansion tween(swarmEffect, { scaleX: 3.0, scaleY: 3.0 }, { duration: 2000, easing: tween.easeOut }); // Animate particles spiraling outward for (var i = 0; i < particles.length; i++) { var particle = particles[i]; tween(particle, { x: particle.x * 3, y: particle.y * 3, rotation: Math.PI * 4 }, { duration: 2000, delay: i * 100, easing: tween.easeOut }); } // Apply nanite effects over time var swarmDuration = 180; // 3 seconds var _swarmTick = function swarmTick() { swarmDuration--; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - swarmCenter.x; var dy = enemy.y - swarmCenter.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= swarmRadius) { // Apply nanite damage enemy.health -= swarmDamage; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } // Apply enhanced poison effect enemy.poisoned = true; enemy.poisonDamage = swarmDamage * 0.5; enemy.poisonDuration = 300; // Extended duration // Self-replication chance if (Math.random() < 0.3 && swarmDuration > 60) { // 30% chance to spread to nearby enemies for (var j = 0; j < enemies.length; j++) { var nearbyEnemy = enemies[j]; if (nearbyEnemy !== enemy) { var nearbyDx = nearbyEnemy.x - enemy.x; var nearbyDy = nearbyEnemy.y - enemy.y; var nearbyDistance = Math.sqrt(nearbyDx * nearbyDx + nearbyDy * nearbyDy); if (nearbyDistance <= CELL_SIZE * 2) { nearbyEnemy.poisoned = true; nearbyEnemy.poisonDamage = swarmDamage * 0.3; nearbyEnemy.poisonDuration = 240; break; } } } } } } if (swarmDuration > 0) { LK.setTimeout(_swarmTick, 100); // Continue every 100ms } else { // Clean up swarm effect tween(swarmEffect, { alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (swarmEffect.parent) { swarmEffect.parent.removeChild(swarmEffect); } } }); } }; LK.setTimeout(_swarmTick, 100); // Start swarm effects var notification = game.addChild(new Notification("🤖 NANITE SWARM DEPLOYED! 🤖")); notification.x = 2048 / 2; notification.y = grid.height - 50; return true; }; self.timeDilation = function () { // Create temporal distortion field that slows time for enemies var dilationCenter = self.tower; var dilationRadius = CELL_SIZE * 5; var dilationDuration = 600; // 10 seconds // Create visual time dilation effect var dilationEffect = new Container(); dilationEffect.x = dilationCenter.x; dilationEffect.y = dilationCenter.y; var dilationGraphics = dilationEffect.attachAsset('timeDilation', { anchorX: 0.5, anchorY: 0.5 }); dilationGraphics.alpha = 0.4; dilationGraphics.tint = 0x9966FF; dilationGraphics.blendMode = 1; // Additive game.addChild(dilationEffect); // Pulsing time distortion effect tween(dilationGraphics, { scaleX: 1.3, scaleY: 1.3, alpha: 0.7 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(dilationGraphics, { scaleX: 1.0, scaleY: 1.0, alpha: 0.4 }, { duration: 1000, easing: tween.easeInOut }); } }); // Apply time dilation effects var affectedEnemies = []; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - dilationCenter.x; var dy = enemy.y - dilationCenter.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= dilationRadius) { // Store original speed if (!enemy.originalDilationSpeed) { enemy.originalDilationSpeed = enemy.speed; } // Extreme slow effect (90% speed reduction) enemy.speed = enemy.originalDilationSpeed * 0.1; enemy.temporallyDilated = true; enemy.dilationDuration = dilationDuration; affectedEnemies.push(enemy); // Visual distortion effect on enemy tween(enemy, { tint: 0x9966FF, alpha: 0.8 }, { duration: 300, easing: tween.easeOut }); } } // During dilation, towers in the field get boosted fire rate var boostedTowers = []; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.x - dilationCenter.x; var dy = tower.y - dilationCenter.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= dilationRadius && tower !== self.tower) { tower.originalDilationFireRate = tower.fireRate; tower.fireRate = Math.floor(tower.fireRate * 0.3); // 70% faster firing tower.temporallyBoosted = true; boostedTowers.push(tower); } } // Clean up after duration LK.setTimeout(function () { // Restore enemy speeds for (var i = 0; i < affectedEnemies.length; i++) { var enemy = affectedEnemies[i]; if (enemy.parent && !enemy.isDestroyed) { enemy.speed = enemy.originalDilationSpeed; enemy.temporallyDilated = false; // Restore enemy appearance tween(enemy, { alpha: 1.0 }, { duration: 300, easing: tween.easeIn }); } } // Restore tower fire rates for (var i = 0; i < boostedTowers.length; i++) { var tower = boostedTowers[i]; if (tower.parent && !tower.isDestroyed) { tower.fireRate = tower.originalDilationFireRate; tower.temporallyBoosted = false; } } // Remove visual effect tween(dilationEffect, { alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: 1000, easing: tween.easeIn, onFinish: function onFinish() { if (dilationEffect.parent) { dilationEffect.parent.removeChild(dilationEffect); } } }); }, dilationDuration * 16.67); // Convert frames to milliseconds var notification = game.addChild(new Notification("⏰ TIME DILATION ACTIVATED! ⏰")); notification.x = 2048 / 2; notification.y = grid.height - 50; return true; }; self.update = function () { if (self.currentCooldown > 0) { self.currentCooldown--; } }; self.destroy = function () { if (self.isDestroyed) return; self.isDestroyed = true; tween.stop(self); if (self.parent) { self.parent.removeChild(self); } Container.prototype.destroy.call(self); }; return self; }); var TutorialSystem = Container.expand(function () { var self = Container.call(this); self.currentStep = 0; self.isActive = false; self.isWaitingForAction = false; self.tutorialCompleted = storage.tutorialCompleted || false; self.currentPhase = 'introduction'; // introduction, basics, combat, advanced, endgame self.highlightOverlay = null; self.actionArrow = null; self.tutorialSteps = [ // INTRODUCTION PHASE { phase: 'introduction', title: "Welcome to Space Tower Defense!", text: "Commander, our space station is under attack!\nYou must defend against waves of alien invaders.", highlight: null, action: null, waitForAction: false }, { phase: 'introduction', title: "Your Mission", text: "Build defensive towers to stop enemies from reaching\nthe bottom of the screen and destroying our base.", highlight: null, action: null, waitForAction: false }, { phase: 'introduction', title: "Resources Overview", text: "Gold: Used to build and upgrade towers\nLives: Lost when enemies reach the bottom\nScore: Points earned for defeating enemies", highlight: "ui", action: null, waitForAction: false }, // BASICS PHASE { phase: 'basics', title: "Building Your First Tower", text: "Let's start by building a basic tower.\nDrag the 'Default' tower from the bottom panel.", highlight: "sourceTowers", action: "buildTower", waitForAction: true }, { phase: 'basics', title: "Tower Placement", text: "Good! Towers need 2x2 space and cannot block enemy paths.\nThe green preview shows valid placement areas.", highlight: null, action: null, waitForAction: false }, { phase: 'basics', title: "Tower Information", text: "Tap on your tower to see its stats and upgrade options.\nTry tapping the tower you just built.", highlight: "tower", action: "selectTower", waitForAction: true }, { phase: 'basics', title: "Upgrading Towers", text: "Excellent! Upgrading increases damage and fire rate.\nUpgrades get more expensive at higher levels.", highlight: null, action: null, waitForAction: false }, { phase: 'basics', title: "Starting Combat", text: "Now let's face some enemies!\nTap the 'Start Game' button to begin wave 1.", highlight: "startButton", action: "startGame", waitForAction: true }, // COMBAT PHASE { phase: 'combat', title: "Enemy Types - Normal", text: "The first enemies are normal type - balanced health and speed.\nYour towers will automatically target and fire at them.", highlight: null, action: null, waitForAction: false }, { phase: 'combat', title: "Earning Gold", text: "Great! You earn gold for each enemy defeated.\nUse this gold to build more towers and upgrades.", highlight: "gold", action: null, waitForAction: false }, { phase: 'combat', title: "Tower Specialization", text: "Different tower types are effective against different enemies.\nLet's build a Rapid tower for faster firing.", highlight: "sourceTowers", action: "buildRapidTower", waitForAction: true }, { phase: 'combat', title: "Enemy Types - Fast", text: "Wave 2 brings fast enemies (blue).\nRapid towers are excellent against quick targets.", highlight: null, action: null, waitForAction: false }, { phase: 'combat', title: "Tower Range", text: "Each tower has a different range.\nSniper towers have long range but fire slowly.", highlight: null, action: null, waitForAction: false }, { phase: 'combat', title: "Build a Sniper Tower", text: "Try building a Sniper tower (orange) for long-range support.\nPlace it where it can cover a large area.", highlight: "sourceTowers", action: "buildSniperTower", waitForAction: true }, // ADVANCED PHASE { phase: 'advanced', title: "Special Enemy Types", text: "As waves progress, you'll face special enemies:\n• Flying (yellow) - immune to some towers\n• Immune (red) - resistant to effects", highlight: null, action: null, waitForAction: false }, { phase: 'advanced', title: "Splash Damage", text: "Splash towers (green) deal area damage.\nThey're effective against groups of enemies.", highlight: null, action: null, waitForAction: false }, { phase: 'advanced', title: "Support Towers", text: "Slow towers (purple) reduce enemy speed.\nPoison towers (cyan) deal damage over time.", highlight: null, action: null, waitForAction: false }, { phase: 'advanced', title: "Hero Units", text: "Deploy hero units for additional firepower!\nHeroes can move around and have special abilities.", highlight: "heroPanel", action: null, waitForAction: false }, { phase: 'advanced', title: "Deploy a Tank Hero", text: "Try deploying a Tank hero (green).\nTap the tank button, then tap on the battlefield.", highlight: "heroPanel", action: "deployHero", waitForAction: true }, { phase: 'advanced', title: "Hero Abilities", text: "Heroes gain experience and can use special abilities.\nTap on your hero to activate its ability when ready.", highlight: null, action: null, waitForAction: false }, { phase: 'advanced', title: "Station Modules", text: "Customize your space station with modules!\nTap the 'Customize' button to manage station upgrades.", highlight: "stationPanel", action: null, waitForAction: false }, // ENDGAME PHASE { phase: 'endgame', title: "Boss Waves", text: "Every 10th wave is a Boss wave with powerful enemies.\nBoss enemies have much more health but give more gold.", highlight: "waveIndicator", action: null, waitForAction: false }, { phase: 'endgame', title: "Environmental Hazards", text: "Watch for space hazards like asteroids, nebulas, and solar flares.\nThese can help or hinder your defense strategy.", highlight: null, action: null, waitForAction: false }, { phase: 'endgame', title: "Advanced Strategies", text: "• Upgrade key towers to maximum level\n• Use hero abilities strategically\n• Adapt to enemy types each wave", highlight: null, action: null, waitForAction: false }, { phase: 'endgame', title: "Tower Specializations", text: "At level 3, some towers can specialize:\n• Sniper: Anti-Armor or Long-Range\n• Splash: Nuclear or Chain Reaction", highlight: null, action: null, waitForAction: false }, { phase: 'endgame', title: "Path Branching", text: "Enemies may take different routes through your defenses.\nSome paths are shorter but more defended.", highlight: null, action: null, waitForAction: false }, { phase: 'endgame', title: "Tutorial Complete!", text: "You're ready to defend the station, Commander!\nSurvive all 50 waves to achieve victory.", highlight: null, action: null, waitForAction: false }]; var tutorialContainer = new Container(); self.addChild(tutorialContainer); var overlay = tutorialContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); overlay.width = 1900; overlay.height = 450; overlay.tint = 0x0A0A0A; overlay.alpha = 0.95; // Phase indicator var phaseText = new Text2("", { size: 40, fill: 0xFFD700, weight: 600 }); phaseText.anchor.set(0.5, 0.5); phaseText.y = -160; tutorialContainer.addChild(phaseText); var titleText = new Text2("", { size: 65, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -100; tutorialContainer.addChild(titleText); var bodyText = new Text2("", { size: 45, fill: 0xE0E0E0, weight: 400 }); bodyText.anchor.set(0.5, 0.5); bodyText.y = 0; tutorialContainer.addChild(bodyText); // Progress indicator var progressText = new Text2("", { size: 35, fill: 0xCCCCCC, weight: 400 }); progressText.anchor.set(0.5, 0.5); progressText.y = 80; tutorialContainer.addChild(progressText); var buttonContainer = new Container(); buttonContainer.y = 140; tutorialContainer.addChild(buttonContainer); var nextButton = new Container(); var nextBg = nextButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); nextBg.width = 200; nextBg.height = 80; nextBg.tint = 0x00AA00; var nextText = new Text2("Next", { size: 55, fill: 0xFFFFFF, weight: 800 }); nextText.anchor.set(0.5, 0.5); nextButton.addChild(nextText); nextButton.x = -250; buttonContainer.addChild(nextButton); var skipButton = new Container(); var skipBg = skipButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); skipBg.width = 200; skipBg.height = 80; skipBg.tint = 0xAA0000; var skipText = new Text2("Skip", { size: 55, fill: 0xFFFFFF, weight: 800 }); skipText.anchor.set(0.5, 0.5); skipButton.addChild(skipText); skipButton.x = 0; buttonContainer.addChild(skipButton); var prevButton = new Container(); var prevBg = prevButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); prevBg.width = 200; prevBg.height = 80; prevBg.tint = 0x0066CC; var prevText = new Text2("Back", { size: 55, fill: 0xFFFFFF, weight: 800 }); prevText.anchor.set(0.5, 0.5); prevButton.addChild(prevText); prevButton.x = 250; buttonContainer.addChild(prevButton); self.createHighlight = function (target) { if (self.highlightOverlay) { self.highlightOverlay.parent.removeChild(self.highlightOverlay); } self.highlightOverlay = new Container(); var highlight = self.highlightOverlay.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); highlight.width = 300; highlight.height = 300; highlight.tint = 0xFFD700; highlight.alpha = 0.3; // Pulsing animation tween(highlight, { scaleX: 1.2, scaleY: 1.2, alpha: 0.6 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.highlightOverlay.isDestroyed) { tween(highlight, { scaleX: 1.0, scaleY: 1.0, alpha: 0.3 }, { duration: 1000, easing: tween.easeInOut }); } } }); game.addChild(self.highlightOverlay); return self.highlightOverlay; }; self.removeHighlight = function () { if (self.highlightOverlay && self.highlightOverlay.parent) { tween.stop(self.highlightOverlay); self.highlightOverlay.parent.removeChild(self.highlightOverlay); self.highlightOverlay = null; } }; self.showStep = function (stepIndex) { if (stepIndex >= self.tutorialSteps.length) { self.endTutorial(); return; } var step = self.tutorialSteps[stepIndex]; var phaseDisplay = step.phase.charAt(0).toUpperCase() + step.phase.slice(1) + " Phase"; phaseText.setText(phaseDisplay); titleText.setText(step.title); bodyText.setText(step.text); progressText.setText("Step " + (stepIndex + 1) + " of " + self.tutorialSteps.length); // Update button states nextButton.visible = !step.waitForAction; skipButton.visible = true; prevButton.visible = stepIndex > 0; if (stepIndex === self.tutorialSteps.length - 1) { nextText.setText("Finish!"); skipText.setText("Start!"); } // Handle highlights self.removeHighlight(); if (step.highlight) { switch (step.highlight) { case "ui": // Highlight UI area if (self.highlightOverlay) { self.highlightOverlay.x = 2048 / 2; self.highlightOverlay.y = 100; } break; case "sourceTowers": // Highlight tower selection area if (sourceTowers.length > 0) { var highlight = self.createHighlight(); highlight.x = sourceTowers[0].x; highlight.y = sourceTowers[0].y; } break; case "gold": // Highlight gold display var highlight = self.createHighlight(); highlight.x = 2048 / 2 - 400; highlight.y = 100; break; case "heroPanel": // Highlight hero panel var highlight = self.createHighlight(); highlight.x = 2048 / 2; highlight.y = 200; break; case "stationPanel": // Highlight station panel var highlight = self.createHighlight(); highlight.x = 2048 - 200; highlight.y = 250; break; case "waveIndicator": // Highlight wave indicator var highlight = self.createHighlight(); highlight.x = 2048 / 2; highlight.y = 2732 - 80; break; case "startButton": // Highlight start game button if (waveIndicator && waveIndicator.waveMarkers.length > 0) { var highlight = self.createHighlight(); highlight.x = waveIndicator.waveMarkers[0].x + waveIndicator.x; highlight.y = waveIndicator.waveMarkers[0].y + waveIndicator.y; } break; } } // Set up action waiting if (step.waitForAction) { self.isWaitingForAction = true; self.expectedAction = step.action; } }; self.startTutorial = function () { self.isActive = true; self.visible = true; self.currentStep = 0; self.showStep(0); // Position tutorial in center of screen tutorialContainer.x = 2048 / 2; tutorialContainer.y = 1400; }; self.nextStep = function () { if (self.isWaitingForAction) return; self.currentStep++; self.showStep(self.currentStep); }; self.prevStep = function () { if (self.currentStep > 0) { self.currentStep--; self.showStep(self.currentStep); } }; self.endTutorial = function () { self.isActive = false; self.visible = false; self.removeHighlight(); storage.tutorialCompleted = true; self.tutorialCompleted = true; // Auto-start the game after tutorial if (waveIndicator && !waveIndicator.gameStarted) { waveIndicator.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; } var notification = game.addChild(new Notification("Tutorial completed! Good luck, Commander!")); notification.x = 2048 / 2; notification.y = grid.height - 100; }; self.checkActionCompleted = function (action) { if (!self.isWaitingForAction || self.expectedAction !== action) return; self.isWaitingForAction = false; self.expectedAction = null; // Show next button and auto-advance after delay nextButton.visible = true; LK.setTimeout(function () { if (self.isActive && !self.isWaitingForAction) { self.nextStep(); } }, 1500); }; nextButton.down = function () { self.nextStep(); }; skipButton.down = function () { self.endTutorial(); }; prevButton.down = function () { self.prevStep(); }; self.visible = false; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xE0E0E0, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; // Tutorial action tracking if (tutorialSystem && tutorialSystem.isActive) { tutorialSystem.checkActionCompleted('startGame'); } var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; // Play wave start sound LK.getSound('waveStart').play(); } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0xFFFF00; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Boss Normal"; break; case 'fast': block.tint = 0x00AAFF; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xAA0000; waveType = "Boss Immune"; break; case 'flying': block.tint = 0xFFFF00; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the color of the wave type switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; break; case 'fast': block.tint = 0x00AAFF; break; case 'immune': block.tint = 0xAA0000; break; case 'flying': block.tint = 0xFFFF00; break; default: block.tint = 0xFF0000; break; } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a crown or some indicator to the wave marker for boss waves var bossIndicator = marker.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); bossIndicator.width = 30; bossIndicator.height = 30; bossIndicator.tint = 0xFFD700; // Gold color bossIndicator.y = -block.height / 2 - 15; // Change the wave type text to indicate boss waveType = "BOSS"; } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2(waveType, { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 3; waveTypeShadow.y = 3; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (every 10th wave) if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; for (var i = 0; i < totalWaves + 1; i++) { var marker = self.waveMarkers[i]; if (i === 0) { continue; } var block = marker.children[0]; if (i - 1 < currentWave) { block.alpha = .5; } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { waveTimer++; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; if (currentWave != 1) { var waveType = self.getWaveTypeName(currentWave); var enemyCount = self.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } } }; self.handleWaveProgression(); }; return self; }); var Wormhole = Container.expand(function (x, y, targetX, targetY) { var self = Container.call(this); self.x = x; self.y = y; self.targetX = targetX; self.targetY = targetY; self.radius = 90; self.isDestroyed = false; self.cooldown = 0; var wormholeGraphics = self.attachAsset('wormhole', { anchorX: 0.5, anchorY: 0.5 }); wormholeGraphics.alpha = 0.7; wormholeGraphics.blendMode = 1; // Additive blending // Spinning animation tween(wormholeGraphics, { rotation: Math.PI * 2 }, { duration: 3000, easing: tween.linear, onFinish: function onFinish() { if (!self.isDestroyed) { wormholeGraphics.rotation = 0; } } }); // Create target wormhole visual self.targetWormhole = new Container(); self.targetWormhole.x = targetX; self.targetWormhole.y = targetY; var targetGraphics = self.targetWormhole.attachAsset('wormhole', { anchorX: 0.5, anchorY: 0.5 }); targetGraphics.alpha = 0.5; targetGraphics.tint = 0xFF1493; // Different color for exit targetGraphics.blendMode = 1; game.addChild(self.targetWormhole); // Sync rotation with main wormhole tween(targetGraphics, { rotation: -Math.PI * 2 }, { duration: 3000, easing: tween.linear, onFinish: function onFinish() { if (!self.isDestroyed) { targetGraphics.rotation = 0; } } }); self.update = function () { if (self.cooldown > 0) { self.cooldown--; return; } // Check for enemies within range for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.isFlying) continue; // Flying enemies ignore wormholes var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.radius) { // Teleport enemy self.teleportEnemy(enemy); self.cooldown = 180; // 3 second cooldown break; } } }; self.teleportEnemy = function (enemy) { // Create teleport effect at source var sourceEffect = new Container(); sourceEffect.x = self.x; sourceEffect.y = self.y; var sourceGraphics = sourceEffect.attachAsset('teleportEffect', { anchorX: 0.5, anchorY: 0.5 }); sourceGraphics.tint = 0x9400D3; sourceGraphics.alpha = 0.8; game.addChild(sourceEffect); // Create teleport effect at destination var destEffect = new Container(); destEffect.x = self.targetX; destEffect.y = self.targetY; var destGraphics = destEffect.attachAsset('teleportEffect', { anchorX: 0.5, anchorY: 0.5 }); destGraphics.tint = 0xFF1493; destGraphics.alpha = 0.8; game.addChild(destEffect); // Animate effects tween(sourceEffect, { scaleX: 2, scaleY: 2, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { if (sourceEffect.parent) { sourceEffect.parent.removeChild(sourceEffect); } } }); tween(destEffect, { scaleX: 2, scaleY: 2, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { if (destEffect.parent) { destEffect.parent.removeChild(destEffect); } } }); // Teleport enemy enemy.x = self.targetX; enemy.y = self.targetY; enemy.currentCellX = Math.floor((self.targetX - grid.x) / CELL_SIZE); enemy.currentCellY = Math.floor((self.targetY - grid.y) / CELL_SIZE); enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = null; // Reset pathfinding // Flash the wormhole tween(wormholeGraphics, { scaleX: 1.5, scaleY: 1.5, alpha: 1.0 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(wormholeGraphics, { scaleX: 1.0, scaleY: 1.0, alpha: 0.7 }, { duration: 300, easing: tween.easeIn }); } }); }; self.destroy = function () { if (self.isDestroyed) return; self.isDestroyed = true; // Remove target wormhole if (self.targetWormhole && self.targetWormhole.parent) { self.targetWormhole.parent.removeChild(self.targetWormhole); } var wormholeIndex = wormholes.indexOf(self); if (wormholeIndex !== -1) { wormholes.splice(wormholeIndex, 1); } tween.stop(self); if (self.parent) { self.parent.removeChild(self); } Container.prototype.destroy.call(self); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000011 }); /**** * Game Code ****/ // Add background image // Base tower structures for each type // Tower gun/weapon parts - Level 1 // Tower gun/weapon parts - Level 2 // Tower gun/weapon parts - Level 3 // Tower gun/weapon parts - Level 4 // Tower gun/weapon parts - Level 5 // Tower gun/weapon parts - Level 6 (Max Level) // Support structures and details // Visual indicators // Specialization visual elements var backgroundImage = game.attachAsset('spaceBackground', { anchorX: 0.5, anchorY: 0.5 }); backgroundImage.x = 2048 / 2; backgroundImage.y = 2732 / 2; backgroundImage.alpha = 0.8; var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; // Object pooling for bullets var bulletPool = []; var maxBulletPoolSize = 100; function getBulletFromPool(startX, startY, targetEnemy, damage, speed) { var bullet; if (bulletPool.length > 0) { bullet = bulletPool.pop(); // Reset bullet properties bullet.targetEnemy = targetEnemy; bullet.damage = damage || 10; bullet.speed = speed || 5; bullet.x = startX; bullet.y = startY; bullet.isDestroyed = false; bullet.visible = true; bullet.alpha = 1; if (bullet.children[0]) { bullet.children[0].alpha = 1; bullet.children[0].scaleX = 1; bullet.children[0].scaleY = 1; } } else { bullet = new Bullet(startX, startY, targetEnemy, damage, speed); } return bullet; } function returnBulletToPool(bullet) { if (bulletPool.length < maxBulletPoolSize && bullet.parent) { // Clean up target enemy reference properly if (bullet.targetEnemy && bullet.targetEnemy.bulletsTargetingThis) { var bulletIndex = bullet.targetEnemy.bulletsTargetingThis.indexOf(bullet); if (bulletIndex !== -1) { bullet.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } // Stop all tweens before pooling tween.stop(bullet); if (bullet.children && bullet.children[0]) { tween.stop(bullet.children[0]); } bullet.parent.removeChild(bullet); bullet.targetEnemy = null; bullet.visible = false; bulletPool.push(bullet); } else { bullet.destroy(); } } var defenses = []; var selectedTower = null; var gold = 60; // Reduced starting gold to make early choices more meaningful var lives = 20; var score = 0; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; // Hero system variables var heroes = []; var maxHeroes = 3; var heroDeploymentMode = false; var heroPreview = null; var selectedHeroType = 'tank'; // Station customization variables var stationCustomizer = null; var researchLabs = []; var shieldGenerators = []; var teleporterNodes = []; // Environmental hazards variables var asteroids = []; var nebulaClouds = []; var solarFlares = []; var wormholes = []; var hazardTimer = 0; var nextHazardTime = 1800; // 30 seconds at 60fps // Branching routes system variables var pathJunctions = []; var alternativePaths = []; var routeChoiceInfluences = []; var pathDifficulties = {}; // Tower specialization variables var specializationMenus = []; var towerLinks = []; // Ultimate ability variables var activeUltimates = []; var ultimateEffects = []; // Load station data from storage var stationData = storage.stationData || { modules: [], researchPoints: 0, completedTechs: [] }; // Initialize route visualizer var routeVisualizer = new RouteVisualizer(); game.addChild(routeVisualizer); // Initialize branching routes system function initializeBranchingRoutes() { // Create main junction points in the middle area var mainJunction = new PathJunction(grid.x + 12 * CELL_SIZE, grid.y + 15 * CELL_SIZE, [{ targetX: 10, targetY: 22, // Left branch - shorter but more defended isShortPath: true, pathLength: 8, towerDensity: 0, currentEnemyCount: 0, recentSwarmTraffic: 0, hasAntiAir: false, difficulty: 20 }, { targetX: 14, targetY: 22, // Right branch - longer but less defended isShortPath: false, pathLength: 12, towerDensity: 0, currentEnemyCount: 0, recentSwarmTraffic: 0, hasAntiAir: false, difficulty: 15 }]); // Secondary junction for more complex routing var secondaryJunction = new PathJunction(grid.x + 8 * CELL_SIZE, grid.y + 12 * CELL_SIZE, [{ targetX: 6, targetY: 18, // Far left route isShortPath: false, pathLength: 15, towerDensity: 0, currentEnemyCount: 0, recentSwarmTraffic: 0, hasAntiAir: false, difficulty: 25 }, { targetX: 12, targetY: 15, // Center route to main junction isShortPath: true, pathLength: 6, towerDensity: 0, currentEnemyCount: 0, recentSwarmTraffic: 0, hasAntiAir: false, difficulty: 30 }]); game.addChild(mainJunction); game.addChild(secondaryJunction); pathJunctions.push(mainJunction); pathJunctions.push(secondaryJunction); var notification = game.addChild(new Notification("🔀 Multi-path routing system activated! 🔀")); notification.x = 2048 / 2; notification.y = grid.height - 150; } // Initialize the branching system after a short delay LK.setTimeout(function () { initializeBranchingRoutes(); }, 2000); function getModuleCost(moduleType, level) { var baseCosts = { research: 50, shield: 75, teleporter: 100 }; var baseCost = baseCosts[moduleType] || 50; return Math.floor(baseCost * Math.pow(1.5, level - 1)); } function getTechnology(techId) { var technologies = { 'tower_damage': { id: 'tower_damage', name: 'Enhanced Weapons', cost: 100, onComplete: function onComplete() { // Increase all tower damage by 20% for (var i = 0; i < towers.length; i++) { towers[i].damage = Math.floor(towers[i].damage * 1.2); } } }, 'tower_range': { id: 'tower_range', name: 'Extended Range', cost: 150, onComplete: function onComplete() { // Increase all tower ranges for (var i = 0; i < towers.length; i++) { towers[i].refreshCellsInRange(); } } }, 'shield_efficiency': { id: 'shield_efficiency', name: 'Shield Efficiency', cost: 200, onComplete: function onComplete() { // Reduce shield energy costs for (var i = 0; i < shieldGenerators.length; i++) { shieldGenerators[i].maxShieldEnergy *= 1.5; shieldGenerators[i].shieldEnergy = shieldGenerators[i].maxShieldEnergy; } } } }; return technologies[techId]; } // Spatial partitioning for collision optimization var spatialGrid = { cellSize: CELL_SIZE * 2, cells: {}, getKey: function getKey(x, y) { var gridX = Math.floor(x / this.cellSize); var gridY = Math.floor(y / this.cellSize); return gridX + ',' + gridY; }, addEnemy: function addEnemy(enemy) { var key = this.getKey(enemy.x, enemy.y); if (!this.cells[key]) this.cells[key] = []; this.cells[key].push(enemy); }, removeEnemy: function removeEnemy(enemy) { var key = this.getKey(enemy.x, enemy.y); if (this.cells[key]) { var index = this.cells[key].indexOf(enemy); if (index !== -1) { this.cells[key].splice(index, 1); } } }, getNearbyEnemies: function getNearbyEnemies(x, y, range) { var nearby = []; var checkRadius = Math.ceil(range / this.cellSize); var centerX = Math.floor(x / this.cellSize); var centerY = Math.floor(y / this.cellSize); for (var dx = -checkRadius; dx <= checkRadius; dx++) { for (var dy = -checkRadius; dy <= checkRadius; dy++) { var key = centerX + dx + ',' + (centerY + dy); if (this.cells[key]) { nearby = nearby.concat(this.cells[key]); } } } return nearby; }, clear: function clear() { this.cells = {}; } }; var enemiesToSpawn = 10; // Default number of enemies per wave // Visual effects limiting var activeEffects = []; var maxActiveEffects = 20; var effectSkipCounter = 0; function canCreateEffect() { // Count active effects and remove destroyed ones activeEffects = activeEffects.filter(function (effect) { return effect.parent && !effect.isDestroyed; }); // Limit based on performance if (activeEffects.length >= maxActiveEffects) { effectSkipCounter++; // Skip every other effect when at limit return effectSkipCounter % 2 === 0; } return true; } function registerEffect(effect) { if (activeEffects.length < maxActiveEffects) { activeEffects.push(effect); } } var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); } function setGold(value) { gold = value; updateUI(); } // Performance monitoring and adaptive logic var performanceMonitor = { frameTime: 16.67, // Target 60 FPS lastTime: 0, avgFrameTime: 16.67, frameCount: 0, performanceLevel: 1.0, // 1.0 = full performance, 0.5 = half performance update: function update() { var currentTime = Date.now(); if (this.lastTime > 0) { var deltaTime = currentTime - this.lastTime; this.avgFrameTime = this.avgFrameTime * 0.9 + deltaTime * 0.1; // Adjust performance level based on frame time if (this.avgFrameTime > 25) { // Below 40 FPS this.performanceLevel = Math.max(0.3, this.performanceLevel - 0.1); } else if (this.avgFrameTime < 18) { // Above 55 FPS this.performanceLevel = Math.min(1.0, this.performanceLevel + 0.05); } } this.lastTime = currentTime; this.frameCount++; }, shouldSkipUpdate: function shouldSkipUpdate() { return Math.random() > this.performanceLevel; } }; // Memory monitoring system var memoryMonitor = { lastEnemyCount: 0, lastBulletCount: 0, lastTowerCount: 0, lastEffectCount: 0, update: function update() { // Track object counts for memory leak detection this.lastEnemyCount = enemies.length; this.lastBulletCount = bullets.length; this.lastTowerCount = towers.length; this.lastEffectCount = activeEffects.length; // Log warnings if counts are unexpectedly high if (this.lastEnemyCount > 100) { console.warn("High enemy count detected:", this.lastEnemyCount); } if (this.lastBulletCount > 200) { console.warn("High bullet count detected:", this.lastBulletCount); } if (this.lastEffectCount > 50) { console.warn("High effect count detected:", this.lastEffectCount); } }, forceCleanup: function forceCleanup() { // Emergency cleanup when memory usage is too high console.log("Performing emergency cleanup..."); // Clean up destroyed effects activeEffects = activeEffects.filter(function (effect) { return effect.parent && !effect.isDestroyed; }); // Clean up orphaned bullets for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent || bullets[i].isDestroyed) { bullets.splice(i, 1); } } // Clean up bullet pool bulletPool = bulletPool.filter(function (bullet) { return !bullet.isDestroyed; }); console.log("Cleanup complete. Objects remaining:", { enemies: enemies.length, bullets: bullets.length, towers: towers.length, effects: activeEffects.length, pooledBullets: bulletPool.length }); } }; var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY) { var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 8; // Increased base cost from 5 switch (towerType) { case 'rapid': cost = 12; // Reduced from 15 - cheaper early game tower break; case 'sniper': cost = 20; // Reduced from 25 for better accessibility break; case 'splash': cost = 28; // Reduced from 35 break; case 'slow': cost = 32; // Reduced from 45 - utility tower should be accessible break; case 'poison': cost = 38; // Reduced from 55 - was too expensive break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); grid.pathFind(); grid.renderDebug(); // Play tower placement sound LK.getSound('towerPlace').play(); // Tutorial action tracking if (tutorialSystem && tutorialSystem.isActive) { if (towerType === 'default') { tutorialSystem.checkActionCompleted('buildTower'); } else if (towerType === 'rapid') { tutorialSystem.checkActionCompleted('buildRapidTower'); } else if (towerType === 'sniper') { tutorialSystem.checkActionCompleted('buildSniperTower'); } } return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { // Handle hero deployment if (heroDeploymentMode) { // Check if tap is in valid deployment area (not on towers or paths) var deploymentValid = true; var gridX = Math.floor((x - grid.x) / CELL_SIZE); var gridY = Math.floor((y - grid.y) / CELL_SIZE); // Check if position is within grid bounds and not on blocked areas if (gridX < 2 || gridX >= 22 || gridY < 6 || gridY >= 25) { deploymentValid = false; } else { var cell = grid.getCell(gridX, gridY); if (!cell || cell.type !== 0) { deploymentValid = false; } } if (deploymentValid && heroes.length < maxHeroes) { var hero = new Hero(selectedHeroType); hero.x = x; hero.y = y; enemyLayerTop.addChild(hero); // Add heroes to top layer heroes.push(hero); updateHeroPanel(); heroDeploymentMode = false; // Tutorial action tracking if (tutorialSystem && tutorialSystem.isActive) { tutorialSystem.checkActionCompleted('deployHero'); } var notification = game.addChild(new Notification(selectedHeroType.charAt(0).toUpperCase() + selectedHeroType.slice(1) + " hero deployed!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } else { var notification = game.addChild(new Notification("Cannot deploy hero here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; heroDeploymentMode = false; return; } } var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } // Improved touch detection with larger hit areas for mobile var touchPadding = 40; // Extra padding for easier touch for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; var hitArea = { left: tower.x - tower.width / 2 - touchPadding, right: tower.x + tower.width / 2 + touchPadding, top: tower.y - tower.height / 2 - touchPadding, bottom: tower.y + tower.height / 2 + touchPadding }; if (x >= hitArea.left && x <= hitArea.right && y >= hitArea.top && y <= hitArea.bottom) { // Check if player can afford this tower if (gold >= getTowerCost(tower.towerType)) { towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); // Improved drag offset for better mobile experience var dragOffsetY = CELL_SIZE * 2; // Larger offset to keep preview visible above finger towerPreview.snapToGrid(x, y - dragOffsetY); // Add immediate visual feedback with scaling animation tween(tower, { scaleX: 1.1, scaleY: 1.1 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(tower, { scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeIn }); } }); } else { // Visual feedback for unaffordable towers tween(tower, { scaleX: 0.95, scaleY: 0.95, alpha: 0.7 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(tower, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 150, easing: tween.easeIn }); } }); var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } break; } } }; game.move = function (x, y, obj) { if (isDragging) { // Shift the y position upward by 1.5 tiles to show preview above finger towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } }; game.up = function (x, y, obj) { var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; if (towerPreview.canPlace) { if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; if (isDragging) { var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } } }; var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2; waveIndicator.y = 2732 - 80; game.addChild(waveIndicator); var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; var sourceTowers = []; var towerSpacing = 300; // Increase spacing for larger towers var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; // Initialize station customizer stationCustomizer = new StationCustomizer(); stationCustomizer.x = 0; stationCustomizer.y = 0; game.addChild(stationCustomizer); // Create station customization panel var stationPanel = new Container(); var stationPanelBg = stationPanel.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); stationPanelBg.width = 400; stationPanelBg.height = 100; stationPanelBg.tint = 0x2E7D32; stationPanelBg.alpha = 0.9; var stationTitleText = new Text2("Station", { size: 50, fill: 0xFFFFFF, weight: 800 }); stationTitleText.anchor.set(0.5, 0.5); stationTitleText.y = -15; stationPanel.addChild(stationTitleText); var customizeButton = new Container(); var customizeBg = customizeButton.attachAsset('heroAbilityIcon', { anchorX: 0.5, anchorY: 0.5 }); customizeBg.width = 80; customizeBg.height = 40; customizeBg.tint = 0x4CAF50; customizeButton.y = 20; var customizeText = new Text2("Customize", { size: 30, fill: 0xFFFFFF, weight: 600 }); customizeText.anchor.set(0.5, 0.5); customizeButton.addChild(customizeText); customizeButton.down = function () { stationCustomizer.toggleCustomizationMode(); }; stationPanel.addChild(customizeButton); stationPanel.x = 2048 - 200; // Position on right side stationPanel.y = 200; // Below the score display which is at y=50 game.addChild(stationPanel); // Create hero deployment panel var heroPanel = new Container(); var heroPanelBg = heroPanel.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); heroPanelBg.width = 800; heroPanelBg.height = 120; heroPanelBg.tint = 0x1A237E; heroPanelBg.alpha = 0.9; var heroTitleText = new Text2("Heroes (" + heroes.length + "/" + maxHeroes + ")", { size: 50, fill: 0xFFFFFF, weight: 800 }); heroTitleText.anchor.set(0.5, 0.5); heroTitleText.y = -30; heroPanel.addChild(heroTitleText); // Hero type buttons var heroTypes = ['tank', 'support', 'dps']; var heroTypeButtons = []; for (var i = 0; i < heroTypes.length; i++) { var heroButton = new Container(); var buttonBg = heroButton.attachAsset('heroAbilityIcon', { anchorX: 0.5, anchorY: 0.5 }); switch (heroTypes[i]) { case 'tank': buttonBg.tint = 0x4CAF50; break; case 'support': buttonBg.tint = 0x2196F3; break; case 'dps': buttonBg.tint = 0xFF5722; break; } buttonBg.width = 80; buttonBg.height = 80; heroButton.heroType = heroTypes[i]; heroButton.x = -240 + i * 120; heroButton.y = 10; heroButton.down = function () { if (heroes.length >= maxHeroes) { var notification = game.addChild(new Notification("Maximum heroes deployed!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } selectedHeroType = this.heroType; heroDeploymentMode = true; var notification = game.addChild(new Notification("Tap anywhere to deploy " + this.heroType + " hero")); notification.x = 2048 / 2; notification.y = grid.height - 50; }; heroPanel.addChild(heroButton); heroTypeButtons.push(heroButton); } heroPanel.x = 2048 / 2; heroPanel.y = 120; game.addChild(heroPanel); function updateHeroPanel() { heroTitleText.setText("Heroes (" + heroes.length + "/" + maxHeroes + ")"); } enemiesToSpawn = 10; // Initialize tutorial system for new players var tutorialSystem = new TutorialSystem(); game.addChild(tutorialSystem); // Start background music LK.playMusic('bgMusic'); // Show tutorial automatically for new players var hasSeenTutorial = storage.tutorialCompleted || false; if (!hasSeenTutorial) { tutorialSystem.startTutorial(); } // Add tutorial restart button for experienced players var tutorialRestartButton = new Container(); var restartBg = tutorialRestartButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); restartBg.width = 200; restartBg.height = 60; restartBg.tint = 0x4A90E2; restartBg.alpha = 0.9; var restartText = new Text2("Tutorial", { size: 35, fill: 0xFFFFFF, weight: 600 }); restartText.anchor.set(0.5, 0.5); tutorialRestartButton.addChild(restartText); tutorialRestartButton.x = 150; tutorialRestartButton.y = 2732 - 50; tutorialRestartButton.down = function () { if (!tutorialSystem.isActive) { tutorialSystem.startTutorial(); } }; game.addChild(tutorialRestartButton); game.update = function () { performanceMonitor.update(); // Clear and rebuild spatial grid each frame for enemies spatialGrid.clear(); for (var i = 0; i < enemies.length; i++) { spatialGrid.addEnemy(enemies[i]); } if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; if (isBossWave && waveType !== 'swarm') { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; // Play boss spawn sound LK.getSound('bossSpawn').play(); } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy = new Enemy(waveType); // Add enemy to the appropriate layer based on type if (enemy.isFlying) { // Add flying enemy to the top layer enemyLayerTop.addChild(enemy); // If it's a flying enemy, add its shadow to the middle layer if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else { // Add normal/ground enemies to the bottom layer enemyLayerBottom.addChild(enemy); } // Scale difficulty with wave number but don't apply to boss // as bosses already have their health multiplier // Use more balanced scaling that starts linear and becomes logarithmic var healthMultiplier; if (currentWave <= 10) { // Linear scaling for early waves healthMultiplier = 1 + (currentWave - 1) * 0.15; // 15% per wave for first 10 waves } else if (currentWave <= 25) { // Moderate scaling for mid waves healthMultiplier = 2.35 + (currentWave - 10) * 0.08; // Start at 2.35x, add 8% per wave } else { // Logarithmic scaling for late waves to prevent exponential growth healthMultiplier = 3.55 + Math.log(currentWave - 24) * 0.5; // Logarithmic growth } enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // All enemy types now spawn in the middle 6 tiles at the top spacing var gridWidth = 24; var midPoint = Math.floor(gridWidth / 2); // 12 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading enemy.cellX = spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; } } // Update heroes for (var i = heroes.length - 1; i >= 0; i--) { var hero = heroes[i]; if (hero.health <= 0) { // Hero defeated - remove from play but don't reset progression var notification = game.addChild(new Notification(hero.heroType.charAt(0).toUpperCase() + hero.heroType.slice(1) + " hero defeated!")); notification.x = 2048 / 2; notification.y = grid.height - 100; hero.destroy(); heroes.splice(i, 1); updateHeroPanel(); continue; } hero.update(); } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; // Skip some enemy updates when performance is poor if (performanceMonitor.shouldSkipUpdate() && enemy.health > 0) { continue; } // Handle hero targeting for enemies if (enemy.heroTarget && enemy.heroTarget.parent && enemy.heroTarget.health > 0) { if (enemy.heroTargetDuration > 0) { enemy.heroTargetDuration--; // Move towards hero instead of following path var dx = enemy.heroTarget.x - enemy.x; var dy = enemy.heroTarget.y - enemy.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > CELL_SIZE * 0.5) { var angle = Math.atan2(dy, dx); enemy.x += Math.cos(angle) * enemy.speed * 30; enemy.y += Math.sin(angle) * enemy.speed * 30; } else { // Attack hero enemy.heroTarget.health -= 10; if (enemy.heroTarget.health <= 0) { enemy.heroTarget.health = 0; } } continue; // Skip normal pathfinding } else { enemy.heroTarget = null; enemy.heroTargetDuration = 0; } } else { enemy.heroTarget = null; enemy.heroTargetDuration = 0; } if (enemy.health <= 0) { // Improved gold rewards that scale better with difficulty var goldEarned; if (enemy.isBoss) { // Boss gold scales more reasonably: 25 base + 3 per wave goldEarned = Math.floor(25 + (enemy.waveNumber - 1) * 3); } else { // Regular enemy gold: better scaling for later waves if (enemy.waveNumber <= 10) { goldEarned = Math.floor(2 + (enemy.waveNumber - 1) * 0.3); // 2-4.7 gold for waves 1-10 } else if (enemy.waveNumber <= 25) { goldEarned = Math.floor(4.7 + (enemy.waveNumber - 10) * 0.4); // 4.7-10.7 gold for waves 11-25 } else { goldEarned = Math.floor(10.7 + (enemy.waveNumber - 25) * 0.2); // Slower growth for late waves } } // Bonus gold for enemies who took difficult routes if (enemy.chosenBranch && enemy.chosenBranch.difficulty > 50) { var routeBonus = Math.floor(goldEarned * 0.3); goldEarned += routeBonus; if (routeBonus > 0) { var notification = game.addChild(new Notification("Route bonus: +" + routeBonus + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 200; } } var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Use proper destroy method for comprehensive cleanup enemy.destroy(); continue; } if (grid.updateEnemy(enemy)) { // Check if shields are active var shieldsActive = false; for (var s = 0; s < stationCustomizer.modules.length; s++) { var module = stationCustomizer.modules[s]; if (module.moduleType === 'shield' && module.shieldActive) { shieldsActive = true; break; } } if (shieldsActive) { // Shields absorb damage var notification = game.addChild(new Notification("Enemy blocked by shields!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { // Play game over sound LK.getSound('gameOver').play(); LK.showGameOver(); } } // Use proper destroy method for comprehensive cleanup enemy.destroy(); } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } // Limit bullet count when performance drops if (performanceMonitor.performanceLevel < 0.7 && bullets.length > 50) { // Remove oldest bullets when performance is poor var bulletsToRemove = Math.min(10, bullets.length - 30); for (var i = 0; i < bulletsToRemove; i++) { var oldBullet = bullets[i]; if (oldBullet.targetEnemy) { var bulletIndex = oldBullet.targetEnemy.bulletsTargetingThis.indexOf(oldBullet); if (bulletIndex !== -1) { oldBullet.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } returnBulletToPool(oldBullet); bullets.splice(i, 1); i--; // Adjust index after removal bulletsToRemove--; } } if (towerPreview.visible) { towerPreview.checkPlacement(); } // Update station customizer if (stationCustomizer) { stationCustomizer.update(); // Update module arrays for quick access researchLabs = stationCustomizer.modules.filter(function (m) { return m.moduleType === 'research'; }); shieldGenerators = stationCustomizer.modules.filter(function (m) { return m.moduleType === 'shield'; }); teleporterNodes = stationCustomizer.modules.filter(function (m) { return m.moduleType === 'teleporter'; }); // Link teleporter nodes for (var i = 0; i < teleporterNodes.length; i++) { teleporterNodes[i].linkedNodes = teleporterNodes.filter(function (node, index) { return index !== i; }); } } // Update tower specializations and support links for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (tower.specialization && tower.specialization.update) { tower.specialization.update(); } } // Clean up destroyed specialization menus specializationMenus = specializationMenus.filter(function (menu) { return menu.parent && !menu.isDestroyed; }); // Environmental hazards management if (waveIndicator && waveIndicator.gameStarted) { hazardTimer++; if (hazardTimer >= nextHazardTime) { hazardTimer = 0; nextHazardTime = 1200 + Math.random() * 1200; // 20-40 seconds // Spawn random environmental hazard var hazardType = Math.floor(Math.random() * 4); switch (hazardType) { case 0: // Asteroid field if (asteroids.length < 3) { var validPositions = []; for (var x = 2; x < 22; x++) { for (var y = 6; y < 25; y++) { var cell = grid.getCell(x, y); if (cell && cell.type === 0) { validPositions.push({ x: x, y: y }); } } } if (validPositions.length > 0) { var pos = validPositions[Math.floor(Math.random() * validPositions.length)]; var asteroid = new Asteroid(grid.x + pos.x * CELL_SIZE + CELL_SIZE / 2, grid.y + pos.y * CELL_SIZE + CELL_SIZE / 2); game.addChild(asteroid); asteroids.push(asteroid); var notification = game.addChild(new Notification("⚠️ Asteroid field detected! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } break; case 1: // Nebula cloud if (nebulaClouds.length < 2) { var cloudX = grid.x + (5 + Math.random() * 14) * CELL_SIZE; var cloudY = grid.y + (8 + Math.random() * 12) * CELL_SIZE; var nebula = new NebulaCloud(cloudX, cloudY); game.addChild(nebula); nebulaClouds.push(nebula); var notification = game.addChild(new Notification("🌌 Nebula cloud formation! 🌌")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Remove after 15 seconds LK.setTimeout(function () { if (nebula.parent) { nebula.destroy(); } }, 15000); } break; case 2: // Solar flare if (solarFlares.length === 0) { var flare = new SolarFlare(); game.addChild(flare); solarFlares.push(flare); } break; case 3: // Wormhole if (wormholes.length < 2) { // Create entry and exit points var entryX = grid.x + (3 + Math.random() * 6) * CELL_SIZE; var entryY = grid.y + (8 + Math.random() * 8) * CELL_SIZE; var exitX = grid.x + (15 + Math.random() * 6) * CELL_SIZE; var exitY = grid.y + (16 + Math.random() * 6) * CELL_SIZE; var wormhole = new Wormhole(entryX, entryY, exitX, exitY); game.addChild(wormhole); wormholes.push(wormhole); var notification = game.addChild(new Notification("🌀 Wormhole opened! 🌀")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Remove after 20 seconds LK.setTimeout(function () { if (wormhole.parent) { wormhole.destroy(); } }, 20000); } break; } } } // Update path junctions and route analysis for (var i = 0; i < pathJunctions.length; i++) { if (pathJunctions[i].parent) { pathJunctions[i].update(); } } // Update environmental hazards for (var i = asteroids.length - 1; i >= 0; i--) { if (asteroids[i].parent) { // Asteroids can be damaged by bullets for (var j = 0; j < bullets.length; j++) { var bullet = bullets[j]; var dx = bullet.x - asteroids[i].x; var dy = bullet.y - asteroids[i].y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= 60) { // Hit asteroid asteroids[i].takeDamage(bullet.damage); returnBulletToPool(bullet); break; } } } } for (var i = 0; i < nebulaClouds.length; i++) { if (nebulaClouds[i].parent) { nebulaClouds[i].update(); } } for (var i = 0; i < solarFlares.length; i++) { if (solarFlares[i].parent) { solarFlares[i].update(); } } for (var i = 0; i < wormholes.length; i++) { if (wormholes[i].parent) { wormholes[i].update(); } } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { // Play victory sound LK.getSound('victory').play(); LK.showYouWin(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Asteroid = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.health = 150;
self.maxHealth = 150;
self.isDestroyed = false;
self.gridX = Math.floor((x - grid.x) / CELL_SIZE);
self.gridY = Math.floor((y - grid.y) / CELL_SIZE);
var asteroidGraphics = self.attachAsset('asteroid', {
anchorX: 0.5,
anchorY: 0.5
});
// Add rotation animation
tween(asteroidGraphics, {
rotation: Math.PI * 2
}, {
duration: 8000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed) {
asteroidGraphics.rotation = 0;
}
}
});
// Health bar
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarOutline.y = healthBar.y = -asteroidGraphics.height / 2 - 15;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBar.x = -healthBar.width / 2;
healthBar.tint = 0x8B4513;
self.healthBar = healthBar;
self.takeDamage = function (damage) {
self.health -= damage;
if (self.health <= 0) {
self.health = 0;
self.destroy();
} else {
self.healthBar.width = self.health / self.maxHealth * 70;
}
};
self.down = function () {
// Allow manual destruction by tapping
self.takeDamage(50);
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Create debris effect
for (var i = 0; i < 5; i++) {
var debris = new Container();
debris.x = self.x + (Math.random() - 0.5) * 100;
debris.y = self.y + (Math.random() - 0.5) * 100;
var debrisGraphics = debris.attachAsset('asteroidDebris', {
anchorX: 0.5,
anchorY: 0.5
});
game.addChild(debris);
tween(debris, {
x: debris.x + (Math.random() - 0.5) * 200,
y: debris.y + (Math.random() - 0.5) * 200,
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (debris.parent) {
debris.parent.removeChild(debris);
}
}
});
}
// Clear grid cell
var cell = grid.getCell(self.gridX, self.gridY);
if (cell) {
cell.type = 0;
}
// Remove from asteroids array
var asteroidIndex = asteroids.indexOf(self);
if (asteroidIndex !== -1) {
asteroids.splice(asteroidIndex, 1);
}
// Recalculate paths
grid.pathFind();
grid.renderDebug();
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
// Block the grid cell
var cell = grid.getCell(self.gridX, self.gridY);
if (cell) {
cell.type = 1;
}
return self;
});
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
// Create sci-fi projectile based on bullet type
var assetName = 'bullet'; // default
var bulletGraphics;
if (self.type === 'rapid') {
bulletGraphics = self.attachAsset('energyBeam', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0x00aaff;
} else if (self.type === 'sniper') {
bulletGraphics = self.attachAsset('laserBurst', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0xff0044;
} else if (self.type === 'splash') {
bulletGraphics = self.attachAsset('plasmaBolt', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0x44ff00;
} else {
bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Add pulsing glow effect to projectiles
tween(bulletGraphics, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(bulletGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
self.update = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
returnBulletToPool(self);
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Create sci-fi impact flash
if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDestroyed) {
tween(self.targetEnemy, {
tint: 0xffffff
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDestroyed) {
tween(self.targetEnemy, {
tint: 0xffffff
}, {
duration: 200,
easing: tween.easeIn
});
}
}
});
}
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
// Play enemy destroy sound
LK.getSound('enemyDestroy').play();
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
// Play enemy hit sound
LK.getSound('enemyHit').play();
}
// Apply special effects based on bullet type
if (self.type === 'splash') {
// Create visual splash effect with limiting
if (canCreateEffect()) {
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
registerEffect(splashEffect);
}
// Determine splash radius and damage based on specialization
var splashRadius = CELL_SIZE * 1.5;
var splashDamageMultiplier = 0.5;
if (self.specialization === 'nuclear') {
splashRadius = CELL_SIZE * 3;
splashDamageMultiplier = 0.8;
// Create nuclear explosion effect
if (canCreateEffect()) {
var nuclearEffect = new Container();
nuclearEffect.x = self.targetEnemy.x;
nuclearEffect.y = self.targetEnemy.y;
var nuclearGraphics = nuclearEffect.attachAsset('nuclearBlast', {
anchorX: 0.5,
anchorY: 0.5
});
nuclearGraphics.tint = 0xFF0000;
nuclearGraphics.alpha = 0.8;
game.addChild(nuclearEffect);
tween(nuclearEffect, {
scaleX: 3,
scaleY: 3,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (nuclearEffect.parent) {
nuclearEffect.parent.removeChild(nuclearEffect);
}
}
});
}
}
// Splash damage to nearby enemies
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var splashDx = otherEnemy.x - self.targetEnemy.x;
var splashDy = otherEnemy.y - self.targetEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
var splashDamage = self.damage * splashDamageMultiplier;
otherEnemy.health -= splashDamage;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
// Chain reaction effect
if (self.specialization === 'chainreaction' && Math.random() < 0.3) {
// 30% chance to chain to another enemy
for (var j = 0; j < enemies.length; j++) {
var chainEnemy = enemies[j];
if (chainEnemy !== otherEnemy && chainEnemy !== self.targetEnemy) {
var chainDx = chainEnemy.x - otherEnemy.x;
var chainDy = chainEnemy.y - otherEnemy.y;
var chainDistance = Math.sqrt(chainDx * chainDx + chainDy * chainDy);
if (chainDistance <= CELL_SIZE * 2) {
chainEnemy.health -= splashDamage * 0.7;
if (chainEnemy.health <= 0) {
chainEnemy.health = 0;
} else {
chainEnemy.healthBar.width = chainEnemy.health / chainEnemy.maxHealth * 70;
}
break;
}
}
}
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect with limiting
if (canCreateEffect()) {
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
registerEffect(slowEffect);
}
// Apply slow effect
// Make slow percentage scale with tower level (default 50%, up to 80% at max level)
var slowPct = 0.5;
if (self.sourceTowerLevel !== undefined) {
// Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
self.targetEnemy.slowDuration = 180; // Reset duration
}
}
} else if (self.type === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect with limiting
if (canCreateEffect()) {
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
registerEffect(poisonEffect);
}
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper with limiting
if (canCreateEffect()) {
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
registerEffect(sniperEffect);
}
}
returnBulletToPool(self);
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this bullet and its graphics
tween.stop(self);
if (self.children && self.children[0]) {
tween.stop(self.children[0]);
}
// Remove from target enemy's bullets array
if (self.targetEnemy && self.targetEnemy.bulletsTargetingThis) {
var bulletIndex = self.targetEnemy.bulletsTargetingThis.indexOf(self);
if (bulletIndex !== -1) {
self.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
// Remove from bullets array
var bulletArrayIndex = bullets.indexOf(self);
if (bulletArrayIndex !== -1) {
bullets.splice(bulletArrayIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Nullify all object references
self.targetEnemy = null;
// Call parent destroy
Container.prototype.destroy.call(self);
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.setText("-");
cellGraphics.tint = 0x220044; // Deep space color
return;
}
numberLabel.visible = true;
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff; // Energy field blue
} else {
// Create starfield effect with bluish tint
var starfieldTint = 0x001122 + (tint << 8) + (tint >> 1);
cellGraphics.tint = starfieldTint;
}
while (debugArrows.length > data.targets.length) {
self.removeChild(debugArrows.pop());
}
for (var a = 0; a < data.targets.length; a++) {
var destination = data.targets[a];
var ox = destination.x - data.x;
var oy = destination.y - data.y;
var angle = Math.atan2(oy, ox);
if (!debugArrows[a]) {
debugArrows[a] = LK.getAsset('arrow', {
anchorX: -.5,
anchorY: 0.5
});
debugArrows[a].alpha = .5;
self.addChildAt(debugArrows[a], 1);
}
debugArrows[a].rotation = angle;
}
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.tint = 0x008800;
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 1000) / 10);
};
});
// This update method was incorrectly placed here and should be removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
// Create energy particles around main effect
var particles = [];
for (var i = 0; i < 6; i++) {
var particle = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
particle.width = 8;
particle.height = 8;
var angle = i / 6 * Math.PI * 2;
particle.x = Math.cos(angle) * 30;
particle.y = Math.sin(angle) * 30;
particles.push(particle);
}
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0x88FF44;
}
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0xCC44FF;
}
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0x44FFCC;
}
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0xFF8844;
}
break;
}
effectGraphics.alpha = 0.7;
// Animate particles spiraling outward
for (var i = 0; i < particles.length; i++) {
var particle = particles[i];
var delay = i * 50;
tween(particle, {
x: particle.x * 2,
y: particle.y * 2,
scaleX: 0.5,
scaleY: 0.5,
alpha: 0
}, {
duration: 400,
delay: delay,
easing: tween.easeOut
});
}
self.alpha = 0;
// Animate the effect
tween(self, {
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this effect and its children
tween.stop(self);
for (var i = 0; i < self.children.length; i++) {
if (self.children[i]) {
tween.stop(self.children[i]);
}
}
// Remove from activeEffects array
var effectIndex = activeEffects.indexOf(self);
if (effectIndex !== -1) {
activeEffects.splice(effectIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Call parent destroy
Container.prototype.destroy.call(self);
};
return self;
});
// Base enemy class for common functionality
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'normal';
self.speed = .01;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
self.isDestroyed = false;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale up boss enemies
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Fall back to regular enemy asset if specific type asset not found
// Apply tint to differentiate enemy types
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
break;
case 'immune':
enemyGraphics.tint = 0xAA0000; // Red for immune enemies
break;
case 'flying':
enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
break;
case 'swarm':
enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
break;
}*/
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
shadowGraphics.scaleX = 1.8;
shadowGraphics.scaleY = 1.8;
}
// Position shadow slightly offset
self.shadow.x = 20; // Offset right
self.shadow.y = 20; // Offset down
// Ensure shadow has the same rotation as the enemy
shadowGraphics.rotation = enemyGraphics.rotation;
}
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
// Add visual type indicator for better enemy identification
if (self.type !== 'normal') {
var typeIndicator = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
typeIndicator.width = 16;
typeIndicator.height = 16;
typeIndicator.y = -enemyGraphics.height / 2 - 35; // Above health bar
// Color code the indicator based on enemy type
switch (self.type) {
case 'fast':
typeIndicator.tint = 0x00AAFF;
break;
case 'immune':
typeIndicator.tint = 0xAA0000;
break;
case 'flying':
typeIndicator.tint = 0xFFFF00;
break;
case 'swarm':
typeIndicator.tint = 0xFF00FF;
break;
}
// Make boss indicators larger and more prominent
if (self.isBoss) {
typeIndicator.width = 24;
typeIndicator.height = 24;
typeIndicator.tint = 0xFFD700; // Gold for bosses
// Add pulsing effect for boss indicator
tween(typeIndicator, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(typeIndicator, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
});
}
}
self.update = function () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Improved visual clarity for enemy types with better differentiation
var baseTint = 0xFFFFFF;
// First set base tint based on enemy type for better visual clarity
switch (self.type) {
case 'fast':
baseTint = 0x4488FF; // Bright blue for fast enemies
break;
case 'immune':
baseTint = 0xFF4444; // Bright red for immune enemies
break;
case 'flying':
baseTint = 0xFFDD44; // Bright yellow for flying enemies
break;
case 'swarm':
baseTint = 0xFF44DD; // Bright magenta for swarm enemies
break;
case 'normal':
default:
baseTint = 0xCCCCCC; // Light gray for normal enemies
break;
}
// Apply boss scaling to tint if this is a boss
if (self.isBoss) {
// Make boss enemies more vibrant and add a red outline effect
var r = baseTint >> 16 & 0xFF;
var g = baseTint >> 8 & 0xFF;
var b = baseTint & 0xFF;
// Boost color intensity for bosses
r = Math.min(255, Math.floor(r * 1.3));
g = Math.min(255, Math.floor(g * 1.3));
b = Math.min(255, Math.floor(b * 1.3));
baseTint = r << 16 | g << 8 | b;
}
// Then apply status effect overlays
if (self.isImmune) {
// Keep immune tint but add pulsing effect for better visibility
enemyGraphics.tint = baseTint;
// Add shield shimmer effect for immune enemies
if (!self.shieldEffect && !self.isDestroyed) {
self.shieldEffect = true;
tween(enemyGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(enemyGraphics, {
alpha: 1.0
}, {
duration: 500,
easing: tween.easeInOut
});
}
}
});
}
} else if (self.poisoned && self.slowed) {
// Blend poison green with slow purple over the base tint
enemyGraphics.tint = 0x4C7FD4; // Keep existing blend color
} else if (self.poisoned) {
// Poison overlay - green tint
var r = Math.floor((baseTint >> 16 & 0xFF) * 0.5);
var g = Math.min(255, Math.floor((baseTint >> 8 & 0xFF) * 0.5 + 170));
var b = Math.floor((baseTint & 0xFF) * 0.5 + 85);
enemyGraphics.tint = r << 16 | g << 8 | b;
// Add crackling energy effect for poisoned
if (!self.poisonFlicker && !self.isDestroyed) {
self.poisonFlicker = true;
tween(enemyGraphics, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(enemyGraphics, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.poisonFlicker = false;
}
});
}
}
});
}
} else if (self.slowed) {
// Slow overlay - purple tint
var r = Math.floor((baseTint >> 16 & 0xFF) * 0.5 + 76);
var g = Math.floor((baseTint >> 8 & 0xFF) * 0.5);
var b = Math.min(255, Math.floor((baseTint & 0xFF) * 0.5 + 127));
enemyGraphics.tint = r << 16 | g << 8 | b;
} else {
enemyGraphics.tint = baseTint;
self.shieldEffect = false;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05 && !self.isDestroyed) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this enemy and its graphics
tween.stop(self);
if (self.children && self.children[0]) {
tween.stop(self.children[0]);
}
// Clear all bullets targeting this enemy
for (var i = 0; i < self.bulletsTargetingThis.length; i++) {
var bullet = self.bulletsTargetingThis[i];
if (bullet) {
bullet.targetEnemy = null;
}
}
self.bulletsTargetingThis = [];
// Remove from enemies array
var enemyIndex = enemies.indexOf(self);
if (enemyIndex !== -1) {
enemies.splice(enemyIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Clean up shadow if it's a flying enemy
if (self.isFlying && self.shadow) {
if (self.shadow.parent) {
self.shadow.parent.removeChild(self.shadow);
}
self.shadow = null;
}
// Nullify all object references
self.targetEnemy = null;
self.currentTarget = null;
self.flyingTarget = null;
self.healthBar = null;
self.energyCore = null;
// Call parent destroy
Container.prototype.destroy.call(self);
};
return self;
});
var GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2("+" + value, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
self.cells = [];
self.spawns = [];
self.goals = [];
for (var i = 0; i < gridWidth; i++) {
self.cells[i] = [];
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j] = {
score: 0,
pathId: 0,
towersInRange: []
};
}
}
/*
Cell Types
0: Transparent floor
1: Wall
2: Spawn
3: Goal
*/
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0;
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = 0;
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = 0;
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
// Mark junction cells for branching
for (var i = 0; i < pathJunctions.length; i++) {
var junction = pathJunctions[i];
var junctionCell = self.getCell(junction.gridX, junction.gridY);
if (junctionCell) {
junctionCell.isJunction = true;
junctionCell.junction = junction;
}
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.left.type != 1) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.right.type != 1) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.right.type != 1) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.left.type != 1) {
processNode(node.downLeft, targetScore, node);
}
targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId != pathId) {
console.warn("Spawn blocked");
return true;
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05 && !enemy.isDestroyed) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05 && !enemy.isDestroyed) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget) {
// Check if enemy is at a junction
if (cell && cell.isJunction && !enemy.chosenBranch) {
var junction = cell.junction;
var chosenBranch = junction.choosePath(enemy);
if (chosenBranch) {
enemy.chosenBranch = chosenBranch;
enemy.currentTarget = grid.getCell(chosenBranch.targetX, chosenBranch.targetY);
// Show visual path indicator
if (routeVisualizer && Math.random() < 0.3) {
// Show for 30% of enemies
routeVisualizer.showPaths(junction, enemy);
}
// Add notification for significant route changes
if (Math.random() < 0.1) {
// 10% chance
var routeType = chosenBranch.isShortPath ? "direct" : "alternate";
var notification = game.addChild(new Notification("Enemy taking " + routeType + " route!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
} else {
enemy.currentTarget = cell.targets[0];
}
} else {
enemy.currentTarget = cell.targets[0];
}
}
if (enemy.currentTarget) {
if (cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var Hero = Container.expand(function (heroType) {
var self = Container.call(this);
self.heroType = heroType || 'tank';
self.level = storage['hero_' + heroType + '_level'] || 1;
self.experience = storage['hero_' + heroType + '_xp'] || 0;
self.maxHealth = 200;
self.health = self.maxHealth;
self.isDestroyed = false;
self.movementSpeed = 1.5;
self.abilities = [];
self.currentTarget = null;
self.lastAbilityUse = 0;
self.abilityCooldown = 300; // 5 seconds at 60fps
// Set hero-specific stats
switch (self.heroType) {
case 'tank':
self.maxHealth = 300 + (self.level - 1) * 50;
self.movementSpeed = 1.0;
self.abilityCooldown = 600; // 10 seconds
break;
case 'support':
self.maxHealth = 150 + (self.level - 1) * 30;
self.movementSpeed = 2.0;
self.abilityCooldown = 480; // 8 seconds
break;
case 'dps':
self.maxHealth = 180 + (self.level - 1) * 35;
self.movementSpeed = 1.8;
self.abilityCooldown = 360; // 6 seconds
break;
}
self.health = self.maxHealth;
// Get appropriate asset
var assetName = 'hero_' + self.heroType;
var heroGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Create health bar
var healthBarOutline = self.attachAsset('heroHealthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('heroHealthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarOutline.y = healthBar.y = -heroGraphics.height / 2 - 20;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBar.x = -healthBar.width / 2;
self.healthBar = healthBar;
// Create XP bar
var xpBarOutline = self.attachAsset('heroXpBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var xpBar = self.attachAsset('heroXpBar', {
anchorX: 0,
anchorY: 0.5
});
xpBarOutline.y = xpBar.y = -heroGraphics.height / 2 - 35;
xpBarOutline.x = -xpBarOutline.width / 2;
xpBar.x = -xpBar.width / 2;
self.xpBar = xpBar;
// Create ability icon
var abilityIcon = self.attachAsset('heroAbilityIcon', {
anchorX: 0.5,
anchorY: 0.5
});
abilityIcon.y = heroGraphics.height / 2 + 20;
self.abilityIcon = abilityIcon;
// Level indicator
var levelText = new Text2(self.level.toString(), {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
levelText.anchor.set(0.5, 0.5);
levelText.y = heroGraphics.height / 2 + 20;
levelText.x = 40;
self.addChild(levelText);
self.levelText = levelText;
self.getExperienceForNextLevel = function () {
return self.level * 100; // 100 XP per level
};
self.gainExperience = function (amount) {
self.experience += amount;
var xpNeeded = self.getExperienceForNextLevel();
if (self.experience >= xpNeeded) {
self.experience -= xpNeeded;
self.level++;
self.levelText.setText(self.level.toString());
// Save progression
storage['hero_' + self.heroType + '_level'] = self.level;
// Level up effects
self.onLevelUp();
// Visual level up effect
tween(heroGraphics, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFFD700
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(heroGraphics, {
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
// Save XP
storage['hero_' + self.heroType + '_xp'] = self.experience;
self.updateXpBar();
};
self.onLevelUp = function () {
// Increase health
var healthIncrease = self.heroType === 'tank' ? 50 : self.heroType === 'support' ? 30 : 35;
self.maxHealth += healthIncrease;
self.health = self.maxHealth; // Full heal on level up
self.updateHealthBar();
};
self.updateHealthBar = function () {
self.healthBar.width = self.health / self.maxHealth * 80;
};
self.updateXpBar = function () {
var xpProgress = self.experience / self.getExperienceForNextLevel();
self.xpBar.width = xpProgress * 80;
};
self.useAbility = function () {
if (LK.ticks - self.lastAbilityUse < self.abilityCooldown) {
return false; // Ability on cooldown
}
self.lastAbilityUse = LK.ticks;
// Play hero ability sound
LK.getSound('heroAbility').play();
// Ability cooldown visual effect
tween(self.abilityIcon, {
tint: 0x888888,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: self.abilityCooldown,
easing: tween.linear,
onFinish: function onFinish() {
tween(self.abilityIcon, {
tint: 0xFFD700,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeOut
});
}
});
switch (self.heroType) {
case 'tank':
return self.tankAbility();
case 'support':
return self.supportAbility();
case 'dps':
return self.dpsAbility();
}
return false;
};
self.tankAbility = function () {
// Taunt - Draw all enemies within range to attack hero
var tauntRadius = CELL_SIZE * 4;
var taunted = 0;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= tauntRadius) {
enemy.heroTarget = self;
enemy.heroTargetDuration = 300; // 5 seconds
taunted++;
}
}
// Visual effect
var effectRadius = tauntRadius;
var tauntEffect = new Container();
tauntEffect.x = self.x;
tauntEffect.y = self.y;
var effectGraphics = tauntEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.width = effectGraphics.height = effectRadius * 2;
effectGraphics.tint = 0x4CAF50;
effectGraphics.alpha = 0.6;
game.addChild(tauntEffect);
tween(tauntEffect, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (tauntEffect.parent) {
tauntEffect.parent.removeChild(tauntEffect);
}
}
});
return taunted > 0;
};
self.supportAbility = function () {
// Buff nearby towers
var buffRadius = CELL_SIZE * 3;
var buffed = 0;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= buffRadius) {
tower.heroBuff = {
damage: 1.5,
fireRate: 0.7,
duration: 600 // 10 seconds
};
buffed++;
// Visual effect on tower
tween(tower.children[0], {
tint: 0x2196F3
}, {
duration: 600,
easing: tween.linear,
onFinish: function onFinish() {
tower.children[0].tint = 0x445566;
}
});
}
}
return buffed > 0;
};
self.dpsAbility = function () {
// Area damage attack
var damageRadius = CELL_SIZE * 2.5;
var damaged = 0;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= damageRadius) {
var damage = 100 + (self.level - 1) * 20;
enemy.health -= damage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
damaged++;
// Visual damage effect
tween(enemy, {
tint: 0xFF5722
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(enemy, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
}
// Visual explosion effect
var explosionEffect = new Container();
explosionEffect.x = self.x;
explosionEffect.y = self.y;
var effectGraphics = explosionEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.width = effectGraphics.height = damageRadius * 2;
effectGraphics.tint = 0xFF5722;
effectGraphics.alpha = 0.8;
game.addChild(explosionEffect);
tween(explosionEffect, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (explosionEffect.parent) {
explosionEffect.parent.removeChild(explosionEffect);
}
}
});
return damaged > 0;
};
self.update = function () {
if (self.health <= 0) {
return;
}
// Update bars
self.updateHealthBar();
self.updateXpBar();
// Move towards enemies or patrol
if (!self.currentTarget) {
self.findTarget();
}
if (self.currentTarget && self.currentTarget.parent && self.currentTarget.health > 0) {
var dx = self.currentTarget.x - self.x;
var dy = self.currentTarget.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > CELL_SIZE * 0.8) {
// Move towards target
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.movementSpeed;
self.y += Math.sin(angle) * self.movementSpeed;
} else {
// Attack target
if (self.heroType === 'tank') {
self.currentTarget.health -= 15;
} else if (self.heroType === 'dps') {
self.currentTarget.health -= 25;
} else {
self.currentTarget.health -= 10;
}
if (self.currentTarget.health <= 0) {
self.currentTarget.health = 0;
self.gainExperience(20);
self.currentTarget = null;
} else {
self.currentTarget.healthBar.width = self.currentTarget.health / self.currentTarget.maxHealth * 70;
}
}
} else {
self.currentTarget = null;
}
};
self.findTarget = function () {
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health <= 0) continue;
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance && distance < CELL_SIZE * 5) {
closestDistance = distance;
closestEnemy = enemy;
}
}
self.currentTarget = closestEnemy;
};
self.down = function (x, y, obj) {
// Use ability when tapped
if (self.useAbility()) {
var notification = game.addChild(new Notification("Hero ability activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
tween.stop(self);
for (var i = 0; i < self.children.length; i++) {
if (self.children[i]) {
tween.stop(self.children[i]);
}
}
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var NebulaCloud = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.radius = 100;
self.isDestroyed = false;
var nebulaGraphics = self.attachAsset('nebulaCloud', {
anchorX: 0.5,
anchorY: 0.5
});
nebulaGraphics.alpha = 0.6;
// Pulsing animation
tween(nebulaGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(nebulaGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.6
}, {
duration: 2000,
easing: tween.easeInOut
});
}
}
});
self.update = function () {
// Apply effects to units within the nebula
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
// Slow effect
if (!enemy.nebulaSlowed) {
enemy.originalSpeed = enemy.originalSpeed || enemy.speed;
enemy.speed *= 0.7; // 30% speed reduction
enemy.nebulaSlowed = true;
}
// Stealth effect - harder for towers to target
enemy.nebulaStealthed = true;
} else {
// Remove effects when outside nebula
if (enemy.nebulaSlowed) {
enemy.speed = enemy.originalSpeed || enemy.speed;
enemy.nebulaSlowed = false;
}
enemy.nebulaStealthed = false;
}
}
// Apply slow effect to heroes
for (var i = 0; i < heroes.length; i++) {
var hero = heroes[i];
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
if (!hero.nebulaSlowed) {
hero.originalMovementSpeed = hero.originalMovementSpeed || hero.movementSpeed;
hero.movementSpeed *= 0.7;
hero.nebulaSlowed = true;
}
} else {
if (hero.nebulaSlowed) {
hero.movementSpeed = hero.originalMovementSpeed || hero.movementSpeed;
hero.nebulaSlowed = false;
}
}
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove effects from all units
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.nebulaSlowed) {
enemy.speed = enemy.originalSpeed || enemy.speed;
enemy.nebulaSlowed = false;
}
enemy.nebulaStealthed = false;
}
for (var i = 0; i < heroes.length; i++) {
var hero = heroes[i];
if (hero.nebulaSlowed) {
hero.movementSpeed = hero.originalMovementSpeed || hero.movementSpeed;
hero.nebulaSlowed = false;
}
}
var nebulaIndex = nebulaClouds.indexOf(self);
if (nebulaIndex !== -1) {
nebulaClouds.splice(nebulaIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300;
buttonBackground.height = 100;
buttonBackground.tint = 0x0088FF;
var buttonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 60;
notificationGraphics.height = 80;
notificationGraphics.tint = 0x1A1A1A;
notificationGraphics.alpha = 0.95;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var PathJunction = Container.expand(function (x, y, branches) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.branches = branches || []; // Array of possible path directions
self.gridX = Math.floor((x - grid.x) / CELL_SIZE);
self.gridY = Math.floor((y - grid.y) / CELL_SIZE);
self.isDestroyed = false;
// Visual junction indicator
var junctionGraphics = self.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
junctionGraphics.width = CELL_SIZE * 0.8;
junctionGraphics.height = CELL_SIZE * 0.8;
junctionGraphics.tint = 0x9c27b0;
junctionGraphics.alpha = 0.6;
// Pulsing animation to indicate junction
tween(junctionGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(junctionGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.6
}, {
duration: 1500,
easing: tween.easeInOut
});
}
}
});
// Choose path for enemy based on various factors
self.choosePath = function (enemy) {
if (self.branches.length === 0) return null;
var pathScores = [];
// Evaluate each branch
for (var i = 0; i < self.branches.length; i++) {
var branch = self.branches[i];
var score = 0;
// Base score - some randomness for variety
score += Math.random() * 100;
// Factor in enemy type preferences
switch (enemy.type) {
case 'fast':
// Fast enemies prefer shorter paths
score += branch.isShortPath ? 150 : 50;
break;
case 'immune':
// Immune enemies prefer heavily defended paths (they can tank it)
score += branch.towerDensity * 80;
break;
case 'flying':
// Flying enemies prefer paths with fewer anti-air towers
score += branch.hasAntiAir ? 20 : 120;
break;
case 'swarm':
// Swarm enemies prefer paths other swarm enemies haven't taken recently
score += branch.recentSwarmTraffic > 3 ? 30 : 100;
break;
default:
// Normal enemies prefer balanced paths
score += branch.difficulty < 50 ? 80 : 60;
break;
}
// Factor in current congestion (avoid clustering)
var congestionPenalty = branch.currentEnemyCount * 25;
score -= congestionPenalty;
// Factor in tower coverage (enemies avoid heavy defenses unless immune)
if (!enemy.isImmune) {
score -= branch.towerDensity * 40;
}
// Factor in path length (generally prefer shorter paths)
score -= branch.pathLength * 2;
pathScores.push({
branch: branch,
score: score
});
}
// Sort by score and add some weighted randomness
pathScores.sort(function (a, b) {
return b.score - a.score;
});
// Weighted selection - higher scores more likely but not guaranteed
var totalWeight = 0;
for (var i = 0; i < pathScores.length; i++) {
var weight = Math.max(1, pathScores[i].score);
pathScores[i].weight = weight;
totalWeight += weight;
}
var randomValue = Math.random() * totalWeight;
var currentWeight = 0;
for (var i = 0; i < pathScores.length; i++) {
currentWeight += pathScores[i].weight;
if (randomValue <= currentWeight) {
return pathScores[i].branch;
}
}
// Fallback to first option
return pathScores[0].branch;
};
// Update junction statistics
self.update = function () {
// Update branch statistics
for (var i = 0; i < self.branches.length; i++) {
var branch = self.branches[i];
// Count enemies currently on this branch
branch.currentEnemyCount = 0;
branch.recentSwarmTraffic = Math.max(0, branch.recentSwarmTraffic - 0.1);
for (var j = 0; j < enemies.length; j++) {
var enemy = enemies[j];
if (enemy.chosenBranch === branch) {
branch.currentEnemyCount++;
if (enemy.type === 'swarm') {
branch.recentSwarmTraffic += 0.1;
}
}
}
// Calculate tower density in branch area
branch.towerDensity = 0;
branch.hasAntiAir = false;
for (var t = 0; t < towers.length; t++) {
var tower = towers[t];
var dx = tower.x - (grid.x + branch.targetX * CELL_SIZE);
var dy = tower.y - (grid.y + branch.targetY * CELL_SIZE);
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 4) {
branch.towerDensity++;
if (tower.id === 'sniper' || tower.id === 'rapid') {
branch.hasAntiAir = true;
}
}
}
// Update path difficulty
branch.difficulty = branch.towerDensity * 10 + branch.pathLength;
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
var junctionIndex = pathJunctions.indexOf(self);
if (junctionIndex !== -1) {
pathJunctions.splice(junctionIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var RouteVisualizer = Container.expand(function () {
var self = Container.call(this);
self.pathLines = [];
self.isDestroyed = false;
self.showPaths = function (junction, enemy) {
// Clear existing path lines
self.clearPaths();
// Create visual lines for each possible branch
for (var i = 0; i < junction.branches.length; i++) {
var branch = junction.branches[i];
var pathLine = new Container();
// Create line segments to show the path
var segments = [];
var currentX = junction.gridX;
var currentY = junction.gridY;
// Draw path to target
var targetX = branch.targetX;
var targetY = branch.targetY;
var steps = Math.max(Math.abs(targetX - currentX), Math.abs(targetY - currentY));
for (var step = 0; step <= steps; step++) {
var progress = step / steps;
var segmentX = Math.round(currentX + (targetX - currentX) * progress);
var segmentY = Math.round(currentY + (targetY - currentY) * progress);
var segment = pathLine.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
segment.width = 12;
segment.height = 12;
segment.x = grid.x + segmentX * CELL_SIZE;
segment.y = grid.y + segmentY * CELL_SIZE;
// Color code based on path difficulty
var difficulty = branch.difficulty || 0;
if (difficulty < 30) {
segment.tint = 0x00FF00; // Green for easy
} else if (difficulty < 60) {
segment.tint = 0xFFFF00; // Yellow for medium
} else {
segment.tint = 0xFF0000; // Red for hard
}
segment.alpha = 0.7;
}
game.addChild(pathLine);
self.pathLines.push(pathLine);
// Animate the path appearance
tween(pathLine, {
alpha: 1.0
}, {
duration: 500,
easing: tween.easeOut
});
}
// Auto-hide after a few seconds
LK.setTimeout(function () {
self.clearPaths();
}, 3000);
};
self.clearPaths = function () {
for (var i = 0; i < self.pathLines.length; i++) {
if (self.pathLines[i].parent) {
self.pathLines[i].parent.removeChild(self.pathLines[i]);
}
}
self.pathLines = [];
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
self.clearPaths();
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var SolarFlare = Container.expand(function () {
var self = Container.call(this);
self.duration = 600; // 10 seconds at 60fps
self.remainingTime = self.duration;
self.isActive = true;
self.isDestroyed = false;
// Position in center of battlefield
self.x = grid.x + 12 * CELL_SIZE;
self.y = grid.y + 15 * CELL_SIZE;
var flareGraphics = self.attachAsset('solarFlare', {
anchorX: 0.5,
anchorY: 0.5
});
flareGraphics.alpha = 0.8;
flareGraphics.blendMode = 1; // Additive blending
// Intense pulsing animation
tween(flareGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(flareGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 300,
easing: tween.easeInOut
});
}
}
});
// Show notification
var notification = game.addChild(new Notification("⚡ Solar Flare! Energy towers boosted! ⚡"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
self.update = function () {
self.remainingTime--;
if (self.remainingTime <= 0) {
self.destroy();
return;
}
// Boost energy-based towers (rapid, sniper, default)
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.id === 'rapid' || tower.id === 'sniper' || tower.id === 'default') {
if (!tower.solarFlareBoost) {
tower.originalFireRate = tower.fireRate;
tower.originalDamage = tower.damage;
tower.fireRate = Math.floor(tower.fireRate * 0.6); // 40% faster
tower.damage = Math.floor(tower.damage * 1.3); // 30% more damage
tower.solarFlareBoost = true;
// Visual effect on boosted towers
tween(tower.energyCore, {
tint: 0xFFD700,
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut
});
}
}
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove boosts from all towers
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.solarFlareBoost) {
tower.fireRate = tower.originalFireRate;
tower.damage = tower.originalDamage;
tower.solarFlareBoost = false;
// Reset visual effects
var originalTint = 0x88AACC;
switch (tower.id) {
case 'rapid':
originalTint = 0x00AAFF;
break;
case 'sniper':
originalTint = 0xFF5500;
break;
case 'default':
originalTint = 0x88AACC;
break;
}
tween(tower.energyCore, {
tint: originalTint,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
}
var flareIndex = solarFlares.indexOf(self);
if (flareIndex !== -1) {
solarFlares.splice(flareIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'default';
// Increase size of base for easier touch
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'sniper':
baseGraphics.tint = 0xFF5500;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
case 'slow':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = -20 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -20; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var costLabelShadow = new Text2(towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 4;
costLabelShadow.y = 24 + 12;
self.addChild(costLabelShadow);
// Add cost label
var costLabel = new Text2(towerCost, {
size: 50,
fill: 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 20 + 12;
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var SpecializationMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 300;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 1800;
menuBackground.height = 600;
menuBackground.tint = 0x1A1A1A;
menuBackground.alpha = 0.98;
var titleText = new Text2('Choose Specialization Path', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -200;
self.addChild(titleText);
var subtitleText = new Text2('Your tower has reached level 3 and can specialize!', {
size: 50,
fill: 0xCCCCCC,
weight: 400
});
subtitleText.anchor.set(0.5, 0.5);
subtitleText.y = -140;
self.addChild(subtitleText);
// Create specialization options based on tower type
var options = [];
if (self.tower.id === 'sniper') {
options = [{
type: 'antiarmor',
name: 'Anti-Armor',
desc: 'Pierce shields\n+50% damage vs immune',
color: 0xFF6600
}, {
type: 'longrange',
name: 'Long-Range',
desc: 'Double firing range\nBetter enemy targeting',
color: 0x00AAFF
}];
} else if (self.tower.id === 'splash') {
options = [{
type: 'nuclear',
name: 'Nuclear',
desc: 'Huge damage\nLarger splash radius',
color: 0xFF0000
}, {
type: 'chainreaction',
name: 'Chain Reaction',
desc: 'Damage spreads\nbetween enemies',
color: 0xFFFF00
}];
}
// Create option buttons
for (var i = 0; i < options.length; i++) {
var option = options[i];
var button = new Container();
var buttonBg = button.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 700;
buttonBg.height = 200;
buttonBg.tint = option.color;
buttonBg.alpha = 0.8;
var nameText = new Text2(option.name, {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
nameText.anchor.set(0.5, 0.5);
nameText.y = -40;
button.addChild(nameText);
var descText = new Text2(option.desc, {
size: 45,
fill: 0xFFFFFF,
weight: 400
});
descText.anchor.set(0.5, 0.5);
descText.y = 40;
button.addChild(descText);
button.x = (i - 0.5) * 800;
button.y = 50;
button.optionType = option.type;
button.down = function () {
self.selectSpecialization(this.optionType);
};
self.addChild(button);
}
// Close button
var closeButton = new Container();
var closeBg = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBg.width = 100;
closeBg.height = 100;
closeBg.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = 850;
closeButton.y = -250;
closeButton.down = function () {
self.destroy();
};
self.addChild(closeButton);
self.selectSpecialization = function (branchType) {
// Apply specialization to tower
var specialization = new TowerSpecialization(self.tower, branchType);
self.tower.specialization = specialization;
self.tower.addChild(specialization);
specialization.applySpecialization();
var notification = game.addChild(new Notification("Tower specialized: " + branchType));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
self.destroy();
};
return self;
});
var StationCustomizer = Container.expand(function () {
var self = Container.call(this);
self.modules = [];
self.moduleSlots = [];
self.selectedSlot = null;
self.customizationMode = false;
// Create module slots around the station
var slotPositions = [{
x: -2,
y: -2
}, {
x: 0,
y: -2
}, {
x: 2,
y: -2
}, {
x: -2,
y: 0
}, {
x: 2,
y: 0
}, {
x: -2,
y: 2
}, {
x: 0,
y: 2
}, {
x: 2,
y: 2
}];
for (var i = 0; i < slotPositions.length; i++) {
var slot = new Container();
var slotGraphics = slot.attachAsset('moduleSocket', {
anchorX: 0.5,
anchorY: 0.5
});
slotGraphics.alpha = 0.5;
slotGraphics.tint = 0x556677;
slot.gridX = slotPositions[i].x;
slot.gridY = slotPositions[i].y;
slot.x = grid.x + (12 + slot.gridX) * CELL_SIZE;
slot.y = grid.y + (15 + slot.gridY) * CELL_SIZE;
slot.module = null;
slot.slotIndex = i;
slot.down = function () {
if (self.customizationMode) {
self.selectedSlot = this;
self.showModuleMenu();
}
};
self.addChild(slot);
self.moduleSlots.push(slot);
}
self.showModuleMenu = function () {
if (!self.selectedSlot) return;
var menu = new Container();
var menuBg = menu.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBg.width = 600;
menuBg.height = 300;
menuBg.tint = 0x333333;
var moduleTypes = ['research', 'shield', 'teleporter'];
for (var i = 0; i < moduleTypes.length; i++) {
var button = new Container();
var buttonBg = button.attachAsset('techTreeNode', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 120;
buttonBg.height = 80;
var cost = getModuleCost(moduleTypes[i], 1);
var buttonText = new Text2(moduleTypes[i] + '\n' + cost + 'g', {
size: 30,
fill: 0xFFFFFF,
weight: 600
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.moduleType = moduleTypes[i];
button.x = -180 + i * 180;
button.y = 50;
button.down = function () {
self.buildModule(this.moduleType);
menu.destroy();
};
menu.addChild(button);
}
menu.x = 2048 / 2;
menu.y = 1400;
game.addChild(menu);
LK.setTimeout(function () {
if (menu.parent) {
menu.parent.removeChild(menu);
}
}, 5000);
};
self.buildModule = function (moduleType) {
if (!self.selectedSlot || self.selectedSlot.module) return;
var cost = getModuleCost(moduleType, 1);
if (gold < cost) {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
var module;
switch (moduleType) {
case 'research':
module = new ResearchLab();
break;
case 'shield':
module = new ShieldGenerator();
break;
case 'teleporter':
module = new TeleporterNode();
break;
default:
module = new StationModule(moduleType);
}
module.gridX = self.selectedSlot.gridX;
module.gridY = self.selectedSlot.gridY;
module.x = self.selectedSlot.x;
module.y = self.selectedSlot.y;
self.selectedSlot.module = module;
self.selectedSlot.children[0].alpha = 0; // Hide slot graphic
self.addChild(module);
self.modules.push(module);
setGold(gold - cost);
var notification = game.addChild(new Notification(moduleType + " module built!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
self.selectedSlot = null;
};
self.toggleCustomizationMode = function () {
self.customizationMode = !self.customizationMode;
for (var i = 0; i < self.moduleSlots.length; i++) {
var slot = self.moduleSlots[i];
if (!slot.module) {
slot.children[0].alpha = self.customizationMode ? 0.8 : 0.3;
}
}
var modeText = self.customizationMode ? "ON" : "OFF";
var notification = game.addChild(new Notification("Customization mode: " + modeText));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
};
self.update = function () {
for (var i = 0; i < self.modules.length; i++) {
if (self.modules[i].update) {
self.modules[i].update();
}
}
};
return self;
});
var StationModule = Container.expand(function (moduleType) {
var self = Container.call(this);
self.moduleType = moduleType || 'basic';
self.level = 1;
self.maxLevel = 3;
self.isActive = true;
self.gridX = 0;
self.gridY = 0;
self.isDestroyed = false;
// Get appropriate asset
var assetName = 'stationModule';
switch (moduleType) {
case 'research':
assetName = 'researchLab';
break;
case 'shield':
assetName = 'shieldGenerator';
break;
case 'teleporter':
assetName = 'teleporter';
break;
}
var moduleGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add energy core
var energyCore = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
energyCore.width = 30;
energyCore.height = 30;
energyCore.tint = 0x00aaff;
// Level indicators
var levelIndicators = [];
for (var i = 0; i < self.maxLevel; i++) {
var indicator = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = 12;
indicator.height = 12;
indicator.x = -30 + i * 30;
indicator.y = 60;
indicator.tint = i < self.level ? 0xffffff : 0x444444;
levelIndicators.push(indicator);
}
self.updateLevelIndicators = function () {
for (var i = 0; i < levelIndicators.length; i++) {
levelIndicators[i].tint = i < self.level ? 0xffffff : 0x444444;
}
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
var upgradeCost = getModuleCost(self.moduleType, self.level + 1);
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
self.updateLevelIndicators();
self.onUpgrade();
return true;
}
}
return false;
};
self.onUpgrade = function () {
// Override in specific module types
};
self.update = function () {
if (!self.isActive) return;
// Pulsing energy core
if (LK.ticks % 120 === 0) {
tween(energyCore, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(energyCore, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var TeleporterNode = StationModule.expand(function () {
var self = StationModule.call(this, 'teleporter');
self.teleportEnergy = 100;
self.maxTeleportEnergy = 100;
self.linkedNodes = [];
self.onUpgrade = function () {
self.maxTeleportEnergy = 100 + (self.level - 1) * 25;
self.teleportEnergy = self.maxTeleportEnergy;
};
self.teleportHero = function (hero, targetNode) {
if (self.teleportEnergy >= 30 && targetNode && targetNode.teleportEnergy >= 30) {
// Create teleport effect at source
var sourceEffect = new Container();
sourceEffect.x = self.x;
sourceEffect.y = self.y;
var sourceGraphics = sourceEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
sourceGraphics.tint = 0x9c27b0;
sourceGraphics.alpha = 0.8;
game.addChild(sourceEffect);
// Create teleport effect at destination
var destEffect = new Container();
destEffect.x = targetNode.x;
destEffect.y = targetNode.y;
var destGraphics = destEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
destGraphics.tint = 0x9c27b0;
destGraphics.alpha = 0.8;
game.addChild(destEffect);
// Animate effects
tween(sourceEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (sourceEffect.parent) {
sourceEffect.parent.removeChild(sourceEffect);
}
}
});
tween(destEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (destEffect.parent) {
destEffect.parent.removeChild(destEffect);
}
}
});
// Teleport hero
hero.x = targetNode.x;
hero.y = targetNode.y;
// Consume energy
self.teleportEnergy = Math.max(0, self.teleportEnergy - 30);
targetNode.teleportEnergy = Math.max(0, targetNode.teleportEnergy - 30);
return true;
}
return false;
};
self.redirectEnemyPath = function () {
if (self.teleportEnergy >= 50 && self.linkedNodes.length > 0) {
var targetNode = self.linkedNodes[Math.floor(Math.random() * self.linkedNodes.length)];
// Find nearby enemies and redirect one
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy.isFlying) {
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 2) {
// Teleport enemy
enemy.x = targetNode.x;
enemy.y = targetNode.y;
enemy.currentCellX = targetNode.gridX;
enemy.currentCellY = targetNode.gridY;
enemy.currentTarget = null;
self.teleportEnergy = Math.max(0, self.teleportEnergy - 50);
var notification = game.addChild(new Notification("Enemy redirected through teleporter!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
break;
}
}
}
}
};
self.update = function () {
StationModule.prototype.update.call(self);
// Regenerate teleport energy
if (LK.ticks % 120 === 0 && self.teleportEnergy < self.maxTeleportEnergy) {
// Every 2 seconds
self.teleportEnergy = Math.min(self.maxTeleportEnergy, self.teleportEnergy + self.level * 3);
}
};
self.down = function () {
self.redirectEnemyPath();
};
return self;
});
var ShieldGenerator = StationModule.expand(function () {
var self = StationModule.call(this, 'shield');
self.shieldEnergy = 100;
self.maxShieldEnergy = 100;
self.shieldActive = false;
self.shieldEffect = null;
self.onUpgrade = function () {
self.maxShieldEnergy = 100 + (self.level - 1) * 50;
self.shieldEnergy = self.maxShieldEnergy;
};
self.activateShield = function () {
if (self.shieldEnergy >= 50 && !self.shieldActive) {
self.shieldActive = true;
self.shieldEnergy = Math.max(0, self.shieldEnergy - 50);
// Create visual shield effect
self.shieldEffect = new Container();
self.shieldEffect.x = self.x;
self.shieldEffect.y = self.y;
var shieldGraphics = self.shieldEffect.attachAsset('shieldEffect', {
anchorX: 0.5,
anchorY: 0.5
});
shieldGraphics.alpha = 0.3;
shieldGraphics.tint = 0x00ffff;
game.addChild(self.shieldEffect);
// Shield lasts for 10 seconds
LK.setTimeout(function () {
self.deactivateShield();
}, 10000);
var notification = game.addChild(new Notification("Station shields activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
self.deactivateShield = function () {
self.shieldActive = false;
if (self.shieldEffect && self.shieldEffect.parent) {
self.shieldEffect.parent.removeChild(self.shieldEffect);
self.shieldEffect = null;
}
};
self.update = function () {
StationModule.prototype.update.call(self);
// Regenerate shield energy
if (LK.ticks % 180 === 0 && self.shieldEnergy < self.maxShieldEnergy) {
// Every 3 seconds
self.shieldEnergy = Math.min(self.maxShieldEnergy, self.shieldEnergy + self.level * 5);
}
// Update shield effect position
if (self.shieldEffect) {
self.shieldEffect.x = self.x;
self.shieldEffect.y = self.y;
}
};
self.down = function () {
self.activateShield();
};
return self;
});
var ResearchLab = StationModule.expand(function () {
var self = StationModule.call(this, 'research');
self.researchPoints = 0;
self.activeResearch = null;
self.completedTechs = [];
self.onUpgrade = function () {
// Higher level labs generate research points faster
self.researchPointsPerTick = self.level;
};
self.update = function () {
StationModule.prototype.update.call(self);
// Generate research points
if (LK.ticks % 60 === 0) {
// Every second
self.researchPoints += self.level;
storage.researchPoints = self.researchPoints;
}
// Complete active research
if (self.activeResearch && self.researchPoints >= self.activeResearch.cost) {
self.completeResearch();
}
};
self.startResearch = function (techId) {
var tech = getTechnology(techId);
if (tech && !self.activeResearch) {
self.activeResearch = tech;
var notification = game.addChild(new Notification("Research started: " + tech.name));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
self.completeResearch = function () {
if (self.activeResearch) {
self.researchPoints -= self.activeResearch.cost;
self.completedTechs.push(self.activeResearch.id);
storage.completedTechs = self.completedTechs;
// Apply research benefits
self.activeResearch.onComplete();
var notification = game.addChild(new Notification("Research complete: " + self.activeResearch.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
self.activeResearch = null;
}
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// Standardized method to get the current range of the tower
self.getRange = function () {
// Always calculate range based on tower type and level
var baseRange;
switch (self.id) {
case 'sniper':
// Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost
if (self.level === self.maxLevel) {
baseRange = 12 * CELL_SIZE; // Significantly increased range for max level
} else {
baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE;
}
break;
case 'splash':
// Splash: base 2, +0.2 per level (max ~4 blocks at max level)
return (2 + (self.level - 1) * 0.2) * CELL_SIZE;
case 'rapid':
// Rapid: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// Slow: base 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
default:
// Default: base 3, +0.5 per level
baseRange = (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
// Apply specialization range multiplier
if (self.specialization && self.specialization.branchType === 'longrange') {
baseRange *= 2.0;
}
return baseRange;
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
switch (self.id) {
case 'rapid':
self.fireRate = 35; // Slightly slower to reduce overwhelming power
self.damage = 8; // Increased from 5 for better effectiveness
self.range = 2.8 * CELL_SIZE; // Slightly better range
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 100; // Slower but more impactful
self.damage = 40; // Increased significantly for true sniper feel
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 80; // Slightly slower
self.damage = 18; // Increased base damage
self.range = 2.2 * CELL_SIZE; // Slightly better range
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 45; // Faster to apply slow effects more consistently
self.damage = 12; // Increased from 8 for better utility
self.range = 3.8 * CELL_SIZE; // Better range for support role
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 60; // Faster to apply poison more effectively
self.damage = 15; // Increased from 12
self.range = 3.5 * CELL_SIZE; // Better range
self.bulletSpeed = 5;
break;
}
// Create tower base structure
var baseAssetName = 'tower_base_' + (self.id === 'default' ? 'default' : self.id);
var baseGraphics = self.attachAsset(baseAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Create support structures for higher levels
self.supportStructures = [];
// Create energy core based on tower type
var energyCore = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
energyCore.width = 40;
energyCore.height = 40;
switch (self.id) {
case 'rapid':
energyCore.tint = 0x00AAFF;
break;
case 'sniper':
energyCore.tint = 0xFF5500;
break;
case 'splash':
energyCore.tint = 0x33CC00;
break;
case 'slow':
energyCore.tint = 0x9900FF;
break;
case 'poison':
energyCore.tint = 0x00FFAA;
break;
default:
energyCore.tint = 0x88AACC;
}
// Add pulsing energy core animation
self.energyCore = energyCore;
tween(energyCore, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(energyCore, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 1000,
easing: tween.easeInOut
});
}
});
// Method to update tower visual appearance based on level
self.updateTowerVisuals = function () {
// Ensure gunContainer exists before trying to use it
if (!gunContainer) {
console.warn("gunContainer not found, skipping visual update");
return;
}
// Clear existing gun graphics
if (self.gunGraphics && self.gunGraphics.parent) {
gunContainer.removeChild(self.gunGraphics);
}
// Clear existing support structures
for (var i = 0; i < self.supportStructures.length; i++) {
if (self.supportStructures[i].parent) {
self.removeChild(self.supportStructures[i]);
}
}
self.supportStructures = [];
// Create new gun graphics based on current level
var gunAssetName = 'tower_gun_' + (self.id === 'default' ? 'default' : self.id) + '_' + self.level;
self.gunGraphics = gunContainer.attachAsset(gunAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add support structures for higher levels
if (self.level >= 2) {
// Add support beams
for (var i = 0; i < 2; i++) {
var beam = self.attachAsset('tower_support_beam', {
anchorX: 0.5,
anchorY: 1.0
});
beam.x = i === 0 ? -30 : 30;
beam.y = baseGraphics.height / 2 - 10;
beam.rotation = i === 0 ? -0.3 : 0.3;
self.supportStructures.push(beam);
}
}
if (self.level >= 3) {
// Add radar dish for better targeting
var radar = self.attachAsset('tower_radar_dish', {
anchorX: 0.5,
anchorY: 0.5
});
radar.x = 0;
radar.y = -baseGraphics.height / 2 - 20;
radar.alpha = 0.8;
self.supportStructures.push(radar);
// Rotate radar dish slowly
tween(radar, {
rotation: Math.PI * 2
}, {
duration: 4000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed && radar.parent) {
radar.rotation = 0;
}
}
});
}
if (self.level >= 4) {
// Add antenna for enhanced range
var antenna = self.attachAsset('tower_antenna', {
anchorX: 0.5,
anchorY: 1.0
});
antenna.x = 0;
antenna.y = -baseGraphics.height / 2 - 60;
self.supportStructures.push(antenna);
// Add armor plates
for (var i = 0; i < 4; i++) {
var armor = self.attachAsset('tower_armor_plate', {
anchorX: 0.5,
anchorY: 0.5
});
var angle = i / 4 * Math.PI * 2;
armor.x = Math.cos(angle) * 40;
armor.y = Math.sin(angle) * 40;
armor.rotation = angle;
armor.alpha = 0.7;
self.supportStructures.push(armor);
}
}
if (self.level >= 5) {
// Add energy conduits
for (var i = 0; i < 3; i++) {
var conduit = self.attachAsset('tower_energy_conduit', {
anchorX: 0.5,
anchorY: 0.5
});
var angle = i / 3 * Math.PI * 2;
conduit.x = Math.cos(angle) * 50;
conduit.y = Math.sin(angle) * 50;
conduit.rotation = angle + Math.PI / 2;
// Pulsing energy effect
tween(conduit, {
alpha: 0.4
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed && conduit.parent) {
tween(conduit, {
alpha: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
}
});
self.supportStructures.push(conduit);
}
}
if (self.level >= 6) {
// Max level - add ultimate enhancement visual
var enhancement = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
enhancement.width = 80;
enhancement.height = 80;
enhancement.tint = 0xFFD700;
enhancement.alpha = 0.3;
enhancement.y = 0;
// Rotating golden aura for max level
tween(enhancement, {
rotation: Math.PI * 2,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 3000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed && enhancement.parent) {
enhancement.rotation = 0;
enhancement.scaleX = 1.0;
enhancement.scaleY = 1.0;
}
}
});
self.supportStructures.push(enhancement);
}
// Update energy core size based on level
var coreSize = 40 + (self.level - 1) * 8;
self.energyCore.width = coreSize;
self.energyCore.height = coreSize;
// Add specialization visuals if present
if (self.specialization) {
self.updateSpecializationVisuals();
}
};
// Method to add specialization visual indicators
self.updateSpecializationVisuals = function () {
if (!self.specialization) return;
var specAssetName;
switch (self.specialization.branchType) {
case 'antiarmor':
specAssetName = 'tower_spec_armor_pierce';
break;
case 'longrange':
specAssetName = 'tower_spec_long_range';
break;
case 'nuclear':
specAssetName = 'tower_spec_nuclear';
break;
case 'chainreaction':
specAssetName = 'tower_spec_chain_reaction';
break;
default:
return;
}
var specVisual = self.attachAsset(specAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
specVisual.y = -baseGraphics.height / 2 - 40;
specVisual.alpha = 0.9;
// Pulsing effect for specialization indicator
tween(specVisual, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.6
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed && specVisual.parent) {
tween(specVisual, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.9
}, {
duration: 1200,
easing: tween.easeInOut
});
}
}
});
self.supportStructures.push(specVisual);
};
// Initialize tower visuals
self.updateTowerVisuals();
var gunContainer = new Container();
self.addChild(gunContainer);
var gunGraphics = gunContainer.attachAsset('defense', {
anchorX: 0.5,
anchorY: 0.5
});
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1);
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF;
break;
case 'poison':
towerLevelIndicator.tint = 0x00FFAA;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// More balanced upgrade cost scaling
if (self.level === self.maxLevel - 1) {
// Final upgrade is expensive but not prohibitive
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(1.8, self.level - 1) * 2.5);
} else {
// Use 1.8x multiplier instead of 2x for more manageable costs
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(1.8, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// Play upgrade sound
LK.getSound('towerUpgrade').play();
// Update tower visuals for new level
self.updateTowerVisuals();
// Check for specialization unlock at level 3
if (self.level === 3 && !self.specialization && (self.id === 'sniper' || self.id === 'splash')) {
var specMenu = new SpecializationMenu(self);
game.addChild(specMenu);
specMenu.x = 2048 / 2;
tween(specMenu, {
y: 2732 - 300
}, {
duration: 300,
easing: tween.backOut
});
specializationMenus.push(specMenu);
return true;
}
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type with balanced scaling
switch (self.id) {
case 'rapid':
if (self.level === self.maxLevel) {
// Max level gets significant boost
self.fireRate = Math.max(8, 35 - self.level * 4.2);
self.damage = 8 + self.level * 6; // More modest scaling
self.bulletSpeed = 7 + self.level * 1.5;
} else {
self.fireRate = Math.max(18, 35 - self.level * 2.8);
self.damage = 8 + self.level * 3.5;
self.bulletSpeed = 7 + self.level * 0.8;
}
break;
case 'sniper':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(15, 100 - self.level * 12);
self.damage = 40 + self.level * 18; // High damage scaling
self.bulletSpeed = 25 + self.level * 2;
} else {
self.fireRate = Math.max(30, 100 - self.level * 8);
self.damage = 40 + self.level * 12;
self.bulletSpeed = 25 + self.level * 1;
}
break;
case 'splash':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(12, 80 - self.level * 10);
self.damage = 18 + self.level * 12; // Strong splash scaling
self.bulletSpeed = 4 + self.level * 1.5;
} else {
self.fireRate = Math.max(25, 80 - self.level * 6);
self.damage = 18 + self.level * 8;
self.bulletSpeed = 4 + self.level * 0.8;
}
break;
case 'slow':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(8, 45 - self.level * 5.5);
self.damage = 12 + self.level * 8; // Better damage for utility tower
self.bulletSpeed = 5 + self.level * 1.2;
} else {
self.fireRate = Math.max(20, 45 - self.level * 3.5);
self.damage = 12 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.7;
}
break;
case 'poison':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(10, 60 - self.level * 7.5);
self.damage = 15 + self.level * 10; // Strong poison scaling
self.bulletSpeed = 5 + self.level * 1.3;
} else {
self.fireRate = Math.max(22, 60 - self.level * 5);
self.damage = 15 + self.level * 6;
self.bulletSpeed = 5 + self.level * 0.8;
}
break;
default:
if (self.level === self.maxLevel) {
self.fireRate = Math.max(10, 60 - self.level * 8);
self.damage = 10 + self.level * 12;
self.bulletSpeed = 5 + self.level * 1.5;
} else {
self.fireRate = Math.max(25, 60 - self.level * 5);
self.damage = 10 + self.level * 7;
self.bulletSpeed = 5 + self.level * 0.8;
}
}
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
// Use spatial partitioning for better performance
var nearbyEnemies = spatialGrid.getNearbyEnemies(self.x, self.y, self.getRange());
for (var i = 0; i < nearbyEnemies.length; i++) {
var enemy = nearbyEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is in range
if (distance <= self.getRange()) {
// Apply nebula stealth effect - reduce targeting chance
var targetingChance = enemy.nebulaStealthed ? 0.3 : 1.0;
if (Math.random() > targetingChance) {
continue; // Skip this enemy due to stealth
}
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, use the original path-based targeting
// Get the cell for this enemy
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Use the cell's score (distance to exit) for prioritization
// Lower score means closer to exit
if (cell.score < closestScore) {
closestScore = cell.score;
closestEnemy = enemy;
}
}
}
}
}
if (!closestEnemy) {
self.targetEnemy = null;
}
return closestEnemy;
};
self.update = function () {
// Update ultimate ability
if (self.ultimate) {
self.ultimate.update();
}
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
var currentFireRate = self.fireRate;
if (self.heroBuff && self.heroBuff.duration > 0) {
currentFireRate = Math.floor(self.fireRate * self.heroBuff.fireRate);
}
if (LK.ticks - self.lastFired >= currentFireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
tutorialSystem.checkActionCompleted('selectTower');
}
// Check for ultimate ability activation (double tap or long press simulation)
if (self.ultimate && self.ultimate.canActivate()) {
// Simple activation - tap when ultimate is ready
if (self.ultimate.activate()) {
return; // Ultimate was activated, don't show menu
}
}
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
game.addChild(rangeIndicator);
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
// Apply hero buff if present
var finalDamage = self.damage;
var finalFireRate = self.fireRate;
if (self.heroBuff && self.heroBuff.duration > 0) {
finalDamage = Math.floor(self.damage * self.heroBuff.damage);
finalFireRate = Math.floor(self.fireRate * self.heroBuff.fireRate);
self.heroBuff.duration--;
if (self.heroBuff.duration <= 0) {
self.heroBuff = null;
}
}
// Apply support bonus from linked towers
if (self.supportBonus) {
finalDamage = Math.floor(finalDamage * self.supportBonus);
finalFireRate = Math.floor(finalFireRate / self.supportBonus);
}
// Apply specialization effects
if (self.specialization) {
if (self.specialization.branchType === 'antiarmor' && self.targetEnemy.isImmune) {
finalDamage = Math.floor(finalDamage * 1.5);
}
}
var bullet = getBulletFromPool(bulletX, bulletY, self.targetEnemy, finalDamage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
// Customize bullet appearance based on tower type
switch (self.id) {
case 'rapid':
bullet.children[0].tint = 0x00AAFF;
bullet.children[0].width = 20;
bullet.children[0].height = 20;
break;
case 'sniper':
bullet.children[0].tint = 0xFF5500;
bullet.children[0].width = 15;
bullet.children[0].height = 15;
break;
case 'splash':
bullet.children[0].tint = 0x33CC00;
bullet.children[0].width = 40;
bullet.children[0].height = 40;
break;
case 'slow':
bullet.children[0].tint = 0x9900FF;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// Play shooting sound
LK.getSound('towerShoot').play();
// --- Sci-fi charge and firing effect ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
// Add energy charge effect to core
if (self.energyCore) {
tween.stop(self.energyCore, {
scaleX: true,
scaleY: true,
alpha: true
});
tween(self.energyCore, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 1.5
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.energyCore, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
// Always use the original resting position for recoil, never accumulate offset
if (gunContainer._restX === undefined) {
gunContainer._restX = 0;
}
if (gunContainer._restY === undefined) {
gunContainer._restY = 0;
}
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset to resting position before animating (in case of interrupted tweens)
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Calculate recoil offset (recoil back along the gun's rotation)
var recoilDistance = 8;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
// Animate recoil back from the resting position
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original position/scale
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 1;
}
}
}
self.refreshCellsInRange();
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this tower and its components
tween.stop(self);
tween.stop(gunContainer);
if (self.energyCore) {
tween.stop(self.energyCore);
}
// Clean up all support structures and their tweens
if (self.supportStructures) {
for (var i = 0; i < self.supportStructures.length; i++) {
if (self.supportStructures[i]) {
tween.stop(self.supportStructures[i]);
}
}
self.supportStructures = [];
}
// Clean up gun graphics
if (self.gunGraphics) {
tween.stop(self.gunGraphics);
self.gunGraphics = null;
}
// Clear cells in range references
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
// Clear grid cells occupied by this tower
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (cell) {
cell.type = 0;
}
}
}
// Remove from towers array
var towerIndex = towers.indexOf(self);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Remove any associated range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
game.removeChild(game.children[i]);
}
}
// Close any upgrade menus for this tower
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu && child.tower === self;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
// Clear selected tower if it's this tower
if (selectedTower === self) {
selectedTower = null;
}
// Clean up ultimate ability
if (self.ultimate) {
self.ultimate.destroy();
self.ultimate = null;
}
// Nullify all object references
self.targetEnemy = null;
self.energyCore = null;
// Call parent destroy
Container.prototype.destroy.call(self);
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'default';
self.hasEnoughGold = true;
var rangeIndicator = new Container();
self.addChild(rangeIndicator);
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3;
var previewGraphics = self.attachAsset('towerpreview', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
switch (self.towerType) {
case 'rapid':
previewGraphics.tint = 0x00AAFF;
break;
case 'sniper':
previewGraphics.tint = 0xFF5500;
break;
case 'splash':
previewGraphics.tint = 0x33CC00;
break;
case 'slow':
previewGraphics.tint = 0x9900FF;
break;
case 'poison':
previewGraphics.tint = 0x00FFAA;
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (!cell || cell.type !== 0) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
self.blockedByEnemy = false;
if (validGridPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) {
continue;
}
// Only check non-flying enemies, flying enemies can pass over towers
if (!enemy.isFlying) {
if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
self.canPlace = validGridPlacement && !self.blockedByEnemy;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var TowerSpecialization = Container.expand(function (tower, branchType) {
var self = Container.call(this);
self.tower = tower;
self.branchType = branchType || 'none';
self.linkedTowers = [];
self.isDestroyed = false;
// Add specialization visual indicator
var specIcon = self.attachAsset('specializationIcon', {
anchorX: 0.5,
anchorY: 0.5
});
specIcon.width = 30;
specIcon.height = 30;
specIcon.y = -self.tower.children[0].height / 2 - 20;
// Set specialization color based on branch
switch (self.branchType) {
case 'antiarmor':
specIcon.tint = 0xFF6600;
break;
case 'longrange':
specIcon.tint = 0x00AAFF;
break;
case 'nuclear':
specIcon.tint = 0xFF0000;
break;
case 'chainreaction':
specIcon.tint = 0xFFFF00;
break;
default:
specIcon.tint = 0xFFD700;
}
self.applySpecialization = function () {
switch (self.branchType) {
case 'antiarmor':
// Anti-armor: pierce shields, extra damage to immune enemies
self.tower.armorPiercing = true;
self.tower.damage = Math.floor(self.tower.damage * 1.5);
break;
case 'longrange':
// Long-range: significantly extended range
self.tower.rangeMultiplier = 2.0;
self.tower.refreshCellsInRange();
break;
case 'nuclear':
// Nuclear: huge splash damage, slower fire rate
self.tower.damage = Math.floor(self.tower.damage * 2.5);
self.tower.fireRate = Math.floor(self.tower.fireRate * 1.8);
self.tower.splashRadius = CELL_SIZE * 3;
break;
case 'chainreaction':
// Chain reaction: damage spreads between enemies
self.tower.chainReaction = true;
self.tower.chainRange = CELL_SIZE * 2;
self.tower.chainDamage = 0.7;
break;
}
// Update tower visuals to show specialization
if (self.tower.updateSpecializationVisuals) {
self.tower.updateSpecializationVisuals();
}
};
self.findNearbyTowers = function () {
var nearby = [];
var searchRadius = CELL_SIZE * 4;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower !== self.tower && tower.specialization) {
var dx = tower.x - self.tower.x;
var dy = tower.y - self.tower.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= searchRadius) {
nearby.push(tower);
}
}
}
return nearby;
};
self.createSupportLinks = function () {
var nearbyTowers = self.findNearbyTowers();
for (var i = 0; i < nearbyTowers.length; i++) {
var tower = nearbyTowers[i];
if (self.linkedTowers.indexOf(tower) === -1) {
self.linkedTowers.push(tower);
// Create visual link beam
var linkBeam = new Container();
linkBeam.x = self.tower.x;
linkBeam.y = self.tower.y;
var beamGraphics = linkBeam.attachAsset('linkBeam', {
anchorX: 0,
anchorY: 0.5
});
var dx = tower.x - self.tower.x;
var dy = tower.y - self.tower.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
beamGraphics.width = distance;
beamGraphics.rotation = angle;
beamGraphics.alpha = 0.5;
beamGraphics.tint = 0x00FF88;
game.addChild(linkBeam);
self.tower.linkBeam = linkBeam;
}
}
};
self.applySupportBonus = function () {
// Linked towers get damage and fire rate bonus
var bonusMultiplier = 1 + self.linkedTowers.length * 0.15;
self.tower.supportBonus = bonusMultiplier;
};
self.update = function () {
if (self.branchType !== 'none') {
self.createSupportLinks();
self.applySupportBonus();
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove link beams
if (self.tower.linkBeam && self.tower.linkBeam.parent) {
self.tower.linkBeam.parent.removeChild(self.tower.linkBeam);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var TowerUltimate = Container.expand(function (tower, ultimateType) {
var self = Container.call(this);
self.tower = tower;
self.ultimateType = ultimateType || 'none';
self.cooldownTime = 1800; // 30 seconds at 60fps
self.currentCooldown = 0;
self.isDestroyed = false;
// Add ultimate indicator
var ultimateIcon = self.attachAsset('ultimateIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
ultimateIcon.y = -self.tower.children[0].height / 2 - 35;
ultimateIcon.alpha = 0.3; // Dim when on cooldown
// Set ultimate-specific properties
switch (self.ultimateType) {
case 'orbital':
self.cooldownTime = 2400; // 40 seconds
ultimateIcon.tint = 0xFF0000;
break;
case 'nanite':
self.cooldownTime = 2100; // 35 seconds
ultimateIcon.tint = 0x00FF88;
break;
case 'temporal':
self.cooldownTime = 1800; // 30 seconds
ultimateIcon.tint = 0x9966FF;
break;
default:
ultimateIcon.tint = 0xFFD700;
}
self.canActivate = function () {
return self.currentCooldown <= 0 && !self.isDestroyed;
};
self.activate = function () {
if (!self.canActivate()) return false;
self.currentCooldown = self.cooldownTime;
// Visual cooldown effect
ultimateIcon.alpha = 0.3;
tween(ultimateIcon, {
alpha: 1.0
}, {
duration: self.cooldownTime,
easing: tween.linear
});
switch (self.ultimateType) {
case 'orbital':
return self.orbitalStrike();
case 'nanite':
return self.naniteSwarm();
case 'temporal':
return self.timeDilation();
}
return false;
};
self.orbitalStrike = function () {
// Find target area with most enemies
var bestTarget = null;
var maxEnemies = 0;
var strikeRadius = CELL_SIZE * 3;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var nearbyCount = 0;
for (var j = 0; j < enemies.length; j++) {
var otherEnemy = enemies[j];
var dx = otherEnemy.x - enemy.x;
var dy = otherEnemy.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= strikeRadius) {
nearbyCount++;
}
}
if (nearbyCount > maxEnemies) {
maxEnemies = nearbyCount;
bestTarget = enemy;
}
}
if (bestTarget) {
// Create orbital strike effect
var strikeEffect = new Container();
strikeEffect.x = bestTarget.x;
strikeEffect.y = bestTarget.y;
var strikeGraphics = strikeEffect.attachAsset('orbitalStrike', {
anchorX: 0.5,
anchorY: 0.5
});
strikeGraphics.alpha = 0.0;
strikeGraphics.scaleX = 0.1;
strikeGraphics.scaleY = 0.1;
strikeGraphics.blendMode = 1; // Additive
game.addChild(strikeEffect);
// Warning phase
tween(strikeGraphics, {
alpha: 0.7,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
// Strike phase - massive damage
var strikeDamage = self.tower.damage * 15;
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
var dx = enemy.x - bestTarget.x;
var dy = enemy.y - bestTarget.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= strikeRadius) {
enemy.health -= strikeDamage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Visual damage effect
tween(enemy, {
tint: 0xFFFFFF,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(enemy, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
}
// Final explosion effect
tween(strikeGraphics, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (strikeEffect.parent) {
strikeEffect.parent.removeChild(strikeEffect);
}
}
});
}
});
var notification = game.addChild(new Notification("🛰️ ORBITAL STRIKE INCOMING! 🛰️"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return true;
}
return false;
};
self.naniteSwarm = function () {
// Create nanite swarm that spreads poison and self-replicates
var swarmCenter = self.tower;
var swarmRadius = CELL_SIZE * 4;
var swarmDamage = self.tower.damage * 3;
// Create visual swarm effect
var swarmEffect = new Container();
swarmEffect.x = swarmCenter.x;
swarmEffect.y = swarmCenter.y;
var swarmGraphics = swarmEffect.attachAsset('naniteSwarm', {
anchorX: 0.5,
anchorY: 0.5
});
swarmGraphics.alpha = 0.8;
swarmGraphics.tint = 0x00FF88;
game.addChild(swarmEffect);
// Create multiple nanite particles
var particles = [];
for (var i = 0; i < 12; i++) {
var particle = swarmEffect.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
particle.width = 15;
particle.height = 15;
particle.tint = 0x44FFAA;
var angle = i / 12 * Math.PI * 2;
particle.x = Math.cos(angle) * 50;
particle.y = Math.sin(angle) * 50;
particles.push(particle);
}
// Animate swarm expansion
tween(swarmEffect, {
scaleX: 3.0,
scaleY: 3.0
}, {
duration: 2000,
easing: tween.easeOut
});
// Animate particles spiraling outward
for (var i = 0; i < particles.length; i++) {
var particle = particles[i];
tween(particle, {
x: particle.x * 3,
y: particle.y * 3,
rotation: Math.PI * 4
}, {
duration: 2000,
delay: i * 100,
easing: tween.easeOut
});
}
// Apply nanite effects over time
var swarmDuration = 180; // 3 seconds
var _swarmTick = function swarmTick() {
swarmDuration--;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - swarmCenter.x;
var dy = enemy.y - swarmCenter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= swarmRadius) {
// Apply nanite damage
enemy.health -= swarmDamage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Apply enhanced poison effect
enemy.poisoned = true;
enemy.poisonDamage = swarmDamage * 0.5;
enemy.poisonDuration = 300; // Extended duration
// Self-replication chance
if (Math.random() < 0.3 && swarmDuration > 60) {
// 30% chance to spread to nearby enemies
for (var j = 0; j < enemies.length; j++) {
var nearbyEnemy = enemies[j];
if (nearbyEnemy !== enemy) {
var nearbyDx = nearbyEnemy.x - enemy.x;
var nearbyDy = nearbyEnemy.y - enemy.y;
var nearbyDistance = Math.sqrt(nearbyDx * nearbyDx + nearbyDy * nearbyDy);
if (nearbyDistance <= CELL_SIZE * 2) {
nearbyEnemy.poisoned = true;
nearbyEnemy.poisonDamage = swarmDamage * 0.3;
nearbyEnemy.poisonDuration = 240;
break;
}
}
}
}
}
}
if (swarmDuration > 0) {
LK.setTimeout(_swarmTick, 100); // Continue every 100ms
} else {
// Clean up swarm effect
tween(swarmEffect, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
if (swarmEffect.parent) {
swarmEffect.parent.removeChild(swarmEffect);
}
}
});
}
};
LK.setTimeout(_swarmTick, 100); // Start swarm effects
var notification = game.addChild(new Notification("🤖 NANITE SWARM DEPLOYED! 🤖"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return true;
};
self.timeDilation = function () {
// Create temporal distortion field that slows time for enemies
var dilationCenter = self.tower;
var dilationRadius = CELL_SIZE * 5;
var dilationDuration = 600; // 10 seconds
// Create visual time dilation effect
var dilationEffect = new Container();
dilationEffect.x = dilationCenter.x;
dilationEffect.y = dilationCenter.y;
var dilationGraphics = dilationEffect.attachAsset('timeDilation', {
anchorX: 0.5,
anchorY: 0.5
});
dilationGraphics.alpha = 0.4;
dilationGraphics.tint = 0x9966FF;
dilationGraphics.blendMode = 1; // Additive
game.addChild(dilationEffect);
// Pulsing time distortion effect
tween(dilationGraphics, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(dilationGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.4
}, {
duration: 1000,
easing: tween.easeInOut
});
}
});
// Apply time dilation effects
var affectedEnemies = [];
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - dilationCenter.x;
var dy = enemy.y - dilationCenter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= dilationRadius) {
// Store original speed
if (!enemy.originalDilationSpeed) {
enemy.originalDilationSpeed = enemy.speed;
}
// Extreme slow effect (90% speed reduction)
enemy.speed = enemy.originalDilationSpeed * 0.1;
enemy.temporallyDilated = true;
enemy.dilationDuration = dilationDuration;
affectedEnemies.push(enemy);
// Visual distortion effect on enemy
tween(enemy, {
tint: 0x9966FF,
alpha: 0.8
}, {
duration: 300,
easing: tween.easeOut
});
}
}
// During dilation, towers in the field get boosted fire rate
var boostedTowers = [];
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - dilationCenter.x;
var dy = tower.y - dilationCenter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= dilationRadius && tower !== self.tower) {
tower.originalDilationFireRate = tower.fireRate;
tower.fireRate = Math.floor(tower.fireRate * 0.3); // 70% faster firing
tower.temporallyBoosted = true;
boostedTowers.push(tower);
}
}
// Clean up after duration
LK.setTimeout(function () {
// Restore enemy speeds
for (var i = 0; i < affectedEnemies.length; i++) {
var enemy = affectedEnemies[i];
if (enemy.parent && !enemy.isDestroyed) {
enemy.speed = enemy.originalDilationSpeed;
enemy.temporallyDilated = false;
// Restore enemy appearance
tween(enemy, {
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
}
// Restore tower fire rates
for (var i = 0; i < boostedTowers.length; i++) {
var tower = boostedTowers[i];
if (tower.parent && !tower.isDestroyed) {
tower.fireRate = tower.originalDilationFireRate;
tower.temporallyBoosted = false;
}
}
// Remove visual effect
tween(dilationEffect, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: function onFinish() {
if (dilationEffect.parent) {
dilationEffect.parent.removeChild(dilationEffect);
}
}
});
}, dilationDuration * 16.67); // Convert frames to milliseconds
var notification = game.addChild(new Notification("⏰ TIME DILATION ACTIVATED! ⏰"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return true;
};
self.update = function () {
if (self.currentCooldown > 0) {
self.currentCooldown--;
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var TutorialSystem = Container.expand(function () {
var self = Container.call(this);
self.currentStep = 0;
self.isActive = false;
self.isWaitingForAction = false;
self.tutorialCompleted = storage.tutorialCompleted || false;
self.currentPhase = 'introduction'; // introduction, basics, combat, advanced, endgame
self.highlightOverlay = null;
self.actionArrow = null;
self.tutorialSteps = [
// INTRODUCTION PHASE
{
phase: 'introduction',
title: "Welcome to Space Tower Defense!",
text: "Commander, our space station is under attack!\nYou must defend against waves of alien invaders.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'introduction',
title: "Your Mission",
text: "Build defensive towers to stop enemies from reaching\nthe bottom of the screen and destroying our base.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'introduction',
title: "Resources Overview",
text: "Gold: Used to build and upgrade towers\nLives: Lost when enemies reach the bottom\nScore: Points earned for defeating enemies",
highlight: "ui",
action: null,
waitForAction: false
},
// BASICS PHASE
{
phase: 'basics',
title: "Building Your First Tower",
text: "Let's start by building a basic tower.\nDrag the 'Default' tower from the bottom panel.",
highlight: "sourceTowers",
action: "buildTower",
waitForAction: true
}, {
phase: 'basics',
title: "Tower Placement",
text: "Good! Towers need 2x2 space and cannot block enemy paths.\nThe green preview shows valid placement areas.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'basics',
title: "Tower Information",
text: "Tap on your tower to see its stats and upgrade options.\nTry tapping the tower you just built.",
highlight: "tower",
action: "selectTower",
waitForAction: true
}, {
phase: 'basics',
title: "Upgrading Towers",
text: "Excellent! Upgrading increases damage and fire rate.\nUpgrades get more expensive at higher levels.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'basics',
title: "Starting Combat",
text: "Now let's face some enemies!\nTap the 'Start Game' button to begin wave 1.",
highlight: "startButton",
action: "startGame",
waitForAction: true
},
// COMBAT PHASE
{
phase: 'combat',
title: "Enemy Types - Normal",
text: "The first enemies are normal type - balanced health and speed.\nYour towers will automatically target and fire at them.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Earning Gold",
text: "Great! You earn gold for each enemy defeated.\nUse this gold to build more towers and upgrades.",
highlight: "gold",
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Tower Specialization",
text: "Different tower types are effective against different enemies.\nLet's build a Rapid tower for faster firing.",
highlight: "sourceTowers",
action: "buildRapidTower",
waitForAction: true
}, {
phase: 'combat',
title: "Enemy Types - Fast",
text: "Wave 2 brings fast enemies (blue).\nRapid towers are excellent against quick targets.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Tower Range",
text: "Each tower has a different range.\nSniper towers have long range but fire slowly.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Build a Sniper Tower",
text: "Try building a Sniper tower (orange) for long-range support.\nPlace it where it can cover a large area.",
highlight: "sourceTowers",
action: "buildSniperTower",
waitForAction: true
},
// ADVANCED PHASE
{
phase: 'advanced',
title: "Special Enemy Types",
text: "As waves progress, you'll face special enemies:\n• Flying (yellow) - immune to some towers\n• Immune (red) - resistant to effects",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Splash Damage",
text: "Splash towers (green) deal area damage.\nThey're effective against groups of enemies.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Support Towers",
text: "Slow towers (purple) reduce enemy speed.\nPoison towers (cyan) deal damage over time.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Hero Units",
text: "Deploy hero units for additional firepower!\nHeroes can move around and have special abilities.",
highlight: "heroPanel",
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Deploy a Tank Hero",
text: "Try deploying a Tank hero (green).\nTap the tank button, then tap on the battlefield.",
highlight: "heroPanel",
action: "deployHero",
waitForAction: true
}, {
phase: 'advanced',
title: "Hero Abilities",
text: "Heroes gain experience and can use special abilities.\nTap on your hero to activate its ability when ready.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Station Modules",
text: "Customize your space station with modules!\nTap the 'Customize' button to manage station upgrades.",
highlight: "stationPanel",
action: null,
waitForAction: false
},
// ENDGAME PHASE
{
phase: 'endgame',
title: "Boss Waves",
text: "Every 10th wave is a Boss wave with powerful enemies.\nBoss enemies have much more health but give more gold.",
highlight: "waveIndicator",
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Environmental Hazards",
text: "Watch for space hazards like asteroids, nebulas, and solar flares.\nThese can help or hinder your defense strategy.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Advanced Strategies",
text: "• Upgrade key towers to maximum level\n• Use hero abilities strategically\n• Adapt to enemy types each wave",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Tower Specializations",
text: "At level 3, some towers can specialize:\n• Sniper: Anti-Armor or Long-Range\n• Splash: Nuclear or Chain Reaction",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Path Branching",
text: "Enemies may take different routes through your defenses.\nSome paths are shorter but more defended.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Tutorial Complete!",
text: "You're ready to defend the station, Commander!\nSurvive all 50 waves to achieve victory.",
highlight: null,
action: null,
waitForAction: false
}];
var tutorialContainer = new Container();
self.addChild(tutorialContainer);
var overlay = tutorialContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
overlay.width = 1900;
overlay.height = 450;
overlay.tint = 0x0A0A0A;
overlay.alpha = 0.95;
// Phase indicator
var phaseText = new Text2("", {
size: 40,
fill: 0xFFD700,
weight: 600
});
phaseText.anchor.set(0.5, 0.5);
phaseText.y = -160;
tutorialContainer.addChild(phaseText);
var titleText = new Text2("", {
size: 65,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -100;
tutorialContainer.addChild(titleText);
var bodyText = new Text2("", {
size: 45,
fill: 0xE0E0E0,
weight: 400
});
bodyText.anchor.set(0.5, 0.5);
bodyText.y = 0;
tutorialContainer.addChild(bodyText);
// Progress indicator
var progressText = new Text2("", {
size: 35,
fill: 0xCCCCCC,
weight: 400
});
progressText.anchor.set(0.5, 0.5);
progressText.y = 80;
tutorialContainer.addChild(progressText);
var buttonContainer = new Container();
buttonContainer.y = 140;
tutorialContainer.addChild(buttonContainer);
var nextButton = new Container();
var nextBg = nextButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
nextBg.width = 200;
nextBg.height = 80;
nextBg.tint = 0x00AA00;
var nextText = new Text2("Next", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
nextText.anchor.set(0.5, 0.5);
nextButton.addChild(nextText);
nextButton.x = -250;
buttonContainer.addChild(nextButton);
var skipButton = new Container();
var skipBg = skipButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
skipBg.width = 200;
skipBg.height = 80;
skipBg.tint = 0xAA0000;
var skipText = new Text2("Skip", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
skipText.anchor.set(0.5, 0.5);
skipButton.addChild(skipText);
skipButton.x = 0;
buttonContainer.addChild(skipButton);
var prevButton = new Container();
var prevBg = prevButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
prevBg.width = 200;
prevBg.height = 80;
prevBg.tint = 0x0066CC;
var prevText = new Text2("Back", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
prevText.anchor.set(0.5, 0.5);
prevButton.addChild(prevText);
prevButton.x = 250;
buttonContainer.addChild(prevButton);
self.createHighlight = function (target) {
if (self.highlightOverlay) {
self.highlightOverlay.parent.removeChild(self.highlightOverlay);
}
self.highlightOverlay = new Container();
var highlight = self.highlightOverlay.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
highlight.width = 300;
highlight.height = 300;
highlight.tint = 0xFFD700;
highlight.alpha = 0.3;
// Pulsing animation
tween(highlight, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.6
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.highlightOverlay.isDestroyed) {
tween(highlight, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.3
}, {
duration: 1000,
easing: tween.easeInOut
});
}
}
});
game.addChild(self.highlightOverlay);
return self.highlightOverlay;
};
self.removeHighlight = function () {
if (self.highlightOverlay && self.highlightOverlay.parent) {
tween.stop(self.highlightOverlay);
self.highlightOverlay.parent.removeChild(self.highlightOverlay);
self.highlightOverlay = null;
}
};
self.showStep = function (stepIndex) {
if (stepIndex >= self.tutorialSteps.length) {
self.endTutorial();
return;
}
var step = self.tutorialSteps[stepIndex];
var phaseDisplay = step.phase.charAt(0).toUpperCase() + step.phase.slice(1) + " Phase";
phaseText.setText(phaseDisplay);
titleText.setText(step.title);
bodyText.setText(step.text);
progressText.setText("Step " + (stepIndex + 1) + " of " + self.tutorialSteps.length);
// Update button states
nextButton.visible = !step.waitForAction;
skipButton.visible = true;
prevButton.visible = stepIndex > 0;
if (stepIndex === self.tutorialSteps.length - 1) {
nextText.setText("Finish!");
skipText.setText("Start!");
}
// Handle highlights
self.removeHighlight();
if (step.highlight) {
switch (step.highlight) {
case "ui":
// Highlight UI area
if (self.highlightOverlay) {
self.highlightOverlay.x = 2048 / 2;
self.highlightOverlay.y = 100;
}
break;
case "sourceTowers":
// Highlight tower selection area
if (sourceTowers.length > 0) {
var highlight = self.createHighlight();
highlight.x = sourceTowers[0].x;
highlight.y = sourceTowers[0].y;
}
break;
case "gold":
// Highlight gold display
var highlight = self.createHighlight();
highlight.x = 2048 / 2 - 400;
highlight.y = 100;
break;
case "heroPanel":
// Highlight hero panel
var highlight = self.createHighlight();
highlight.x = 2048 / 2;
highlight.y = 200;
break;
case "stationPanel":
// Highlight station panel
var highlight = self.createHighlight();
highlight.x = 2048 - 200;
highlight.y = 250;
break;
case "waveIndicator":
// Highlight wave indicator
var highlight = self.createHighlight();
highlight.x = 2048 / 2;
highlight.y = 2732 - 80;
break;
case "startButton":
// Highlight start game button
if (waveIndicator && waveIndicator.waveMarkers.length > 0) {
var highlight = self.createHighlight();
highlight.x = waveIndicator.waveMarkers[0].x + waveIndicator.x;
highlight.y = waveIndicator.waveMarkers[0].y + waveIndicator.y;
}
break;
}
}
// Set up action waiting
if (step.waitForAction) {
self.isWaitingForAction = true;
self.expectedAction = step.action;
}
};
self.startTutorial = function () {
self.isActive = true;
self.visible = true;
self.currentStep = 0;
self.showStep(0);
// Position tutorial in center of screen
tutorialContainer.x = 2048 / 2;
tutorialContainer.y = 1400;
};
self.nextStep = function () {
if (self.isWaitingForAction) return;
self.currentStep++;
self.showStep(self.currentStep);
};
self.prevStep = function () {
if (self.currentStep > 0) {
self.currentStep--;
self.showStep(self.currentStep);
}
};
self.endTutorial = function () {
self.isActive = false;
self.visible = false;
self.removeHighlight();
storage.tutorialCompleted = true;
self.tutorialCompleted = true;
// Auto-start the game after tutorial
if (waveIndicator && !waveIndicator.gameStarted) {
waveIndicator.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
}
var notification = game.addChild(new Notification("Tutorial completed! Good luck, Commander!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
};
self.checkActionCompleted = function (action) {
if (!self.isWaitingForAction || self.expectedAction !== action) return;
self.isWaitingForAction = false;
self.expectedAction = null;
// Show next button and auto-advance after delay
nextButton.visible = true;
LK.setTimeout(function () {
if (self.isActive && !self.isWaitingForAction) {
self.nextStep();
}
}, 1500);
};
nextButton.down = function () {
self.nextStep();
};
skipButton.down = function () {
self.endTutorial();
};
prevButton.down = function () {
self.prevStep();
};
self.visible = false;
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xE0E0E0,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' gold');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 0;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2;
startBlock.tint = 0x00AA00;
// Add shadow for start text
var startTextShadow = new Text2("Start Game", {
size: 50,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 4;
startTextShadow.y = 4;
startMarker.addChild(startTextShadow);
var startText = new Text2("Start Game", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startMarker.addChild(startText);
startMarker.x = -self.indicatorWidth;
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
tutorialSystem.checkActionCompleted('startGame');
}
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Play wave start sound
LK.getSound('waveStart').play();
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['normal', 'fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Flying";
block.tint = 0xFFFF00;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
waveType = "Boss Normal";
break;
case 'fast':
block.tint = 0x00AAFF;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0xAA0000;
waveType = "Boss Immune";
break;
case 'flying':
block.tint = 0xFFFF00;
waveType = "Boss Flying";
break;
}
}
enemyCount = 1;
// Make the wave indicator for boss waves stand out
// Set boss wave color to the color of the wave type
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
break;
case 'fast':
block.tint = 0x00AAFF;
break;
case 'immune':
block.tint = 0xAA0000;
break;
case 'flying':
block.tint = 0xFFFF00;
break;
default:
block.tint = 0xFF0000;
break;
}
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave && enemyType !== 'swarm') {
// Add a crown or some indicator to the wave marker for boss waves
var bossIndicator = marker.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
bossIndicator.width = 30;
bossIndicator.height = 30;
bossIndicator.tint = 0xFFD700; // Gold color
bossIndicator.y = -block.height / 2 - 15;
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 3;
waveTypeShadow.y = 3;
marker.addChild(waveTypeShadow);
// Add wave type text - 30% smaller than before
var waveTypeText = new Text2(waveType, {
size: 56,
fill: 0xFFFFFF,
weight: 800
});
waveTypeText.anchor.set(0.5, 0.5);
waveTypeText.y = 0;
marker.addChild(waveTypeText);
// Add shadow for wave number - 20% larger than before
var waveNumShadow = new Text2((i + 1).toString(), {
size: 48,
fill: 0x000000,
weight: 800
});
waveNumShadow.anchor.set(1.0, 1.0);
waveNumShadow.x = blockWidth / 2 - 16 + 5;
waveNumShadow.y = block.height / 2 - 12 + 5;
marker.addChild(waveNumShadow);
// Main wave number text - 20% larger than before
var waveNum = new Text2((i + 1).toString(), {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
waveNum.anchor.set(1.0, 1.0);
waveNum.x = blockWidth / 2 - 16;
waveNum.y = block.height / 2 - 12;
marker.addChild(waveNum);
marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
self.addChild(marker);
self.waveMarkers.push(marker);
}
// Get wave type for a specific wave number
self.getWaveType = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return "normal";
}
// If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType
// then we should return a different boss type
var waveType = self.waveTypes[waveNumber - 1];
return waveType;
};
// Get enemy count for a specific wave number
self.getEnemyCount = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return 10;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Add boss prefix for boss waves (every 10th wave)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
var Wormhole = Container.expand(function (x, y, targetX, targetY) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.targetX = targetX;
self.targetY = targetY;
self.radius = 90;
self.isDestroyed = false;
self.cooldown = 0;
var wormholeGraphics = self.attachAsset('wormhole', {
anchorX: 0.5,
anchorY: 0.5
});
wormholeGraphics.alpha = 0.7;
wormholeGraphics.blendMode = 1; // Additive blending
// Spinning animation
tween(wormholeGraphics, {
rotation: Math.PI * 2
}, {
duration: 3000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed) {
wormholeGraphics.rotation = 0;
}
}
});
// Create target wormhole visual
self.targetWormhole = new Container();
self.targetWormhole.x = targetX;
self.targetWormhole.y = targetY;
var targetGraphics = self.targetWormhole.attachAsset('wormhole', {
anchorX: 0.5,
anchorY: 0.5
});
targetGraphics.alpha = 0.5;
targetGraphics.tint = 0xFF1493; // Different color for exit
targetGraphics.blendMode = 1;
game.addChild(self.targetWormhole);
// Sync rotation with main wormhole
tween(targetGraphics, {
rotation: -Math.PI * 2
}, {
duration: 3000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed) {
targetGraphics.rotation = 0;
}
}
});
self.update = function () {
if (self.cooldown > 0) {
self.cooldown--;
return;
}
// Check for enemies within range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.isFlying) continue; // Flying enemies ignore wormholes
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
// Teleport enemy
self.teleportEnemy(enemy);
self.cooldown = 180; // 3 second cooldown
break;
}
}
};
self.teleportEnemy = function (enemy) {
// Create teleport effect at source
var sourceEffect = new Container();
sourceEffect.x = self.x;
sourceEffect.y = self.y;
var sourceGraphics = sourceEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
sourceGraphics.tint = 0x9400D3;
sourceGraphics.alpha = 0.8;
game.addChild(sourceEffect);
// Create teleport effect at destination
var destEffect = new Container();
destEffect.x = self.targetX;
destEffect.y = self.targetY;
var destGraphics = destEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
destGraphics.tint = 0xFF1493;
destGraphics.alpha = 0.8;
game.addChild(destEffect);
// Animate effects
tween(sourceEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (sourceEffect.parent) {
sourceEffect.parent.removeChild(sourceEffect);
}
}
});
tween(destEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (destEffect.parent) {
destEffect.parent.removeChild(destEffect);
}
}
});
// Teleport enemy
enemy.x = self.targetX;
enemy.y = self.targetY;
enemy.currentCellX = Math.floor((self.targetX - grid.x) / CELL_SIZE);
enemy.currentCellY = Math.floor((self.targetY - grid.y) / CELL_SIZE);
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = null; // Reset pathfinding
// Flash the wormhole
tween(wormholeGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 1.0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(wormholeGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeIn
});
}
});
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove target wormhole
if (self.targetWormhole && self.targetWormhole.parent) {
self.targetWormhole.parent.removeChild(self.targetWormhole);
}
var wormholeIndex = wormholes.indexOf(self);
if (wormholeIndex !== -1) {
wormholes.splice(wormholeIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000011
});
/****
* Game Code
****/
// Add background image
// Base tower structures for each type
// Tower gun/weapon parts - Level 1
// Tower gun/weapon parts - Level 2
// Tower gun/weapon parts - Level 3
// Tower gun/weapon parts - Level 4
// Tower gun/weapon parts - Level 5
// Tower gun/weapon parts - Level 6 (Max Level)
// Support structures and details
// Visual indicators
// Specialization visual elements
var backgroundImage = game.attachAsset('spaceBackground', {
anchorX: 0.5,
anchorY: 0.5
});
backgroundImage.x = 2048 / 2;
backgroundImage.y = 2732 / 2;
backgroundImage.alpha = 0.8;
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
// Object pooling for bullets
var bulletPool = [];
var maxBulletPoolSize = 100;
function getBulletFromPool(startX, startY, targetEnemy, damage, speed) {
var bullet;
if (bulletPool.length > 0) {
bullet = bulletPool.pop();
// Reset bullet properties
bullet.targetEnemy = targetEnemy;
bullet.damage = damage || 10;
bullet.speed = speed || 5;
bullet.x = startX;
bullet.y = startY;
bullet.isDestroyed = false;
bullet.visible = true;
bullet.alpha = 1;
if (bullet.children[0]) {
bullet.children[0].alpha = 1;
bullet.children[0].scaleX = 1;
bullet.children[0].scaleY = 1;
}
} else {
bullet = new Bullet(startX, startY, targetEnemy, damage, speed);
}
return bullet;
}
function returnBulletToPool(bullet) {
if (bulletPool.length < maxBulletPoolSize && bullet.parent) {
// Clean up target enemy reference properly
if (bullet.targetEnemy && bullet.targetEnemy.bulletsTargetingThis) {
var bulletIndex = bullet.targetEnemy.bulletsTargetingThis.indexOf(bullet);
if (bulletIndex !== -1) {
bullet.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
// Stop all tweens before pooling
tween.stop(bullet);
if (bullet.children && bullet.children[0]) {
tween.stop(bullet.children[0]);
}
bullet.parent.removeChild(bullet);
bullet.targetEnemy = null;
bullet.visible = false;
bulletPool.push(bullet);
} else {
bullet.destroy();
}
}
var defenses = [];
var selectedTower = null;
var gold = 60; // Reduced starting gold to make early choices more meaningful
var lives = 20;
var score = 0;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var sourceTower = null;
// Hero system variables
var heroes = [];
var maxHeroes = 3;
var heroDeploymentMode = false;
var heroPreview = null;
var selectedHeroType = 'tank';
// Station customization variables
var stationCustomizer = null;
var researchLabs = [];
var shieldGenerators = [];
var teleporterNodes = [];
// Environmental hazards variables
var asteroids = [];
var nebulaClouds = [];
var solarFlares = [];
var wormholes = [];
var hazardTimer = 0;
var nextHazardTime = 1800; // 30 seconds at 60fps
// Branching routes system variables
var pathJunctions = [];
var alternativePaths = [];
var routeChoiceInfluences = [];
var pathDifficulties = {};
// Tower specialization variables
var specializationMenus = [];
var towerLinks = [];
// Ultimate ability variables
var activeUltimates = [];
var ultimateEffects = [];
// Load station data from storage
var stationData = storage.stationData || {
modules: [],
researchPoints: 0,
completedTechs: []
};
// Initialize route visualizer
var routeVisualizer = new RouteVisualizer();
game.addChild(routeVisualizer);
// Initialize branching routes system
function initializeBranchingRoutes() {
// Create main junction points in the middle area
var mainJunction = new PathJunction(grid.x + 12 * CELL_SIZE, grid.y + 15 * CELL_SIZE, [{
targetX: 10,
targetY: 22,
// Left branch - shorter but more defended
isShortPath: true,
pathLength: 8,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 20
}, {
targetX: 14,
targetY: 22,
// Right branch - longer but less defended
isShortPath: false,
pathLength: 12,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 15
}]);
// Secondary junction for more complex routing
var secondaryJunction = new PathJunction(grid.x + 8 * CELL_SIZE, grid.y + 12 * CELL_SIZE, [{
targetX: 6,
targetY: 18,
// Far left route
isShortPath: false,
pathLength: 15,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 25
}, {
targetX: 12,
targetY: 15,
// Center route to main junction
isShortPath: true,
pathLength: 6,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 30
}]);
game.addChild(mainJunction);
game.addChild(secondaryJunction);
pathJunctions.push(mainJunction);
pathJunctions.push(secondaryJunction);
var notification = game.addChild(new Notification("🔀 Multi-path routing system activated! 🔀"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
// Initialize the branching system after a short delay
LK.setTimeout(function () {
initializeBranchingRoutes();
}, 2000);
function getModuleCost(moduleType, level) {
var baseCosts = {
research: 50,
shield: 75,
teleporter: 100
};
var baseCost = baseCosts[moduleType] || 50;
return Math.floor(baseCost * Math.pow(1.5, level - 1));
}
function getTechnology(techId) {
var technologies = {
'tower_damage': {
id: 'tower_damage',
name: 'Enhanced Weapons',
cost: 100,
onComplete: function onComplete() {
// Increase all tower damage by 20%
for (var i = 0; i < towers.length; i++) {
towers[i].damage = Math.floor(towers[i].damage * 1.2);
}
}
},
'tower_range': {
id: 'tower_range',
name: 'Extended Range',
cost: 150,
onComplete: function onComplete() {
// Increase all tower ranges
for (var i = 0; i < towers.length; i++) {
towers[i].refreshCellsInRange();
}
}
},
'shield_efficiency': {
id: 'shield_efficiency',
name: 'Shield Efficiency',
cost: 200,
onComplete: function onComplete() {
// Reduce shield energy costs
for (var i = 0; i < shieldGenerators.length; i++) {
shieldGenerators[i].maxShieldEnergy *= 1.5;
shieldGenerators[i].shieldEnergy = shieldGenerators[i].maxShieldEnergy;
}
}
}
};
return technologies[techId];
}
// Spatial partitioning for collision optimization
var spatialGrid = {
cellSize: CELL_SIZE * 2,
cells: {},
getKey: function getKey(x, y) {
var gridX = Math.floor(x / this.cellSize);
var gridY = Math.floor(y / this.cellSize);
return gridX + ',' + gridY;
},
addEnemy: function addEnemy(enemy) {
var key = this.getKey(enemy.x, enemy.y);
if (!this.cells[key]) this.cells[key] = [];
this.cells[key].push(enemy);
},
removeEnemy: function removeEnemy(enemy) {
var key = this.getKey(enemy.x, enemy.y);
if (this.cells[key]) {
var index = this.cells[key].indexOf(enemy);
if (index !== -1) {
this.cells[key].splice(index, 1);
}
}
},
getNearbyEnemies: function getNearbyEnemies(x, y, range) {
var nearby = [];
var checkRadius = Math.ceil(range / this.cellSize);
var centerX = Math.floor(x / this.cellSize);
var centerY = Math.floor(y / this.cellSize);
for (var dx = -checkRadius; dx <= checkRadius; dx++) {
for (var dy = -checkRadius; dy <= checkRadius; dy++) {
var key = centerX + dx + ',' + (centerY + dy);
if (this.cells[key]) {
nearby = nearby.concat(this.cells[key]);
}
}
}
return nearby;
},
clear: function clear() {
this.cells = {};
}
};
var enemiesToSpawn = 10; // Default number of enemies per wave
// Visual effects limiting
var activeEffects = [];
var maxActiveEffects = 20;
var effectSkipCounter = 0;
function canCreateEffect() {
// Count active effects and remove destroyed ones
activeEffects = activeEffects.filter(function (effect) {
return effect.parent && !effect.isDestroyed;
});
// Limit based on performance
if (activeEffects.length >= maxActiveEffects) {
effectSkipCounter++;
// Skip every other effect when at limit
return effectSkipCounter % 2 === 0;
}
return true;
}
function registerEffect(effect) {
if (activeEffects.length < maxActiveEffects) {
activeEffects.push(effect);
}
}
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(livesText);
LK.gui.top.addChild(scoreText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
}
function setGold(value) {
gold = value;
updateUI();
}
// Performance monitoring and adaptive logic
var performanceMonitor = {
frameTime: 16.67,
// Target 60 FPS
lastTime: 0,
avgFrameTime: 16.67,
frameCount: 0,
performanceLevel: 1.0,
// 1.0 = full performance, 0.5 = half performance
update: function update() {
var currentTime = Date.now();
if (this.lastTime > 0) {
var deltaTime = currentTime - this.lastTime;
this.avgFrameTime = this.avgFrameTime * 0.9 + deltaTime * 0.1;
// Adjust performance level based on frame time
if (this.avgFrameTime > 25) {
// Below 40 FPS
this.performanceLevel = Math.max(0.3, this.performanceLevel - 0.1);
} else if (this.avgFrameTime < 18) {
// Above 55 FPS
this.performanceLevel = Math.min(1.0, this.performanceLevel + 0.05);
}
}
this.lastTime = currentTime;
this.frameCount++;
},
shouldSkipUpdate: function shouldSkipUpdate() {
return Math.random() > this.performanceLevel;
}
};
// Memory monitoring system
var memoryMonitor = {
lastEnemyCount: 0,
lastBulletCount: 0,
lastTowerCount: 0,
lastEffectCount: 0,
update: function update() {
// Track object counts for memory leak detection
this.lastEnemyCount = enemies.length;
this.lastBulletCount = bullets.length;
this.lastTowerCount = towers.length;
this.lastEffectCount = activeEffects.length;
// Log warnings if counts are unexpectedly high
if (this.lastEnemyCount > 100) {
console.warn("High enemy count detected:", this.lastEnemyCount);
}
if (this.lastBulletCount > 200) {
console.warn("High bullet count detected:", this.lastBulletCount);
}
if (this.lastEffectCount > 50) {
console.warn("High effect count detected:", this.lastEffectCount);
}
},
forceCleanup: function forceCleanup() {
// Emergency cleanup when memory usage is too high
console.log("Performing emergency cleanup...");
// Clean up destroyed effects
activeEffects = activeEffects.filter(function (effect) {
return effect.parent && !effect.isDestroyed;
});
// Clean up orphaned bullets
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent || bullets[i].isDestroyed) {
bullets.splice(i, 1);
}
}
// Clean up bullet pool
bulletPool = bulletPool.filter(function (bullet) {
return !bullet.isDestroyed;
});
console.log("Cleanup complete. Objects remaining:", {
enemies: enemies.length,
bullets: bullets.length,
towers: towers.length,
effects: activeEffects.length,
pooledBullets: bulletPool.length
});
}
};
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
grid.pathFind();
grid.renderDebug();
debugLayer.addChild(grid);
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
var cells = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cells.push({
cell: cell,
originalType: cell.type
});
cell.type = 1;
}
}
}
var blocked = grid.pathFind();
for (var i = 0; i < cells.length; i++) {
cells[i].cell.type = cells[i].originalType;
}
grid.pathFind();
grid.renderDebug();
return blocked;
}
function getTowerCost(towerType) {
var cost = 8; // Increased base cost from 5
switch (towerType) {
case 'rapid':
cost = 12; // Reduced from 15 - cheaper early game tower
break;
case 'sniper':
cost = 20; // Reduced from 25 for better accessibility
break;
case 'splash':
cost = 28; // Reduced from 35
break;
case 'slow':
cost = 32; // Reduced from 45 - utility tower should be accessible
break;
case 'poison':
cost = 38; // Reduced from 55 - was too expensive
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
grid.pathFind();
grid.renderDebug();
// Play tower placement sound
LK.getSound('towerPlace').play();
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
if (towerType === 'default') {
tutorialSystem.checkActionCompleted('buildTower');
} else if (towerType === 'rapid') {
tutorialSystem.checkActionCompleted('buildRapidTower');
} else if (towerType === 'sniper') {
tutorialSystem.checkActionCompleted('buildSniperTower');
}
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
// Handle hero deployment
if (heroDeploymentMode) {
// Check if tap is in valid deployment area (not on towers or paths)
var deploymentValid = true;
var gridX = Math.floor((x - grid.x) / CELL_SIZE);
var gridY = Math.floor((y - grid.y) / CELL_SIZE);
// Check if position is within grid bounds and not on blocked areas
if (gridX < 2 || gridX >= 22 || gridY < 6 || gridY >= 25) {
deploymentValid = false;
} else {
var cell = grid.getCell(gridX, gridY);
if (!cell || cell.type !== 0) {
deploymentValid = false;
}
}
if (deploymentValid && heroes.length < maxHeroes) {
var hero = new Hero(selectedHeroType);
hero.x = x;
hero.y = y;
enemyLayerTop.addChild(hero); // Add heroes to top layer
heroes.push(hero);
updateHeroPanel();
heroDeploymentMode = false;
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
tutorialSystem.checkActionCompleted('deployHero');
}
var notification = game.addChild(new Notification(selectedHeroType.charAt(0).toUpperCase() + selectedHeroType.slice(1) + " hero deployed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
} else {
var notification = game.addChild(new Notification("Cannot deploy hero here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
heroDeploymentMode = false;
return;
}
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Improved touch detection with larger hit areas for mobile
var touchPadding = 40; // Extra padding for easier touch
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
var hitArea = {
left: tower.x - tower.width / 2 - touchPadding,
right: tower.x + tower.width / 2 + touchPadding,
top: tower.y - tower.height / 2 - touchPadding,
bottom: tower.y + tower.height / 2 + touchPadding
};
if (x >= hitArea.left && x <= hitArea.right && y >= hitArea.top && y <= hitArea.bottom) {
// Check if player can afford this tower
if (gold >= getTowerCost(tower.towerType)) {
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
// Improved drag offset for better mobile experience
var dragOffsetY = CELL_SIZE * 2; // Larger offset to keep preview visible above finger
towerPreview.snapToGrid(x, y - dragOffsetY);
// Add immediate visual feedback with scaling animation
tween(tower, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(tower, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
}
});
} else {
// Visual feedback for unaffordable towers
tween(tower, {
scaleX: 0.95,
scaleY: 0.95,
alpha: 0.7
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(tower, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 150,
easing: tween.easeIn
});
}
});
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
break;
}
}
};
game.move = function (x, y, obj) {
if (isDragging) {
// Shift the y position upward by 1.5 tiles to show preview above finger
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
}
};
game.up = function (x, y, obj) {
var clickedOnTower = false;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var towerLeft = tower.x - tower.width / 2;
var towerRight = tower.x + tower.width / 2;
var towerTop = tower.y - tower.height / 2;
var towerBottom = tower.y + tower.height / 2;
if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) {
clickedOnTower = true;
break;
}
}
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
var menuWidth = 2048;
var menuHeight = 450;
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
hideUpgradeMenu(menu);
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
grid.renderDebug();
}
}
if (isDragging) {
isDragging = false;
if (towerPreview.canPlace) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 80;
game.addChild(waveIndicator);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200;
nextWaveButton.y = 2732 - 100 + 20;
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
// Initialize station customizer
stationCustomizer = new StationCustomizer();
stationCustomizer.x = 0;
stationCustomizer.y = 0;
game.addChild(stationCustomizer);
// Create station customization panel
var stationPanel = new Container();
var stationPanelBg = stationPanel.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
stationPanelBg.width = 400;
stationPanelBg.height = 100;
stationPanelBg.tint = 0x2E7D32;
stationPanelBg.alpha = 0.9;
var stationTitleText = new Text2("Station", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
stationTitleText.anchor.set(0.5, 0.5);
stationTitleText.y = -15;
stationPanel.addChild(stationTitleText);
var customizeButton = new Container();
var customizeBg = customizeButton.attachAsset('heroAbilityIcon', {
anchorX: 0.5,
anchorY: 0.5
});
customizeBg.width = 80;
customizeBg.height = 40;
customizeBg.tint = 0x4CAF50;
customizeButton.y = 20;
var customizeText = new Text2("Customize", {
size: 30,
fill: 0xFFFFFF,
weight: 600
});
customizeText.anchor.set(0.5, 0.5);
customizeButton.addChild(customizeText);
customizeButton.down = function () {
stationCustomizer.toggleCustomizationMode();
};
stationPanel.addChild(customizeButton);
stationPanel.x = 2048 - 200; // Position on right side
stationPanel.y = 200; // Below the score display which is at y=50
game.addChild(stationPanel);
// Create hero deployment panel
var heroPanel = new Container();
var heroPanelBg = heroPanel.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
heroPanelBg.width = 800;
heroPanelBg.height = 120;
heroPanelBg.tint = 0x1A237E;
heroPanelBg.alpha = 0.9;
var heroTitleText = new Text2("Heroes (" + heroes.length + "/" + maxHeroes + ")", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
heroTitleText.anchor.set(0.5, 0.5);
heroTitleText.y = -30;
heroPanel.addChild(heroTitleText);
// Hero type buttons
var heroTypes = ['tank', 'support', 'dps'];
var heroTypeButtons = [];
for (var i = 0; i < heroTypes.length; i++) {
var heroButton = new Container();
var buttonBg = heroButton.attachAsset('heroAbilityIcon', {
anchorX: 0.5,
anchorY: 0.5
});
switch (heroTypes[i]) {
case 'tank':
buttonBg.tint = 0x4CAF50;
break;
case 'support':
buttonBg.tint = 0x2196F3;
break;
case 'dps':
buttonBg.tint = 0xFF5722;
break;
}
buttonBg.width = 80;
buttonBg.height = 80;
heroButton.heroType = heroTypes[i];
heroButton.x = -240 + i * 120;
heroButton.y = 10;
heroButton.down = function () {
if (heroes.length >= maxHeroes) {
var notification = game.addChild(new Notification("Maximum heroes deployed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
selectedHeroType = this.heroType;
heroDeploymentMode = true;
var notification = game.addChild(new Notification("Tap anywhere to deploy " + this.heroType + " hero"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
};
heroPanel.addChild(heroButton);
heroTypeButtons.push(heroButton);
}
heroPanel.x = 2048 / 2;
heroPanel.y = 120;
game.addChild(heroPanel);
function updateHeroPanel() {
heroTitleText.setText("Heroes (" + heroes.length + "/" + maxHeroes + ")");
}
enemiesToSpawn = 10;
// Initialize tutorial system for new players
var tutorialSystem = new TutorialSystem();
game.addChild(tutorialSystem);
// Start background music
LK.playMusic('bgMusic');
// Show tutorial automatically for new players
var hasSeenTutorial = storage.tutorialCompleted || false;
if (!hasSeenTutorial) {
tutorialSystem.startTutorial();
}
// Add tutorial restart button for experienced players
var tutorialRestartButton = new Container();
var restartBg = tutorialRestartButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
restartBg.width = 200;
restartBg.height = 60;
restartBg.tint = 0x4A90E2;
restartBg.alpha = 0.9;
var restartText = new Text2("Tutorial", {
size: 35,
fill: 0xFFFFFF,
weight: 600
});
restartText.anchor.set(0.5, 0.5);
tutorialRestartButton.addChild(restartText);
tutorialRestartButton.x = 150;
tutorialRestartButton.y = 2732 - 50;
tutorialRestartButton.down = function () {
if (!tutorialSystem.isActive) {
tutorialSystem.startTutorial();
}
};
game.addChild(tutorialRestartButton);
game.update = function () {
performanceMonitor.update();
// Clear and rebuild spatial grid each frame for enemies
spatialGrid.clear();
for (var i = 0; i < enemies.length; i++) {
spatialGrid.addEnemy(enemies[i]);
}
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
// Get wave type and enemy count from the wave indicator
var waveType = waveIndicator.getWaveType(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
// Check if this is a boss wave
var isBossWave = currentWave % 10 === 0 && currentWave > 0;
if (isBossWave && waveType !== 'swarm') {
// Boss waves have just 1 enemy regardless of what the wave indicator says
enemyCount = 1;
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
// Play boss spawn sound
LK.getSound('bossSpawn').play();
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy = new Enemy(waveType);
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Scale difficulty with wave number but don't apply to boss
// as bosses already have their health multiplier
// Use more balanced scaling that starts linear and becomes logarithmic
var healthMultiplier;
if (currentWave <= 10) {
// Linear scaling for early waves
healthMultiplier = 1 + (currentWave - 1) * 0.15; // 15% per wave for first 10 waves
} else if (currentWave <= 25) {
// Moderate scaling for mid waves
healthMultiplier = 2.35 + (currentWave - 10) * 0.08; // Start at 2.35x, add 8% per wave
} else {
// Logarithmic scaling for late waves to prevent exponential growth
healthMultiplier = 3.55 + Math.log(currentWave - 24) * 0.5; // Logarithmic growth
}
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// All enemy types now spawn in the middle 6 tiles at the top spacing
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
// Find a column that isn't occupied by another enemy that's not yet in view
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
// Check if any enemy is already in this column but not yet in view
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
// If all columns are occupied, use original random method
var spawnX;
if (availableColumns.length > 0) {
// Choose a random unoccupied column
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
// Fallback to random if all columns are occupied
spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
}
var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
}
}
// Update heroes
for (var i = heroes.length - 1; i >= 0; i--) {
var hero = heroes[i];
if (hero.health <= 0) {
// Hero defeated - remove from play but don't reset progression
var notification = game.addChild(new Notification(hero.heroType.charAt(0).toUpperCase() + hero.heroType.slice(1) + " hero defeated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
hero.destroy();
heroes.splice(i, 1);
updateHeroPanel();
continue;
}
hero.update();
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
// Skip some enemy updates when performance is poor
if (performanceMonitor.shouldSkipUpdate() && enemy.health > 0) {
continue;
}
// Handle hero targeting for enemies
if (enemy.heroTarget && enemy.heroTarget.parent && enemy.heroTarget.health > 0) {
if (enemy.heroTargetDuration > 0) {
enemy.heroTargetDuration--;
// Move towards hero instead of following path
var dx = enemy.heroTarget.x - enemy.x;
var dy = enemy.heroTarget.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > CELL_SIZE * 0.5) {
var angle = Math.atan2(dy, dx);
enemy.x += Math.cos(angle) * enemy.speed * 30;
enemy.y += Math.sin(angle) * enemy.speed * 30;
} else {
// Attack hero
enemy.heroTarget.health -= 10;
if (enemy.heroTarget.health <= 0) {
enemy.heroTarget.health = 0;
}
}
continue; // Skip normal pathfinding
} else {
enemy.heroTarget = null;
enemy.heroTargetDuration = 0;
}
} else {
enemy.heroTarget = null;
enemy.heroTargetDuration = 0;
}
if (enemy.health <= 0) {
// Improved gold rewards that scale better with difficulty
var goldEarned;
if (enemy.isBoss) {
// Boss gold scales more reasonably: 25 base + 3 per wave
goldEarned = Math.floor(25 + (enemy.waveNumber - 1) * 3);
} else {
// Regular enemy gold: better scaling for later waves
if (enemy.waveNumber <= 10) {
goldEarned = Math.floor(2 + (enemy.waveNumber - 1) * 0.3); // 2-4.7 gold for waves 1-10
} else if (enemy.waveNumber <= 25) {
goldEarned = Math.floor(4.7 + (enemy.waveNumber - 10) * 0.4); // 4.7-10.7 gold for waves 11-25
} else {
goldEarned = Math.floor(10.7 + (enemy.waveNumber - 25) * 0.2); // Slower growth for late waves
}
}
// Bonus gold for enemies who took difficult routes
if (enemy.chosenBranch && enemy.chosenBranch.difficulty > 50) {
var routeBonus = Math.floor(goldEarned * 0.3);
goldEarned += routeBonus;
if (routeBonus > 0) {
var notification = game.addChild(new Notification("Route bonus: +" + routeBonus + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
}
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Use proper destroy method for comprehensive cleanup
enemy.destroy();
continue;
}
if (grid.updateEnemy(enemy)) {
// Check if shields are active
var shieldsActive = false;
for (var s = 0; s < stationCustomizer.modules.length; s++) {
var module = stationCustomizer.modules[s];
if (module.moduleType === 'shield' && module.shieldActive) {
shieldsActive = true;
break;
}
}
if (shieldsActive) {
// Shields absorb damage
var notification = game.addChild(new Notification("Enemy blocked by shields!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
lives = Math.max(0, lives - 1);
updateUI();
if (lives <= 0) {
// Play game over sound
LK.getSound('gameOver').play();
LK.showGameOver();
}
}
// Use proper destroy method for comprehensive cleanup
enemy.destroy();
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
// Limit bullet count when performance drops
if (performanceMonitor.performanceLevel < 0.7 && bullets.length > 50) {
// Remove oldest bullets when performance is poor
var bulletsToRemove = Math.min(10, bullets.length - 30);
for (var i = 0; i < bulletsToRemove; i++) {
var oldBullet = bullets[i];
if (oldBullet.targetEnemy) {
var bulletIndex = oldBullet.targetEnemy.bulletsTargetingThis.indexOf(oldBullet);
if (bulletIndex !== -1) {
oldBullet.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
returnBulletToPool(oldBullet);
bullets.splice(i, 1);
i--; // Adjust index after removal
bulletsToRemove--;
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Update station customizer
if (stationCustomizer) {
stationCustomizer.update();
// Update module arrays for quick access
researchLabs = stationCustomizer.modules.filter(function (m) {
return m.moduleType === 'research';
});
shieldGenerators = stationCustomizer.modules.filter(function (m) {
return m.moduleType === 'shield';
});
teleporterNodes = stationCustomizer.modules.filter(function (m) {
return m.moduleType === 'teleporter';
});
// Link teleporter nodes
for (var i = 0; i < teleporterNodes.length; i++) {
teleporterNodes[i].linkedNodes = teleporterNodes.filter(function (node, index) {
return index !== i;
});
}
}
// Update tower specializations and support links
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.specialization && tower.specialization.update) {
tower.specialization.update();
}
}
// Clean up destroyed specialization menus
specializationMenus = specializationMenus.filter(function (menu) {
return menu.parent && !menu.isDestroyed;
});
// Environmental hazards management
if (waveIndicator && waveIndicator.gameStarted) {
hazardTimer++;
if (hazardTimer >= nextHazardTime) {
hazardTimer = 0;
nextHazardTime = 1200 + Math.random() * 1200; // 20-40 seconds
// Spawn random environmental hazard
var hazardType = Math.floor(Math.random() * 4);
switch (hazardType) {
case 0:
// Asteroid field
if (asteroids.length < 3) {
var validPositions = [];
for (var x = 2; x < 22; x++) {
for (var y = 6; y < 25; y++) {
var cell = grid.getCell(x, y);
if (cell && cell.type === 0) {
validPositions.push({
x: x,
y: y
});
}
}
}
if (validPositions.length > 0) {
var pos = validPositions[Math.floor(Math.random() * validPositions.length)];
var asteroid = new Asteroid(grid.x + pos.x * CELL_SIZE + CELL_SIZE / 2, grid.y + pos.y * CELL_SIZE + CELL_SIZE / 2);
game.addChild(asteroid);
asteroids.push(asteroid);
var notification = game.addChild(new Notification("⚠️ Asteroid field detected! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
break;
case 1:
// Nebula cloud
if (nebulaClouds.length < 2) {
var cloudX = grid.x + (5 + Math.random() * 14) * CELL_SIZE;
var cloudY = grid.y + (8 + Math.random() * 12) * CELL_SIZE;
var nebula = new NebulaCloud(cloudX, cloudY);
game.addChild(nebula);
nebulaClouds.push(nebula);
var notification = game.addChild(new Notification("🌌 Nebula cloud formation! 🌌"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Remove after 15 seconds
LK.setTimeout(function () {
if (nebula.parent) {
nebula.destroy();
}
}, 15000);
}
break;
case 2:
// Solar flare
if (solarFlares.length === 0) {
var flare = new SolarFlare();
game.addChild(flare);
solarFlares.push(flare);
}
break;
case 3:
// Wormhole
if (wormholes.length < 2) {
// Create entry and exit points
var entryX = grid.x + (3 + Math.random() * 6) * CELL_SIZE;
var entryY = grid.y + (8 + Math.random() * 8) * CELL_SIZE;
var exitX = grid.x + (15 + Math.random() * 6) * CELL_SIZE;
var exitY = grid.y + (16 + Math.random() * 6) * CELL_SIZE;
var wormhole = new Wormhole(entryX, entryY, exitX, exitY);
game.addChild(wormhole);
wormholes.push(wormhole);
var notification = game.addChild(new Notification("🌀 Wormhole opened! 🌀"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Remove after 20 seconds
LK.setTimeout(function () {
if (wormhole.parent) {
wormhole.destroy();
}
}, 20000);
}
break;
}
}
}
// Update path junctions and route analysis
for (var i = 0; i < pathJunctions.length; i++) {
if (pathJunctions[i].parent) {
pathJunctions[i].update();
}
}
// Update environmental hazards
for (var i = asteroids.length - 1; i >= 0; i--) {
if (asteroids[i].parent) {
// Asteroids can be damaged by bullets
for (var j = 0; j < bullets.length; j++) {
var bullet = bullets[j];
var dx = bullet.x - asteroids[i].x;
var dy = bullet.y - asteroids[i].y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 60) {
// Hit asteroid
asteroids[i].takeDamage(bullet.damage);
returnBulletToPool(bullet);
break;
}
}
}
}
for (var i = 0; i < nebulaClouds.length; i++) {
if (nebulaClouds[i].parent) {
nebulaClouds[i].update();
}
}
for (var i = 0; i < solarFlares.length; i++) {
if (solarFlares[i].parent) {
solarFlares[i].update();
}
}
for (var i = 0; i < wormholes.length; i++) {
if (wormholes[i].parent) {
wormholes[i].update();
}
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
// Play victory sound
LK.getSound('victory').play();
LK.showYouWin();
}
};
White circle with two eyes, seen from above.. In-Game asset. 2d. High contrast. No shadows
White simple circular enemy seen from above, black outline. Black eyes, with a single shield in-font of it. Black and white only. Blue background.
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
wormwhole for TD game towdown. In-Game asset. 2d. High contrast. No shadows
tank hero character space galaxy theme tower defense game. In-Game asset. 2d. High contrast. No shadows
support hero character space galaxy theme tower defense game. In-Game asset. 2d. High contrast. No shadows
poison tower space galaxy theme tower defense game, advanced base level tower. In-Game asset. 2d. High contrast. No shadows