User prompt
Change the name in the tutorial to Hangar Panic
User prompt
the basic turret glow isn't visible. make it bigger and pulsing ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add a glow below the basic turret at after the tutorial is hidden. The glow disappears as soon as the player places their first turret ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
make sure the tutorial lets people know they can drag a turret over top of another to auto-sell
User prompt
Please fix the bug: 'towerTypes is not defined' in or related to this line: 'tween(targetStar, {' Line Number: 3247
User prompt
Once the tutorial window is gone, add wiggle effect to basic turret every 3 seconds until first turret is placed. Also, change the name of the "poison" turret to "hack". Make sure all instances of poison are changed so nothing breaks ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Position left gradient fade at the left edge of the screen
User prompt
the left gradient should be at the left edge of the screen
User prompt
add a gradient fade out to the left and right edges of the timeline
User prompt
Upcoming waves in the wave timeline should be darker the furthur they are from being active ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
add a page that explains boost
User prompt
move the whole tutorial window and all its contents up by 150px
User prompt
add a button click sound when the tutorial buttons are pressed
Code edit (1 edits merged)
Please save this source code
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 6; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(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 === 'hack') { // Prevent hack effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual hack effect var hackEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'hack'); game.addChild(hackEffect); // Apply hack effect self.targetEnemy.hacked = true; self.targetEnemy.hackDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.hackDuration = 300; // 5 seconds at 60 FPS } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } else if (self.type === 'drain') { // Create visual drain effect var drainEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'drain'); game.addChild(drainEffect); // Apply splash damage to nearby enemies (same as splash but half damage) var drainRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var drainDx = otherEnemy.x - self.targetEnemy.x; var drainDy = otherEnemy.y - self.targetEnemy.y; var drainDistance = Math.sqrt(drainDx * drainDx + drainDy * drainDy); if (drainDistance <= drainRadius) { // Apply drain damage (50% of original damage, same as splash) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var City = Container.expand(function (spriteVariant) { var self = Container.call(this); self.spriteVariant = spriteVariant || 0; var cityGraphics = self.attachAsset('city', { anchorX: 0.5, anchorY: 0.5 }); // Make cities slightly smaller to fit nicely cityGraphics.scaleX = 0.9; cityGraphics.scaleY = 0.9; // Use different tints for different sprite variants var cityTints = [0x888888, 0x666666, 0x999999, 0x777777, 0xAAAAAA, 0x555555]; cityGraphics.tint = cityTints[self.spriteVariant % cityTints.length]; self.cityGraphics = cityGraphics; // Add glowing effect to forcefield tiles self.startGlowEffect = function () { function glowCycle() { // Glow brighter (more white tint) tween(self.cityGraphics, { tint: 0xCCCCFF }, { duration: 1500, easing: tween.easeInOut, onFinish: function onFinish() { // Return to original color tween(self.cityGraphics, { tint: cityTints[self.spriteVariant % cityTints.length] }, { duration: 1500, easing: tween.easeInOut, onFinish: function onFinish() { // Continue the glow cycle glowCycle(); } }); } }); } // Start the glow cycle glowCycle(); }; // Start glowing immediately self.startGlowEffect(); self.flashRed = function () { // Flash red when hit by enemy tween(self.cityGraphics, { tint: 0xFF0000 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self.cityGraphics, { tint: cityTints[self.spriteVariant % cityTints.length] }, { duration: 300, easing: tween.easeIn }); } }); }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('ground', { 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 = 0xFFFFFF; return; } numberLabel.visible = false; // Hide numbers for path cells too var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0xFFFFFF; } else { cellGraphics.tint = 0xFFFFFF; } // Remove all arrows - no longer showing direction indicators self.removeArrows(); break; } case 1: { // Check if this is a tower cell (has a tower on it) var hasTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (tower.gridX <= data.x && tower.gridX + 1 >= data.x && tower.gridY <= data.y && tower.gridY + 1 >= data.y) { hasTower = true; break; } } // Remove current cell graphics self.removeChild(cellGraphics); // Use grass under towers, wall graphics for outer walls if (hasTower) { cellGraphics = self.attachAsset('ground', { anchorX: 0.5, anchorY: 0.5 }); } else { cellGraphics = self.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); } cellGraphics.tint = 0xFFFFFF; numberLabel.visible = false; break; } case 3: { cellGraphics.tint = 0xFFFFFF; numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); numberLabel.visible = false; // Hide all tile numbers }; }); // 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; // Use different assets based on effect type var assetId = 'rangeCircle'; if (type === 'splash') { assetId = 'splash_explosion'; } var effectGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'hack': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'drain': effectGraphics.tint = 0x8800FF; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .012; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 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; // Add 5% speed variability to individual mobs (random multiplier between 0.95 and 1.05) var speedVariability = 0.95 + Math.random() * 0.1; self.speed = self.speed * speedVariability; // 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; 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 hacked, clear any such effects self.slowed = false; self.slowEffect = false; self.hacked = false; self.hackEffect = 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 hack effect if (self.hacked) { // Visual indication of hacked status if (!self.hackEffect) { self.hackEffect = true; } // Apply hack damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.hackDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.hackDuration--; if (self.hackDuration <= 0) { self.hacked = false; self.hackEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.hacked && self.slowed) { // Combine hack (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.hacked) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var EnergyBoostIndicator = Container.expand(function (value) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 120, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 4; shadowText.y = 4; self.addChild(shadowText); var energyText = new Text2("+" + value, { size: 120, fill: 0xFFD700, weight: 800 }); energyText.anchor.set(0.5, 0.5); self.addChild(energyText); self.x = 2048 / 2; self.y = 2732 / 2; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.5, scaleY: 1.5, y: 2732 / 2 - 100 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2, y: 2732 / 2 - 200 }, { duration: 800, easing: tween.easeIn, delay: 500, onFinish: function onFinish() { self.destroy(); } }); } }); 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; } 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); // Check for collision with cities instead of just checking cell type for (var c = 0; c < cities.length; c++) { var city = cities[c]; var dx = enemy.x - city.x; var dy = enemy.y - city.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is close enough to hit the city (collision detection) if (distance <= CELL_SIZE * 0.8) { // Flash the city red when hit city.flashRed(); // Play shield hit sound LK.getSound('shield_hit').play(); // Shake the shields text - only if not already shaking if (!livesText.isShaking) { livesText.isShaking = true; var originalX = livesText.x; var shakeAmount = 10; // Stop any existing shakes and color tweens tween.stop(livesText, { x: true, fill: true }); // Reset position before starting new shake livesText.x = originalX; // Change text color to red and start shaking tween(livesText, { fill: 0xFF0000 }, { duration: 0 }); // First shake to the right tween(livesText, { x: originalX + shakeAmount }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { // Then shake to the left tween(livesText, { x: originalX - shakeAmount }, { duration: 100, easing: tween.easeInOut, onFinish: function onFinish() { // Finally return to center and restore blue color tween(livesText, { x: originalX }, { duration: 50, easing: tween.easeIn, onFinish: function onFinish() { // Return text color to neon blue tween(livesText, { fill: 0x00FFFF }, { duration: 0, onFinish: function onFinish() { // Mark shake as complete livesText.isShaking = false; } }); } }); } }); } }); } // Shake the city that was hit tween.stop(city, { x: true }); var cityOriginalX = city.x; var cityShakeAmount = 8; tween(city, { x: cityOriginalX + cityShakeAmount }, { duration: 40, easing: tween.easeOut, onFinish: function onFinish() { tween(city, { x: cityOriginalX - cityShakeAmount }, { duration: 80, easing: tween.easeInOut, onFinish: function onFinish() { tween(city, { x: cityOriginalX }, { duration: 40, easing: tween.easeIn }); } }); } }); // Slightly smaller than full cell size for better collision return true; // Enemy hits city, dies and player loses life } } 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) { 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) { 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) { 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 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("↓ BOOST ↓", { size: 50, fill: 0x000000, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { // Check if all on-screen enemies are dead var allEnemiesDead = enemies.length === 0; var waveComplete = !waveInProgress; // Show boost button when all on-screen enemies are dead and game has started, but hide if shields are 0 if (waveIndicator && waveIndicator.gameStarted && allEnemiesDead && waveComplete && lives > 0) { self.enabled = true; self.visible = true; buttonBackground.tint = 0xFFD700; self.alpha = 1; buttonText.setText("↓ BOOST ↓"); } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; buttonText.setText("↓ BOOST ↓"); } }; self.down = function () { if (!self.enabled) { return; } // Give 10% energy boost, but never less than 2 var energyBoost = Math.max(2, Math.floor(energy * 0.1)); setEnergy(energy + energyBoost); // Display energy boost indicator in center of screen var energyBoostIndicator = new EnergyBoostIndicator(energyBoost); game.addChild(energyBoostIndicator); // Hide boost button after use self.visible = false; // Call the next wave immediately waveTimer = nextWaveTime; }; 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: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Increase size of base for easier touch var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); // Lock overlay will be handled externally to avoid alpha inheritance self.lockOverlay = null; // Reference will be set externally 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 'hack': baseGraphics.tint = 0x00FFAA; break; case 'drain': baseGraphics.tint = 0xFFD700; break; case 'basic': default: baseGraphics.tint = 0xAAAAAA; } var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var typeLabelShadow = new Text2(self.towerType.toUpperCase(), { size: 25, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -40 + 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(self.towerType.toUpperCase(), { size: 25, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -40; // 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 = 4; 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 = 0; self.addChild(costLabel); self.update = function () { // Check if tower type is unlocked based on current wave var towerTypes = ['basic', 'rapid', 'sniper', 'splash', 'slow', 'hack', 'drain']; var towerIndex = towerTypes.indexOf(self.towerType); var unlockWaves = [0, 2, 3, 4, 5, 6, 7]; // Wave requirements for each tower type var isUnlocked = towerIndex >= 0 && currentWave >= unlockWaves[towerIndex]; // Lock overlay visibility will be handled externally // Store unlock status for external reference self.isUnlocked = isUnlocked; // Check if player can afford this tower (only matters if unlocked) var canAfford = isUnlocked && energy >= getTowerCost(self.towerType); // Set opacity based on unlock status and affordability if (!isUnlocked) { self.alpha = 0.4; // Darker for locked towers } else { self.alpha = canAfford ? 1 : 0.5; // Normal affordability logic for unlocked towers } }; self.down = function (x, y, obj) { // Check if tower type is unlocked var towerTypes = ['basic', 'rapid', 'sniper', 'splash', 'slow', 'hack', 'drain']; var towerIndex = towerTypes.indexOf(self.towerType); var unlockWaves = [0, 2, 3, 4, 5, 6, 7]; // Wave requirements for each tower type var isUnlocked = towerIndex >= 0 && currentWave >= unlockWaves[towerIndex]; // Prevent interaction with locked towers if (!isUnlocked) { var requiredWave = unlockWaves[towerIndex]; var wavesUntilUnlock = requiredWave - currentWave; var notification = game.addChild(new Notification("Unlocks in " + wavesUntilUnlock + " wave" + (wavesUntilUnlock > 1 ? "s" : "") + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } // Only count clicks on drain tower before game starts if (self.towerType === 'drain' && waveIndicator && !waveIndicator.gameStarted) { drainTowerClickCount++; if (drainTowerClickCount >= 6 && !drainTowerCostReduced) { drainTowerCostReduced = true; var notification = game.addChild(new Notification("Drain Tower cost reduced to 5!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case 'sniper': // Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost if (self.level === self.maxLevel) { return 12 * CELL_SIZE; // Significantly increased range for max level } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case 'splash': // Splash: base 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 'hack': // Hack: base 3.2, +0.5 per level return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; case 'drain': // Drain: base 6, +0.6 per level (larger than sniper) return (6 + (self.level - 1) * 0.6) * CELL_SIZE; case 'basic': default: // Basic: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 6; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 18; // Adjusted for better DPS balance self.damage = 3.2; // Increased base damage self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 8.4; self.rapidFireSide = 0; // Track which side to fire from (0 = left, 1 = right) break; case 'sniper': self.fireRate = 90; self.damage = 25; self.range = 5 * CELL_SIZE; self.bulletSpeed = 30; break; case 'splash': self.fireRate = 75; self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4.8; break; case 'slow': self.fireRate = 50; self.damage = 8; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 6; break; case 'hack': self.fireRate = 70; self.damage = 12; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 6; break; case 'drain': self.fireRate = 150; self.damage = 1.875; // Quarter of original damage (7.5 / 4) self.range = 6 * CELL_SIZE; // Larger than sniper base range (5) self.bulletSpeed = 4.8; break; } var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); // Store the base tint color for later use with turret var baseTintColor; switch (self.id) { case 'rapid': baseTintColor = 0x00AAFF; baseGraphics.tint = baseTintColor; break; case 'sniper': baseTintColor = 0xFF5500; baseGraphics.tint = baseTintColor; break; case 'splash': baseTintColor = 0x33CC00; baseGraphics.tint = baseTintColor; break; case 'slow': baseTintColor = 0x9900FF; baseGraphics.tint = baseTintColor; break; case 'hack': baseTintColor = 0x00FFAA; baseGraphics.tint = baseTintColor; break; case 'drain': baseTintColor = 0xFFD700; baseGraphics.tint = baseTintColor; break; case 'basic': default: baseTintColor = 0xAAAAAA; baseGraphics.tint = baseTintColor; } 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); } var gunContainer = new Container(); self.addChild(gunContainer); // Use tower-specific arrow asset based on tower type var arrowAssetId = 'arrow'; if (self.id !== 'basic') { arrowAssetId = 'arrow_' + self.id; } var gunGraphics = gunContainer.attachAsset(arrowAssetId, { anchorX: self.id === 'drain' ? 0.5 : 0.35, anchorY: 0.5 }); // Turret remains default color (no tinting applied) 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 'hack': towerLevelIndicator.tint = 0x00FFAA; break; case 'drain': towerLevelIndicator.tint = 0xFFD700; break; case 'basic': 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; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (energy >= upgradeCost) { setEnergy(energy - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rapid') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(3, 18 - self.level * 4.2); // Adjusted for better DPS scaling self.damage = 3.2 + self.level * 4.8; // Increased damage scaling self.bulletSpeed = 8.4 + self.level * 2.88; // double the effect } else { self.fireRate = Math.max(8, 18 - self.level * 1.4); // Slightly slower fire rate reduction self.damage = 3.2 + self.level * 1.8; // Increased damage scaling self.bulletSpeed = 8.4 + self.level * 0.84; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 6 + self.level * 2.88; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 6 + self.level * 0.6; } } self.refreshCellsInRange(); self.updateLevelIndicators(); // Play upgrade sound effect LK.getSound('tower_upgrade').play(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough energy to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; 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); // Check if enemy is in range if (distance <= self.getRange()) { // 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 () { if (self.id === 'drain') { // Drain tower doesn't need to target specific enemies, it affects all in range var enemiesInRange = false; 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.getRange()) { enemiesInRange = true; break; } } if (enemiesInRange && LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } else { 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; if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } } }; self.down = function (x, y, obj) { // Check if clicking on the same tower that's already selected if (selectedTower === self && singleUpgradeButton && singleUpgradeButton.visible) { // Hide UI and deselect hideUpgradeButtons(); selectedTower = null; grid.renderDebug(); return; } // Hide any existing UI hideUpgradeButtons(); // Remove any existing range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; // Create range indicator var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; // Insert range indicator after debugLayer but before towerLayer var debugLayerIndex = game.getChildIndex(debugLayer); game.addChildAt(rangeIndicator, debugLayerIndex + 1); 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.15; // Tint range marker to match tower type color switch (self.id) { case 'rapid': rangeGraphics.tint = 0x00AAFF; break; case 'sniper': rangeGraphics.tint = 0xFF5500; break; case 'splash': rangeGraphics.tint = 0x33CC00; break; case 'slow': rangeGraphics.tint = 0x9900FF; break; case 'hack': rangeGraphics.tint = 0x00FFAA; break; case 'drain': rangeGraphics.tint = 0xFFD700; break; case 'basic': default: rangeGraphics.tint = 0xAAAAAA; } // Create single stats window if it doesn't exist if (!singleStatsWindow) { singleStatsWindow = new TowerStatsWindow(self); game.addChild(singleStatsWindow); } else { singleStatsWindow.tower = self; singleStatsWindow.updateStats(); } // Check if turret is in bottom left area (bottom 1/3 and left 1/3 of grid) var gridHeight = grid.cells[0].length; var gridWidth = grid.cells.length; var isInBottomThird = self.gridY >= gridHeight * 2 / 3; var isInLeftThird = self.gridX <= gridWidth / 3; if (isInBottomThird && isInLeftThird) { // Position stats window on the right side of the grid, moved 380px left singleStatsWindow.x = grid.x + gridWidth * CELL_SIZE + 50 - 380; singleStatsWindow.y = grid.y + grid.cells[0].length * CELL_SIZE - 100 - 300 - 20 + 5; } else { // Position stats window at bottom left of play grid (original position) singleStatsWindow.x = grid.x + 200 + 100 - 15 - 20; singleStatsWindow.y = grid.y + grid.cells[0].length * CELL_SIZE - 100 - 300 - 20 + 5; } singleStatsWindow.visible = true; // Create single sell button if it doesn't exist if (!singleSellButton) { singleSellButton = new Container(); game.addChild(singleSellButton); var sellButtonBg = singleSellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBg.width = 180; sellButtonBg.height = 80; sellButtonBg.tint = 0xCC0000; // Add sell icon var sellIcon = singleSellButton.attachAsset('defense', { anchorX: 0.5, anchorY: 0.5 }); sellIcon.width = 30; sellIcon.height = 30; sellIcon.x = -35; sellIcon.y = 0; sellIcon.tint = 0xFFFFFF; var sellButtonText = new Text2('', { size: 32, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButtonText.x = 10; singleSellButton.addChild(sellButtonText); singleSellButton.down = function (x, y, obj) { if (!selectedTower) { return; } var totalInvestment = selectedTower.getTotalValue ? selectedTower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setEnergy(energy + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " energy!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = selectedTower.gridX; var gridY = selectedTower.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(selectedTower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } var towerIndex = towers.indexOf(selectedTower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(selectedTower); grid.pathFind(); grid.renderDebug(); hideUpgradeButtons(); selectedTower = null; }; } // Create single upgrade button if it doesn't exist if (!singleUpgradeButton) { singleUpgradeButton = new Container(); game.addChild(singleUpgradeButton); var upgradeButtonBg = singleUpgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); upgradeButtonBg.width = 180; upgradeButtonBg.height = 80; // Add upgrade icon var upgradeIcon = singleUpgradeButton.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); upgradeIcon.width = 30; upgradeIcon.height = 30; upgradeIcon.x = -35; upgradeIcon.y = 0; upgradeIcon.tint = 0xFFFFFF; var upgradeButtonText = new Text2('', { size: 32, fill: 0xFFFFFF, weight: 800 }); upgradeButtonText.anchor.set(0.5, 0.5); upgradeButtonText.x = 10; singleUpgradeButton.addChild(upgradeButtonText); singleUpgradeButton.down = function (x, y, obj) { if (!selectedTower) { return; } if (selectedTower.level >= selectedTower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (selectedTower.upgrade()) { // Update UI after upgrade updateSingleUI(); // Update stats window if it exists if (singleStatsWindow && singleStatsWindow.visible) { singleStatsWindow.updateStats(); } } }; } // Update and position UI elements updateSingleUI(); function updateSingleUI() { if (!selectedTower) { return; } // Update sell button var totalInvestment = selectedTower.getTotalValue ? selectedTower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); singleSellButton.children[2].setText(sellValue.toString()); singleSellButton.x = selectedTower.x - 120; singleSellButton.y = selectedTower.y + 120; // Update upgrade button var isMaxLevel = selectedTower.level >= selectedTower.maxLevel; var baseUpgradeCost = getTowerCost(selectedTower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (selectedTower.level === selectedTower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, selectedTower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, selectedTower.level - 1)); } singleUpgradeButton.children[0].tint = isMaxLevel ? 0x888888 : energy >= upgradeCost ? 0x00AA00 : 0x888888; singleUpgradeButton.children[2].setText(isMaxLevel ? 'MAX' : upgradeCost.toString()); singleUpgradeButton.x = selectedTower.x + 120; singleUpgradeButton.y = selectedTower.y + 120; // Update range circle if it exists var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === selectedTower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = selectedTower.getRange() * 2; rangeGraphics.alpha = 0.15; // Update tint to match tower type color switch (selectedTower.id) { case 'rapid': rangeGraphics.tint = 0x00AAFF; break; case 'sniper': rangeGraphics.tint = 0xFF5500; break; case 'splash': rangeGraphics.tint = 0x33CC00; break; case 'slow': rangeGraphics.tint = 0x9900FF; break; case 'poison': rangeGraphics.tint = 0x00FFAA; break; case 'drain': rangeGraphics.tint = 0xFFD700; break; case 'basic': default: rangeGraphics.tint = 0xAAAAAA; } } } updateSingleUI(); // Show UI elements singleSellButton.visible = true; singleUpgradeButton.visible = true; }; 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.id === 'drain') { // Drain tower uses pulse effect instead of bullets var currentRange = self.getRange(); // Create pulse effect var pulseEffect = new Container(); pulseEffect.x = self.x; pulseEffect.y = self.y; // Add pulse effect after debugLayer (ground) but before towerLayer and enemyLayer var debugLayerIndex = game.getChildIndex(debugLayer); game.addChildAt(pulseEffect, debugLayerIndex + 1); var pulseGraphics = pulseEffect.attachAsset('drain_pulse', { anchorX: 0.5, anchorY: 0.5 }); pulseGraphics.width = pulseGraphics.height = currentRange * 2; pulseGraphics.alpha = 0; pulseGraphics.scaleX = 0.3; pulseGraphics.scaleY = 0.3; // Add random rotation to the pulse pulseGraphics.rotation = Math.random() * Math.PI * 2; // Animate pulse expanding slower and fade out before full size tween(pulseGraphics, { alpha: 0.3, scaleX: 0.9, scaleY: 0.9 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { tween(pulseGraphics, { alpha: 0.05, scaleX: 1.2, scaleY: 1.2 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { pulseEffect.destroy(); } }); } }); // Apply damage to all enemies in range 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 <= currentRange) { // Apply damage enemy.health -= self.damage; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } // Create visual drain effect on each enemy hit var drainEffect = new EffectIndicator(enemy.x, enemy.y, 'drain'); game.addChild(drainEffect); } } // Play drain tower shooting sound LK.getSound('drain_shoot').play(); // Tower pulse effect - for drain tower, scale the turret instead of base var elementToScale = self.id === 'drain' ? gunContainer : baseGraphics; tween.stop(elementToScale, { scaleX: true, scaleY: true }); tween(elementToScale, { scaleX: 1.1, scaleY: 1.1 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(elementToScale, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeIn }); } }); } else 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; // Add alternating offset for rapid fire towers if (self.id === 'rapid') { var offsetDistance = 15; // Distance from center var perpendicularAngle = gunContainer.rotation + Math.PI / 2; // 90 degrees from gun direction var offsetMultiplier = self.rapidFireSide === 0 ? -1 : 1; // Left or right bulletX += Math.cos(perpendicularAngle) * offsetDistance * offsetMultiplier; bulletY += Math.sin(perpendicularAngle) * offsetDistance * offsetMultiplier; // Toggle side for next shot self.rapidFireSide = 1 - self.rapidFireSide; } var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet rotation to match turret rotation bullet.rotation = gunContainer.rotation; // 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 'hack': 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 turret shooting sound LK.getSound('turret_shoot').play(); // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; 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(); }; 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.15; // Update range marker tint to match tower type color and reduce visibility switch (self.towerType) { case 'rapid': rangeGraphics.tint = 0x00AAFF; break; case 'sniper': rangeGraphics.tint = 0xFF5500; break; case 'splash': rangeGraphics.tint = 0x33CC00; break; case 'slow': rangeGraphics.tint = 0x9900FF; break; case 'poison': rangeGraphics.tint = 0x00FFAA; break; case 'drain': rangeGraphics.tint = 0xFFD700; break; case 'basic': default: rangeGraphics.tint = 0xAAAAAA; } 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 = energy >= getTowerCost(self.towerType); // Check for tower replacement opportunity self.replacementTower = null; for (var i = 0; i < towers.length; i++) { var existingTower = towers[i]; // Check if the preview overlaps exactly with an existing tower if (existingTower.gridX === self.gridX && existingTower.gridY === self.gridY) { self.replacementTower = existingTower; break; } } // 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; // Update range marker tint to match tower type color switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; rangeGraphics.tint = 0x00AAFF; break; case 'sniper': previewGraphics.tint = 0xFF5500; rangeGraphics.tint = 0xFF5500; break; case 'splash': previewGraphics.tint = 0x33CC00; rangeGraphics.tint = 0x33CC00; break; case 'slow': previewGraphics.tint = 0x9900FF; rangeGraphics.tint = 0x9900FF; break; case 'hack': previewGraphics.tint = 0x00FFAA; rangeGraphics.tint = 0x00FFAA; break; case 'drain': previewGraphics.tint = 0xFFD700; rangeGraphics.tint = 0xFFD700; break; case 'basic': default: previewGraphics.tint = 0xAAAAAA; rangeGraphics.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 { // Check if we're replacing an existing tower var isReplacement = self.replacementTower !== null; if (isReplacement) { // For replacement, we can place on any tower location validGridPlacement = true; } else { // For new placement, check if cells are free 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 = energy >= 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 TowerStatsWindow = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; var windowBackground = self.attachAsset('info_panel', { anchorX: 0.5, anchorY: 0.5 }); var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1), { size: 40, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0.5, 0); towerTypeText.x = 0; towerTypeText.y = -90; self.addChild(towerTypeText); // Left column stats (LVL and DMG) var leftStatsText = new Text2('LVL: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDMG: ' + self.tower.damage, { size: 32, fill: 0xFFFFFF, weight: 400 }); leftStatsText.anchor.set(0.5, 0.5); leftStatsText.x = -120; leftStatsText.y = 15; self.addChild(leftStatsText); // Right column stats (SPD and RNG) var rightStatsText = new Text2('SPD: ' + (60 / self.tower.fireRate).toFixed(1) + '/s\nRNG: ' + (self.tower.getRange() / CELL_SIZE).toFixed(1), { size: 32, fill: 0xFFFFFF, weight: 400 }); rightStatsText.anchor.set(0.5, 0.5); rightStatsText.x = 120; rightStatsText.y = 15; self.addChild(rightStatsText); // Update stats when tower is upgraded self.updateStats = function () { leftStatsText.setText('LVL: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDMG: ' + self.tower.damage); rightStatsText.setText('SPD: ' + (60 / self.tower.fireRate).toFixed(1) + '/s\nRNG: ' + (self.tower.getRange() / CELL_SIZE).toFixed(1)); towerTypeText.setText(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1)); }; return self; }); var TutorialOverlay = Container.expand(function () { var self = Container.call(this); self.currentPanel = 0; self.totalPanels = 5; self.isVisible = true; // Semi-transparent dark background var backdrop = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); backdrop.width = 4096; backdrop.height = 5464; backdrop.tint = 0x000000; backdrop.alpha = 0.8; // Main tutorial panel var panel = self.attachAsset('info_panel', { anchorX: 0.5, anchorY: 0.5 }); panel.width = 1600; panel.height = 1200; panel.x = 0; panel.y = 0; // Title text var titleText = new Text2('Hangar Panic', { size: 60, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0); titleText.x = 0; titleText.y = -280; self.addChild(titleText); // Main content text var contentText = new Text2('', { size: 40, fill: 0xFFFFFF, weight: 400 }); contentText.anchor.set(0.5, 0.5); contentText.x = 0; contentText.y = 40; self.addChild(contentText); // Navigation buttons positioned at bottom right, side by side var prevButton = new Container(); var prevSprite = prevButton.attachAsset('tutorial_button', { anchorX: 0.5, anchorY: 0.5 }); prevSprite.width = 260; prevSprite.height = 120; prevSprite.tint = 0x0081f9; var prevText = new Text2('< <', { size: 32, fill: 0xFFFFFF, weight: 800 }); prevText.anchor.set(0.5, 0.5); prevButton.addChild(prevText); prevButton.x = 0; prevButton.y = 480; self.addChild(prevButton); // Next button positioned beside prev button var nextButton = new Container(); var nextSprite = nextButton.attachAsset('tutorial_button', { anchorX: 0.5, anchorY: 0.5 }); nextSprite.width = 260; nextSprite.height = 120; nextSprite.tint = 0x0081f9; var nextText = new Text2('> >', { size: 32, fill: 0xFFFFFF, weight: 800 }); nextText.anchor.set(0.5, 0.5); nextButton.addChild(nextText); nextButton.x = 270; nextButton.y = 480; self.addChild(nextButton); // Skip button with different sprite at bottom left of panel var skipButton = new Container(); var skipSprite = skipButton.attachAsset('tutorial_button', { anchorX: 0.5, anchorY: 0.5 }); skipSprite.width = 260; skipSprite.height = 120; skipSprite.tint = 0xFF4444; var skipText = new Text2('SKIP', { size: 32, fill: 0xFFFFFF, weight: 800 }); skipText.anchor.set(0.5, 0.5); skipButton.addChild(skipText); skipButton.x = -266; skipButton.y = 480; self.addChild(skipButton); // Tutorial content for each panel var tutorialContent = ["Enemy drones are invading!\n\nDrag turrets from the bottom to build them.\nDifferent turrets unlock as waves progress.\n\nTurrets cost ENERGY to build and upgrade.", "Enemies follow the path from top to bottom.\nThey want to reach your shield wall.\n\nIf enemies reach the shield, you lose SHIELDS.\nGame over when all shields are gone!", "Turret Types:\n• BASIC - Balanced damage and range\n• RAPID - Fast firing, short range\n• SNIPER - Long range, high damage\n• SPLASH - Area damage\n• SLOW - Slows enemies\n• HACK - Damage over time\n• DRAIN - Energy bonus when enemies die", "ENERGY BOOST\n\nThe BOOST button appears between waves!\n\nClick it to instantly gain 10% energy\n(minimum 2 energy bonus)\n\nUse it to build that crucial turret\nor upgrade before the next wave hits!", "Click START GAME when ready!\n\nClick turrets to upgrade them.\nDrag turrets over existing ones to auto-sell!\nEarn energy by defeating enemies.\nSurvive all 50 waves to win!\n\nGood luck, Commander!"]; self.updatePanel = function () { contentText.setText(tutorialContent[self.currentPanel]); // Update button states prevSprite.tint = self.currentPanel > 0 ? 0x0081f9 : 0x666666; if (self.currentPanel === self.totalPanels - 1) { nextText.setText('DONE'); nextSprite.tint = 0x00FF00; } else { nextText.setText('> >'); nextSprite.tint = 0x0081f9; } }; // Button handlers prevButton.down = function () { LK.getSound('turret_shoot').play(); if (self.currentPanel > 0) { self.currentPanel--; self.updatePanel(); } }; nextButton.down = function () { LK.getSound('turret_shoot').play(); if (self.currentPanel < self.totalPanels - 1) { self.currentPanel++; self.updatePanel(); } else { // Last panel - close tutorial self.closeTutorial(); } }; skipButton.down = function () { LK.getSound('turret_shoot').play(); self.closeTutorial(); }; self.closeTutorial = function () { if (!self.isVisible) { return; } self.isVisible = false; tween(self, { alpha: 0 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; // Initialize first panel self.updatePanel(); return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; self.zIndex = 10000; // Ensure upgrade menu appears above everything var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : energy >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' energy', { 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 + ' energy', { 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 + ' energy'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' energy'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } // Update stats window if it exists var statsWindows = game.children.filter(function (child) { return child instanceof TowerStatsWindow && child.tower === self.tower; }); for (var i = 0; i < statsWindows.length; i++) { statsWindows[i].updateStats(); } 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.15; // Tint range marker to match tower type color switch (self.tower.id) { case 'rapid': rangeGraphics.tint = 0x00AAFF; break; case 'sniper': rangeGraphics.tint = 0xFF5500; break; case 'splash': rangeGraphics.tint = 0x33CC00; break; case 'slow': rangeGraphics.tint = 0x9900FF; break; case 'poison': rangeGraphics.tint = 0x00FFAA; break; case 'drain': rangeGraphics.tint = 0xFFD700; break; default: rangeGraphics.tint = 0xAAAAAA; } } 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); setEnergy(energy + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " energy!")); 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 = energy >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' energy'; 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 * 0.7; 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) { // Close tutorial when start game is clicked if (tutorialOverlay && tutorialOverlay.isVisible) { tutorialOverlay.closeTutorial(); } 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; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; 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 * 0.7; // --- 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 = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10 + (waveNumber - totalWaves) * 5; } return self.enemyCounts[waveNumber - 1] + Math.max(0, (waveNumber - 5) * 2); }; // 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 * 0.7; 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 * 0.7; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146 * 0.7; 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 * 0.7; 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; // Play wave start sound LK.getSound('wave_start').play(); 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; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x0a0a1a, title: 'Hangar Defender' }); /**** * Game Code ****/ // Create outer space background elements var spaceBackground = new Container(); game.addChild(spaceBackground); // Create multiple layers of stars for depth for (var starLayer = 0; starLayer < 3; starLayer++) { var numStars = starLayer === 0 ? 150 : starLayer === 1 ? 100 : 50; var starSize = starLayer === 0 ? 2 : starLayer === 1 ? 3 : 4; var starAlpha = starLayer === 0 ? 0.6 : starLayer === 1 ? 0.8 : 1.0; for (var i = 0; i < numStars; i++) { var star = spaceBackground.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); star.width = starSize; star.height = starSize; star.x = Math.random() * 2048; star.y = Math.random() * 2732; star.tint = Math.random() > 0.7 ? 0xffffaa : 0xffffff; star.alpha = starAlpha; // Add twinkling effect to some stars if (Math.random() > 0.8) { var twinkleDelay = Math.random() * 3000; LK.setTimeout(function (targetStar) { return function () { function twinkle() { tween(targetStar, { alpha: targetStar.alpha * 0.3 }, { duration: 800 + Math.random() * 400, easing: tween.easeInOut, onFinish: function onFinish() { tween(targetStar, { alpha: starAlpha }, { duration: 800 + Math.random() * 400, easing: tween.easeInOut, onFinish: function onFinish() { // Schedule next twinkle LK.setTimeout(twinkle, 2000 + Math.random() * 4000); } }); } }); } twinkle(); }; }(star), twinkleDelay); } } } // Create nebula effects using large, semi-transparent colored circles for (var i = 0; i < 8; i++) { var nebula = spaceBackground.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); nebula.width = 400 + Math.random() * 600; nebula.height = nebula.width; nebula.x = Math.random() * 2400 - 200; // Allow some off-screen positioning nebula.y = Math.random() * 3000 - 200; // Random nebula colors (purples, blues, magentas) var nebulaColors = [0x4a0e4e, 0x2e1065, 0x1a237e, 0x4a148c, 0x6a1b9a, 0x311b92]; nebula.tint = nebulaColors[Math.floor(Math.random() * nebulaColors.length)]; nebula.alpha = 0.1 + Math.random() * 0.15; // Slow rotation for nebula tween(nebula, { rotation: Math.PI * 2 }, { duration: 60000 + Math.random() * 120000, easing: tween.linear, loop: true }); } var towerTypes = ['basic', 'rapid', 'sniper', 'splash', 'slow', 'hack', 'drain']; var isHidingUpgradeMenu = false; var drainTowerClickCount = 0; var drainTowerCostReduced = false; // Single reusable UI elements var singleUpgradeButton = null; var singleSellButton = null; var singleStatsWindow = null; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; // Also clean up any stats windows for the same tower var statsWindows = game.children.filter(function (child) { return child instanceof TowerStatsWindow && child.tower === menu.tower; }); for (var i = 0; i < statsWindows.length; i++) { statsWindows[i].destroy(); } tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { // Remove from GUI layer or game layer if (menu.parent) { menu.parent.removeChild(menu); } menu.destroy(); isHidingUpgradeMenu = false; } }); } function hideUpgradeButtons() { // Hide single UI elements if (singleUpgradeButton) { singleUpgradeButton.visible = false; } if (singleSellButton) { singleSellButton.visible = false; } if (singleStatsWindow) { singleStatsWindow.visible = false; } // Remove range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var cities = []; var selectedTower = null; var energy = 800; 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 * 0.8 * 0.8; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave var energyText = new Text2('ENERGY: ' + energy, { size: 45, fill: 0xFFD700, weight: 800 }); energyText.anchor.set(0.5, 0.5); var livesText = new Text2('SHIELDS: ' + lives, { size: 45, fill: 0x00FFFF, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('SCORE: ' + score, { size: 45, fill: 0x00FF00, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var bottomMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.bottom.addChild(energyText); LK.gui.bottom.addChild(livesText); LK.gui.bottom.addChild(scoreText); livesText.x = 0; livesText.y = -bottomMargin - 165 - 150; energyText.x = -spacing; energyText.y = -bottomMargin - 165 - 150; scoreText.x = spacing; scoreText.y = -bottomMargin - 165 - 150; function updateUI() { energyText.setText('ENERGY: ' + energy); livesText.setText('SHIELDS: ' + lives); scoreText.setText('SCORE: ' + score); } function setEnergy(value) { energy = value; updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = -250; // Grid positioned at the very top of the screen grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); // Create city sprites on exit tiles var cityVariantCounter = 0; for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var cell = grid.cells[i][j]; if (cell.type === 3) { // Exit tiles var city = new City(cityVariantCounter); city.x = grid.x + i * CELL_SIZE; city.y = grid.y + j * CELL_SIZE - 240; debugLayer.addChild(city); cities.push(city); cityVariantCounter++; } } } game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); // Add tutorial overlay var tutorialOverlay = new TutorialOverlay(); tutorialOverlay.x = 2048 / 2; tutorialOverlay.y = 2732 / 2 - 150; game.addChild(tutorialOverlay); 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 = 50; switch (towerType) { case 'rapid': cost = 150; break; case 'sniper': cost = 250; break; case 'splash': cost = 350; break; case 'slow': cost = 450; break; case 'hack': cost = 550; break; case 'drain': cost = drainTowerCostReduced ? 50 : 1000; 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 (energy >= towerCost) { var tower = new Tower(towerType || 'basic'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setEnergy(energy - towerCost); grid.pathFind(); grid.renderDebug(); // Play tower-specific build sound var soundId = 'build_' + (towerType || 'basic'); if (soundId === 'build_basic') { soundId = 'build_default'; } LK.getSound(soundId).play(); return true; } else { var notification = game.addChild(new Notification("Not enough energy!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { // Check if any upgrade UI is visible var upgradeUIVisible = singleUpgradeButton && singleUpgradeButton.visible || singleSellButton && singleSellButton.visible || singleStatsWindow && singleStatsWindow.visible; if (upgradeUIVisible) { return; } for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { // Check if tower type is unlocked before allowing drag var towerIndex = towerTypes.indexOf(tower.towerType); var unlockWaves = [0, 2, 3, 4, 5, 6, 7]; // Wave requirements for each tower type var isUnlocked = towerIndex >= 0 && currentWave >= unlockWaves[towerIndex]; if (!isUnlocked) { // Don't allow dragging locked towers break; } // Close tutorial when dragging starts if (tutorialOverlay && tutorialOverlay.isVisible) { tutorialOverlay.closeTutorial(); } towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); // Apply the same offset as in move handler to ensure consistency when starting drag towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); break; } } }; game.move = function (x, y, obj) { if (isDragging) { // Shift the y position upward by 1.5 tiles to show preview above finger towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } }; game.up = function (x, y, obj) { var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } // Check if we have any upgrade UI elements visible var hasUpgradeUI = singleUpgradeButton && singleUpgradeButton.visible || singleSellButton && singleSellButton.visible || singleStatsWindow && singleStatsWindow.visible; var clickedOnUI = false; // Check if clicked on single stats window if (singleStatsWindow && singleStatsWindow.visible) { var windowWidth = 400; var windowHeight = 200; var windowLeft = singleStatsWindow.x - windowWidth / 2; var windowRight = singleStatsWindow.x + windowWidth / 2; var windowTop = singleStatsWindow.y - windowHeight / 2; var windowBottom = singleStatsWindow.y + windowHeight / 2; if (x >= windowLeft && x <= windowRight && y >= windowTop && y <= windowBottom) { clickedOnUI = true; } } // Check if clicked on single upgrade button if (singleUpgradeButton && singleUpgradeButton.visible) { var buttonWidth = 180; var buttonHeight = 80; var buttonLeft = singleUpgradeButton.x - buttonWidth / 2; var buttonRight = singleUpgradeButton.x + buttonWidth / 2; var buttonTop = singleUpgradeButton.y - buttonHeight / 2; var buttonBottom = singleUpgradeButton.y + buttonHeight / 2; if (x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom) { clickedOnUI = true; } } // Check if clicked on single sell button if (singleSellButton && singleSellButton.visible) { var buttonWidth = 180; var buttonHeight = 80; var buttonLeft = singleSellButton.x - buttonWidth / 2; var buttonRight = singleSellButton.x + buttonWidth / 2; var buttonTop = singleSellButton.y - buttonHeight / 2; var buttonBottom = singleSellButton.y + buttonHeight / 2; if (x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom) { clickedOnUI = true; } } // Check if clicked on source towers var clickedOnSourceTower = false; for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { clickedOnSourceTower = true; break; } } // Hide upgrade buttons when clicking on a blank spot (not on towers, UI, source towers, or while dragging) if (hasUpgradeUI && !isDragging && !clickedOnTower && !clickedOnUI && !clickedOnSourceTower) { hideUpgradeButtons(); selectedTower = null; grid.renderDebug(); } if (isDragging) { isDragging = false; if (towerPreview.canPlace) { // Check if we're replacing an existing tower if (towerPreview.replacementTower) { // Calculate sell value for existing tower var existingTower = towerPreview.replacementTower; var totalInvestment = existingTower.getTotalValue ? existingTower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var newTowerCost = getTowerCost(towerPreview.towerType); var netCost = newTowerCost - sellValue; if (energy >= netCost) { // Remove existing tower from arrays and layers var towerIndex = towers.indexOf(existingTower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(existingTower); // Clear cells occupied by old tower for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(existingTower.gridX + i, existingTower.gridY + j); if (cell) { cell.type = 0; var oldTowerIndex = cell.towersInRange.indexOf(existingTower); if (oldTowerIndex !== -1) { cell.towersInRange.splice(oldTowerIndex, 1); } } } } // Place new tower var newTower = new Tower(towerPreview.towerType); newTower.placeOnGrid(towerPreview.gridX, towerPreview.gridY); towerLayer.addChild(newTower); towers.push(newTower); setEnergy(energy - netCost); // Show notification about replacement var notification = game.addChild(new Notification("Tower replaced! Net cost: " + netCost + " energy")); notification.x = 2048 / 2; notification.y = grid.height - 50; grid.pathFind(); grid.renderDebug(); } else { var notification = game.addChild(new Notification("Not enough energy for replacement!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else { // Normal placement logic 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(); } } } }; // Create background strip for wave display var waveBackgroundStrip = new Container(); var waveBackground = waveBackgroundStrip.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); waveBackground.width = 2048; // Full screen width waveBackground.height = 140; // Height to accommodate wave display waveBackground.tint = 0x222222; // Dark background waveBackground.alpha = 0.8; // Semi-transparent waveBackgroundStrip.x = 2048 / 2; waveBackgroundStrip.y = 2732 - 160; game.addChild(waveBackgroundStrip); var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2; waveIndicator.y = 2732 - 160; game.addChild(waveIndicator); // Add gradient fade overlays for timeline edges var timelineContainer = new Container(); timelineContainer.x = 2048 / 2; timelineContainer.y = 2732 - 160; // Left gradient fade var leftGradient = new Container(); var leftFadeWidth = 200; for (var i = 0; i < leftFadeWidth; i++) { var fadeStrip = leftGradient.attachAsset('towerLevelIndicator', { anchorX: 0, anchorY: 0.5 }); fadeStrip.width = 1; fadeStrip.height = 140; fadeStrip.x = i - 2048 / 2; fadeStrip.tint = 0x0a0a1a; // Match game background color fadeStrip.alpha = 1 - i / leftFadeWidth; // Fade from opaque to transparent } timelineContainer.addChild(leftGradient); // Right gradient fade var rightGradient = new Container(); var rightFadeWidth = 200; for (var i = 0; i < rightFadeWidth; i++) { var fadeStrip = rightGradient.attachAsset('towerLevelIndicator', { anchorX: 0, anchorY: 0.5 }); fadeStrip.width = 1; fadeStrip.height = 140; fadeStrip.x = 2048 / 2 - rightFadeWidth + i; fadeStrip.tint = 0x0a0a1a; // Match game background color fadeStrip.alpha = i / rightFadeWidth; // Fade from transparent to opaque } timelineContainer.addChild(rightGradient); game.addChild(timelineContainer); var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); // Position boost button over the enemy spawn zone (top-center of grid) nextWaveButton.x = grid.x + 12 * CELL_SIZE - 40; // Center of spawn columns (9-14), moved left 40px nextWaveButton.y = grid.y + 2 * CELL_SIZE + 200 - 10 - 15; // Near top of spawn area, moved down 200px, moved up 10px, moved up 15px nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); var restartButton = new Container(); var restartBackground = restartButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); restartBackground.width = 300; restartBackground.height = 100; restartBackground.tint = 0xFF4444; var restartText = new Text2("Restart", { size: 50, fill: 0xFFFFFF, weight: 800 }); restartText.anchor.set(0.5, 0.5); restartButton.addChild(restartText); restartButton.x = 2048 / 2; restartButton.y = 2732 / 2; restartButton.visible = false; restartButton.down = function () { // Reset all game state currentWave = 0; waveTimer = 0; waveInProgress = false; waveSpawned = false; energy = 800; lives = 20; score = 0; drainTowerClickCount = 0; drainTowerCostReduced = false; // Clear all arrays enemies = []; towers = []; bullets = []; cities = []; // Clear all layers enemyLayerBottom.removeChildren(); enemyLayerMiddle.removeChildren(); enemyLayerTop.removeChildren(); towerLayer.removeChildren(); // Clean up existing lock overlays for (var i = 0; i < sourceTowerLocks.length; i++) { if (sourceTowerLocks[i].parent) { game.removeChild(sourceTowerLocks[i]); } } sourceTowerLocks = []; // Re-add source towers for (var i = 0; i < sourceTowers.length; i++) { towerLayer.addChild(sourceTowers[i]); // Recreate lock overlays for source towers var tower = sourceTowers[i]; var lockOverlay = new Container(); var lockBackground = lockOverlay.attachAsset('lock', { anchorX: 0.5, anchorY: 0.5 }); lockBackground.tint = 0x000000; lockBackground.alpha = 0.8; lockBackground.scaleX = 1.1; lockBackground.scaleY = 1.1; var lockIcon = lockOverlay.attachAsset('lock_icon', { anchorX: 0.5, anchorY: 0.5 }); lockIcon.width = 192; lockIcon.height = 192; lockIcon.tint = 0xFFFFFF; lockIcon.alpha = 1.0; lockIcon.y = -10; lockOverlay.x = tower.x; lockOverlay.y = tower.y; game.addChild(lockOverlay); sourceTowerLocks.push(lockOverlay); tower.lockOverlay = lockOverlay; } // Reset grid for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var cell = grid.cells[i][j]; // Reset to original cell type var cellType = i === 0 || i === grid.cells.length - 1 || j <= 4 || j >= grid.cells[i].length - 4 ? 1 : 0; if (i > 11 - 3 && i <= 11 + 3) { if (j === 0) { cellType = 2; } else if (j <= 4) { cellType = 0; } else if (j === grid.cells[i].length - 1) { cellType = 3; } else if (j >= grid.cells[i].length - 4) { cellType = 0; } } cell.type = cellType; cell.towersInRange = []; } } // Reset wave indicator waveIndicator.gameStarted = false; waveIndicator.waveMarkers[0].children[0].tint = 0x00AA00; waveIndicator.waveMarkers[0].children[1].setText("Start Game"); waveIndicator.waveMarkers[0].children[2].setText("Start Game"); // Reset wave marker visibility for (var i = 1; i < waveIndicator.waveMarkers.length; i++) { waveIndicator.waveMarkers[i].children[0].alpha = 1; } // Clear any upgrade menus from game layer and GUI layer var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }).concat(LK.gui.children.filter(function (child) { return child instanceof UpgradeMenu; })); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } // Clear any stats windows var statsWindows = game.children.filter(function (child) { return child instanceof TowerStatsWindow; }); for (var i = 0; i < statsWindows.length; i++) { statsWindows[i].destroy(); } // Clear range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; pathId = 1; maxScore = 0; // Recreate city sprites on exit tiles var cityVariantCounter = 0; for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var cell = grid.cells[i][j]; if (cell.type === 3) { // Exit tiles var city = new City(cityVariantCounter); city.x = grid.x + i * CELL_SIZE; city.y = grid.y + j * CELL_SIZE - 240; debugLayer.addChild(city); cities.push(city); cityVariantCounter++; } } } // Recalculate paths and update display grid.pathFind(); grid.renderDebug(); updateUI(); // Unpause the game LK.resume(); }; // Show restart button when game is paused LK.on('pause', function () { restartButton.visible = true; }); // Hide restart button when game is resumed LK.on('resume', function () { restartButton.visible = false; }); game.addChild(restartButton); // Start background music LK.playMusic('background_music'); var unlockWaves = [0, 2, 3, 4, 5, 6, 7]; // Wave requirements for each tower type var sourceTowers = []; var sourceTowerLocks = []; // Container for lock overlays separate from turrets var towerSpacing = 240; // Spacing between tower types var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 2 - 180 - 180 + 50 + 60 + 40; var _loop = function _loop() { tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); // Create separate lock overlay that won't be affected by turret alpha var lockOverlay = new Container(); var lockBackground = lockOverlay.attachAsset('lock', { anchorX: 0.5, anchorY: 0.5 }); lockBackground.tint = 0x000000; lockBackground.alpha = 0.8; lockBackground.scaleX = 1.1; lockBackground.scaleY = 1.1; var lockIcon = lockOverlay.attachAsset('lock_icon', { anchorX: 0.5, anchorY: 0.5 }); lockIcon.width = 192; lockIcon.height = 192; lockIcon.tint = 0xFFFFFF; lockIcon.alpha = 1.0; // Ensure lock icon is fully visible lockIcon.y = -10; // Position slightly above center for better visibility // Position lock overlay to match tower position lockOverlay.x = tower.x; lockOverlay.y = tower.y; // Add lock overlay to game (higher level than turret layer) game.addChild(lockOverlay); sourceTowerLocks.push(lockOverlay); // Set reference in tower for easier management tower.lockOverlay = lockOverlay; // Add glowing shadow effect for each source tower shadowGlow = tower.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); shadowGlow.width = shadowGlow.height = CELL_SIZE * 3; shadowGlow.alpha = 0.2; shadowGlow.blendMode = 1; // Additive blend for glow effect // Set glow color based on tower type switch (tower.towerType) { case 'rapid': shadowGlow.tint = 0x00AAFF; break; case 'sniper': shadowGlow.tint = 0xFF5500; break; case 'splash': shadowGlow.tint = 0x33CC00; break; case 'slow': shadowGlow.tint = 0x9900FF; break; case 'poison': shadowGlow.tint = 0x00FFAA; break; case 'drain': shadowGlow.tint = 0xFFD700; break; case 'basic': default: shadowGlow.tint = 0xAAAAAA; } // Insert shadow behind the tower (at index 0) tower.addChildAt(shadowGlow, 0); // Add special bright glow for basic turret that shows after tutorial is hidden if (tower.towerType === 'basic') { // Create a bright glow below the basic turret var basicGlow = tower.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); basicGlow.width = basicGlow.height = CELL_SIZE * 6; // Made bigger (was 4) basicGlow.y = CELL_SIZE * 0.5; // Position below the turret basicGlow.alpha = 0; basicGlow.tint = 0xFFFFFF; // Bright white glow basicGlow.blendMode = 1; // Additive blend for glow effect // Insert glow behind the tower (at index 0, before shadowGlow) tower.addChildAt(basicGlow, 0); // Store reference for later control tower.basicGlow = basicGlow; } // Add pulsing glow animation function createGlowCycle(glowElement) { function glowPulse() { tween(glowElement, { alpha: 0.4, scaleX: 1.2, scaleY: 1.2 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { tween(glowElement, { alpha: 0.2, scaleX: 1.0, scaleY: 1.0 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { glowPulse(); } }); } }); } glowPulse(); } // Start the glow cycle with a random delay to stagger the effects randomDelay = Math.random() * 2000; LK.setTimeout(function () { createGlowCycle(shadowGlow); }, randomDelay); }, tower, lockOverlay, shadowGlow, randomDelay; for (var i = 0; i < towerTypes.length; i++) { _loop(); } sourceTower = null; enemiesToSpawn = 10; game.update = function () { 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; } // 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 exponential scaling for health var healthMultiplier = Math.pow(1.25, currentWave); // ~20% increase per wave 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() * 2; // Spawn closer to top, within visible area 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; } } var _loop2 = function _loop2() { enemy = enemies[a]; if (enemy.health <= 0) { // Play enemy death sound LK.getSound('enemy_death').play(); for (i = 0; i < enemy.bulletsTargetingThis.length; i++) { bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Boss enemies give more gold and score baseEnergyEarned = enemy.isBoss ? Math.floor(500 + (enemy.waveNumber - 1) * 50) : Math.floor(10 + (enemy.waveNumber - 1) * 5); // Check for drain towers in range and apply energy bonus drainBonusEnergy = 0; for (t = 0; t < towers.length; t++) { tower = towers[t]; if (tower.id === 'drain') { dx = enemy.x - tower.x; dy = enemy.y - tower.y; distance = Math.sqrt(dx * dx + dy * dy); if (distance <= tower.getRange()) { // 5% energy bonus per level of drain tower drainBonusEnergy += Math.floor(baseEnergyEarned * 0.05 * tower.level); } } } totalEnergyEarned = baseEnergyEarned + drainBonusEnergy; energyIndicator = new GoldIndicator(totalEnergyEarned, enemy.x, enemy.y); game.addChild(energyIndicator); setEnergy(energy + totalEnergyEarned); // Give more score for defeating a boss scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { notification = game.addChild(new Notification("Boss defeated! +" + totalEnergyEarned + " energy!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); return 1; // continue } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { var _explodeNextTurret = function explodeNextTurret() { if (explosionIndex >= turretsToExplode.length) { // All turrets exploded, wait 1.5 seconds before showing game over LK.setTimeout(function () { LK.showGameOver(); }, 1500); return; } var turret = turretsToExplode[explosionIndex]; explosionIndex++; // Create explosion effect at turret location var explosion = new Container(); explosion.x = turret.x; explosion.y = turret.y; // Add explosion to a high layer so it's visible above everything var debugLayerIndex = game.getChildIndex(debugLayer); game.addChildAt(explosion, debugLayerIndex + 3); var explosionGraphics = explosion.attachAsset('explosion_sprite', { anchorX: 0.5, anchorY: 0.5 }); explosionGraphics.tint = 0xFF4400; // Orange/red explosion explosionGraphics.width = explosionGraphics.height = 150; // Consistent size for turret explosions explosionGraphics.alpha = 1; // Start fully visible explosion.scaleX = 0.8; // Start larger explosion.scaleY = 0.8; // Start larger // Hide the turret that just exploded turret.visible = false; // Play explosion sound LK.getSound('explosion_sound').play(); // Animate explosion appearance tween(explosion, { scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(explosion, { alpha: 0, scaleX: 2.5, scaleY: 2.5 }, { duration: 800, easing: tween.easeIn, onFinish: function onFinish() { explosion.destroy(); } }); } }); // Schedule next explosion with random interval (0.05 - 0.2 seconds) var nextDelay = 50 + Math.random() * 150; // 50-200ms in milliseconds LK.setTimeout(_explodeNextTurret, nextDelay); }; // Start the explosion sequence // Hide all shield/city sprites for (c = 0; c < cities.length; c++) { cities[c].visible = false; } // Create a list of all turrets sorted by row (bottom to top) turretsToExplode = []; for (t = 0; t < towers.length; t++) { turretsToExplode.push(towers[t]); } // Sort turrets by gridY (row) in descending order (bottom to top) turretsToExplode.sort(function (a, b) { if (a.gridY !== b.gridY) { return b.gridY - a.gridY; // Higher gridY first (bottom rows) } return a.gridX - b.gridX; // Then by gridX for consistent ordering within same row }); // Explode turrets one by one with random intervals explosionIndex = 0; _explodeNextTurret(); } } }, enemy, i, bullet, baseEnergyEarned, drainBonusEnergy, t, tower, dx, dy, distance, totalEnergyEarned, energyIndicator, scoreValue, notification, c, turretsToExplode, t, explosionIndex; for (var a = enemies.length - 1; a >= 0; a--) { if (_loop2()) { continue; } } 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); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } // Process wiggle logic BEFORE visibility changes to prevent conflicts for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; // Use exact same logic as SourceTower.down() method for "unlocks in 1 wave" text var towerIndex = towerTypes.indexOf(tower.towerType); var requiredWave = unlockWaves[towerIndex]; var wavesUntilUnlock = requiredWave - currentWave; var shouldWiggle = wavesUntilUnlock === 1; // Add wiggle effect for basic turret until first turret is placed var shouldWiggleBasic = tower.towerType === 'basic' && !tutorialOverlay && waveIndicator && waveIndicator.gameStarted && towers.length === 0; // Also wiggle basic turret if tutorial window is gone if (tower.towerType === 'basic' && towers.length === 0) { var tutorialExists = false; for (var j = 0; j < game.children.length; j++) { if (game.children[j] instanceof TutorialOverlay && game.children[j].isVisible) { tutorialExists = true; break; } } shouldWiggleBasic = shouldWiggleBasic || !tutorialExists && waveIndicator && waveIndicator.gameStarted; } // Add wiggle effect for the next tower to unlock - independent of visibility if ((shouldWiggle || shouldWiggleBasic) && tower.lockOverlay) { // Initialize wiggle timer if not exists if (!tower.lockOverlay.wiggleTimer) { tower.lockOverlay.wiggleTimer = 0; } tower.lockOverlay.wiggleTimer++; // Wiggle every 3 seconds (180 frames at 60 FPS) if (tower.lockOverlay.wiggleTimer >= 180) { tower.lockOverlay.wiggleTimer = 0; // Use IIFE to capture the specific tower instance for proper closure (function (currentTower) { // Stop any existing wiggle animation tween.stop(currentTower.lockOverlay, { rotation: true }); // Create gentle wiggle animation var wiggleAmount = 0.1; // Small rotation amount in radians tween(currentTower.lockOverlay, { rotation: wiggleAmount }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(currentTower.lockOverlay, { rotation: -wiggleAmount }, { duration: 200, easing: tween.easeInOut, onFinish: function onFinish() { tween(currentTower.lockOverlay, { rotation: wiggleAmount * 0.5 }, { duration: 150, easing: tween.easeInOut, onFinish: function onFinish() { tween(currentTower.lockOverlay, { rotation: 0 }, { duration: 100, easing: tween.easeIn }); } }); } }); } }); })(tower); } } else { // Reset wiggle timer for towers not about to unlock if (tower.lockOverlay && tower.lockOverlay.wiggleTimer) { tower.lockOverlay.wiggleTimer = 0; } } } // Update lock overlay visibility for source towers AFTER wiggle logic for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (tower.lockOverlay && tower.isUnlocked !== undefined) { tower.lockOverlay.visible = !tower.isUnlocked; } } // Handle basic turret glow visibility for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (tower.towerType === 'basic' && tower.basicGlow) { // Check if tutorial is hidden and no towers are placed var tutorialExists = false; for (var j = 0; j < game.children.length; j++) { if (game.children[j] instanceof TutorialOverlay && game.children[j].isVisible) { tutorialExists = true; break; } } // Show glow when tutorial is hidden and no towers are placed var shouldShowGlow = !tutorialExists && towers.length === 0 && waveIndicator && waveIndicator.gameStarted; if (shouldShowGlow && tower.basicGlow.alpha === 0) { // Start the bright glow animation with higher visibility tween(tower.basicGlow, { alpha: 0.8, scaleX: 1.2, scaleY: 1.2 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { // Create pulsing glow effect with stronger contrast function basicGlowPulse() { tween(tower.basicGlow, { alpha: 1.0, scaleX: 1.6, scaleY: 1.6 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(tower.basicGlow, { alpha: 0.4, scaleX: 1.0, scaleY: 1.0 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { if (towers.length === 0) { basicGlowPulse(); } } }); } }); } basicGlowPulse(); } }); } else if (!shouldShowGlow && tower.basicGlow.alpha > 0) { // Hide the glow when tutorial appears or first tower is placed tween.stop(tower.basicGlow, { alpha: true, scaleX: true, scaleY: true }); tween(tower.basicGlow, { alpha: 0, scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeOut }); } } } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
===================================================================
--- original.js
+++ change.js
@@ -2410,9 +2410,9 @@
panel.height = 1200;
panel.x = 0;
panel.y = 0;
// Title text
- var titleText = new Text2('HANGAR DEFENDER', {
+ var titleText = new Text2('Hangar Panic', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
soft grass texture, repeating, tiled, top down. In-Game asset. 2d. High contrast. No shadows
steel platform, circular, sci-fi, metal, locking feet on outside, top-down. In-Game asset. 2d. High contrast. No shadows
top down image of a combat drone with a white light on top, pixel art. In-Game asset. 2d. High contrast. No shadows
top down twin barrel sci-fi rapid machine gun cannon. pixel art
pixel art top down insectoid combat drone, blue light on top. In-Game asset. 2d. High contrast. No shadows
Flying pixel art flying drone, top down, insectoid design, yellow light on top. In-Game asset. 2d. High contrast. No shadows
top down sci-fi cannon. pixel art
sci-fi pixel art UI panel. glass with a steel border.. In-Game asset. 2d. High contrast. No shadows
top down sci-fi sniper cannon, red metal, sleek, pixel art
top down sci-fi fat barrel cannon, teal metal, pixel art
top down sci-fi fat barrel cannon with ridges, purple metal, pixel art
top down pixel art of an aggressive war drone with blue wasp stripes with a blue light on top. In-Game asset. 2d. High contrast. No shadows
pixel art sci-fi bullet. In-Game asset. 2d. High contrast. No shadows
top down pixel art view of a rectangular forcefield. In-Game asset. 2d. High contrast. No shadows
acid splash puddle, pixel art, top down, top view In-Game asset. 2d. High contrast. No shadows
top down pixel art, energy crystal, yellow, spiky, charged, held on all sides by sci fi metal clamps In-Game asset. 2d. High contrast. No shadows
crackling circular energy field, pixel art, top down, filled with yellow electricity. In-Game asset. 2d. High contrast. No shadows
sci-fi metallic lock, pixel art. In-Game asset. 2d. High contrast. No shadows
digital pixel art explosion. In-Game asset. 2d. High contrast. No shadows
pixel art range circle, white border, filled with black In-Game asset. 2d. High contrast. No shadows
high tech sci fi pixel art button. greyscale. metallic. no text In-Game asset. 2d. High contrast. No shadows
build_default
Sound effect
build_rapid
Sound effect
build_sniper
Sound effect
build_splash
Sound effect
build_slow
Sound effect
build_poison
Sound effect
build_drain
Sound effect
tower_upgrade
Sound effect
wave_start
Sound effect
turret_shoot
Sound effect
enemy_death
Sound effect
drain_shoot
Sound effect
shield_hit
Sound effect
background_music
Music
explosion_sound
Sound effect
build_hack
Sound effect