/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed, bulletType) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; self.type = bulletType || 'default'; // Use tower-specific bullet asset var assetName = 'bullet_' + self.type; var bulletGraphics = self.attachAsset(assetName, { 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 var actualDamage = self.damage; // Apply damage multiplier for transitioned shielded enemies if (self.targetEnemy.damageMultiplier) { actualDamage *= self.targetEnemy.damageMultiplier; } self.targetEnemy.health -= actualDamage; 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); // Create red area effect that lasts longer var redAreaEffect = new Container(); var redArea = redAreaEffect.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); redArea.width = redArea.height = CELL_SIZE * 3; redArea.tint = 0xFF0000; // Red color redArea.alpha = 0.4; redAreaEffect.x = self.targetEnemy.x; redAreaEffect.y = self.targetEnemy.y; game.addChild(redAreaEffect); // Animate the red area effect with tween tween(redAreaEffect, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { redAreaEffect.destroy(); } }); // 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) var splashDamage = self.damage * 0.5; // Apply damage multiplier for transitioned shielded enemies if (otherEnemy.damageMultiplier) { splashDamage *= otherEnemy.damageMultiplier; } otherEnemy.health -= splashDamage; 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 === 'poison') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual poison effect var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Apply poison effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS // Create poison area on ground var poisonArea = new PoisonArea(self.targetEnemy.x, self.targetEnemy.y, self.damage * 0.15, 600); game.addChild(poisonArea); poisonAreas.push(poisonArea); } } 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); } self.destroy(); } else { var angle = Math.atan2(dy, dx); // Always rotate bullet to face the enemy perpendicularly bulletGraphics.rotation = angle; self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; // Make rapid tower shuriken rotate while flying (in addition to directional rotation) if (self.type === 'rapid') { bulletGraphics.rotation += 0.3; // Additional spinning for shuriken effect } } }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; // debugArrows array removed // Number label removed 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(); } }; // removeArrows method removed self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { // Arrow cleanup removed numberLabel.setText("-"); cellGraphics.tint = 0x880000; return; } // Number display and color tinting removed var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; } else { cellGraphics.tint = 0xffffff; // Set to white/transparent } // Arrow rendering removed break; } case 1: { // Arrow cleanup removed cellGraphics.tint = 0xaaaaaa; // Number visibility removed break; } case 3: { // Arrow cleanup removed cellGraphics.tint = 0x008800; // Number visibility removed break; } } // Number text setting removed }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; 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 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'explosion': effectGraphics.tint = 0xFF4500; effectGraphics.width = effectGraphics.height = CELL_SIZE * 3; 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 = .025; // Increased enemy speed for faster movement self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 8; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Fast falling and tunnel properties self.isFalling = true; self.fallSpeed = 0.15; // Fast falling speed from sky self.tunnelSpeed = 0.01; // Slow speed inside tunnels self.groundSpeed = 0.08; // Faster walking speed on ground self.originalSpeed = self.speed; self.inTunnel = false; self.lastInTunnel = 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 = 8; break; case 'immune': self.isImmune = true; self.maxHealth = 6; self.originalMaxHealth = self.maxHealth; // Track original health for transition self.hasTransitioned = false; // Track if already transitioned to second image break; case 'lizard': self.isFlying = true; self.maxHealth = 6; break; case 'swarm': self.maxHealth = 4; // Much weaker enemies break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy_normal'; if (self.type !== 'normal') { if (self.type === 'immune') { // Randomly choose between two shielded enemy assets assetId = Math.random() < 0.5 ? 'enemy_immune_asset' : 'enemy_immune2_asset'; } else if (self.type === 'lizard') { assetId = 'enemy_lizard'; } else { assetId = 'enemy_' + self.type + '_asset'; } } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Make lizard enemies semi-transparent if (self.type === 'lizard') { enemyGraphics.alpha = 0.7; } // 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 shadowAssetId = self.type === 'lizard' ? 'enemy_lizard' : assetId || 'enemy_normal'; var shadowGraphics = self.shadow.attachAsset(shadowAssetId, { 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; } else { // Update health bar width based on current health self.healthBar.width = self.health / self.maxHealth * 70; } // Handle shielded enemy transition when health drops to half if (self.type === 'immune' && !self.hasTransitioned && self.health <= self.originalMaxHealth / 2) { self.hasTransitioned = true; // Change to second shielded enemy asset var newAssetId = Math.random() < 0.5 ? 'enemy_immune_asset' : 'enemy_immune2_asset'; // Remove old graphics if (enemyGraphics && enemyGraphics.parent) { enemyGraphics.parent.removeChild(enemyGraphics); } // Create new graphics with second asset enemyGraphics = self.attachAsset(newAssetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Make enemy take double damage from now on self.damageMultiplier = 2; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; } 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.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Combine poison (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.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GameOverScreen = Container.expand(function () { var self = Container.call(this); // Pause the game when game over screen shows gamePaused = true; // Update highest score in storage var currentHighest = storage.highestScore || 0; if (score > currentHighest) { storage.highestScore = score; } // Hide score and gold counters from GUI if (scoreCounterContainer && scoreCounterContainer.parent) { scoreCounterContainer.parent.removeChild(scoreCounterContainer); } if (goldCounterContainer && goldCounterContainer.parent) { goldCounterContainer.parent.removeChild(goldCounterContainer); } // Create full screen background overlay var overlay = self.attachAsset('notification', { anchorX: 0, anchorY: 0 }); overlay.width = 2048; overlay.height = 2732; overlay.x = 0; overlay.y = 0; overlay.tint = 0x1a0f3d; // Deep purple background for more color overlay.alpha = 0.9; // Background elements removed for cleaner game over screen // Create main menu container var menuContainer = new Container(); self.addChild(menuContainer); // Add game over image behind all text elements (moved after menu container creation) var gameOverImage = self.attachAsset('game_over_image', { anchorX: 0.5, anchorY: 0.5 }); gameOverImage.x = 2048 / 2; gameOverImage.y = 2732 / 2 - 200; gameOverImage.alpha = 0.3; // Make it more subtle so text is readable // Animate the game over image appearing tween(gameOverImage, { alpha: 0.6, scaleX: 1.2, scaleY: 1.2 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { tween(gameOverImage, { scaleX: 1, scaleY: 1 }, { duration: 500, easing: tween.easeInOut }); } }); // Game Over title shadow var titleShadow = new Text2("GAME OVER", { size: 120, fill: 0x000000, weight: 800 }); titleShadow.anchor.set(0.5, 0.5); titleShadow.x = 6; titleShadow.y = -300 + 6; menuContainer.addChild(titleShadow); // Game Over title var titleText = new Text2("GAME OVER", { size: 120, fill: 0xFF0000, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -300; menuContainer.addChild(titleText); // Add pulsing animation to title self.animateTitlePulse = function () { var pulseScale = 1 + Math.sin(LK.ticks * 0.05) * 0.1; titleText.scaleX = pulseScale; titleText.scaleY = pulseScale; titleShadow.scaleX = pulseScale; titleShadow.scaleY = pulseScale; }; // Final score display shadow var scoreShadow = new Text2("Final Score: " + score, { size: 80, fill: 0x000000, weight: 800 }); scoreShadow.anchor.set(0.5, 0.5); scoreShadow.x = 4; scoreShadow.y = -150 + 4; menuContainer.addChild(scoreShadow); // Final score display var scoreDisplay = new Text2("Final Score: " + score, { size: 80, fill: 0x00FF00, weight: 800 }); scoreDisplay.anchor.set(0.5, 0.5); scoreDisplay.y = -150; menuContainer.addChild(scoreDisplay); // Add floating animation to score display self.animateScoreFloat = function () { var floatOffset = Math.sin(LK.ticks * 0.03) * 5; scoreDisplay.y = -150 + floatOffset; scoreShadow.y = -150 + 4 + floatOffset; }; // Final gold display shadow var goldShadow = new Text2("Gold Earned: " + gold, { size: 80, fill: 0x000000, weight: 800 }); goldShadow.anchor.set(0.5, 0.5); goldShadow.x = 4; goldShadow.y = -50 + 4; menuContainer.addChild(goldShadow); // Final gold display var goldDisplay = new Text2("Gold Earned: " + gold, { size: 80, fill: 0xFFD700, weight: 800 }); goldDisplay.anchor.set(0.5, 0.5); goldDisplay.y = -50; menuContainer.addChild(goldDisplay); // Add floating animation to gold display with different phase self.animateGoldFloat = function () { var floatOffset = Math.sin(LK.ticks * 0.025 + Math.PI) * 4; goldDisplay.y = -50 + floatOffset; goldShadow.y = -50 + 4 + floatOffset; }; // Restart button background var restartButton = new Container(); var restartButtonBg = restartButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); restartButtonBg.width = 600; restartButtonBg.height = 150; restartButtonBg.tint = 0x00AA00; // Restart button text shadow var restartTextShadow = new Text2("PLAY AGAIN", { size: 70, fill: 0x000000, weight: 800 }); restartTextShadow.anchor.set(0.5, 0.5); restartTextShadow.x = 4; restartTextShadow.y = 4; restartButton.addChild(restartTextShadow); // Restart button text var restartText = new Text2("PLAY AGAIN", { size: 70, fill: 0xFFFFFF, weight: 800 }); restartText.anchor.set(0.5, 0.5); restartButton.addChild(restartText); restartButton.y = 100; menuContainer.addChild(restartButton); // Position menu container at center of screen menuContainer.x = 2048 / 2; menuContainer.y = 2732 / 2; // Add button hover animation self.animateRestartButton = function () { var hoverScale = 1 + Math.sin(LK.ticks * 0.04) * 0.03; restartButton.scaleX = hoverScale; restartButton.scaleY = hoverScale; }; // Restart button functionality restartButton.down = function () { // Add click animation before reloading tween(restartButton, { scaleX: 0.9, scaleY: 0.9 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(restartButton, { scaleX: 1.1, scaleY: 1.1 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { // Restart the game by reloading location.reload(); } }); } }); }; // Update method to run all animations self.update = function () { self.animateTitlePulse(); self.animateScoreFloat(); self.animateGoldFloat(); self.animateRestartButton(); }; 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; // Set spawn areas based on first waypoint (top right corner) if (i === 23 && j >= 4 && j <= 7) { cellType = 2; self.spawns.push(cell); } else if (i >= 20 && i <= 23 && j >= 28 && j <= 31) { // Set goal area based on extended final waypoint (bottom right corner) cellType = 3; self.goals.push(cell); } 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) { // Only create debug cell for non-visual debugging, no graphics var debugCell = new DebugCell(); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; // Don't add to visual display } } } 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 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) { // Define the complete ordered sequence of lines from green top to purple bottom right var lineSequence = [ // 1. Green line (vertical at cloud2.x + 200) - start from top { type: 'vertical', x: cloud2.x + 200, startY: 0, endY: 630, centerX: cloud2.x + 200, color: 'green' }, // 2. Pink line (horizontal) - from green to purple { type: 'horizontal', y: 630, startX: cloud2.x + 200, endX: 1350, centerY: 630, color: 'pink' }, // 3. Purple line (vertical) - from pink to red { type: 'vertical', x: 1350, startY: 630, endY: 900, centerX: 1350, color: 'purple' }, // 4. Red horizontal line - from purple to brown { type: 'horizontal', y: 900, startX: 1350, endX: 450, centerY: 900, color: 'red' }, // 5. Brown line (vertical) - from red to blue { type: 'vertical', x: 450, startY: 900, endY: 1340, centerX: 450, color: 'brown' }, // 6. Blue horizontal line - from brown to yellow { type: 'horizontal', y: 1340, startX: 450, endX: 1350, centerY: 1340, color: 'blue' }, // 7. Yellow line (vertical) - from blue to green horizontal { type: 'vertical', x: 1350, startY: 1340, endY: 1700, centerX: 1350, color: 'yellow' }, // 8. Green horizontal line - from yellow to cyan { type: 'horizontal', y: 1700, startX: 1350, endX: 450, centerY: 1700, color: 'green_horizontal' }, // 9. Cyan line (vertical) - from green horizontal to magenta { type: 'vertical', x: 450, startY: 1700, endY: 2100, centerX: 450, color: 'cyan' }, // 10. Final magenta horizontal line - end at right { type: 'horizontal', y: 2100, startX: 450, endX: 450 + 980, centerY: 2100, color: 'magenta' }]; // Helper function to get the next line in sequence function getNextLineInSequence(enemy) { if (!enemy.currentLineIndex && enemy.currentLineIndex !== 0) { enemy.currentLineIndex = 0; return lineSequence[0]; } var nextIndex = enemy.currentLineIndex + 1; if (nextIndex < lineSequence.length) { enemy.currentLineIndex = nextIndex; return lineSequence[nextIndex]; } return null; } // Helper function to check if enemy is at the end of current line function isAtLineEnd(enemy, line, tolerance) { tolerance = tolerance || 5; // Reduced default tolerance from 15 to 5 if (line.type === 'vertical') { var atStartY = Math.abs(enemy.y - line.startY) < tolerance; var atEndY = Math.abs(enemy.y - line.endY) < tolerance; var onLineX = Math.abs(enemy.x - line.x) < tolerance; return onLineX && (atStartY || atEndY); } else { var atStartX = Math.abs(enemy.x - line.startX) < tolerance; var atEndX = Math.abs(enemy.x - line.endX) < tolerance; var onLineY = Math.abs(enemy.y - line.y) < tolerance; return onLineY && (atStartX || atEndX); } } // Initialize enemy if needed if (!enemy.currentLine) { enemy.currentLineIndex = 0; enemy.currentLine = lineSequence[0]; // Start with green line enemy.targetPoint = { x: lineSequence[0].centerX, y: lineSequence[0].endY }; // Move down the green line to pink connection } // Check if enemy should stop at black line end if (enemy.currentLine && enemy.currentLine.color && enemy.currentLine.color.indexOf('black') !== -1) { // Check if enemy reached the end of any black line var atBlackLineEnd = false; if (enemy.currentLine.type === 'horizontal') { // For horizontal black lines, check if at either end var atStartX = Math.abs(enemy.x - enemy.currentLine.startX) < 5; var atEndX = Math.abs(enemy.x - enemy.currentLine.endX) < 5; var onLineY = Math.abs(enemy.y - enemy.currentLine.centerY) < 5; atBlackLineEnd = onLineY && (atStartX || atEndX); } else if (enemy.currentLine.type === 'vertical') { // For vertical black lines, check if at either end var atStartY = Math.abs(enemy.y - enemy.currentLine.startY) < 5; var atEndY = Math.abs(enemy.y - enemy.currentLine.endY) < 5; var onLineX = Math.abs(enemy.x - enemy.currentLine.centerX) < 5; atBlackLineEnd = onLineX && (atStartY || atEndY); } if (atBlackLineEnd) { // Enemy has reached the end of a black line, transition to next line var nextLine = getNextLineInSequence(enemy); if (nextLine) { enemy.currentLine = nextLine; // Determine the direction to move on the new line if (enemy.currentLine.type === 'vertical') { // For vertical lines, determine direction based on previous line connection if (enemy.y < enemy.currentLine.startY + (enemy.currentLine.endY - enemy.currentLine.startY) / 2) { // If connecting from top half, move down enemy.targetPoint = { x: enemy.currentLine.centerX, y: enemy.currentLine.endY }; } else { // If connecting from bottom half, move up enemy.targetPoint = { x: enemy.currentLine.centerX, y: enemy.currentLine.startY }; } } else { // For horizontal lines, determine direction based on line sequence if (enemy.currentLine.color === 'pink') { // Pink line: move right from green to purple enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else if (enemy.currentLine.color === 'red') { // Red line: move left from purple to brown enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else if (enemy.currentLine.color === 'blue') { // Blue line: move right from brown to yellow enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else if (enemy.currentLine.color === 'green_horizontal') { // Green horizontal line: move left from yellow to cyan enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else if (enemy.currentLine.color === 'magenta') { // Final magenta line: move right to end enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else { // Default behavior for any other horizontal lines var nextVerticalLine = null; if (enemy.currentLineIndex + 1 < lineSequence.length) { nextVerticalLine = lineSequence[enemy.currentLineIndex + 1]; } if (nextVerticalLine && nextVerticalLine.type === 'vertical') { // Move towards the next vertical line's X position if (nextVerticalLine.centerX > enemy.currentLine.startX) { // Next vertical line is to the right, move right enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else { // Next vertical line is to the left, move left enemy.targetPoint = { x: enemy.currentLine.startX, y: enemy.currentLine.centerY }; } } else { // Fallback to original logic if no next vertical line if (enemy.x < enemy.currentLine.startX + (enemy.currentLine.endX - enemy.currentLine.startX) / 2) { // If connecting from left half, move right enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else { // If connecting from right half, move left enemy.targetPoint = { x: enemy.currentLine.startX, y: enemy.currentLine.centerY }; } } } } } else { // No more lines in sequence, enemy stops return false; } } } // Check if enemy reached target point var dx = enemy.targetPoint.x - enemy.x; var dy = enemy.targetPoint.y - enemy.y; var distanceToTarget = Math.sqrt(dx * dx + dy * dy); if (distanceToTarget < 5) { // Reduced tolerance from 15 to 5 for more precise line following // Enemy reached current target, check if at line end if (isAtLineEnd(enemy, enemy.currentLine)) { // At line end, get next line in sequence var nextLine = getNextLineInSequence(enemy); if (nextLine) { enemy.currentLine = nextLine; // Continue to next line normally { // Determine the direction to move on the new line if (enemy.currentLine.type === 'vertical') { // For vertical lines, determine direction based on previous line connection if (enemy.y < enemy.currentLine.startY + (enemy.currentLine.endY - enemy.currentLine.startY) / 2) { // If connecting from top half, move down enemy.targetPoint = { x: enemy.currentLine.centerX, y: enemy.currentLine.endY }; } else { // If connecting from bottom half, move up enemy.targetPoint = { x: enemy.currentLine.centerX, y: enemy.currentLine.startY }; } } else { // For horizontal lines, determine direction based on line sequence if (enemy.currentLine.color === 'pink') { // Pink line: move right from green to purple enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else if (enemy.currentLine.color === 'red') { // Red line: move left from purple to brown enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else if (enemy.currentLine.color === 'blue') { // Blue line: move right from brown to yellow enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else if (enemy.currentLine.color === 'green_horizontal') { // Green horizontal line: move left from yellow to cyan enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else if (enemy.currentLine.color === 'magenta') { // Final magenta line: move right to end enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else { // Default behavior for any other horizontal lines var nextVerticalLine = null; if (enemy.currentLineIndex + 1 < lineSequence.length) { nextVerticalLine = lineSequence[enemy.currentLineIndex + 1]; } if (nextVerticalLine && nextVerticalLine.type === 'vertical') { // Move towards the next vertical line's X position if (nextVerticalLine.centerX > enemy.currentLine.startX) { // Next vertical line is to the right, move right enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else { // Next vertical line is to the left, move left enemy.targetPoint = { x: enemy.currentLine.startX, y: enemy.currentLine.centerY }; } } else { // Fallback to original logic if no next vertical line if (enemy.x < enemy.currentLine.startX + (enemy.currentLine.endX - enemy.currentLine.startX) / 2) { // If connecting from left half, move right enemy.targetPoint = { x: enemy.currentLine.endX, y: enemy.currentLine.centerY }; } else { // If connecting from right half, move left enemy.targetPoint = { x: enemy.currentLine.startX, y: enemy.currentLine.centerY }; } } } } } // Remove tween animation - let normal movement handle line transitions } else { // Enemy has completed the full path sequence if (enemy.currentLine && enemy.currentLine.color === 'magenta') { // Check if enemy reached the right end of the magenta line specifically var atRightEnd = Math.abs(enemy.x - enemy.currentLine.endX) < 5; if (atRightEnd) { // Enemy reached the right end of the magenta line - trigger game loss var gameOverScreen = new GameOverScreen(); game.addChild(gameOverScreen); return true; // Game over condition - enemy reached the end } } // No more lines in sequence, enemy continues past the path return true; } } else { // Not at line end, continue along current line if (enemy.currentLine.type === 'vertical') { // Move to other end of vertical line var newTargetY = Math.abs(enemy.y - enemy.currentLine.startY) < Math.abs(enemy.y - enemy.currentLine.endY) ? enemy.currentLine.endY : enemy.currentLine.startY; enemy.targetPoint = { x: enemy.currentLine.centerX, y: newTargetY }; } else { // Move to other end of horizontal line var newTargetX = Math.abs(enemy.x - enemy.currentLine.startX) < Math.abs(enemy.x - enemy.currentLine.endX) ? enemy.currentLine.endX : enemy.currentLine.startX; enemy.targetPoint = { x: newTargetX, y: enemy.currentLine.centerY }; } } } // Move towards target point - ensure precise line following if (distanceToTarget > 1) { var moveSpeed = enemy.speed * 60; // Speed multiplier for movement on all lines // Calculate normalized movement direction var moveX = dx / distanceToTarget * moveSpeed; var moveY = dy / distanceToTarget * moveSpeed; // Track previous movement direction for rotation if (!enemy.lastMoveX) enemy.lastMoveX = 0; if (!enemy.lastMoveY) enemy.lastMoveY = 0; // For precise line centering, lock position to line axis if (enemy.currentLine) { if (enemy.currentLine.type === 'vertical') { // Lock X position to exact line center for vertical lines enemy.x = enemy.currentLine.centerX; // Use normalized movement speed for consistent speed on all lines enemy.y += moveY; // Keep enemy upright when moving vertically (maintain current horizontal flip) if (enemy.children[0]) { enemy.children[0].rotation = 0; // Always upright for vertical movement // Maintain current horizontal flip state (don't change scaleX) } } else { // Lock Y position to exact line center for horizontal lines enemy.y = enemy.currentLine.centerY; // For all horizontal lines, move along X axis using next vertical line direction logic enemy.x += moveX; // Flip enemy image horizontally only when turning right (stay upright) if (enemy.children[0]) { if (moveX > 0) { // Moving right - flip horizontally (face right) enemy.children[0].scaleX = -Math.abs(enemy.children[0].scaleX); } else { // Moving left - face normal (no flip) enemy.children[0].scaleX = Math.abs(enemy.children[0].scaleX); } } } } else { // Normal movement if no current line enemy.x += moveX; enemy.y += moveY; } // Store current movement for next frame enemy.lastMoveX = moveX; enemy.lastMoveY = moveY; } // Update cell coordinates based on actual position enemy.currentCellX = (enemy.x - grid.x) / CELL_SIZE; enemy.currentCellY = (enemy.y - grid.y) / CELL_SIZE; enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); // Update shadow position for flying enemies 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 horizontal flip with enemy flip if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].scaleX = enemy.children[0].scaleX; enemy.shadow.children[0].rotation = 0; // Shadow always upright } } return false; // Enemy continues moving }; }); var Mine = Container.expand(function () { var self = Container.call(this); var mineGraphics = self.attachAsset('mine', { anchorX: 0.5, anchorY: 0.5 }); self.activated = false; self.gridX = 0; self.gridY = 0; // Animate the mine with a pulsing effect self.pulseDirection = 1; self.pulseScale = 1.0; self.update = function () { // Pulsing animation self.pulseScale += self.pulseDirection * 0.02; if (self.pulseScale >= 1.3) { self.pulseDirection = -1; } else if (self.pulseScale <= 0.9) { self.pulseDirection = 1; } mineGraphics.scaleX = self.pulseScale; mineGraphics.scaleY = self.pulseScale; // Check for enemy collision 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 < 40 && !self.activated) { // Close enough to trigger self.explode(enemy); break; } } }; self.explode = function (triggerEnemy) { if (self.activated) return; self.activated = true; // Create explosion effect var explosion = new EffectIndicator(self.x, self.y, 'explosion'); game.addChild(explosion); // Deal massive damage to trigger enemy var triggerDamage = 200; // Further reduced massive damage // Apply damage multiplier for transitioned shielded enemies if (triggerEnemy.damageMultiplier) { triggerDamage *= triggerEnemy.damageMultiplier; } triggerEnemy.health -= triggerDamage; if (triggerEnemy.health <= 0) { triggerEnemy.health = 0; } else { triggerEnemy.healthBar.width = triggerEnemy.health / triggerEnemy.maxHealth * 70; } // Deal splash damage to nearby enemies var splashRadius = CELL_SIZE * 2; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy !== triggerEnemy) { var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= splashRadius) { var mineSplashDamage = 150; // Further reduced splash damage // Apply damage multiplier for transitioned shielded enemies if (enemy.damageMultiplier) { mineSplashDamage *= enemy.damageMultiplier; } enemy.health -= mineSplashDamage; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } } } } // Remove mine after explosion self.destroy(); var mineIndex = mines.indexOf(self); if (mineIndex !== -1) { mines.splice(mineIndex, 1); } }; 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; }; return self; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 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 PoisonArea = Container.expand(function (x, y, damage, duration) { var self = Container.call(this); self.x = x; self.y = y; self.damage = damage || 5; // Damage per tick self.duration = duration || 600; // 10 seconds at 60 FPS self.tickDamage = 30; // Frames between damage ticks (0.5 seconds) self.lastDamage = 0; // Create visual poison area effect var poisonGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); poisonGraphics.width = poisonGraphics.height = CELL_SIZE * 1.5; poisonGraphics.tint = 0x00AA00; // Green poison color poisonGraphics.alpha = 0.4; self.update = function () { self.duration--; // Apply damage to enemies in the area every 30 ticks if (LK.ticks - self.lastDamage >= self.tickDamage) { self.lastDamage = LK.ticks; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (!enemy.isImmune) { var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= CELL_SIZE * 0.75) { // Poison area radius // Apply poison damage var actualDamage = self.damage; if (enemy.damageMultiplier) { actualDamage *= enemy.damageMultiplier; } enemy.health -= actualDamage; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } // Visual effect on enemy var poisonEffect = new EffectIndicator(enemy.x, enemy.y, 'poison'); game.addChild(poisonEffect); } } } } // Remove area when duration expires if (self.duration <= 0) { self.destroy(); var areaIndex = poisonAreas.indexOf(self); if (areaIndex !== -1) { poisonAreas.splice(areaIndex, 1); } } // Animate the poison area with pulsing effect poisonGraphics.alpha = 0.3 + Math.sin(LK.ticks * 0.1) * 0.1; }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Increase size of base for easier touch - use tower-specific asset var assetName = 'tower_' + (self.towerType === 'default' ? 'default' : self.towerType); var baseGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3, y: -30 // Move tower image up }); // No need for tinting since each tower type has its own colored asset var towerCost = getTowerCost(self.towerType); // Get display name for tower type var displayName; if (self.towerType === 'slow') { displayName = 'Mine'; } else if (self.towerType === 'default') { displayName = 'Rookie'; } else if (self.towerType === 'rapid') { displayName = 'Ninja'; } else { displayName = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1); } // Add shadow for tower type label var typeLabelShadow = new Text2(displayName, { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = 40 + 4; // Move tower name further down self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(displayName, { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = 40; // Move tower name further down 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 = 84 + 12; // Move cost label further down 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 = 80 + 12; // Move cost label further down self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var StartMenu = Container.expand(function () { var self = Container.call(this); // Create full screen background overlay var overlay = self.attachAsset('notification', { anchorX: 0, anchorY: 0 }); overlay.width = 2048; overlay.height = 2732; overlay.x = 0; overlay.y = 0; overlay.tint = 0x001122; overlay.alpha = 0.8; // Background elements removed for cleaner start menu // Create main menu container var menuContainer = new Container(); self.addChild(menuContainer); // Title shadow var titleShadow = new Text2("Defense The Queen", { size: 120, fill: 0x000000, weight: 800 }); titleShadow.anchor.set(0.5, 0.5); titleShadow.x = 6; titleShadow.y = -350 + 6; menuContainer.addChild(titleShadow); // Main title var titleText = new Text2("Defense The Queen", { size: 120, fill: 0xFFD700, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -350; menuContainer.addChild(titleText); // Start button background var startButton = new Container(); var startButtonBg = startButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startButtonBg.width = 600; startButtonBg.height = 150; startButtonBg.tint = 0x00AA00; // Start button text shadow var startTextShadow = new Text2("START GAME", { size: 70, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startButton.addChild(startTextShadow); // Start Game title var startGameTitle = new Text2("Start Game", { size: 80, fill: 0xFFFFFF, weight: 800 }); startGameTitle.anchor.set(0.5, 0.5); startGameTitle.y = -250; menuContainer.addChild(startGameTitle); // Start button text var startText = new Text2("START GAME", { size: 70, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startButton.addChild(startText); startButton.y = -100; menuContainer.addChild(startButton); // Instructions text var instructionsText = new Text2("Prevent the enemies from reaching the queen to protect the royal treasure.", { size: 50, fill: 0xFFFFFF, weight: 400 }); instructionsText.anchor.set(0.5, 0.5); instructionsText.y = 100; menuContainer.addChild(instructionsText); // Get highest score from storage var highestScore = storage.highestScore || 0; // Highest score display shadow var highScoreShadow = new Text2("Highest Score: " + highestScore, { size: 60, fill: 0x000000, weight: 800 }); highScoreShadow.anchor.set(0.5, 0.5); highScoreShadow.x = 4; highScoreShadow.y = 200 + 4; menuContainer.addChild(highScoreShadow); // Highest score display var highScoreText = new Text2("Highest Score: " + highestScore, { size: 60, fill: 0xFFD700, weight: 800 }); highScoreText.anchor.set(0.5, 0.5); highScoreText.y = 200; menuContainer.addChild(highScoreText); // Credits text var creditsText = new Text2("Made with FRVR", { size: 40, fill: 0xCCCCCC, weight: 400 }); creditsText.anchor.set(0.5, 0.5); creditsText.y = 300; menuContainer.addChild(creditsText); // Position menu container at center of screen menuContainer.x = 2048 / 2; menuContainer.y = 2732 / 2; // Start button functionality startButton.down = function () { // Start playing background music immediately when button is pressed LK.playMusic('acxs'); // Add score and gold counters to GUI when game starts LK.gui.right.addChild(scoreCounterContainer); LK.gui.right.addChild(goldCounterContainer); // Hide the start menu with animation tween(self, { alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); // Start countdown sequence var countdownContainer = new Container(); game.addChild(countdownContainer); countdownContainer.x = 2048 / 2; countdownContainer.y = 2732 / 2; // Create countdown text var countdownText = new Text2("3", { size: 300, fill: 0xFFFFFF, weight: 800 }); countdownText.anchor.set(0.5, 0.5); countdownContainer.addChild(countdownText); // Create countdown shadow var countdownShadow = new Text2("3", { size: 300, fill: 0x000000, weight: 800 }); countdownShadow.anchor.set(0.5, 0.5); countdownShadow.x = 8; countdownShadow.y = 8; countdownContainer.addChild(countdownShadow); var countdownNumbers = ["3", "2", "1", "GO!"]; var currentCountdown = 0; function showNextCountdown() { if (currentCountdown < countdownNumbers.length) { var number = countdownNumbers[currentCountdown]; countdownText.setText(number); countdownShadow.setText(number); // Scale animation for each countdown number countdownContainer.scaleX = 0.5; countdownContainer.scaleY = 0.5; countdownContainer.alpha = 0; tween(countdownContainer, { scaleX: 1.2, scaleY: 1.2, alpha: 1 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(countdownContainer, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { currentCountdown++; if (currentCountdown < countdownNumbers.length) { LK.setTimeout(showNextCountdown, 500); } else { // Countdown finished, start the game tween(countdownContainer, { alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { countdownContainer.destroy(); // Auto-start the game if (waveIndicator) { waveIndicator.gameStarted = true; currentWave = 1; // Start with wave 1 waveTimer = 0; // Start wave immediately waveInProgress = true; // Begin the first wave waveSpawned = false; // Mark that we need to spawn enemies var notification = game.addChild(new Notification("Game started! Wave 1 beginning!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } }); } } }); } }); } else { countdownContainer.destroy(); } } // Start the countdown showNextCountdown(); } }); }; // Update method for animations self.update = function () { // Animation methods removed }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 3; 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 7, +1.2 per level, huge boost at max level if (self.level === self.maxLevel) { return 20 * CELL_SIZE; // Massive range for max level } return (7 + (self.level - 1) * 1.2) * CELL_SIZE; case 'splash': // Splash: base 3.5, +0.4 per level return (3.5 + (self.level - 1) * 0.4) * CELL_SIZE; case 'rapid': // Rapid: base 4, +0.7 per level return (4 + (self.level - 1) * 0.7) * CELL_SIZE; case 'slow': // Slow (Mine): base 3, +0.5 per level - increased range for mines return (3 + (self.level - 1) * 0.5) * CELL_SIZE; case 'poison': // Poison: base 4.5, +0.7 per level return (4.5 + (self.level - 1) * 0.7) * CELL_SIZE; default: // Default: base 4.5, +0.7 per level return (4.5 + (self.level - 1) * 0.7) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 35; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 30; self.damage = 15; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'sniper': self.fireRate = 90; self.damage = 50; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'splash': //{g2} self.fireRate = 180; // Much slower fire rate for splash tower self.damage = 30; // Increased damage so enemies need at least 3 hits self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; case 'slow': self.fireRate = 50; self.damage = 20; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'poison': self.fireRate = 70; self.damage = 25; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 5; break; } // Apply level 1 upgrades immediately for consistent scaling if (self.level === 1) { // Apply the upgrade scaling for level 1 to ensure consistency if (self.id === 'rapid') { self.fireRate = Math.max(10, 30 - self.level * 5); self.damage = 15 + self.level * 12; self.bulletSpeed = 7 + self.level * 1.2; } else if (self.id === 'sniper') { self.fireRate = Math.max(30, 90 - self.level * 12); self.damage = 50 + self.level * 25; self.bulletSpeed = 25 + self.level * 3; } else if (self.id === 'splash') { self.fireRate = Math.max(60, 180 - self.level * 20); self.damage = 30 + self.level * 20; self.bulletSpeed = 4 + self.level * 1; } else { self.fireRate = Math.max(15, 60 - self.level * 12); self.damage = 35 + self.level * 20; self.bulletSpeed = 5 + self.level * 1.5; } } // Use tower-specific asset based on tower type var assetName = 'tower_' + (self.id === 'default' ? 'default' : self.id); var baseGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); // No need for tinting since each tower type has its own colored asset var levelIndicators = []; var maxStars = self.maxLevel; var starSpacing = baseGraphics.width / (maxStars + 1); var starSize = CELL_SIZE / 5; for (var i = 0; i < maxStars; i++) { var star = new Container(); // Create star shape using multiple triangles to form a 5-pointed star var starGraphics = star.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); starGraphics.width = starSize; starGraphics.height = starSize; starGraphics.tint = 0x444444; // Dark gray for unfilled stars // Position stars in a row below the tower star.x = -CELL_SIZE + starSpacing * (i + 1); star.y = CELL_SIZE * 0.7; self.addChild(star); levelIndicators.push(star); } // Remove gun graphics - towers now only show their base images self.updateLevelIndicators = function () { for (var i = 0; i < maxStars; i++) { var star = levelIndicators[i]; var starGraphics = star.children[0]; if (i < self.level) { // Filled gold star for achieved levels starGraphics.tint = 0xFFD700; // Gold color // Add slight animation to make stars more prominent tween(starGraphics, { scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(starGraphics, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } else { // Empty dark star for unachieved levels starGraphics.tint = 0x444444; // Dark gray } } }; 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 (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type with enhanced scaling if (self.id === 'rapid') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (triple the effect) self.fireRate = Math.max(3, 30 - self.level * 12); // Much faster firing self.damage = 15 + self.level * 25; // Higher damage self.bulletSpeed = 7 + self.level * 3; // Much faster bullets } else { self.fireRate = Math.max(10, 30 - self.level * 5); // Faster firing rate scaling self.damage = 15 + self.level * 12; // Better damage scaling self.bulletSpeed = 7 + self.level * 1.2; // Better speed scaling } } else if (self.id === 'sniper') { if (self.level === self.maxLevel) { // Sniper gets massive damage boost at max level self.fireRate = Math.max(8, 90 - self.level * 15); // Faster sniping self.damage = 50 + self.level * 60; // Huge damage increase self.bulletSpeed = 25 + self.level * 8; // Much faster bullets } else { self.fireRate = Math.max(30, 90 - self.level * 12); self.damage = 50 + self.level * 25; self.bulletSpeed = 25 + self.level * 3; } } else if (self.id === 'splash') { if (self.level === self.maxLevel) { // Splash gets area damage boost self.fireRate = Math.max(20, 180 - self.level * 35); // Much faster splash attacks self.damage = 30 + self.level * 45; // High damage for area effect self.bulletSpeed = 4 + self.level * 2.5; } else { self.fireRate = Math.max(60, 180 - self.level * 20); self.damage = 30 + self.level * 20; self.bulletSpeed = 4 + self.level * 1; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (triple the effect) self.fireRate = Math.max(5, 60 - self.level * 30); // Much faster firing self.damage = 35 + self.level * 50; // Much higher damage self.bulletSpeed = 5 + self.level * 3.5; // Much faster bullets } else { self.fireRate = Math.max(15, 60 - self.level * 12); // Better fire rate scaling self.damage = 35 + self.level * 20; // Better damage scaling self.bulletSpeed = 5 + self.level * 1.5; // Better speed scaling } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[0]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; 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) { // Lizard enemies can only be targeted by area/splash towers if (enemy.type === 'lizard' && self.id !== 'splash') { // Skip lizard enemies for non-splash towers continue; } // 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 () { 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); // Rotate the entire tower to face the enemy baseGraphics.rotation = angle; if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; // Add actual tower asset preview on top of range circle var assetName = 'tower_' + (self.id === 'default' ? 'default' : self.id); var towerPreviewGraphics = rangeIndicator.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); towerPreviewGraphics.alpha = 0.7; towerPreviewGraphics.scaleX = 1.2; towerPreviewGraphics.scaleY = 1.2; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(baseGraphics.rotation) * 40; var bulletY = self.y + Math.sin(baseGraphics.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed, self.id); // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // Remove recoil animation - towers now only show base images } } }; 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.3; // Use actual tower asset instead of generic preview var assetName = 'tower_' + (self.towerType === 'default' ? 'default' : self.towerType); var previewGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); // Scale tower preview to be slightly larger for better visibility previewGraphics.scaleX = 1.3; previewGraphics.scaleY = 1.3; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateTowerGraphics = function () { // Remove old graphics if (previewGraphics && previewGraphics.parent) { previewGraphics.parent.removeChild(previewGraphics); } // Create new graphics with correct tower asset var assetName = 'tower_' + (self.towerType === 'default' ? 'default' : self.towerType); previewGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); // Scale tower preview to be slightly larger for better visibility previewGraphics.scaleX = 1.3; previewGraphics.scaleY = 1.3; }; 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; // Use white tint for normal state since towers have their own colors previewGraphics.tint = 0xFFFFFF; // Only tint red if cannot place or not enough gold if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; // Special placement rules for mines if (self.towerType === 'slow') { // slow tower is mine // Mines can only be placed on path lines var isOnPath = false; var cellX = self.gridX; var cellY = self.gridY; var cellWorldX = grid.x + cellX * CELL_SIZE + CELL_SIZE / 2; var cellWorldY = grid.y + cellY * CELL_SIZE + CELL_SIZE / 2; // Check if position is on any of the path lines var tolerance = CELL_SIZE / 2; // Check vertical lines var verticalLines = [{ x: cloud2.x + 200, startY: 0, endY: 630 }, // Green line { x: 1350, startY: 630, endY: 900 }, // Purple line { x: 450, startY: 900, endY: 1340 }, // Brown line { x: 1350, startY: 1340, endY: 1700 }, // Yellow line { x: 450, startY: 1700, endY: 2100 } // Cyan line ]; // Check horizontal lines var horizontalLines = [{ y: 630, startX: cloud2.x + 200, endX: 1350 }, // Pink line { y: 900, startX: 1350, endX: 450 }, // Red line { y: 1340, startX: 450, endX: 1350 }, // Blue line { y: 1700, startX: 1350, endX: 450 }, // Green horizontal line { y: 2100, startX: 450, endX: 450 + 980 } // Magenta line ]; // Check if on vertical line for (var i = 0; i < verticalLines.length; i++) { var line = verticalLines[i]; if (Math.abs(cellWorldX - line.x) < tolerance && cellWorldY >= line.startY - tolerance && cellWorldY <= line.endY + tolerance) { isOnPath = true; break; } } // Check if on horizontal line if (!isOnPath) { for (var i = 0; i < horizontalLines.length; i++) { var line = horizontalLines[i]; if (Math.abs(cellWorldY - line.y) < tolerance && cellWorldX >= Math.min(line.startX, line.endX) - tolerance && cellWorldX <= Math.max(line.startX, line.endX) + tolerance) { isOnPath = true; break; } } } validGridPlacement = isOnPath; } else { // Regular tower placement rules if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) { validGridPlacement = false; } else { for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (!cell || cell.type !== 0) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) { continue; } // Only check non-flying enemies, flying enemies can pass over towers if (!enemy.isFlying) { if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) { self.blockedByEnemy = true; break; } } } } } self.canPlace = validGridPlacement && !self.blockedByEnemy; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; self.checkPlacement(); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 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 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; // Create moving orange line self.movingLine = new Container(); var orangeLine = self.movingLine.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); orangeLine.width = 8; orangeLine.height = 140; orangeLine.tint = 0xFFA500; // Orange color self.addChild(self.movingLine); // No start marker needed - wave indicator only shows waves for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Lizard"; enemyType = "lizard"; 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 lizard var bossTypes = ['normal', 'fast', 'immune', 'lizard']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always lizard enemyType = 'lizard'; waveType = "Boss Lizard"; 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 'lizard': block.tint = 0xFFFF00; waveType = "Boss Lizard"; 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 'lizard': 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 lizard block.tint = 0xFFFF00; waveType = "Lizard"; enemyType = "lizard"; 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; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (every 10th wave) if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); self.positionIndicator.visible = false; var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; // Position and animate moving orange line if (self.gameStarted && currentWave < totalWaves) { self.movingLine.visible = true; // Position line at left edge of current wave indicator var currentWaveX = -moveAmount + (currentWave - 1) * blockWidth; // Initialize line position if not set if (self.movingLine.startX === undefined) { self.movingLine.startX = currentWaveX - blockWidth / 2; self.movingLine.x = self.movingLine.startX; } // Animate line slowly to the right var lineSpeed = 0.2; // Much slower movement speed for slower waves self.movingLine.x += lineSpeed; // Check if line reached next wave position (right edge of current wave) var nextWaveX = currentWaveX + blockWidth / 2; if (self.movingLine.x >= nextWaveX) { // Trigger next wave waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; // Reset line position for next wave self.movingLine.startX = undefined; if (currentWave <= totalWaves) { 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; } } } else { self.movingLine.visible = false; } for (var i = 0; i < totalWaves; i++) { var marker = self.waveMarkers[i]; var block = marker.children[0]; if (i < currentWave) { block.alpha = .5; } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } // Wave progression is now handled by the moving orange line above }; self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xF5F5DC }); /**** * Game Code ****/ var isHidingUpgradeMenu = false; var gamePaused = false; // Flag to pause game when game over shows function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var mines = []; var poisonAreas = []; var selectedTower = null; var gold = 35; var lives = 20; var score = 0; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave function updateUI() {} function setGold(value) { gold = value; // Update gold counter text if it exists if (typeof goldText !== 'undefined' && goldText) { var newGoldText = "Gold: " + gold; goldText.setText(newGoldText); goldTextShadow.setText(newGoldText); } updateUI(); } function setScore(value) { var oldScore = score; score = value; // Update score counter text if it exists if (typeof scoreText !== 'undefined' && scoreText) { var newScoreText = "Score: " + score; scoreText.setText(newScoreText); scoreTextShadow.setText(newScoreText); // Check for score milestones and animate var oldTens = Math.floor(oldScore / 10); var newTens = Math.floor(score / 10); var oldHundreds = Math.floor(oldScore / 100); var newHundreds = Math.floor(score / 100); // Animate on every 10 points if (newTens > oldTens) { tween(scoreText, { scaleX: 1.3, scaleY: 1.3 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(scoreText, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeIn }); } }); tween(scoreTextShadow, { scaleX: 1.3, scaleY: 1.3 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(scoreTextShadow, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeIn }); } }); } // Extra animation on every 100 points (bigger scale) if (newHundreds > oldHundreds) { tween(scoreText, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(scoreText, { scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeIn }); } }); tween(scoreTextShadow, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(scoreTextShadow, { scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeIn }); } }); } } } // Enemies paths will be defined later 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 = 150 - CELL_SIZE * 6; grid.pathFind(); grid.renderDebug(); // Add background asset to fill entire screen var backgroundCell = LK.getAsset('ak', { anchorX: 0.5, anchorY: 0.5 }); backgroundCell.x = 2048 / 2; backgroundCell.y = 2732 / 2; backgroundCell.tint = 0xffffff; backgroundCell.alpha = 1.0; // Set texture filtering to nearest neighbor for sharp pixels if (backgroundCell.texture && backgroundCell.texture.baseTexture) { backgroundCell.texture.baseTexture.scaleMode = 0; // NEAREST filtering for sharp pixels } // Add first cloud on the top left var cloud1 = LK.getAsset('cloud', { anchorX: 0.5, anchorY: 0.5 }); cloud1.x = 300; cloud1.y = 125; // Same Y position as sun cloud1.alpha = 0.9; // Add second cloud on the top right var cloud2 = LK.getAsset('cloud', { anchorX: 0.5, anchorY: 0.5 }); cloud2.x = 2048 - 300; cloud2.y = 125; // Same Y position as sun cloud2.alpha = 0.9; debugLayer.addChild(backgroundCell); debugLayer.addChild(cloud1); debugLayer.addChild(cloud2); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY) { var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 5; switch (towerType) { case 'rapid': cost = 15; break; case 'sniper': cost = 25; break; case 'splash': cost = 35; break; case 'slow': cost = 45; break; case 'poison': cost = 55; break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { if (towerType === 'slow') { // Place mine instead of tower var mine = new Mine(); mine.placeOnGrid(gridX, gridY); towerLayer.addChild(mine); mines.push(mine); } else { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); grid.pathFind(); grid.renderDebug(); } setGold(gold - towerCost); return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { 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) { towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.updateTowerGraphics(); // Update graphics to match tower type 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; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; if (towerPreview.canPlace) { if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; if (isDragging) { var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } } }; var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2 - 200; waveIndicator.y = 2732 - 80; game.addChild(waveIndicator); var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); // Create sun asset at the top of the screen var sunAsset = LK.getAsset('sun', { anchorX: 0.5, anchorY: 0.5 }); sunAsset.x = 2048 / 2; sunAsset.y = 125; // Position at the very top of the screen game.addChild(sunAsset); // Create single vertical line segment that extends to connect with purple line var lineSegment = LK.getAsset('verticalLine', { anchorX: 0.5, anchorY: 0.5 }); // Extend the line to connect with purple horizontal line lineSegment.height = 630; // Extended to reach purple line at y=630 lineSegment.x = cloud2.x + 200; // Position to the right of the right cloud lineSegment.y = 315; // Position so bottom of line connects to purple line (y=630) lineSegment.tint = 0xFFA500; // Orange color lineSegment.visible = false; // Make line invisible game.addChild(lineSegment); // Create three horizontal lines var horizontalLine1 = LK.getAsset('gridLineHorizontal', { anchorX: 0, anchorY: 0.5 }); horizontalLine1.width = 1350 - 450; // Extended to connect red line to yellow line horizontalLine1.height = 4; // Line thickness horizontalLine1.x = 450; // Start from red vertical line horizontalLine1.y = 900; // First horizontal line position - moved down horizontalLine1.tint = 0xFF0000; // Red color horizontalLine1.visible = false; // Make line invisible game.addChild(horizontalLine1); var horizontalLine2 = LK.getAsset('gridLineHorizontal', { anchorX: 0, anchorY: 0.5 }); horizontalLine2.width = 1350 - 450; // Extended to connect red line to yellow line horizontalLine2.height = 4; // Line thickness horizontalLine2.x = 450; // Start from red vertical line horizontalLine2.y = 1340; // Second horizontal line position - moved up more horizontalLine2.tint = 0x0000FF; // Blue color horizontalLine2.visible = false; // Make line invisible game.addChild(horizontalLine2); var horizontalLine3 = LK.getAsset('gridLineHorizontal', { anchorX: 0, anchorY: 0.5 }); horizontalLine3.width = 980 - 27; // Extended to connect white line to gray line right endpoint horizontalLine3.height = 4; // Line thickness horizontalLine3.x = 450; // Start from white vertical line horizontalLine3.y = 1700; // Third horizontal line position - moved down horizontalLine3.tint = 0x00FF00; // Green color horizontalLine3.visible = false; // Make line invisible game.addChild(horizontalLine3); // Create four vertical lines with different colors, same length as horizontal lines var verticalLine1 = LK.getAsset('gridLine', { anchorX: 0.5, anchorY: 0.5 }); verticalLine1.width = 4; // Line thickness verticalLine1.height = 450; // Same height as yellow line verticalLine1.x = 450; // First vertical line position - moved left verticalLine1.y = 1120; // Position at center between top and middle horizontal lines verticalLine1.tint = 0x8B4513; // Brown color verticalLine1.visible = false; // Make line invisible game.addChild(verticalLine1); var verticalLine2 = LK.getAsset('gridLine', { anchorX: 0.5, anchorY: 0.5 }); verticalLine2.width = 4; // Line thickness verticalLine2.height = 400; // Extended to connect third black line to gray line verticalLine2.x = 450; // Position at same x as red vertical line verticalLine2.y = 1900; // Position to connect third black line (1700) to gray line (2100) verticalLine2.tint = 0x00FFFF; // Cyan color verticalLine2.visible = false; // Make line invisible game.addChild(verticalLine2); var verticalLine3 = LK.getAsset('gridLine', { anchorX: 0.5, anchorY: 0.5 }); verticalLine3.width = 4; // Line thickness verticalLine3.height = 270; // Extended blue line height to reach top black line verticalLine3.x = 1350; // Move blue line to same x position as yellow line verticalLine3.y = 765; // Move blue line up so top touches black line at y=900 verticalLine3.tint = 0x800080; // Purple color verticalLine3.visible = false; // Make line invisible game.addChild(verticalLine3); var verticalLine4 = LK.getAsset('gridLine', { anchorX: 0.5, anchorY: 0 }); verticalLine4.width = 4; // Line thickness verticalLine4.height = 400; // Shortened from bottom while keeping top at same position verticalLine4.x = 1350; // Fourth vertical line position - moved further right verticalLine4.y = 1340; // Positioned so bottom aligns with black line right end verticalLine4.tint = 0xFFFF00; // Yellow color verticalLine4.visible = false; // Make line invisible game.addChild(verticalLine4); // Create pink horizontal line in the middle var pinkHorizontalLine = LK.getAsset('gridLineHorizontal', { anchorX: 0, anchorY: 0.5 }); pinkHorizontalLine.width = 1350 - (cloud2.x + 200); // Extended to connect green line to blue line pinkHorizontalLine.height = 4; // Same thickness as other lines pinkHorizontalLine.x = cloud2.x + 200; // Start from green vertical line pinkHorizontalLine.y = 630; // Moved slightly up pinkHorizontalLine.tint = 0xFFC0CB; // Pink color pinkHorizontalLine.visible = false; // Make line invisible game.addChild(pinkHorizontalLine); // Create horizontal gray line slightly longer than bottom black line var grayHorizontalLine = LK.getAsset('gridLineHorizontal', { anchorX: 0, anchorY: 0.5 }); grayHorizontalLine.width = 1100; // Extended width to make right corner longer grayHorizontalLine.height = 4; // Same thickness as other lines grayHorizontalLine.x = 2048 / 2 - 577; // Position slightly to the right - moved 50 pixels right (unchanged) grayHorizontalLine.y = 2100; // Move gray line slightly up grayHorizontalLine.tint = 0xFF00FF; // Magenta color grayHorizontalLine.visible = false; // Make line invisible game.addChild(grayHorizontalLine); // Place queen asset to the right of the game end position var queenAsset = LK.getAsset('queen', { anchorX: 0.5, anchorY: 0.5 }); queenAsset.x = 450 + 980 + 150; // Position even further to the right - moved 100px more to the right queenAsset.y = 2100; // Same y position as magenta line // Queen stays in original colors without tint game.addChild(queenAsset); // Queen stays in place without animation // Move queen to front of all visuals by re-adding it to game after all other elements game.removeChild(queenAsset); game.addChild(queenAsset); // Connect line endpoints to form continuous path // Connection 1: Green vertical line bottom to Pink horizontal line left var connection1 = LK.getAsset('gridLineHorizontal', { anchorX: 0, anchorY: 0.5 }); connection1.width = Math.abs(cloud2.x + 200 - (cloud2.x + 200)); // No gap, lines already connect connection1.height = 4; connection1.x = cloud2.x + 200; connection1.y = 630; connection1.tint = 0xFFA500; // Match green line color connection1.visible = false; // Hidden since lines already connect game.addChild(connection1); // Connection 2: Pink horizontal line right to Purple vertical line top var connection2 = LK.getAsset('gridLineHorizontal', { anchorX: 0, anchorY: 0.5 }); connection2.width = Math.abs(1350 - (cloud2.x + 200 + (1350 - (cloud2.x + 200)))); connection2.height = 4; connection2.x = cloud2.x + 200 + (1350 - (cloud2.x + 200)); connection2.y = 630; connection2.tint = 0xFFC0CB; // Match pink line color connection2.visible = false; // Hidden since lines already connect game.addChild(connection2); // Connection 3: Purple vertical line bottom to Red horizontal line right var connection3 = LK.getAsset('gridLine', { anchorX: 0.5, anchorY: 0 }); connection3.width = 4; connection3.height = Math.abs(900 - 630); // Connect purple line bottom to red line connection3.x = 1350; connection3.y = 630; connection3.tint = 0x800080; // Match purple line color connection3.visible = false; // Make line invisible game.addChild(connection3); // Connection 4: Red horizontal line left to Brown vertical line top var connection4 = LK.getAsset('gridLineHorizontal', { anchorX: 0, anchorY: 0.5 }); connection4.width = Math.abs(1350 - 450); connection4.height = 4; connection4.x = 450; connection4.y = 900; connection4.tint = 0xFF0000; // Match red line color connection4.visible = false; // Hidden since lines already connect game.addChild(connection4); // Connection 5: Brown vertical line bottom to Blue horizontal line left var connection5 = LK.getAsset('gridLine', { anchorX: 0.5, anchorY: 0 }); connection5.width = 4; connection5.height = Math.abs(1340 - (900 + 450)); // Connect brown line bottom to blue line connection5.x = 450; connection5.y = 900 + 450; connection5.tint = 0x8B4513; // Match brown line color connection5.visible = false; // Hidden since lines already connect game.addChild(connection5); // Connection 6: Blue horizontal line right to Yellow vertical line top var connection6 = LK.getAsset('gridLineHorizontal', { anchorX: 0, anchorY: 0.5 }); connection6.width = Math.abs(1350 - 450); connection6.height = 4; connection6.x = 450; connection6.y = 1340; connection6.tint = 0x0000FF; // Match blue line color connection6.visible = false; // Hidden since lines already connect game.addChild(connection6); // Connection 7: Yellow vertical line bottom to Green horizontal line left var connection7 = LK.getAsset('gridLine', { anchorX: 0.5, anchorY: 0 }); connection7.width = 4; connection7.height = Math.abs(1700 - 1740); // Connect yellow line bottom to green line connection7.x = 1350; connection7.y = 1340; connection7.tint = 0xFFFF00; // Match yellow line color connection7.visible = false; // Hidden since gap is small game.addChild(connection7); // Connection 8: Green horizontal line left to Cyan vertical line top var connection8 = LK.getAsset('gridLineHorizontal', { anchorX: 1, anchorY: 0.5 }); connection8.width = Math.abs(450 - 450); connection8.height = 4; connection8.x = 450; connection8.y = 1700; connection8.tint = 0x00FF00; // Match green line color connection8.visible = false; // Hidden since lines already connect game.addChild(connection8); // Connection 9: Cyan vertical line bottom to Magenta horizontal line left var connection9 = LK.getAsset('gridLine', { anchorX: 0.5, anchorY: 0 }); connection9.width = 4; connection9.height = Math.abs(2100 - 2100); // Connect cyan line bottom to magenta line connection9.x = 450; connection9.y = 1900 + 200; connection9.tint = 0x00FFFF; // Match cyan line color connection9.visible = false; // Hidden since lines already connect game.addChild(connection9); // Make clouds move slowly back and forth function animateCloudMovement() { // Cloud 1 - move from left to right tween(cloud1, { x: cloud1.x + 50 }, { duration: 4000, easing: tween.easeInOut, onFinish: function onFinish() { // Move back to left tween(cloud1, { x: cloud1.x - 50 }, { duration: 4000, easing: tween.easeInOut, onFinish: function onFinish() { // Repeat the cycle animateCloudMovement(); } }); } }); // Cloud 2 - move from right to left (opposite direction) tween(cloud2, { x: cloud2.x - 50 }, { duration: 4000, easing: tween.easeInOut, onFinish: function onFinish() { // Move back to right tween(cloud2, { x: cloud2.x + 50 }, { duration: 4000, easing: tween.easeInOut }); } }); } // Start the cloud animation animateCloudMovement(); var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; var sourceTowers = []; var towerSpacing = 300; // Increase spacing for larger towers var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; // Create score counter container but don't add to GUI yet var scoreCounterContainer = new Container(); // Score counter background var scoreCounterBg = scoreCounterContainer.attachAsset('notification', { anchorX: 1.0, anchorY: 0.5 }); scoreCounterBg.width = 200; scoreCounterBg.height = 80; scoreCounterBg.tint = 0x444444; scoreCounterBg.alpha = 0.9; scoreCounterBg.x = -20; // Offset from right edge scoreCounterBg.y = -100; // Position above gold counter // Score text shadow var scoreTextShadow = new Text2("Score: " + score, { size: 40, fill: 0x000000, weight: 800 }); scoreTextShadow.anchor.set(1.0, 0.5); scoreTextShadow.x = -30 + 2; // Offset from right edge + shadow offset scoreTextShadow.y = -100 + 2; // Position above gold counter + shadow offset scoreCounterContainer.addChild(scoreTextShadow); // Score text var scoreText = new Text2("Score: " + score, { size: 40, fill: 0x00FF00, weight: 800 }); scoreText.anchor.set(1.0, 0.5); scoreText.x = -30; // Offset from right edge scoreText.y = -100; // Position above gold counter scoreCounterContainer.addChild(scoreText); // Create gold counter container but don't add to GUI yet var goldCounterContainer = new Container(); // Gold counter background var goldCounterBg = goldCounterContainer.attachAsset('notification', { anchorX: 1.0, anchorY: 0.5 }); goldCounterBg.width = 200; goldCounterBg.height = 80; goldCounterBg.tint = 0x444444; goldCounterBg.alpha = 0.9; goldCounterBg.x = -20; // Offset from right edge goldCounterBg.y = 0; // Gold text shadow var goldTextShadow = new Text2("Gold: " + gold, { size: 40, fill: 0x000000, weight: 800 }); goldTextShadow.anchor.set(1.0, 0.5); goldTextShadow.x = -30 + 2; // Offset from right edge + shadow offset goldTextShadow.y = 2; // Shadow offset goldCounterContainer.addChild(goldTextShadow); // Gold text var goldText = new Text2("Gold: " + gold, { size: 40, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(1.0, 0.5); goldText.x = -30; // Offset from right edge goldText.y = 0; goldCounterContainer.addChild(goldText); // Play background music immediately when game loads LK.playMusic('acxs'); // Create and show start menu var startMenu = new StartMenu(); game.addChild(startMenu); game.update = function () { // Check if game is paused (game over screen showing) if (gamePaused) { return; // Exit update loop when game is paused } // Make sun slowly rotate sunAsset.rotation += 0.005; 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.12, currentWave); // ~20% increase per wave enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Set enemy initial position and properties - spawn at exact center of green line enemy.x = cloud2.x + 200; // Start at exact X position of green line enemy.y = 0; // Start at top of screen enemy.cellX = Math.round((enemy.x - grid.x) / CELL_SIZE); enemy.cellY = Math.round((enemy.y - grid.y) / CELL_SIZE); enemy.currentCellX = (enemy.x - grid.x) / CELL_SIZE; enemy.currentCellY = (enemy.y - grid.y) / CELL_SIZE; 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; } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Boss enemies give more gold and score var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : Math.floor(Math.random() * 11) + 10; // Random 10-20 for normal enemies setScore(score + scoreValue); // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // 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); 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) { // Show custom game over screen instead of default var gameOverScreen = new GameOverScreen(); game.addChild(gameOverScreen); // Pause the game by stopping all game logic return; // Exit update loop to pause game } } } 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(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed, bulletType) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
self.type = bulletType || 'default';
// Use tower-specific bullet asset
var assetName = 'bullet_' + self.type;
var bulletGraphics = self.attachAsset(assetName, {
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
var actualDamage = self.damage;
// Apply damage multiplier for transitioned shielded enemies
if (self.targetEnemy.damageMultiplier) {
actualDamage *= self.targetEnemy.damageMultiplier;
}
self.targetEnemy.health -= actualDamage;
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);
// Create red area effect that lasts longer
var redAreaEffect = new Container();
var redArea = redAreaEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
redArea.width = redArea.height = CELL_SIZE * 3;
redArea.tint = 0xFF0000; // Red color
redArea.alpha = 0.4;
redAreaEffect.x = self.targetEnemy.x;
redAreaEffect.y = self.targetEnemy.y;
game.addChild(redAreaEffect);
// Animate the red area effect with tween
tween(redAreaEffect, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
redAreaEffect.destroy();
}
});
// 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)
var splashDamage = self.damage * 0.5;
// Apply damage multiplier for transitioned shielded enemies
if (otherEnemy.damageMultiplier) {
splashDamage *= otherEnemy.damageMultiplier;
}
otherEnemy.health -= splashDamage;
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 === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
// Create poison area on ground
var poisonArea = new PoisonArea(self.targetEnemy.x, self.targetEnemy.y, self.damage * 0.15, 600);
game.addChild(poisonArea);
poisonAreas.push(poisonArea);
}
} 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);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
// Always rotate bullet to face the enemy perpendicularly
bulletGraphics.rotation = angle;
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
// Make rapid tower shuriken rotate while flying (in addition to directional rotation)
if (self.type === 'rapid') {
bulletGraphics.rotation += 0.3; // Additional spinning for shuriken effect
}
}
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
// debugArrows array removed
// Number label removed
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();
}
};
// removeArrows method removed
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
// Arrow cleanup removed
numberLabel.setText("-");
cellGraphics.tint = 0x880000;
return;
}
// Number display and color tinting removed
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff;
} else {
cellGraphics.tint = 0xffffff; // Set to white/transparent
}
// Arrow rendering removed
break;
}
case 1:
{
// Arrow cleanup removed
cellGraphics.tint = 0xaaaaaa;
// Number visibility removed
break;
}
case 3:
{
// Arrow cleanup removed
cellGraphics.tint = 0x008800;
// Number visibility removed
break;
}
}
// Number text setting removed
};
});
// This update method was incorrectly placed here and should be removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
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 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'explosion':
effectGraphics.tint = 0xFF4500;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 3;
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 = .025; // Increased enemy speed for faster movement
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 8;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
// Fast falling and tunnel properties
self.isFalling = true;
self.fallSpeed = 0.15; // Fast falling speed from sky
self.tunnelSpeed = 0.01; // Slow speed inside tunnels
self.groundSpeed = 0.08; // Faster walking speed on ground
self.originalSpeed = self.speed;
self.inTunnel = false;
self.lastInTunnel = 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 = 8;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 6;
self.originalMaxHealth = self.maxHealth; // Track original health for transition
self.hasTransitioned = false; // Track if already transitioned to second image
break;
case 'lizard':
self.isFlying = true;
self.maxHealth = 6;
break;
case 'swarm':
self.maxHealth = 4; // Much weaker enemies
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy_normal';
if (self.type !== 'normal') {
if (self.type === 'immune') {
// Randomly choose between two shielded enemy assets
assetId = Math.random() < 0.5 ? 'enemy_immune_asset' : 'enemy_immune2_asset';
} else if (self.type === 'lizard') {
assetId = 'enemy_lizard';
} else {
assetId = 'enemy_' + self.type + '_asset';
}
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Make lizard enemies semi-transparent
if (self.type === 'lizard') {
enemyGraphics.alpha = 0.7;
}
// 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 shadowAssetId = self.type === 'lizard' ? 'enemy_lizard' : assetId || 'enemy_normal';
var shadowGraphics = self.shadow.attachAsset(shadowAssetId, {
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;
} else {
// Update health bar width based on current health
self.healthBar.width = self.health / self.maxHealth * 70;
}
// Handle shielded enemy transition when health drops to half
if (self.type === 'immune' && !self.hasTransitioned && self.health <= self.originalMaxHealth / 2) {
self.hasTransitioned = true;
// Change to second shielded enemy asset
var newAssetId = Math.random() < 0.5 ? 'enemy_immune_asset' : 'enemy_immune2_asset';
// Remove old graphics
if (enemyGraphics && enemyGraphics.parent) {
enemyGraphics.parent.removeChild(enemyGraphics);
}
// Create new graphics with second asset
enemyGraphics = self.attachAsset(newAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale up boss enemies
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Make enemy take double damage from now on
self.damageMultiplier = 2;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
} 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.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
// Combine poison (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.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
var GameOverScreen = Container.expand(function () {
var self = Container.call(this);
// Pause the game when game over screen shows
gamePaused = true;
// Update highest score in storage
var currentHighest = storage.highestScore || 0;
if (score > currentHighest) {
storage.highestScore = score;
}
// Hide score and gold counters from GUI
if (scoreCounterContainer && scoreCounterContainer.parent) {
scoreCounterContainer.parent.removeChild(scoreCounterContainer);
}
if (goldCounterContainer && goldCounterContainer.parent) {
goldCounterContainer.parent.removeChild(goldCounterContainer);
}
// Create full screen background overlay
var overlay = self.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
overlay.width = 2048;
overlay.height = 2732;
overlay.x = 0;
overlay.y = 0;
overlay.tint = 0x1a0f3d; // Deep purple background for more color
overlay.alpha = 0.9;
// Background elements removed for cleaner game over screen
// Create main menu container
var menuContainer = new Container();
self.addChild(menuContainer);
// Add game over image behind all text elements (moved after menu container creation)
var gameOverImage = self.attachAsset('game_over_image', {
anchorX: 0.5,
anchorY: 0.5
});
gameOverImage.x = 2048 / 2;
gameOverImage.y = 2732 / 2 - 200;
gameOverImage.alpha = 0.3; // Make it more subtle so text is readable
// Animate the game over image appearing
tween(gameOverImage, {
alpha: 0.6,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(gameOverImage, {
scaleX: 1,
scaleY: 1
}, {
duration: 500,
easing: tween.easeInOut
});
}
});
// Game Over title shadow
var titleShadow = new Text2("GAME OVER", {
size: 120,
fill: 0x000000,
weight: 800
});
titleShadow.anchor.set(0.5, 0.5);
titleShadow.x = 6;
titleShadow.y = -300 + 6;
menuContainer.addChild(titleShadow);
// Game Over title
var titleText = new Text2("GAME OVER", {
size: 120,
fill: 0xFF0000,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -300;
menuContainer.addChild(titleText);
// Add pulsing animation to title
self.animateTitlePulse = function () {
var pulseScale = 1 + Math.sin(LK.ticks * 0.05) * 0.1;
titleText.scaleX = pulseScale;
titleText.scaleY = pulseScale;
titleShadow.scaleX = pulseScale;
titleShadow.scaleY = pulseScale;
};
// Final score display shadow
var scoreShadow = new Text2("Final Score: " + score, {
size: 80,
fill: 0x000000,
weight: 800
});
scoreShadow.anchor.set(0.5, 0.5);
scoreShadow.x = 4;
scoreShadow.y = -150 + 4;
menuContainer.addChild(scoreShadow);
// Final score display
var scoreDisplay = new Text2("Final Score: " + score, {
size: 80,
fill: 0x00FF00,
weight: 800
});
scoreDisplay.anchor.set(0.5, 0.5);
scoreDisplay.y = -150;
menuContainer.addChild(scoreDisplay);
// Add floating animation to score display
self.animateScoreFloat = function () {
var floatOffset = Math.sin(LK.ticks * 0.03) * 5;
scoreDisplay.y = -150 + floatOffset;
scoreShadow.y = -150 + 4 + floatOffset;
};
// Final gold display shadow
var goldShadow = new Text2("Gold Earned: " + gold, {
size: 80,
fill: 0x000000,
weight: 800
});
goldShadow.anchor.set(0.5, 0.5);
goldShadow.x = 4;
goldShadow.y = -50 + 4;
menuContainer.addChild(goldShadow);
// Final gold display
var goldDisplay = new Text2("Gold Earned: " + gold, {
size: 80,
fill: 0xFFD700,
weight: 800
});
goldDisplay.anchor.set(0.5, 0.5);
goldDisplay.y = -50;
menuContainer.addChild(goldDisplay);
// Add floating animation to gold display with different phase
self.animateGoldFloat = function () {
var floatOffset = Math.sin(LK.ticks * 0.025 + Math.PI) * 4;
goldDisplay.y = -50 + floatOffset;
goldShadow.y = -50 + 4 + floatOffset;
};
// Restart button background
var restartButton = new Container();
var restartButtonBg = restartButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
restartButtonBg.width = 600;
restartButtonBg.height = 150;
restartButtonBg.tint = 0x00AA00;
// Restart button text shadow
var restartTextShadow = new Text2("PLAY AGAIN", {
size: 70,
fill: 0x000000,
weight: 800
});
restartTextShadow.anchor.set(0.5, 0.5);
restartTextShadow.x = 4;
restartTextShadow.y = 4;
restartButton.addChild(restartTextShadow);
// Restart button text
var restartText = new Text2("PLAY AGAIN", {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
restartText.anchor.set(0.5, 0.5);
restartButton.addChild(restartText);
restartButton.y = 100;
menuContainer.addChild(restartButton);
// Position menu container at center of screen
menuContainer.x = 2048 / 2;
menuContainer.y = 2732 / 2;
// Add button hover animation
self.animateRestartButton = function () {
var hoverScale = 1 + Math.sin(LK.ticks * 0.04) * 0.03;
restartButton.scaleX = hoverScale;
restartButton.scaleY = hoverScale;
};
// Restart button functionality
restartButton.down = function () {
// Add click animation before reloading
tween(restartButton, {
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(restartButton, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
// Restart the game by reloading
location.reload();
}
});
}
});
};
// Update method to run all animations
self.update = function () {
self.animateTitlePulse();
self.animateScoreFloat();
self.animateGoldFloat();
self.animateRestartButton();
};
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;
// Set spawn areas based on first waypoint (top right corner)
if (i === 23 && j >= 4 && j <= 7) {
cellType = 2;
self.spawns.push(cell);
} else if (i >= 20 && i <= 23 && j >= 28 && j <= 31) {
// Set goal area based on extended final waypoint (bottom right corner)
cellType = 3;
self.goals.push(cell);
}
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) {
// Only create debug cell for non-visual debugging, no graphics
var debugCell = new DebugCell();
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
// Don't add to visual display
}
}
}
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 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) {
// Define the complete ordered sequence of lines from green top to purple bottom right
var lineSequence = [
// 1. Green line (vertical at cloud2.x + 200) - start from top
{
type: 'vertical',
x: cloud2.x + 200,
startY: 0,
endY: 630,
centerX: cloud2.x + 200,
color: 'green'
},
// 2. Pink line (horizontal) - from green to purple
{
type: 'horizontal',
y: 630,
startX: cloud2.x + 200,
endX: 1350,
centerY: 630,
color: 'pink'
},
// 3. Purple line (vertical) - from pink to red
{
type: 'vertical',
x: 1350,
startY: 630,
endY: 900,
centerX: 1350,
color: 'purple'
},
// 4. Red horizontal line - from purple to brown
{
type: 'horizontal',
y: 900,
startX: 1350,
endX: 450,
centerY: 900,
color: 'red'
},
// 5. Brown line (vertical) - from red to blue
{
type: 'vertical',
x: 450,
startY: 900,
endY: 1340,
centerX: 450,
color: 'brown'
},
// 6. Blue horizontal line - from brown to yellow
{
type: 'horizontal',
y: 1340,
startX: 450,
endX: 1350,
centerY: 1340,
color: 'blue'
},
// 7. Yellow line (vertical) - from blue to green horizontal
{
type: 'vertical',
x: 1350,
startY: 1340,
endY: 1700,
centerX: 1350,
color: 'yellow'
},
// 8. Green horizontal line - from yellow to cyan
{
type: 'horizontal',
y: 1700,
startX: 1350,
endX: 450,
centerY: 1700,
color: 'green_horizontal'
},
// 9. Cyan line (vertical) - from green horizontal to magenta
{
type: 'vertical',
x: 450,
startY: 1700,
endY: 2100,
centerX: 450,
color: 'cyan'
},
// 10. Final magenta horizontal line - end at right
{
type: 'horizontal',
y: 2100,
startX: 450,
endX: 450 + 980,
centerY: 2100,
color: 'magenta'
}];
// Helper function to get the next line in sequence
function getNextLineInSequence(enemy) {
if (!enemy.currentLineIndex && enemy.currentLineIndex !== 0) {
enemy.currentLineIndex = 0;
return lineSequence[0];
}
var nextIndex = enemy.currentLineIndex + 1;
if (nextIndex < lineSequence.length) {
enemy.currentLineIndex = nextIndex;
return lineSequence[nextIndex];
}
return null;
}
// Helper function to check if enemy is at the end of current line
function isAtLineEnd(enemy, line, tolerance) {
tolerance = tolerance || 5; // Reduced default tolerance from 15 to 5
if (line.type === 'vertical') {
var atStartY = Math.abs(enemy.y - line.startY) < tolerance;
var atEndY = Math.abs(enemy.y - line.endY) < tolerance;
var onLineX = Math.abs(enemy.x - line.x) < tolerance;
return onLineX && (atStartY || atEndY);
} else {
var atStartX = Math.abs(enemy.x - line.startX) < tolerance;
var atEndX = Math.abs(enemy.x - line.endX) < tolerance;
var onLineY = Math.abs(enemy.y - line.y) < tolerance;
return onLineY && (atStartX || atEndX);
}
}
// Initialize enemy if needed
if (!enemy.currentLine) {
enemy.currentLineIndex = 0;
enemy.currentLine = lineSequence[0]; // Start with green line
enemy.targetPoint = {
x: lineSequence[0].centerX,
y: lineSequence[0].endY
}; // Move down the green line to pink connection
}
// Check if enemy should stop at black line end
if (enemy.currentLine && enemy.currentLine.color && enemy.currentLine.color.indexOf('black') !== -1) {
// Check if enemy reached the end of any black line
var atBlackLineEnd = false;
if (enemy.currentLine.type === 'horizontal') {
// For horizontal black lines, check if at either end
var atStartX = Math.abs(enemy.x - enemy.currentLine.startX) < 5;
var atEndX = Math.abs(enemy.x - enemy.currentLine.endX) < 5;
var onLineY = Math.abs(enemy.y - enemy.currentLine.centerY) < 5;
atBlackLineEnd = onLineY && (atStartX || atEndX);
} else if (enemy.currentLine.type === 'vertical') {
// For vertical black lines, check if at either end
var atStartY = Math.abs(enemy.y - enemy.currentLine.startY) < 5;
var atEndY = Math.abs(enemy.y - enemy.currentLine.endY) < 5;
var onLineX = Math.abs(enemy.x - enemy.currentLine.centerX) < 5;
atBlackLineEnd = onLineX && (atStartY || atEndY);
}
if (atBlackLineEnd) {
// Enemy has reached the end of a black line, transition to next line
var nextLine = getNextLineInSequence(enemy);
if (nextLine) {
enemy.currentLine = nextLine;
// Determine the direction to move on the new line
if (enemy.currentLine.type === 'vertical') {
// For vertical lines, determine direction based on previous line connection
if (enemy.y < enemy.currentLine.startY + (enemy.currentLine.endY - enemy.currentLine.startY) / 2) {
// If connecting from top half, move down
enemy.targetPoint = {
x: enemy.currentLine.centerX,
y: enemy.currentLine.endY
};
} else {
// If connecting from bottom half, move up
enemy.targetPoint = {
x: enemy.currentLine.centerX,
y: enemy.currentLine.startY
};
}
} else {
// For horizontal lines, determine direction based on line sequence
if (enemy.currentLine.color === 'pink') {
// Pink line: move right from green to purple
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else if (enemy.currentLine.color === 'red') {
// Red line: move left from purple to brown
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else if (enemy.currentLine.color === 'blue') {
// Blue line: move right from brown to yellow
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else if (enemy.currentLine.color === 'green_horizontal') {
// Green horizontal line: move left from yellow to cyan
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else if (enemy.currentLine.color === 'magenta') {
// Final magenta line: move right to end
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else {
// Default behavior for any other horizontal lines
var nextVerticalLine = null;
if (enemy.currentLineIndex + 1 < lineSequence.length) {
nextVerticalLine = lineSequence[enemy.currentLineIndex + 1];
}
if (nextVerticalLine && nextVerticalLine.type === 'vertical') {
// Move towards the next vertical line's X position
if (nextVerticalLine.centerX > enemy.currentLine.startX) {
// Next vertical line is to the right, move right
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else {
// Next vertical line is to the left, move left
enemy.targetPoint = {
x: enemy.currentLine.startX,
y: enemy.currentLine.centerY
};
}
} else {
// Fallback to original logic if no next vertical line
if (enemy.x < enemy.currentLine.startX + (enemy.currentLine.endX - enemy.currentLine.startX) / 2) {
// If connecting from left half, move right
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else {
// If connecting from right half, move left
enemy.targetPoint = {
x: enemy.currentLine.startX,
y: enemy.currentLine.centerY
};
}
}
}
}
} else {
// No more lines in sequence, enemy stops
return false;
}
}
}
// Check if enemy reached target point
var dx = enemy.targetPoint.x - enemy.x;
var dy = enemy.targetPoint.y - enemy.y;
var distanceToTarget = Math.sqrt(dx * dx + dy * dy);
if (distanceToTarget < 5) {
// Reduced tolerance from 15 to 5 for more precise line following
// Enemy reached current target, check if at line end
if (isAtLineEnd(enemy, enemy.currentLine)) {
// At line end, get next line in sequence
var nextLine = getNextLineInSequence(enemy);
if (nextLine) {
enemy.currentLine = nextLine;
// Continue to next line normally
{
// Determine the direction to move on the new line
if (enemy.currentLine.type === 'vertical') {
// For vertical lines, determine direction based on previous line connection
if (enemy.y < enemy.currentLine.startY + (enemy.currentLine.endY - enemy.currentLine.startY) / 2) {
// If connecting from top half, move down
enemy.targetPoint = {
x: enemy.currentLine.centerX,
y: enemy.currentLine.endY
};
} else {
// If connecting from bottom half, move up
enemy.targetPoint = {
x: enemy.currentLine.centerX,
y: enemy.currentLine.startY
};
}
} else {
// For horizontal lines, determine direction based on line sequence
if (enemy.currentLine.color === 'pink') {
// Pink line: move right from green to purple
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else if (enemy.currentLine.color === 'red') {
// Red line: move left from purple to brown
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else if (enemy.currentLine.color === 'blue') {
// Blue line: move right from brown to yellow
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else if (enemy.currentLine.color === 'green_horizontal') {
// Green horizontal line: move left from yellow to cyan
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else if (enemy.currentLine.color === 'magenta') {
// Final magenta line: move right to end
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else {
// Default behavior for any other horizontal lines
var nextVerticalLine = null;
if (enemy.currentLineIndex + 1 < lineSequence.length) {
nextVerticalLine = lineSequence[enemy.currentLineIndex + 1];
}
if (nextVerticalLine && nextVerticalLine.type === 'vertical') {
// Move towards the next vertical line's X position
if (nextVerticalLine.centerX > enemy.currentLine.startX) {
// Next vertical line is to the right, move right
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else {
// Next vertical line is to the left, move left
enemy.targetPoint = {
x: enemy.currentLine.startX,
y: enemy.currentLine.centerY
};
}
} else {
// Fallback to original logic if no next vertical line
if (enemy.x < enemy.currentLine.startX + (enemy.currentLine.endX - enemy.currentLine.startX) / 2) {
// If connecting from left half, move right
enemy.targetPoint = {
x: enemy.currentLine.endX,
y: enemy.currentLine.centerY
};
} else {
// If connecting from right half, move left
enemy.targetPoint = {
x: enemy.currentLine.startX,
y: enemy.currentLine.centerY
};
}
}
}
}
}
// Remove tween animation - let normal movement handle line transitions
} else {
// Enemy has completed the full path sequence
if (enemy.currentLine && enemy.currentLine.color === 'magenta') {
// Check if enemy reached the right end of the magenta line specifically
var atRightEnd = Math.abs(enemy.x - enemy.currentLine.endX) < 5;
if (atRightEnd) {
// Enemy reached the right end of the magenta line - trigger game loss
var gameOverScreen = new GameOverScreen();
game.addChild(gameOverScreen);
return true; // Game over condition - enemy reached the end
}
}
// No more lines in sequence, enemy continues past the path
return true;
}
} else {
// Not at line end, continue along current line
if (enemy.currentLine.type === 'vertical') {
// Move to other end of vertical line
var newTargetY = Math.abs(enemy.y - enemy.currentLine.startY) < Math.abs(enemy.y - enemy.currentLine.endY) ? enemy.currentLine.endY : enemy.currentLine.startY;
enemy.targetPoint = {
x: enemy.currentLine.centerX,
y: newTargetY
};
} else {
// Move to other end of horizontal line
var newTargetX = Math.abs(enemy.x - enemy.currentLine.startX) < Math.abs(enemy.x - enemy.currentLine.endX) ? enemy.currentLine.endX : enemy.currentLine.startX;
enemy.targetPoint = {
x: newTargetX,
y: enemy.currentLine.centerY
};
}
}
}
// Move towards target point - ensure precise line following
if (distanceToTarget > 1) {
var moveSpeed = enemy.speed * 60; // Speed multiplier for movement on all lines
// Calculate normalized movement direction
var moveX = dx / distanceToTarget * moveSpeed;
var moveY = dy / distanceToTarget * moveSpeed;
// Track previous movement direction for rotation
if (!enemy.lastMoveX) enemy.lastMoveX = 0;
if (!enemy.lastMoveY) enemy.lastMoveY = 0;
// For precise line centering, lock position to line axis
if (enemy.currentLine) {
if (enemy.currentLine.type === 'vertical') {
// Lock X position to exact line center for vertical lines
enemy.x = enemy.currentLine.centerX;
// Use normalized movement speed for consistent speed on all lines
enemy.y += moveY;
// Keep enemy upright when moving vertically (maintain current horizontal flip)
if (enemy.children[0]) {
enemy.children[0].rotation = 0; // Always upright for vertical movement
// Maintain current horizontal flip state (don't change scaleX)
}
} else {
// Lock Y position to exact line center for horizontal lines
enemy.y = enemy.currentLine.centerY;
// For all horizontal lines, move along X axis using next vertical line direction logic
enemy.x += moveX;
// Flip enemy image horizontally only when turning right (stay upright)
if (enemy.children[0]) {
if (moveX > 0) {
// Moving right - flip horizontally (face right)
enemy.children[0].scaleX = -Math.abs(enemy.children[0].scaleX);
} else {
// Moving left - face normal (no flip)
enemy.children[0].scaleX = Math.abs(enemy.children[0].scaleX);
}
}
}
} else {
// Normal movement if no current line
enemy.x += moveX;
enemy.y += moveY;
}
// Store current movement for next frame
enemy.lastMoveX = moveX;
enemy.lastMoveY = moveY;
}
// Update cell coordinates based on actual position
enemy.currentCellX = (enemy.x - grid.x) / CELL_SIZE;
enemy.currentCellY = (enemy.y - grid.y) / CELL_SIZE;
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
// Update shadow position for flying enemies
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 horizontal flip with enemy flip
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].scaleX = enemy.children[0].scaleX;
enemy.shadow.children[0].rotation = 0; // Shadow always upright
}
}
return false; // Enemy continues moving
};
});
var Mine = Container.expand(function () {
var self = Container.call(this);
var mineGraphics = self.attachAsset('mine', {
anchorX: 0.5,
anchorY: 0.5
});
self.activated = false;
self.gridX = 0;
self.gridY = 0;
// Animate the mine with a pulsing effect
self.pulseDirection = 1;
self.pulseScale = 1.0;
self.update = function () {
// Pulsing animation
self.pulseScale += self.pulseDirection * 0.02;
if (self.pulseScale >= 1.3) {
self.pulseDirection = -1;
} else if (self.pulseScale <= 0.9) {
self.pulseDirection = 1;
}
mineGraphics.scaleX = self.pulseScale;
mineGraphics.scaleY = self.pulseScale;
// Check for enemy collision
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 < 40 && !self.activated) {
// Close enough to trigger
self.explode(enemy);
break;
}
}
};
self.explode = function (triggerEnemy) {
if (self.activated) return;
self.activated = true;
// Create explosion effect
var explosion = new EffectIndicator(self.x, self.y, 'explosion');
game.addChild(explosion);
// Deal massive damage to trigger enemy
var triggerDamage = 200; // Further reduced massive damage
// Apply damage multiplier for transitioned shielded enemies
if (triggerEnemy.damageMultiplier) {
triggerDamage *= triggerEnemy.damageMultiplier;
}
triggerEnemy.health -= triggerDamage;
if (triggerEnemy.health <= 0) {
triggerEnemy.health = 0;
} else {
triggerEnemy.healthBar.width = triggerEnemy.health / triggerEnemy.maxHealth * 70;
}
// Deal splash damage to nearby enemies
var splashRadius = CELL_SIZE * 2;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy !== triggerEnemy) {
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= splashRadius) {
var mineSplashDamage = 150; // Further reduced splash damage
// Apply damage multiplier for transitioned shielded enemies
if (enemy.damageMultiplier) {
mineSplashDamage *= enemy.damageMultiplier;
}
enemy.health -= mineSplashDamage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
}
}
}
// Remove mine after explosion
self.destroy();
var mineIndex = mines.indexOf(self);
if (mineIndex !== -1) {
mines.splice(mineIndex, 1);
}
};
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;
};
return self;
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300;
buttonBackground.height = 100;
buttonBackground.tint = 0x0088FF;
var buttonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 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 PoisonArea = Container.expand(function (x, y, damage, duration) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.damage = damage || 5; // Damage per tick
self.duration = duration || 600; // 10 seconds at 60 FPS
self.tickDamage = 30; // Frames between damage ticks (0.5 seconds)
self.lastDamage = 0;
// Create visual poison area effect
var poisonGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
poisonGraphics.width = poisonGraphics.height = CELL_SIZE * 1.5;
poisonGraphics.tint = 0x00AA00; // Green poison color
poisonGraphics.alpha = 0.4;
self.update = function () {
self.duration--;
// Apply damage to enemies in the area every 30 ticks
if (LK.ticks - self.lastDamage >= self.tickDamage) {
self.lastDamage = LK.ticks;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy.isImmune) {
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 0.75) {
// Poison area radius
// Apply poison damage
var actualDamage = self.damage;
if (enemy.damageMultiplier) {
actualDamage *= enemy.damageMultiplier;
}
enemy.health -= actualDamage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Visual effect on enemy
var poisonEffect = new EffectIndicator(enemy.x, enemy.y, 'poison');
game.addChild(poisonEffect);
}
}
}
}
// Remove area when duration expires
if (self.duration <= 0) {
self.destroy();
var areaIndex = poisonAreas.indexOf(self);
if (areaIndex !== -1) {
poisonAreas.splice(areaIndex, 1);
}
}
// Animate the poison area with pulsing effect
poisonGraphics.alpha = 0.3 + Math.sin(LK.ticks * 0.1) * 0.1;
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'default';
// Increase size of base for easier touch - use tower-specific asset
var assetName = 'tower_' + (self.towerType === 'default' ? 'default' : self.towerType);
var baseGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3,
y: -30 // Move tower image up
});
// No need for tinting since each tower type has its own colored asset
var towerCost = getTowerCost(self.towerType);
// Get display name for tower type
var displayName;
if (self.towerType === 'slow') {
displayName = 'Mine';
} else if (self.towerType === 'default') {
displayName = 'Rookie';
} else if (self.towerType === 'rapid') {
displayName = 'Ninja';
} else {
displayName = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
}
// Add shadow for tower type label
var typeLabelShadow = new Text2(displayName, {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = 40 + 4; // Move tower name further down
self.addChild(typeLabelShadow);
// Add tower type label
var typeLabel = new Text2(displayName, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = 40; // Move tower name further down
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 = 84 + 12; // Move cost label further down
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 = 80 + 12; // Move cost label further down
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var StartMenu = Container.expand(function () {
var self = Container.call(this);
// Create full screen background overlay
var overlay = self.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
overlay.width = 2048;
overlay.height = 2732;
overlay.x = 0;
overlay.y = 0;
overlay.tint = 0x001122;
overlay.alpha = 0.8;
// Background elements removed for cleaner start menu
// Create main menu container
var menuContainer = new Container();
self.addChild(menuContainer);
// Title shadow
var titleShadow = new Text2("Defense The Queen", {
size: 120,
fill: 0x000000,
weight: 800
});
titleShadow.anchor.set(0.5, 0.5);
titleShadow.x = 6;
titleShadow.y = -350 + 6;
menuContainer.addChild(titleShadow);
// Main title
var titleText = new Text2("Defense The Queen", {
size: 120,
fill: 0xFFD700,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -350;
menuContainer.addChild(titleText);
// Start button background
var startButton = new Container();
var startButtonBg = startButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startButtonBg.width = 600;
startButtonBg.height = 150;
startButtonBg.tint = 0x00AA00;
// Start button text shadow
var startTextShadow = new Text2("START GAME", {
size: 70,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 4;
startTextShadow.y = 4;
startButton.addChild(startTextShadow);
// Start Game title
var startGameTitle = new Text2("Start Game", {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
startGameTitle.anchor.set(0.5, 0.5);
startGameTitle.y = -250;
menuContainer.addChild(startGameTitle);
// Start button text
var startText = new Text2("START GAME", {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startButton.addChild(startText);
startButton.y = -100;
menuContainer.addChild(startButton);
// Instructions text
var instructionsText = new Text2("Prevent the enemies from reaching the queen to protect the royal treasure.", {
size: 50,
fill: 0xFFFFFF,
weight: 400
});
instructionsText.anchor.set(0.5, 0.5);
instructionsText.y = 100;
menuContainer.addChild(instructionsText);
// Get highest score from storage
var highestScore = storage.highestScore || 0;
// Highest score display shadow
var highScoreShadow = new Text2("Highest Score: " + highestScore, {
size: 60,
fill: 0x000000,
weight: 800
});
highScoreShadow.anchor.set(0.5, 0.5);
highScoreShadow.x = 4;
highScoreShadow.y = 200 + 4;
menuContainer.addChild(highScoreShadow);
// Highest score display
var highScoreText = new Text2("Highest Score: " + highestScore, {
size: 60,
fill: 0xFFD700,
weight: 800
});
highScoreText.anchor.set(0.5, 0.5);
highScoreText.y = 200;
menuContainer.addChild(highScoreText);
// Credits text
var creditsText = new Text2("Made with FRVR", {
size: 40,
fill: 0xCCCCCC,
weight: 400
});
creditsText.anchor.set(0.5, 0.5);
creditsText.y = 300;
menuContainer.addChild(creditsText);
// Position menu container at center of screen
menuContainer.x = 2048 / 2;
menuContainer.y = 2732 / 2;
// Start button functionality
startButton.down = function () {
// Start playing background music immediately when button is pressed
LK.playMusic('acxs');
// Add score and gold counters to GUI when game starts
LK.gui.right.addChild(scoreCounterContainer);
LK.gui.right.addChild(goldCounterContainer);
// Hide the start menu with animation
tween(self, {
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
// Start countdown sequence
var countdownContainer = new Container();
game.addChild(countdownContainer);
countdownContainer.x = 2048 / 2;
countdownContainer.y = 2732 / 2;
// Create countdown text
var countdownText = new Text2("3", {
size: 300,
fill: 0xFFFFFF,
weight: 800
});
countdownText.anchor.set(0.5, 0.5);
countdownContainer.addChild(countdownText);
// Create countdown shadow
var countdownShadow = new Text2("3", {
size: 300,
fill: 0x000000,
weight: 800
});
countdownShadow.anchor.set(0.5, 0.5);
countdownShadow.x = 8;
countdownShadow.y = 8;
countdownContainer.addChild(countdownShadow);
var countdownNumbers = ["3", "2", "1", "GO!"];
var currentCountdown = 0;
function showNextCountdown() {
if (currentCountdown < countdownNumbers.length) {
var number = countdownNumbers[currentCountdown];
countdownText.setText(number);
countdownShadow.setText(number);
// Scale animation for each countdown number
countdownContainer.scaleX = 0.5;
countdownContainer.scaleY = 0.5;
countdownContainer.alpha = 0;
tween(countdownContainer, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 1
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(countdownContainer, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
currentCountdown++;
if (currentCountdown < countdownNumbers.length) {
LK.setTimeout(showNextCountdown, 500);
} else {
// Countdown finished, start the game
tween(countdownContainer, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
countdownContainer.destroy();
// Auto-start the game
if (waveIndicator) {
waveIndicator.gameStarted = true;
currentWave = 1; // Start with wave 1
waveTimer = 0; // Start wave immediately
waveInProgress = true; // Begin the first wave
waveSpawned = false; // Mark that we need to spawn enemies
var notification = game.addChild(new Notification("Game started! Wave 1 beginning!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
});
}
}
});
}
});
} else {
countdownContainer.destroy();
}
}
// Start the countdown
showNextCountdown();
}
});
};
// Update method for animations
self.update = function () {
// Animation methods removed
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 3;
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 7, +1.2 per level, huge boost at max level
if (self.level === self.maxLevel) {
return 20 * CELL_SIZE; // Massive range for max level
}
return (7 + (self.level - 1) * 1.2) * CELL_SIZE;
case 'splash':
// Splash: base 3.5, +0.4 per level
return (3.5 + (self.level - 1) * 0.4) * CELL_SIZE;
case 'rapid':
// Rapid: base 4, +0.7 per level
return (4 + (self.level - 1) * 0.7) * CELL_SIZE;
case 'slow':
// Slow (Mine): base 3, +0.5 per level - increased range for mines
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Poison: base 4.5, +0.7 per level
return (4.5 + (self.level - 1) * 0.7) * CELL_SIZE;
default:
// Default: base 4.5, +0.7 per level
return (4.5 + (self.level - 1) * 0.7) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 35;
self.lastFired = 0;
self.targetEnemy = null;
switch (self.id) {
case 'rapid':
self.fireRate = 30;
self.damage = 15;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90;
self.damage = 50;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
//{g2}
self.fireRate = 180; // Much slower fire rate for splash tower
self.damage = 30; // Increased damage so enemies need at least 3 hits
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 50;
self.damage = 20;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70;
self.damage = 25;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
// Apply level 1 upgrades immediately for consistent scaling
if (self.level === 1) {
// Apply the upgrade scaling for level 1 to ensure consistency
if (self.id === 'rapid') {
self.fireRate = Math.max(10, 30 - self.level * 5);
self.damage = 15 + self.level * 12;
self.bulletSpeed = 7 + self.level * 1.2;
} else if (self.id === 'sniper') {
self.fireRate = Math.max(30, 90 - self.level * 12);
self.damage = 50 + self.level * 25;
self.bulletSpeed = 25 + self.level * 3;
} else if (self.id === 'splash') {
self.fireRate = Math.max(60, 180 - self.level * 20);
self.damage = 30 + self.level * 20;
self.bulletSpeed = 4 + self.level * 1;
} else {
self.fireRate = Math.max(15, 60 - self.level * 12);
self.damage = 35 + self.level * 20;
self.bulletSpeed = 5 + self.level * 1.5;
}
}
// Use tower-specific asset based on tower type
var assetName = 'tower_' + (self.id === 'default' ? 'default' : self.id);
var baseGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// No need for tinting since each tower type has its own colored asset
var levelIndicators = [];
var maxStars = self.maxLevel;
var starSpacing = baseGraphics.width / (maxStars + 1);
var starSize = CELL_SIZE / 5;
for (var i = 0; i < maxStars; i++) {
var star = new Container();
// Create star shape using multiple triangles to form a 5-pointed star
var starGraphics = star.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
starGraphics.width = starSize;
starGraphics.height = starSize;
starGraphics.tint = 0x444444; // Dark gray for unfilled stars
// Position stars in a row below the tower
star.x = -CELL_SIZE + starSpacing * (i + 1);
star.y = CELL_SIZE * 0.7;
self.addChild(star);
levelIndicators.push(star);
}
// Remove gun graphics - towers now only show their base images
self.updateLevelIndicators = function () {
for (var i = 0; i < maxStars; i++) {
var star = levelIndicators[i];
var starGraphics = star.children[0];
if (i < self.level) {
// Filled gold star for achieved levels
starGraphics.tint = 0xFFD700; // Gold color
// Add slight animation to make stars more prominent
tween(starGraphics, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(starGraphics, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
} else {
// Empty dark star for unachieved levels
starGraphics.tint = 0x444444; // Dark gray
}
}
};
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 (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type with enhanced scaling
if (self.id === 'rapid') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (triple the effect)
self.fireRate = Math.max(3, 30 - self.level * 12); // Much faster firing
self.damage = 15 + self.level * 25; // Higher damage
self.bulletSpeed = 7 + self.level * 3; // Much faster bullets
} else {
self.fireRate = Math.max(10, 30 - self.level * 5); // Faster firing rate scaling
self.damage = 15 + self.level * 12; // Better damage scaling
self.bulletSpeed = 7 + self.level * 1.2; // Better speed scaling
}
} else if (self.id === 'sniper') {
if (self.level === self.maxLevel) {
// Sniper gets massive damage boost at max level
self.fireRate = Math.max(8, 90 - self.level * 15); // Faster sniping
self.damage = 50 + self.level * 60; // Huge damage increase
self.bulletSpeed = 25 + self.level * 8; // Much faster bullets
} else {
self.fireRate = Math.max(30, 90 - self.level * 12);
self.damage = 50 + self.level * 25;
self.bulletSpeed = 25 + self.level * 3;
}
} else if (self.id === 'splash') {
if (self.level === self.maxLevel) {
// Splash gets area damage boost
self.fireRate = Math.max(20, 180 - self.level * 35); // Much faster splash attacks
self.damage = 30 + self.level * 45; // High damage for area effect
self.bulletSpeed = 4 + self.level * 2.5;
} else {
self.fireRate = Math.max(60, 180 - self.level * 20);
self.damage = 30 + self.level * 20;
self.bulletSpeed = 4 + self.level * 1;
}
} else {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for all other towers (triple the effect)
self.fireRate = Math.max(5, 60 - self.level * 30); // Much faster firing
self.damage = 35 + self.level * 50; // Much higher damage
self.bulletSpeed = 5 + self.level * 3.5; // Much faster bullets
} else {
self.fireRate = Math.max(15, 60 - self.level * 12); // Better fire rate scaling
self.damage = 35 + self.level * 20; // Better damage scaling
self.bulletSpeed = 5 + self.level * 1.5; // Better speed scaling
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[0];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
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) {
// Lizard enemies can only be targeted by area/splash towers
if (enemy.type === 'lizard' && self.id !== 'splash') {
// Skip lizard enemies for non-splash towers
continue;
}
// 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 () {
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);
// Rotate the entire tower to face the enemy
baseGraphics.rotation = angle;
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
game.addChild(rangeIndicator);
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
// Add actual tower asset preview on top of range circle
var assetName = 'tower_' + (self.id === 'default' ? 'default' : self.id);
var towerPreviewGraphics = rangeIndicator.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
towerPreviewGraphics.alpha = 0.7;
towerPreviewGraphics.scaleX = 1.2;
towerPreviewGraphics.scaleY = 1.2;
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(baseGraphics.rotation) * 40;
var bulletY = self.y + Math.sin(baseGraphics.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed, self.id);
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// Remove recoil animation - towers now only show base images
}
}
};
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.3;
// Use actual tower asset instead of generic preview
var assetName = 'tower_' + (self.towerType === 'default' ? 'default' : self.towerType);
var previewGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale tower preview to be slightly larger for better visibility
previewGraphics.scaleX = 1.3;
previewGraphics.scaleY = 1.3;
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateTowerGraphics = function () {
// Remove old graphics
if (previewGraphics && previewGraphics.parent) {
previewGraphics.parent.removeChild(previewGraphics);
}
// Create new graphics with correct tower asset
var assetName = 'tower_' + (self.towerType === 'default' ? 'default' : self.towerType);
previewGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale tower preview to be slightly larger for better visibility
previewGraphics.scaleX = 1.3;
previewGraphics.scaleY = 1.3;
};
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;
// Use white tint for normal state since towers have their own colors
previewGraphics.tint = 0xFFFFFF;
// Only tint red if cannot place or not enough gold
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
// Special placement rules for mines
if (self.towerType === 'slow') {
// slow tower is mine
// Mines can only be placed on path lines
var isOnPath = false;
var cellX = self.gridX;
var cellY = self.gridY;
var cellWorldX = grid.x + cellX * CELL_SIZE + CELL_SIZE / 2;
var cellWorldY = grid.y + cellY * CELL_SIZE + CELL_SIZE / 2;
// Check if position is on any of the path lines
var tolerance = CELL_SIZE / 2;
// Check vertical lines
var verticalLines = [{
x: cloud2.x + 200,
startY: 0,
endY: 630
},
// Green line
{
x: 1350,
startY: 630,
endY: 900
},
// Purple line
{
x: 450,
startY: 900,
endY: 1340
},
// Brown line
{
x: 1350,
startY: 1340,
endY: 1700
},
// Yellow line
{
x: 450,
startY: 1700,
endY: 2100
} // Cyan line
];
// Check horizontal lines
var horizontalLines = [{
y: 630,
startX: cloud2.x + 200,
endX: 1350
},
// Pink line
{
y: 900,
startX: 1350,
endX: 450
},
// Red line
{
y: 1340,
startX: 450,
endX: 1350
},
// Blue line
{
y: 1700,
startX: 1350,
endX: 450
},
// Green horizontal line
{
y: 2100,
startX: 450,
endX: 450 + 980
} // Magenta line
];
// Check if on vertical line
for (var i = 0; i < verticalLines.length; i++) {
var line = verticalLines[i];
if (Math.abs(cellWorldX - line.x) < tolerance && cellWorldY >= line.startY - tolerance && cellWorldY <= line.endY + tolerance) {
isOnPath = true;
break;
}
}
// Check if on horizontal line
if (!isOnPath) {
for (var i = 0; i < horizontalLines.length; i++) {
var line = horizontalLines[i];
if (Math.abs(cellWorldY - line.y) < tolerance && cellWorldX >= Math.min(line.startX, line.endX) - tolerance && cellWorldX <= Math.max(line.startX, line.endX) + tolerance) {
isOnPath = true;
break;
}
}
}
validGridPlacement = isOnPath;
} else {
// Regular tower placement rules
if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (!cell || cell.type !== 0) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
}
self.blockedByEnemy = false;
if (validGridPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) {
continue;
}
// Only check non-flying enemies, flying enemies can pass over towers
if (!enemy.isFlying) {
if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
self.canPlace = validGridPlacement && !self.blockedByEnemy;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 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 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' gold');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 0;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
// Create moving orange line
self.movingLine = new Container();
var orangeLine = self.movingLine.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
orangeLine.width = 8;
orangeLine.height = 140;
orangeLine.tint = 0xFFA500; // Orange color
self.addChild(self.movingLine);
// No start marker needed - wave indicator only shows waves
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Lizard";
enemyType = "lizard";
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 lizard
var bossTypes = ['normal', 'fast', 'immune', 'lizard'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always lizard
enemyType = 'lizard';
waveType = "Boss Lizard";
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 'lizard':
block.tint = 0xFFFF00;
waveType = "Boss Lizard";
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 'lizard':
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 lizard
block.tint = 0xFFFF00;
waveType = "Lizard";
enemyType = "lizard";
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;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Add boss prefix for boss waves (every 10th wave)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
self.positionIndicator.visible = false;
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
// Position and animate moving orange line
if (self.gameStarted && currentWave < totalWaves) {
self.movingLine.visible = true;
// Position line at left edge of current wave indicator
var currentWaveX = -moveAmount + (currentWave - 1) * blockWidth;
// Initialize line position if not set
if (self.movingLine.startX === undefined) {
self.movingLine.startX = currentWaveX - blockWidth / 2;
self.movingLine.x = self.movingLine.startX;
}
// Animate line slowly to the right
var lineSpeed = 0.2; // Much slower movement speed for slower waves
self.movingLine.x += lineSpeed;
// Check if line reached next wave position (right edge of current wave)
var nextWaveX = currentWaveX + blockWidth / 2;
if (self.movingLine.x >= nextWaveX) {
// Trigger next wave
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
// Reset line position for next wave
self.movingLine.startX = undefined;
if (currentWave <= totalWaves) {
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;
}
}
} else {
self.movingLine.visible = false;
}
for (var i = 0; i < totalWaves; i++) {
var marker = self.waveMarkers[i];
var block = marker.children[0];
if (i < currentWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
// Wave progression is now handled by the moving orange line above
};
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xF5F5DC
});
/****
* Game Code
****/
var isHidingUpgradeMenu = false;
var gamePaused = false; // Flag to pause game when game over shows
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var mines = [];
var poisonAreas = [];
var selectedTower = null;
var gold = 35;
var lives = 20;
var score = 0;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
function updateUI() {}
function setGold(value) {
gold = value;
// Update gold counter text if it exists
if (typeof goldText !== 'undefined' && goldText) {
var newGoldText = "Gold: " + gold;
goldText.setText(newGoldText);
goldTextShadow.setText(newGoldText);
}
updateUI();
}
function setScore(value) {
var oldScore = score;
score = value;
// Update score counter text if it exists
if (typeof scoreText !== 'undefined' && scoreText) {
var newScoreText = "Score: " + score;
scoreText.setText(newScoreText);
scoreTextShadow.setText(newScoreText);
// Check for score milestones and animate
var oldTens = Math.floor(oldScore / 10);
var newTens = Math.floor(score / 10);
var oldHundreds = Math.floor(oldScore / 100);
var newHundreds = Math.floor(score / 100);
// Animate on every 10 points
if (newTens > oldTens) {
tween(scoreText, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(scoreText, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn
});
}
});
tween(scoreTextShadow, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(scoreTextShadow, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
// Extra animation on every 100 points (bigger scale)
if (newHundreds > oldHundreds) {
tween(scoreText, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(scoreText, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeIn
});
}
});
tween(scoreTextShadow, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(scoreTextShadow, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
}
}
// Enemies paths will be defined later
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 = 150 - CELL_SIZE * 6;
grid.pathFind();
grid.renderDebug();
// Add background asset to fill entire screen
var backgroundCell = LK.getAsset('ak', {
anchorX: 0.5,
anchorY: 0.5
});
backgroundCell.x = 2048 / 2;
backgroundCell.y = 2732 / 2;
backgroundCell.tint = 0xffffff;
backgroundCell.alpha = 1.0;
// Set texture filtering to nearest neighbor for sharp pixels
if (backgroundCell.texture && backgroundCell.texture.baseTexture) {
backgroundCell.texture.baseTexture.scaleMode = 0; // NEAREST filtering for sharp pixels
}
// Add first cloud on the top left
var cloud1 = LK.getAsset('cloud', {
anchorX: 0.5,
anchorY: 0.5
});
cloud1.x = 300;
cloud1.y = 125; // Same Y position as sun
cloud1.alpha = 0.9;
// Add second cloud on the top right
var cloud2 = LK.getAsset('cloud', {
anchorX: 0.5,
anchorY: 0.5
});
cloud2.x = 2048 - 300;
cloud2.y = 125; // Same Y position as sun
cloud2.alpha = 0.9;
debugLayer.addChild(backgroundCell);
debugLayer.addChild(cloud1);
debugLayer.addChild(cloud2);
debugLayer.addChild(grid);
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
var cells = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cells.push({
cell: cell,
originalType: cell.type
});
cell.type = 1;
}
}
}
var blocked = grid.pathFind();
for (var i = 0; i < cells.length; i++) {
cells[i].cell.type = cells[i].originalType;
}
grid.pathFind();
grid.renderDebug();
return blocked;
}
function getTowerCost(towerType) {
var cost = 5;
switch (towerType) {
case 'rapid':
cost = 15;
break;
case 'sniper':
cost = 25;
break;
case 'splash':
cost = 35;
break;
case 'slow':
cost = 45;
break;
case 'poison':
cost = 55;
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
if (towerType === 'slow') {
// Place mine instead of tower
var mine = new Mine();
mine.placeOnGrid(gridX, gridY);
towerLayer.addChild(mine);
mines.push(mine);
} else {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
grid.pathFind();
grid.renderDebug();
}
setGold(gold - towerCost);
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
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) {
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.updateTowerGraphics(); // Update graphics to match tower type
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;
}
}
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
var menuWidth = 2048;
var menuHeight = 450;
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
hideUpgradeMenu(menu);
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
grid.renderDebug();
}
}
if (isDragging) {
isDragging = false;
if (towerPreview.canPlace) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2 - 200;
waveIndicator.y = 2732 - 80;
game.addChild(waveIndicator);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200;
nextWaveButton.y = 2732 - 100 + 20;
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
// Create sun asset at the top of the screen
var sunAsset = LK.getAsset('sun', {
anchorX: 0.5,
anchorY: 0.5
});
sunAsset.x = 2048 / 2;
sunAsset.y = 125; // Position at the very top of the screen
game.addChild(sunAsset);
// Create single vertical line segment that extends to connect with purple line
var lineSegment = LK.getAsset('verticalLine', {
anchorX: 0.5,
anchorY: 0.5
});
// Extend the line to connect with purple horizontal line
lineSegment.height = 630; // Extended to reach purple line at y=630
lineSegment.x = cloud2.x + 200; // Position to the right of the right cloud
lineSegment.y = 315; // Position so bottom of line connects to purple line (y=630)
lineSegment.tint = 0xFFA500; // Orange color
lineSegment.visible = false; // Make line invisible
game.addChild(lineSegment);
// Create three horizontal lines
var horizontalLine1 = LK.getAsset('gridLineHorizontal', {
anchorX: 0,
anchorY: 0.5
});
horizontalLine1.width = 1350 - 450; // Extended to connect red line to yellow line
horizontalLine1.height = 4; // Line thickness
horizontalLine1.x = 450; // Start from red vertical line
horizontalLine1.y = 900; // First horizontal line position - moved down
horizontalLine1.tint = 0xFF0000; // Red color
horizontalLine1.visible = false; // Make line invisible
game.addChild(horizontalLine1);
var horizontalLine2 = LK.getAsset('gridLineHorizontal', {
anchorX: 0,
anchorY: 0.5
});
horizontalLine2.width = 1350 - 450; // Extended to connect red line to yellow line
horizontalLine2.height = 4; // Line thickness
horizontalLine2.x = 450; // Start from red vertical line
horizontalLine2.y = 1340; // Second horizontal line position - moved up more
horizontalLine2.tint = 0x0000FF; // Blue color
horizontalLine2.visible = false; // Make line invisible
game.addChild(horizontalLine2);
var horizontalLine3 = LK.getAsset('gridLineHorizontal', {
anchorX: 0,
anchorY: 0.5
});
horizontalLine3.width = 980 - 27; // Extended to connect white line to gray line right endpoint
horizontalLine3.height = 4; // Line thickness
horizontalLine3.x = 450; // Start from white vertical line
horizontalLine3.y = 1700; // Third horizontal line position - moved down
horizontalLine3.tint = 0x00FF00; // Green color
horizontalLine3.visible = false; // Make line invisible
game.addChild(horizontalLine3);
// Create four vertical lines with different colors, same length as horizontal lines
var verticalLine1 = LK.getAsset('gridLine', {
anchorX: 0.5,
anchorY: 0.5
});
verticalLine1.width = 4; // Line thickness
verticalLine1.height = 450; // Same height as yellow line
verticalLine1.x = 450; // First vertical line position - moved left
verticalLine1.y = 1120; // Position at center between top and middle horizontal lines
verticalLine1.tint = 0x8B4513; // Brown color
verticalLine1.visible = false; // Make line invisible
game.addChild(verticalLine1);
var verticalLine2 = LK.getAsset('gridLine', {
anchorX: 0.5,
anchorY: 0.5
});
verticalLine2.width = 4; // Line thickness
verticalLine2.height = 400; // Extended to connect third black line to gray line
verticalLine2.x = 450; // Position at same x as red vertical line
verticalLine2.y = 1900; // Position to connect third black line (1700) to gray line (2100)
verticalLine2.tint = 0x00FFFF; // Cyan color
verticalLine2.visible = false; // Make line invisible
game.addChild(verticalLine2);
var verticalLine3 = LK.getAsset('gridLine', {
anchorX: 0.5,
anchorY: 0.5
});
verticalLine3.width = 4; // Line thickness
verticalLine3.height = 270; // Extended blue line height to reach top black line
verticalLine3.x = 1350; // Move blue line to same x position as yellow line
verticalLine3.y = 765; // Move blue line up so top touches black line at y=900
verticalLine3.tint = 0x800080; // Purple color
verticalLine3.visible = false; // Make line invisible
game.addChild(verticalLine3);
var verticalLine4 = LK.getAsset('gridLine', {
anchorX: 0.5,
anchorY: 0
});
verticalLine4.width = 4; // Line thickness
verticalLine4.height = 400; // Shortened from bottom while keeping top at same position
verticalLine4.x = 1350; // Fourth vertical line position - moved further right
verticalLine4.y = 1340; // Positioned so bottom aligns with black line right end
verticalLine4.tint = 0xFFFF00; // Yellow color
verticalLine4.visible = false; // Make line invisible
game.addChild(verticalLine4);
// Create pink horizontal line in the middle
var pinkHorizontalLine = LK.getAsset('gridLineHorizontal', {
anchorX: 0,
anchorY: 0.5
});
pinkHorizontalLine.width = 1350 - (cloud2.x + 200); // Extended to connect green line to blue line
pinkHorizontalLine.height = 4; // Same thickness as other lines
pinkHorizontalLine.x = cloud2.x + 200; // Start from green vertical line
pinkHorizontalLine.y = 630; // Moved slightly up
pinkHorizontalLine.tint = 0xFFC0CB; // Pink color
pinkHorizontalLine.visible = false; // Make line invisible
game.addChild(pinkHorizontalLine);
// Create horizontal gray line slightly longer than bottom black line
var grayHorizontalLine = LK.getAsset('gridLineHorizontal', {
anchorX: 0,
anchorY: 0.5
});
grayHorizontalLine.width = 1100; // Extended width to make right corner longer
grayHorizontalLine.height = 4; // Same thickness as other lines
grayHorizontalLine.x = 2048 / 2 - 577; // Position slightly to the right - moved 50 pixels right (unchanged)
grayHorizontalLine.y = 2100; // Move gray line slightly up
grayHorizontalLine.tint = 0xFF00FF; // Magenta color
grayHorizontalLine.visible = false; // Make line invisible
game.addChild(grayHorizontalLine);
// Place queen asset to the right of the game end position
var queenAsset = LK.getAsset('queen', {
anchorX: 0.5,
anchorY: 0.5
});
queenAsset.x = 450 + 980 + 150; // Position even further to the right - moved 100px more to the right
queenAsset.y = 2100; // Same y position as magenta line
// Queen stays in original colors without tint
game.addChild(queenAsset);
// Queen stays in place without animation
// Move queen to front of all visuals by re-adding it to game after all other elements
game.removeChild(queenAsset);
game.addChild(queenAsset);
// Connect line endpoints to form continuous path
// Connection 1: Green vertical line bottom to Pink horizontal line left
var connection1 = LK.getAsset('gridLineHorizontal', {
anchorX: 0,
anchorY: 0.5
});
connection1.width = Math.abs(cloud2.x + 200 - (cloud2.x + 200)); // No gap, lines already connect
connection1.height = 4;
connection1.x = cloud2.x + 200;
connection1.y = 630;
connection1.tint = 0xFFA500; // Match green line color
connection1.visible = false; // Hidden since lines already connect
game.addChild(connection1);
// Connection 2: Pink horizontal line right to Purple vertical line top
var connection2 = LK.getAsset('gridLineHorizontal', {
anchorX: 0,
anchorY: 0.5
});
connection2.width = Math.abs(1350 - (cloud2.x + 200 + (1350 - (cloud2.x + 200))));
connection2.height = 4;
connection2.x = cloud2.x + 200 + (1350 - (cloud2.x + 200));
connection2.y = 630;
connection2.tint = 0xFFC0CB; // Match pink line color
connection2.visible = false; // Hidden since lines already connect
game.addChild(connection2);
// Connection 3: Purple vertical line bottom to Red horizontal line right
var connection3 = LK.getAsset('gridLine', {
anchorX: 0.5,
anchorY: 0
});
connection3.width = 4;
connection3.height = Math.abs(900 - 630); // Connect purple line bottom to red line
connection3.x = 1350;
connection3.y = 630;
connection3.tint = 0x800080; // Match purple line color
connection3.visible = false; // Make line invisible
game.addChild(connection3);
// Connection 4: Red horizontal line left to Brown vertical line top
var connection4 = LK.getAsset('gridLineHorizontal', {
anchorX: 0,
anchorY: 0.5
});
connection4.width = Math.abs(1350 - 450);
connection4.height = 4;
connection4.x = 450;
connection4.y = 900;
connection4.tint = 0xFF0000; // Match red line color
connection4.visible = false; // Hidden since lines already connect
game.addChild(connection4);
// Connection 5: Brown vertical line bottom to Blue horizontal line left
var connection5 = LK.getAsset('gridLine', {
anchorX: 0.5,
anchorY: 0
});
connection5.width = 4;
connection5.height = Math.abs(1340 - (900 + 450)); // Connect brown line bottom to blue line
connection5.x = 450;
connection5.y = 900 + 450;
connection5.tint = 0x8B4513; // Match brown line color
connection5.visible = false; // Hidden since lines already connect
game.addChild(connection5);
// Connection 6: Blue horizontal line right to Yellow vertical line top
var connection6 = LK.getAsset('gridLineHorizontal', {
anchorX: 0,
anchorY: 0.5
});
connection6.width = Math.abs(1350 - 450);
connection6.height = 4;
connection6.x = 450;
connection6.y = 1340;
connection6.tint = 0x0000FF; // Match blue line color
connection6.visible = false; // Hidden since lines already connect
game.addChild(connection6);
// Connection 7: Yellow vertical line bottom to Green horizontal line left
var connection7 = LK.getAsset('gridLine', {
anchorX: 0.5,
anchorY: 0
});
connection7.width = 4;
connection7.height = Math.abs(1700 - 1740); // Connect yellow line bottom to green line
connection7.x = 1350;
connection7.y = 1340;
connection7.tint = 0xFFFF00; // Match yellow line color
connection7.visible = false; // Hidden since gap is small
game.addChild(connection7);
// Connection 8: Green horizontal line left to Cyan vertical line top
var connection8 = LK.getAsset('gridLineHorizontal', {
anchorX: 1,
anchorY: 0.5
});
connection8.width = Math.abs(450 - 450);
connection8.height = 4;
connection8.x = 450;
connection8.y = 1700;
connection8.tint = 0x00FF00; // Match green line color
connection8.visible = false; // Hidden since lines already connect
game.addChild(connection8);
// Connection 9: Cyan vertical line bottom to Magenta horizontal line left
var connection9 = LK.getAsset('gridLine', {
anchorX: 0.5,
anchorY: 0
});
connection9.width = 4;
connection9.height = Math.abs(2100 - 2100); // Connect cyan line bottom to magenta line
connection9.x = 450;
connection9.y = 1900 + 200;
connection9.tint = 0x00FFFF; // Match cyan line color
connection9.visible = false; // Hidden since lines already connect
game.addChild(connection9);
// Make clouds move slowly back and forth
function animateCloudMovement() {
// Cloud 1 - move from left to right
tween(cloud1, {
x: cloud1.x + 50
}, {
duration: 4000,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Move back to left
tween(cloud1, {
x: cloud1.x - 50
}, {
duration: 4000,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Repeat the cycle
animateCloudMovement();
}
});
}
});
// Cloud 2 - move from right to left (opposite direction)
tween(cloud2, {
x: cloud2.x - 50
}, {
duration: 4000,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Move back to right
tween(cloud2, {
x: cloud2.x + 50
}, {
duration: 4000,
easing: tween.easeInOut
});
}
});
}
// Start the cloud animation
animateCloudMovement();
var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
// Create score counter container but don't add to GUI yet
var scoreCounterContainer = new Container();
// Score counter background
var scoreCounterBg = scoreCounterContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0.5
});
scoreCounterBg.width = 200;
scoreCounterBg.height = 80;
scoreCounterBg.tint = 0x444444;
scoreCounterBg.alpha = 0.9;
scoreCounterBg.x = -20; // Offset from right edge
scoreCounterBg.y = -100; // Position above gold counter
// Score text shadow
var scoreTextShadow = new Text2("Score: " + score, {
size: 40,
fill: 0x000000,
weight: 800
});
scoreTextShadow.anchor.set(1.0, 0.5);
scoreTextShadow.x = -30 + 2; // Offset from right edge + shadow offset
scoreTextShadow.y = -100 + 2; // Position above gold counter + shadow offset
scoreCounterContainer.addChild(scoreTextShadow);
// Score text
var scoreText = new Text2("Score: " + score, {
size: 40,
fill: 0x00FF00,
weight: 800
});
scoreText.anchor.set(1.0, 0.5);
scoreText.x = -30; // Offset from right edge
scoreText.y = -100; // Position above gold counter
scoreCounterContainer.addChild(scoreText);
// Create gold counter container but don't add to GUI yet
var goldCounterContainer = new Container();
// Gold counter background
var goldCounterBg = goldCounterContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0.5
});
goldCounterBg.width = 200;
goldCounterBg.height = 80;
goldCounterBg.tint = 0x444444;
goldCounterBg.alpha = 0.9;
goldCounterBg.x = -20; // Offset from right edge
goldCounterBg.y = 0;
// Gold text shadow
var goldTextShadow = new Text2("Gold: " + gold, {
size: 40,
fill: 0x000000,
weight: 800
});
goldTextShadow.anchor.set(1.0, 0.5);
goldTextShadow.x = -30 + 2; // Offset from right edge + shadow offset
goldTextShadow.y = 2; // Shadow offset
goldCounterContainer.addChild(goldTextShadow);
// Gold text
var goldText = new Text2("Gold: " + gold, {
size: 40,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(1.0, 0.5);
goldText.x = -30; // Offset from right edge
goldText.y = 0;
goldCounterContainer.addChild(goldText);
// Play background music immediately when game loads
LK.playMusic('acxs');
// Create and show start menu
var startMenu = new StartMenu();
game.addChild(startMenu);
game.update = function () {
// Check if game is paused (game over screen showing)
if (gamePaused) {
return; // Exit update loop when game is paused
}
// Make sun slowly rotate
sunAsset.rotation += 0.005;
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.12, currentWave); // ~20% increase per wave
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Set enemy initial position and properties - spawn at exact center of green line
enemy.x = cloud2.x + 200; // Start at exact X position of green line
enemy.y = 0; // Start at top of screen
enemy.cellX = Math.round((enemy.x - grid.x) / CELL_SIZE);
enemy.cellY = Math.round((enemy.y - grid.y) / CELL_SIZE);
enemy.currentCellX = (enemy.x - grid.x) / CELL_SIZE;
enemy.currentCellY = (enemy.y - grid.y) / CELL_SIZE;
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;
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Boss enemies give more gold and score
var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5);
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : Math.floor(Math.random() * 11) + 10; // Random 10-20 for normal enemies
setScore(score + scoreValue);
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// 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);
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) {
// Show custom game over screen instead of default
var gameOverScreen = new GameOverScreen();
game.addChild(gameOverScreen);
// Pause the game by stopping all game logic
return; // Exit update loop to pause game
}
}
}
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();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
};
shuriken. In-Game asset. 2d. High contrast. No shadows
sun. In-Game asset. 2d. High contrast. No shadows
cloud. In-Game asset. 2d. High contrast. No shadows
elinde shuriken olan ninja karınca. In-Game asset. 2d. High contrast. No shadows
cesaretli görünsün ve kafasında acemi şapkası olsun
ikibacağı olsun havalı bir şapkası olsun ve gözünde nişangah olsun
elinde elinde patlamaya hazır bir bomba tutan deli ama şirin görünen kahverengi bir karınca
ayaklarıda görünsün
yaprak mayını. In-Game asset. 2d. High contrast. No shadows
kafasında taç olan ve boynunda anahtar olan tahtında oturan kraliçe karınca. In-Game asset. 2d. High contrast. No shadows
ok yere 90 derece olacak şekilde dik dursun
bomba. In-Game asset. 2d. High contrast. No shadows
zehirli iksir şişesi. In-Game asset. 2d. High contrast. No shadows
ayaklarıda görünsün
kötü adam çekirge. In-Game asset. 2d. High contrast. No shadows
kalkanlı yaban arası kötü adam. In-Game asset. 2d. High contrast. No shadows
kalkanı olmasın
When the evil insects win, they celebrate by swimming in piles of gold coins. Cartoonish, colorful, and fun scene. In the background, defeated allied ants and the lost treasure atmosphere. Filled with vibrant colors and animated details.. In-Game asset. 2d. High contrast. No shadows