User prompt
Add sound assets for various things and a game theme song
User prompt
Add sound assets for various things and a game theme song
User prompt
Now Special ability costs gold to unlock when your tower reaches level 3
User prompt
Add new thingy: Cheats! There are now cheats in the game that Give you ridiculous buffs (Cheats: Unlimited money, One hit kills)
User prompt
Special ability button unlocks at level 3
User prompt
Make the Special ability button a separate button
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; // Use default bullet asset initially, will be updated based on type var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { // Mortar projectile logic if (self.type === 'mortar') { var dx = self.targetX - self.x; var dy = self.targetY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Create explosion effect var mortarEffect = new EffectIndicator(self.targetX, self.targetY, 'mortar'); game.addChild(mortarEffect); // Area damage var explosionRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var enemyDx = enemy.x - self.targetX; var enemyDy = enemy.y - self.targetY; var enemyDist = Math.sqrt(enemyDx * enemyDx + enemyDy * enemyDy); if (enemyDist <= explosionRadius) { // Damage falls off with distance var damageFactor = 1 - enemyDist / explosionRadius * 0.5; enemy.health -= oneHitKillCheat ? 999999 : self.damage * damageFactor; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } } } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } return; } if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; LK.getSound('enemy_hit').play(); if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= oneHitKillCheat ? 999999 : self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Apply slow effect // Make slow percentage scale with tower level (default 50%, up to 80% at max level) var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6 var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; // Slow by X% self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { self.targetEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } else if (self.type === 'wizard') { // Check if tower has charm ability if (self.hasCharmAbility) { // Charm effect instead of confusion if (!self.targetEnemy.isCharmed && !self.targetEnemy.isImmune) { self.targetEnemy.isCharmed = true; self.targetEnemy.charmDuration = 300; // 5 seconds at 60 FPS self.targetEnemy.originalTeam = 'enemy'; // Visual effect for charm var charmEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'charm'); game.addChild(charmEffect); } } else { // Regular confusion effect if (Math.random() < 0.5 && !self.targetEnemy.isConfused && !self.targetEnemy.isImmune) { self.targetEnemy.isConfused = true; self.targetEnemy.confusionDuration = 180; // 3 seconds at 60 FPS // Visual effect for confusion var confuseEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'confuse'); game.addChild(confuseEffect); } } } else if (self.type === 'rapid' && self.hasBleedAbility) { // 10% chance to apply bleed if (Math.random() < 0.1 && !self.targetEnemy.isBleeding && !self.targetEnemy.isImmune) { self.targetEnemy.isBleeding = true; self.targetEnemy.bleedDuration = 300; // 5 seconds at 60 FPS self.targetEnemy.bleedDamage = 5; // 5 damage per second // Visual effect for bleed var bleedEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'bleed'); game.addChild(bleedEffect); } } else if (self.type === 'slow' && self.hasPermafrostAbility) { // Permanent slow effect if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Apply permanent slow if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 0.2; // 80% slow self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 999999; // Permanent self.targetEnemy.permanentSlow = true; } } } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var CheatMenu = Container.expand(function () { var self = Container.call(this); // Background var background = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); background.width = 800; background.height = 600; background.tint = 0x222222; background.alpha = 0.95; // Title var titleText = new Text2('CHEATS', { size: 80, fill: 0xFF0000, weight: 800 }); titleText.anchor.set(0.5, 0); titleText.x = 0; titleText.y = -250; self.addChild(titleText); // Unlimited Money Toggle var moneyToggle = new Container(); var moneyBg = moneyToggle.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); moneyBg.width = 700; moneyBg.height = 120; moneyBg.tint = unlimitedMoneyCheat ? 0x00FF00 : 0x888888; var moneyText = new Text2('Unlimited Money: ' + (unlimitedMoneyCheat ? 'ON' : 'OFF'), { size: 50, fill: 0xFFFFFF, weight: 800 }); moneyText.anchor.set(0.5, 0.5); moneyToggle.addChild(moneyText); moneyToggle.y = -80; self.addChild(moneyToggle); moneyToggle.down = function () { unlimitedMoneyCheat = !unlimitedMoneyCheat; moneyBg.tint = unlimitedMoneyCheat ? 0x00FF00 : 0x888888; moneyText.setText('Unlimited Money: ' + (unlimitedMoneyCheat ? 'ON' : 'OFF')); if (unlimitedMoneyCheat) { setGold(999999); } }; // One Hit Kill Toggle var oneHitToggle = new Container(); var oneHitBg = oneHitToggle.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); oneHitBg.width = 700; oneHitBg.height = 120; oneHitBg.tint = oneHitKillCheat ? 0x00FF00 : 0x888888; var oneHitText = new Text2('One Hit Kills: ' + (oneHitKillCheat ? 'ON' : 'OFF'), { size: 50, fill: 0xFFFFFF, weight: 800 }); oneHitText.anchor.set(0.5, 0.5); oneHitToggle.addChild(oneHitText); oneHitToggle.y = 80; self.addChild(oneHitToggle); oneHitToggle.down = function () { oneHitKillCheat = !oneHitKillCheat; oneHitBg.tint = oneHitKillCheat ? 0x00FF00 : 0x888888; oneHitText.setText('One Hit Kills: ' + (oneHitKillCheat ? 'ON' : 'OFF')); }; // Close button var closeButton = new Container(); var closeBg = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBg.width = 90; closeBg.height = 90; closeBg.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 = background.width / 2 - 57; closeButton.y = -background.height / 2 + 57; self.addChild(closeButton); closeButton.down = function () { self.destroy(); }; // Initial position (centered) self.x = 2048 / 2; self.y = 2732 / 2; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); numberLabel.visible = false; self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.visible = false; cellGraphics.tint = 0x880000; return; } numberLabel.visible = false; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; } else { cellGraphics.tint = 0x88 - tint << 8 | tint; } self.removeArrows(); break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0x008800; numberLabel.visible = false; break; } } }; }); // 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 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'confuse': effectGraphics.tint = 0xFF1493; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.2; break; case 'mortar': effectGraphics.tint = 0x8B4513; effectGraphics.width = effectGraphics.height = CELL_SIZE * 3; break; case 'charm': effectGraphics.tint = 0xFFB6C1; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'bleed': effectGraphics.tint = 0x8B0000; effectGraphics.width = effectGraphics.height = CELL_SIZE * 0.8; 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 = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; self.isHidden = false; self.hideDuration = 0; self.hideTimer = 0; self.hasSpawned = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'flying_horde': self.isFlying = true; self.maxHealth = 25; // Very weak self.speed *= 1.5; // Fast break; case 'mole': self.maxHealth = 120; // Slightly tankier self.speed *= 0.8; // Slower when visible self.hideTimer = 300; // 5 seconds until first hide break; case 'mixed': // Mixed will be handled as a special spawner type self.maxHealth = 1; // Placeholder, should not be attacked self.isSpawner = true; break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle mole digging mechanics if (self.type === 'mole') { self.hideTimer--; if (self.hideTimer <= 0) { if (!self.isHidden) { // Start hiding (go underground) self.isHidden = true; self.hideDuration = 90; // 1.5 seconds at 60 FPS self.hideTimer = 300; // 5 seconds until next hide // Double speed while hidden if (self.originalSpeed === undefined) { self.originalSpeed = self.speed; } self.speed = self.originalSpeed * 2; // Visual effect - make semi-transparent and darker enemyGraphics.alpha = 0.3; enemyGraphics.tint = 0x444444; // Hide health bar while underground healthBarOutline.visible = false; healthBarBG.visible = false; healthBar.visible = false; } else { // Come back up (surface) self.isHidden = false; self.hideTimer = 300; // 5 seconds until next hide // Reset speed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } // Restore visibility enemyGraphics.alpha = 1; enemyGraphics.tint = 0xFFFFFF; // Show health bar again healthBarOutline.visible = true; healthBarBG.visible = true; healthBar.visible = true; } } else if (self.isHidden) { self.hideDuration--; if (self.hideDuration <= 0) { // Force surface if duration is over self.isHidden = false; self.hideTimer = 300; // Reset timer // Reset speed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } // Restore visibility enemyGraphics.alpha = 1; enemyGraphics.tint = 0xFFFFFF; // Show health bar again healthBarOutline.visible = true; healthBarBG.visible = true; healthBar.visible = true; } } } // Handle flying horde spawning if (self.type === 'flying_horde' && !self.hasSpawned && self.currentCellY >= 2) { self.hasSpawned = true; // Spawn 4-6 additional weak flying enemies var spawnCount = 4 + Math.floor(Math.random() * 3); for (var i = 0; i < spawnCount; i++) { var newEnemy = new Enemy('flying'); newEnemy.maxHealth = 15; // Very weak newEnemy.health = newEnemy.maxHealth; newEnemy.speed = self.speed * (0.8 + Math.random() * 0.4); // Varied speed // Position around the spawner var angle = Math.PI * 2 / spawnCount * i + Math.random() * 0.5; var distance = 50 + Math.random() * 30; newEnemy.cellX = self.cellX; newEnemy.cellY = self.cellY; newEnemy.currentCellX = self.currentCellX + Math.cos(angle) * distance / CELL_SIZE; newEnemy.currentCellY = self.currentCellY + Math.sin(angle) * distance / CELL_SIZE; newEnemy.x = grid.x + newEnemy.currentCellX * CELL_SIZE; newEnemy.y = grid.y + newEnemy.currentCellY * CELL_SIZE; newEnemy.waveNumber = self.waveNumber; // Add to flying layer enemyLayerTop.addChild(newEnemy); if (newEnemy.shadow) { enemyLayerMiddle.addChild(newEnemy.shadow); } enemies.push(newEnemy); } } // Handle confusion effect if (self.isConfused && !self.isImmune) { self.confusionDuration--; if (self.confusionDuration <= 0) { self.isConfused = false; // Reset pathfinding when confusion ends self.currentTarget = undefined; } } // Handle charm effect if (self.isCharmed && !self.isImmune) { self.charmDuration--; if (self.charmDuration <= 0) { self.isCharmed = false; self.originalTeam = 'enemy'; // Reset pathfinding when charm ends self.currentTarget = undefined; } } // Handle bleed effect if (self.isBleeding && !self.isImmune) { self.bleedDuration--; // Take 5 damage every second (60 ticks) if (self.bleedDuration % 60 === 0) { self.health -= oneHitKillCheat ? 999999 : self.bleedDamage; if (self.health <= 0) { self.health = 0; } else { self.healthBar.width = self.health / self.maxHealth * 70; } // Visual feedback LK.effects.flashObject(self, 0x8B0000, 100); } if (self.bleedDuration <= 0) { self.isBleeding = false; } } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed, clear any such effects self.slowed = false; self.slowEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.isCharmed) { enemyGraphics.tint = 0xFFB6C1; // Light pink for charmed } else if (self.isConfused) { enemyGraphics.tint = 0xFF1493; // Pink for confused } else if (self.isBleeding) { enemyGraphics.tint = 0x8B0000; // Dark red for bleeding } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; if (i > 11 - 3 && i <= 11 + 3) { if (j === 0) { cellType = 2; self.spawns.push(cell); } else if (j <= 4) { cellType = 0; } else if (j === gridHeight - 1) { cellType = 3; self.goals.push(cell); } else if (j >= gridHeight - 4) { cellType = 0; } } cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 1 && node.left.type != 1) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.right.type != 1) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.right.type != 1) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.left.type != 1) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed; // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) var angle = Math.PI / 2; if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update enemy's position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // If enemy has now reached the entry area, update cell coordinates if (enemy.currentCellY >= 4) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (enemy.isCharmed) { // Charmed enemies attack other enemies var nearestEnemy = null; var nearestDist = Infinity; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== enemy && !otherEnemy.isCharmed) { var dx = otherEnemy.cellX - enemy.cellX; var dy = otherEnemy.cellY - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < nearestDist) { nearestDist = dist; nearestEnemy = otherEnemy; } } } if (nearestEnemy) { // Move towards the nearest enemy enemy.currentTarget = { x: nearestEnemy.cellX, y: nearestEnemy.cellY }; // Deal damage if close enough if (nearestDist < 2) { if (LK.ticks % 60 === 0) { // Attack once per second nearestEnemy.health -= oneHitKillCheat ? 999999 : 10; if (nearestEnemy.health <= 0) { nearestEnemy.health = 0; } else { nearestEnemy.healthBar.width = nearestEnemy.health / nearestEnemy.maxHealth * 70; } LK.effects.flashObject(nearestEnemy, 0xFFB6C1, 100); } } } } else if (enemy.isConfused) { // Confused enemies move towards the nearest tower var nearestTower = null; var nearestDist = Infinity; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.gridX - enemy.cellX; var dy = tower.gridY - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < nearestDist) { nearestDist = dist; nearestTower = tower; } } if (nearestTower) { // Set target to move towards the tower enemy.currentTarget = { x: nearestTower.gridX, y: nearestTower.gridY }; } } else if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { if (!enemy.isConfused && cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var LaserWall = Container.expand(function (tower1, tower2) { var self = Container.call(this); self.tower1 = tower1; self.tower2 = tower2; self.damage = 18; // Base damage per tick (increased by 2) self.level = 1; self.maxLevel = 6; // Create laser visual var laserGraphics = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); laserGraphics.tint = 0xFF0088; laserGraphics.alpha = 0.8; // Position between the two towers self.updatePosition = function () { self.x = Math.min(self.tower1.x, self.tower2.x); self.y = self.tower1.y; var distance = Math.abs(self.tower2.x - self.tower1.x); laserGraphics.width = distance; laserGraphics.height = CELL_SIZE * 0.5; }; self.updatePosition(); // Level indicators var levelIndicators = []; var maxDots = self.maxLevel; var dotSize = CELL_SIZE / 8; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 2; outlineCircle.height = dotSize + 2; outlineCircle.tint = 0x000000; var levelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); levelIndicator.width = dotSize; levelIndicator.height = dotSize; levelIndicator.tint = 0xCCCCCC; dot.y = -CELL_SIZE * 0.4; self.addChild(dot); levelIndicators.push(dot); } self.updateLevelIndicators = function () { var centerX = laserGraphics.width / 2; var spacing = laserGraphics.width / (maxDots + 1); for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; dot.x = spacing * (i + 1); var levelIndicator = dot.children[1]; if (i < self.level) { levelIndicator.tint = 0xFFFFFF; } else { levelIndicator.tint = 0xFF0088; } } }; self.updateLevelIndicators(); self.upgrade = function () { if (self.level < self.maxLevel) { var upgradeCost = Math.floor(40 * Math.pow(2, self.level - 1)); if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(upgradeCost * 3.5 / 2); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; self.damage = 18 + self.level * 24; // Increased base by 2 and maintain scaling // Unlock special ability at level 3 if (self.level === 3) { self.hasSpecialAbility = true; var notification = game.addChild(new Notification("Laser Wall: Sky Piercer unlocked!")); notification.x = 2048 / 2; notification.y = grid.height - 100; } self.updateLevelIndicators(); // Visual feedback tween(laserGraphics, { scaleY: 1.5, alpha: 1 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(laserGraphics, { scaleY: 1, alpha: 0.8 }, { duration: 200, easing: tween.easeIn }); } }); return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade laser wall!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.getTotalValue = function () { var totalInvestment = 0; for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(40 * Math.pow(2, i - 1)); } return totalInvestment; }; self.update = function () { // Check for enemies passing through the laser for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Skip flying enemies unless laser wall has special ability if (enemy.isFlying && !self.hasSpecialAbility) { continue; } // Skip hidden moles unless laser wall has special ability if (enemy.isHidden && !self.hasSpecialAbility) { continue; } // Check if enemy is in the same row as the laser if (Math.abs(enemy.y - self.y) < CELL_SIZE / 2) { // Check if enemy is within the laser's horizontal range if (enemy.x >= self.x && enemy.x <= self.x + laserGraphics.width) { // Deal damage every 30 ticks (0.5 seconds) if (LK.ticks % 30 === 0) { enemy.health -= oneHitKillCheat ? 999999 : self.damage; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } // Visual effect LK.effects.flashObject(enemy, 0xFF0088, 100); // Play laser sound occasionally if (LK.ticks % 120 === 0) { LK.getSound('laser_beam').play(); } } } } } // Pulse effect laserGraphics.alpha = 0.8 + Math.sin(LK.ticks * 0.1) * 0.2; }; self.down = function (x, y, obj) { // Show upgrade menu for laser wall var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } selectedTower = self; var upgradeMenu = new LaserWallUpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 275 }, { duration: 200, easing: tween.backOut }); }; return self; }); var LaserWallUpgradeMenu = Container.expand(function (laserWall) { var self = Container.call(this); self.laserWall = laserWall; self.y = 2732 + 275; 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 titleText = new Text2('Laser Wall', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0, 0); titleText.x = -840; titleText.y = -160; self.addChild(titleText); var statsText = new Text2('Level: ' + self.laserWall.level + '/' + self.laserWall.maxLevel + '\nDamage: ' + self.laserWall.damage + '/0.5s', { 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); // Upgrade button 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.laserWall.level >= self.laserWall.maxLevel; var upgradeCost = 0; if (!isMaxLevel) { upgradeCost = Math.floor(40 * Math.pow(2, self.laserWall.level - 1)); if (self.laserWall.level === self.laserWall.maxLevel - 1) { upgradeCost = Math.floor(upgradeCost * 3.5 / 2); } } 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); // Sell button 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 sellValue = getTowerSellValue(self.laserWall.getTotalValue()); var sellButtonText = new Text2('Remove Wall: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; // Close button 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 () { if (self.laserWall.level >= self.laserWall.maxLevel) { var notification = game.addChild(new Notification("Laser wall is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.laserWall.upgrade()) { statsText.setText('Level: ' + self.laserWall.level + '/' + self.laserWall.maxLevel + '\nDamage: ' + self.laserWall.damage + '/0.5s'); if (self.laserWall.level >= self.laserWall.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } else { var newCost = Math.floor(40 * Math.pow(2, self.laserWall.level - 1)); if (self.laserWall.level === self.laserWall.maxLevel - 1) { newCost = Math.floor(newCost * 3.5 / 2); } buttonText.setText('Upgrade: ' + newCost + ' gold'); } var newSellValue = getTowerSellValue(self.laserWall.getTotalValue()); sellButtonText.setText('Remove Wall: +' + newSellValue + ' gold'); } }; sellButton.down = function () { var sellValue = getTowerSellValue(self.laserWall.getTotalValue()); setGold(gold + sellValue); var notification = game.addChild(new Notification("Laser wall removed for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Remove laser wall from game var wallIndex = laserWalls.indexOf(self.laserWall); if (wallIndex !== -1) { laserWalls.splice(wallIndex, 1); } game.removeChild(self.laserWall); self.destroy(); selectedTower = null; }; closeButton.down = function () { hideUpgradeMenu(self); selectedTower = null; }; self.update = function () { if (self.laserWall.level >= self.laserWall.maxLevel) { return; } var currentUpgradeCost = Math.floor(40 * Math.pow(2, self.laserWall.level - 1)); if (self.laserWall.level === self.laserWall.maxLevel - 1) { currentUpgradeCost = Math.floor(currentUpgradeCost * 3.5 / 2); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; }; return self; }); var Manual = Container.expand(function () { var self = Container.call(this); // Manual background var background = self.attachAsset('manualBackground', { anchorX: 0.5, anchorY: 0.5 }); background.alpha = 0.95; // Title var titleText = new Text2('TOWER DEFENSE MANUAL', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0); titleText.x = 0; titleText.y = -1150; self.addChild(titleText); // Create scrollable content container var contentContainer = new Container(); self.addChild(contentContainer); var yPos = -1000; var sectionSpacing = 350; // Scrolling variables var isDragging = false; var startY = 0; var startContentY = 0; var velocity = 0; var minY = -1000; // Top limit var maxY = 0; // Will be calculated based on content height var contentHeight = 0; // Will be calculated after adding all content // Tower Classes Section self.createSection = function (title, items, startY) { var sectionY = startY; // Section title var sectionTitle = new Text2(title, { size: 60, fill: 0xFFD700, weight: 800 }); sectionTitle.anchor.set(0.5, 0); sectionTitle.x = 0; sectionTitle.y = sectionY; contentContainer.addChild(sectionTitle); sectionY += 80; // Items for (var i = 0; i < items.length; i++) { var item = items[i]; // Item background var itemBg = contentContainer.attachAsset('manualSection', { anchorX: 0.5, anchorY: 0 }); itemBg.x = 0; itemBg.y = sectionY; itemBg.height = 200; itemBg.alpha = 0.3; // Item name var itemName = new Text2(item.name, { size: 50, fill: 0xFFFFFF, weight: 800 }); itemName.anchor.set(0, 0); itemName.x = -800; itemName.y = sectionY + 20; contentContainer.addChild(itemName); // Item stats var itemStats = new Text2(item.stats, { size: 40, fill: 0xCCCCCC, weight: 400 }); itemStats.anchor.set(0, 0); itemStats.x = -800; itemStats.y = sectionY + 80; contentContainer.addChild(itemStats); sectionY += 220; } return sectionY + 50; }; // Tower data var towerData = [{ name: 'BOW (Default)', stats: 'Cost: 5 gold • Damage: 10 • Range: 3 tiles • Rate: 1/s\nBalanced tower good for early game' }, { name: 'CROSSBOW (Rapid)', stats: 'Cost: 15 gold • Damage: 5 • Range: 2.5 tiles • Rate: 2/s\nFast firing tower, good against swarms' }, { name: 'SNIPER', stats: 'Cost: 25 gold • Damage: 25 • Range: 5 tiles • Rate: 0.67/s\nLong range, high damage, slow firing' }, { name: 'CANNON (Splash)', stats: 'Cost: 35 gold • Damage: 15 • Range: 2 tiles • Rate: 0.8/s\nArea damage affects nearby enemies' }, { name: 'ICE WIZARD (Slow)', stats: 'Cost: 45 gold • Damage: 8 • Range: 3.5 tiles • Rate: 1.2/s\nSlows enemies, ineffective vs immune' }, { name: 'FARM', stats: 'Cost: 30 gold • Income: 10 gold/wave (+15 per level)\nGenerates passive income, no combat' }, { name: 'LASER BASE ALPHA', stats: 'Cost: 40 gold • Requires 2+ in same row\nCreates laser wall, blocks ground enemies' }, { name: 'INFERNO (Single)', stats: 'Cost: 50 gold • Damage: 5 DPS • Range: 4 tiles\nContinuous beam that drains health' }, { name: 'TRICKY WIZARD (Weakening)', stats: 'Cost: 40 gold • Damage: 10 • Range: 3 tiles\n50% chance to confuse enemies' }, { name: 'CHURCH (Income)', stats: 'Cost: 60 gold • Gives 1 life per wave\nNo combat ability, provides lives' }, { name: 'MORTAR (Area)', stats: 'Cost: 55 gold • Damage: 30 • Range: 7 tiles\nHigh range area damage, min range 2 tiles' }]; // Enemy data var enemyData = [{ name: 'NORMAL', stats: 'Health: 100 • Speed: Normal • Ground unit\nBasic enemy type' }, { name: 'FAST', stats: 'Health: 100 • Speed: 2x Normal • Ground unit\nQuick moving enemy' }, { name: 'IMMUNE', stats: 'Health: 80 • Speed: Normal • Ground unit\nImmune to slow effects' }, { name: 'FLYING', stats: 'Health: 80 • Speed: Normal • Flying unit\nFlies over obstacles, immune to laser walls' }, { name: 'SWARM', stats: 'Health: 50 • Speed: Normal • Ground unit\nWeaker but comes in large numbers' }, { name: 'FLYING HORDE', stats: 'Health: 25 • Speed: 1.5x Normal • Flying unit\nSpawner creates multiple weak flying enemies' }, { name: 'MOLE DIGGER', stats: 'Health: 120 • Speed: 0.8x/2x Normal • Ground unit\nHides underground periodically, untargetable while hidden' }, { name: 'BOSS', stats: 'Health: 20x Normal • Speed: 0.7x Normal\nAppears every 10th wave, massive health' }, { name: 'MIXED', stats: 'Spawns various random enemy types\nUnpredictable wave composition' }]; // Create sections yPos = self.createSection('TOWER TYPES', towerData, yPos); yPos = self.createSection('ENEMY TYPES', enemyData, yPos); // Game mechanics section var mechanicsData = [{ name: 'TOWER TARGETING', stats: 'First: Closest to exit • Last: Farthest from exit\nStrong: Highest HP • Crowd: Most nearby enemies' }, { name: 'WAVE PROGRESSION', stats: 'Waves 1-10: Fixed types to introduce mechanics\nAfter wave 10: Random types except boss waves' }, { name: 'DIFFICULTY SCALING', stats: 'Enemy health increases 12% per wave\nBoss waves every 10th wave' }, { name: 'TOWER UPGRADES', stats: 'Level 1-5: Exponential cost increase\nLevel 6: Extra powerful, costs 3.5x previous' }, { name: 'SELLING TOWERS', stats: 'Returns 60% of total investment\nStrategic for repositioning' }]; yPos = self.createSection('GAME MECHANICS', mechanicsData, yPos); // Calculate content height and set scroll limits contentHeight = yPos + 1000; // yPos is negative, so we add back the initial offset maxY = Math.min(0, -contentHeight + 1800); // 1800 is approximate visible height // Close button var closeButton = new Container(); self.addChild(closeButton); var closeBg = closeButton.attachAsset('manualButton', { anchorX: 0.5, anchorY: 0.5 }); closeBg.width = 100; closeBg.height = 100; closeBg.tint = 0xFF4444; var closeText = new Text2('X', { size: 60, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = 850; closeButton.y = -1150; closeButton.down = function () { self.destroy(); }; // Touch event handlers for scrolling self.down = function (x, y, obj) { // Don't start scrolling if clicking on the close button // Check if click is on close button using passed x,y coordinates var dx = x - closeButton.x; var dy = y - closeButton.y; if (Math.abs(dx) < 50 && Math.abs(dy) < 50) { return; } isDragging = true; startY = y; startContentY = contentContainer.y; velocity = 0; }; self.move = function (x, y, obj) { if (isDragging) { var deltaY = y - startY; var newY = startContentY + deltaY; // Apply bounds newY = Math.max(maxY, Math.min(minY, newY)); contentContainer.y = newY; // Calculate velocity for momentum velocity = deltaY * 0.5; } }; self.up = function (x, y, obj) { isDragging = false; }; // Update function for momentum scrolling self.update = function () { if (!isDragging && Math.abs(velocity) > 0.1) { // Apply momentum contentContainer.y += velocity; // Apply bounds contentContainer.y = Math.max(maxY, Math.min(minY, contentContainer.y)); // Apply friction velocity *= 0.92; // Stop if velocity is too small if (Math.abs(velocity) < 0.1) { velocity = 0; } } }; // Initial position (hidden) self.x = 2048 / 2; // Center horizontally self.y = 2732 + 1200; // Animate in tween(self, { y: 2732 / 2 }, { duration: 300, easing: tween.backOut }); 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 SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Use specific tower asset based on type var towerAssetId = 'tower_' + self.towerType; if (self.towerType === 'default') { towerAssetId = 'tower_default'; } // Increase size of base for easier touch var baseGraphics = self.attachAsset(towerAssetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); // No need to tint anymore as we have specific assets var towerCost = getTowerCost(self.towerType); // Get display name for tower type var displayName = self.towerType; switch (self.towerType) { case 'default': displayName = 'Bow'; break; case 'rapid': displayName = 'Crossbow'; break; case 'splash': displayName = 'Cannon'; break; case 'slow': displayName = 'Ice Wizard'; break; case 'farm': displayName = 'Farm'; break; case 'laser': displayName = 'Laser Base'; break; case 'inferno': displayName = 'Inferno'; break; case 'wizard': displayName = 'Tricky Wizard'; break; case 'church': displayName = 'Church'; break; case 'mortar': displayName = 'Mortar'; break; } // 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 = -20 + 4; 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 = -20; // Position above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; self.targetingMode = 'first'; // Options: 'first', 'last', 'strong', 'crowd' self.hasSpecialAbility = false; // Unlocked at max level self.specialAbilityActive = false; self.specialAbilityTimer = 0; self.rapidBurstCount = 0; // For archer rapid fire self.infernoTargets = []; // For inferno multi-target // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case 'sniper': // Sniper: base 5, +0.8 per level, infinite range with special ability if (self.hasSpecialAbility) { return 999 * CELL_SIZE; // Effectively infinite range } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case 'splash': // Splash: base 2, +0.2 per level (max ~4 blocks at max level) return (2 + (self.level - 1) * 0.2) * CELL_SIZE; case 'rapid': // Rapid: base 2.5, +0.5 per level return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'slow': // Slow: base 3.5, +0.5 per level return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'farm': // Farm has no range return 0; case 'laser': // Laser has no shooting range return 0; case 'inferno': // Inferno: base 4, +0.3 per level return (4 + (self.level - 1) * 0.3) * CELL_SIZE; case 'wizard': // Wizard: base 3, +0.4 per level return (3 + (self.level - 1) * 0.4) * CELL_SIZE; case 'church': // Church has no range return 0; case 'mortar': // Mortar: base 7, +0.5 per level (high range) return (7 + (self.level - 1) * 0.5) * CELL_SIZE; default: // Default: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 30; self.damage = 5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'sniper': self.fireRate = 90; self.damage = 25; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'splash': self.fireRate = 75; self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; case 'slow': self.fireRate = 50; self.damage = 8; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'farm': self.fireRate = 999999; // Farm doesn't shoot self.damage = 0; self.range = 0; self.bulletSpeed = 0; break; case 'laser': self.fireRate = 999999; // Laser doesn't shoot normally self.damage = 0; self.range = 0; self.bulletSpeed = 0; break; case 'inferno': self.fireRate = 999999; // Inferno uses continuous beam self.damage = 5; // Base DPS self.range = 4 * CELL_SIZE; self.bulletSpeed = 0; break; case 'wizard': self.fireRate = 120; // Slower fire rate self.damage = 10; self.range = 3 * CELL_SIZE; self.bulletSpeed = 6; break; case 'church': self.fireRate = 999999; // Church doesn't shoot self.damage = 0; self.range = 0; self.bulletSpeed = 0; break; case 'mortar': self.fireRate = 180; // Very slow fire rate self.damage = 30; self.range = 7 * CELL_SIZE; self.minRange = 2 * CELL_SIZE; // Can't hit close enemies self.bulletSpeed = 3; break; } // Use specific tower asset based on type var towerAssetId = 'tower_' + self.id; if (self.id === 'default') { towerAssetId = 'tower_default'; } var baseGraphics = self.attachAsset(towerAssetId, { anchorX: 0.5, anchorY: 0.5 }); // No need to tint anymore as we have specific assets var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 6; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); // Use specific defense asset based on type var defenseAssetId = 'defense_' + self.id; if (self.id === 'default') { defenseAssetId = 'defense_default'; } var gunGraphics = gunContainer.attachAsset(defenseAssetId, { anchorX: 0.5, anchorY: 0.5 }); self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { switch (self.id) { case 'rapid': towerLevelIndicator.tint = 0x00AAFF; break; case 'sniper': towerLevelIndicator.tint = 0xFF5500; break; case 'splash': towerLevelIndicator.tint = 0x33CC00; break; case 'slow': towerLevelIndicator.tint = 0x9900FF; break; case 'farm': towerLevelIndicator.tint = 0xFFD700; // Gold color break; case 'laser': towerLevelIndicator.tint = 0xFF0088; // Pink color break; case 'inferno': towerLevelIndicator.tint = 0xFF4500; // Orange-red break; case 'wizard': towerLevelIndicator.tint = 0xFF1493; // Deep pink break; case 'church': towerLevelIndicator.tint = 0xFFD700; // Gold break; case 'mortar': towerLevelIndicator.tint = 0x8B4513; // Saddle brown break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); // Create inferno beam visual if this is an inferno tower if (self.id === 'inferno') { self.infernoBeam = self.attachAsset('infernoBeam', { anchorX: 0, anchorY: 0.5 }); self.infernoBeam.visible = false; self.infernoBeam.alpha = 0.8; self.infernoDamageTimer = 0; } 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.getSpecialAbilityName = function () { switch (self.id) { case 'default': return 'Rapid Fire'; case 'rapid': return 'Bleeding Shots'; case 'sniper': return 'Infinite Range'; case 'inferno': return 'Triple Beam'; case 'splash': return 'Mega Blast'; case 'laser': return 'Sky Piercer'; case 'mortar': return 'Quad Shot'; case 'slow': return 'Permafrost'; case 'wizard': return 'Charm'; case 'farm': return 'Gold Boost'; case 'church': return 'Divine Intervention'; default: return null; } }; self.getSpecialAbilityCost = function () { // Special ability costs 3x the base tower cost return getTowerCost(self.id) * 3; }; self.unlockSpecialAbility = function () { if (self.level >= 3 && !self.hasSpecialAbility) { var cost = self.getSpecialAbilityCost(); if (gold >= cost) { setGold(gold - cost); self.hasSpecialAbility = true; // Show special ability unlocked notification var abilityName = self.getSpecialAbilityName(); if (abilityName) { var notification = game.addChild(new Notification("Special Ability Unlocked: " + abilityName)); notification.x = 2048 / 2; notification.y = grid.height - 100; } return true; } else { var notification = game.addChild(new Notification("Not enough gold for special ability!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; 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++; LK.getSound('tower_upgrade').play(); // Special ability is unlocked separately at level 3 (no longer automatic) // Special ability unlocking is now handled by separate purchase button // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rapid') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect self.damage = 5 + self.level * 10; // double the effect self.bulletSpeed = 7 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades self.damage = 5 + self.level * 3; self.bulletSpeed = 7 + self.level * 0.7; } } else if (self.id === 'inferno') { // Inferno tower damage scales differently (DPS) self.damage = 5 + self.level * 3; } else if (self.id === 'wizard') { if (self.level === self.maxLevel) { self.fireRate = Math.max(30, 120 - self.level * 30); self.damage = 10 + self.level * 15; self.bulletSpeed = 6 + self.level * 1.5; } else { self.fireRate = Math.max(60, 120 - self.level * 10); self.damage = 10 + self.level * 5; self.bulletSpeed = 6 + self.level * 0.5; } } else if (self.id === 'mortar') { if (self.level === self.maxLevel) { self.fireRate = Math.max(60, 180 - self.level * 40); self.damage = 30 + self.level * 25; self.bulletSpeed = 3 + self.level * 1.0; } else { self.fireRate = Math.max(120, 180 - self.level * 10); self.damage = 30 + self.level * 8; self.bulletSpeed = 3 + self.level * 0.3; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var enemiesInRange = []; // First, collect all enemies in range for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range and not hidden underground if (distance <= self.getRange() && !enemy.isHidden) { // Mortar can't hit enemies that are too close if (self.id === 'mortar' && distance < self.minRange) { continue; } enemiesInRange.push({ enemy: enemy, distance: distance, dx: dx, dy: dy }); } } if (enemiesInRange.length === 0) { self.targetEnemy = null; return null; } var targetEnemy = null; switch (self.targetingMode) { case 'first': // Target enemy closest to the base (lowest path score or closest to goal for flying) var lowestScore = Infinity; for (var i = 0; i < enemiesInRange.length; i++) { var enemyData = enemiesInRange[i]; var enemy = enemyData.enemy; if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < lowestScore) { lowestScore = distToGoal; targetEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (enemyData.distance < lowestScore) { lowestScore = enemyData.distance; targetEnemy = 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 < lowestScore) { lowestScore = cell.score; targetEnemy = enemy; } } } } break; case 'last': // Target enemy farthest from the base (highest path score) var highestScore = -Infinity; for (var i = 0; i < enemiesInRange.length; i++) { var enemyData = enemiesInRange[i]; var enemy = enemyData.enemy; if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal (inverse) 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 inverse distance to goal as score if (distToGoal > highestScore) { highestScore = distToGoal; targetEnemy = enemy; } } } else { // For ground enemies var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Higher score means farther from exit if (cell.score > highestScore) { highestScore = cell.score; targetEnemy = enemy; } } } } break; case 'strong': // Target enemy with the most HP var mostHP = -1; for (var i = 0; i < enemiesInRange.length; i++) { var enemy = enemiesInRange[i].enemy; if (enemy.health > mostHP) { mostHP = enemy.health; targetEnemy = enemy; } } break; case 'crowd': // Target enemy with the most other enemies nearby var maxCrowdScore = -1; for (var i = 0; i < enemiesInRange.length; i++) { var enemyData = enemiesInRange[i]; var enemy = enemyData.enemy; var crowdScore = 0; // Count enemies within 2 cells of this enemy for (var j = 0; j < enemies.length; j++) { if (enemies[j] !== enemy) { var edx = enemies[j].x - enemy.x; var edy = enemies[j].y - enemy.y; var edist = Math.sqrt(edx * edx + edy * edy); if (edist <= CELL_SIZE * 2) { crowdScore++; } } } if (crowdScore > maxCrowdScore) { maxCrowdScore = crowdScore; targetEnemy = enemy; } } break; } // For inferno tower with special ability, find up to 3 targets if (self.id === 'inferno' && self.hasSpecialAbility && enemiesInRange.length > 0) { self.infernoTargets = []; // Sort enemies by distance enemiesInRange.sort(function (a, b) { return a.distance - b.distance; }); // Get up to 3 targets for (var i = 0; i < Math.min(3, enemiesInRange.length); i++) { self.infernoTargets.push(enemiesInRange[i].enemy); } return self.infernoTargets[0]; // Return primary target } return targetEnemy; }; self.update = function () { // Farm towers don't target or shoot if (self.id === 'farm') { return; } // Laser towers are handled separately if (self.id === 'laser') { return; } // Church towers don't shoot if (self.id === 'church') { return; } // Inferno tower uses continuous beam if (self.id === 'inferno') { self.targetEnemy = self.findTarget(); if (self.targetEnemy) { // Show and position beam self.infernoBeam.visible = true; var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); var distance = Math.sqrt(dx * dx + dy * dy); self.infernoBeam.rotation = angle; self.infernoBeam.width = distance; gunContainer.rotation = angle; // Deal damage every 10 ticks self.infernoDamageTimer++; if (self.infernoDamageTimer >= 10) { self.infernoDamageTimer = 0; // Calculate damage based on level var damagePerTick = oneHitKillCheat ? 999999 : self.damage + self.level * 3; self.targetEnemy.health -= damagePerTick; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Visual effect LK.effects.flashObject(self.targetEnemy, 0xFF4500, 100); // Handle multi-target ability if (self.hasSpecialAbility && self.infernoTargets.length > 1) { // Damage additional targets (50% damage) for (var i = 1; i < self.infernoTargets.length; i++) { var additionalTarget = self.infernoTargets[i]; if (additionalTarget && additionalTarget.parent && !additionalTarget.isHidden) { additionalTarget.health -= oneHitKillCheat ? 999999 : damagePerTick * 0.5; if (additionalTarget.health <= 0) { additionalTarget.health = 0; } else { additionalTarget.healthBar.width = additionalTarget.health / additionalTarget.maxHealth * 70; } LK.effects.flashObject(additionalTarget, 0xFF4500, 100); } } } } } else { self.infernoBeam.visible = false; self.infernoDamageTimer = 0; self.infernoTargets = []; } return; } self.targetEnemy = self.findTarget(); if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); gunContainer.rotation = angle; // Handle rapid burst fire for archer if (self.rapidBurstCount > 0) { if (LK.ticks - self.lastFired >= 6) { // Very fast fire rate during burst self.fire(); self.lastFired = LK.ticks; self.rapidBurstCount--; if (self.rapidBurstCount === 0) { self.specialAbilityActive = false; } } } else 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; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 325 }, { 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 () { // Handle archer rapid fire ability if (self.id === 'default' && self.hasSpecialAbility && Math.random() < 0.25) { self.rapidBurstCount = 5; self.specialAbilityActive = true; } // Handle mortar quad shot ability if (self.id === 'mortar' && self.hasSpecialAbility) { // Fire 4 projectiles at once if (self.targetEnemy) { for (var i = 0; i < 4; i++) { var offsetAngle = (i - 1.5) * 0.3; // Spread the shots var targetX = self.targetEnemy.x + Math.cos(offsetAngle) * 50; var targetY = self.targetEnemy.y + Math.sin(offsetAngle) * 50; var bulletX = self.x + Math.cos(gunContainer.rotation + offsetAngle) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation + offsetAngle) * 40; var bullet = new Bullet(bulletX, bulletY, null, self.damage, self.bulletSpeed); bullet.type = 'mortar'; bullet.targetX = targetX; bullet.targetY = targetY; // Replace bullet graphics bullet.removeChild(bullet.children[0]); var mortarGraphics = bullet.attachAsset('bullet_mortar', { anchorX: 0.5, anchorY: 0.5 }); game.addChild(bullet); bullets.push(bullet); } } return; } // Handle cannon mega blast ability if (self.id === 'splash' && self.hasSpecialAbility && self.specialAbilityTimer <= 0) { self.fireRate = 25; // Triple fire rate self.specialAbilityTimer = 180; // 3 second duration } if (self.id === 'splash' && self.specialAbilityTimer > 0) { self.specialAbilityTimer--; if (self.specialAbilityTimer === 0) { self.fireRate = 75; // Reset to normal } } if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bulletDamage = oneHitKillCheat ? 999999 : self.damage; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, bulletDamage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; bullet.hasPermafrostAbility = self.hasSpecialAbility; } else if (self.id === 'rapid') { bullet.hasBleedAbility = self.hasSpecialAbility; } else if (self.id === 'wizard') { bullet.hasCharmAbility = self.hasSpecialAbility; } // For mortar, store target position instead of enemy if (self.id === 'mortar') { bullet.targetX = self.targetEnemy.x; bullet.targetY = self.targetEnemy.y; bullet.targetEnemy = null; // Mortar targets location, not enemy } // Replace bullet graphics with tower-specific asset var bulletAssetId = 'bullet'; if (self.id !== 'default') { bulletAssetId = 'bullet_' + self.id; } // Remove the old generic bullet graphic bullet.removeChild(bullet.children[0]); // Attach the specific bullet asset var specificBulletGraphics = bullet.attachAsset(bulletAssetId, { anchorX: 0.5, anchorY: 0.5 }); game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // Play shooting sound based on tower type if (self.id === 'sniper') { LK.getSound('tower_shoot').play(); } else if (self.id === 'splash' || self.id === 'mortar') { LK.getSound('explosion').play(); } else if (self.id === 'slow') { LK.getSound('freeze').play(); } else if (self.id === 'wizard') { LK.getSound('charm').play(); } else { LK.getSound('tower_shoot').play(); } // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; } } } self.refreshCellsInRange(); }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; self.hasEnoughGold = true; var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; break; case 'sniper': previewGraphics.tint = 0xFF5500; break; case 'splash': previewGraphics.tint = 0x33CC00; break; case 'slow': previewGraphics.tint = 0x9900FF; break; case 'farm': previewGraphics.tint = 0xFFD700; // Gold color break; case 'laser': previewGraphics.tint = 0xFF0088; // Pink color break; case 'inferno': previewGraphics.tint = 0xFF4500; // Orange-red break; case 'wizard': previewGraphics.tint = 0xFF1493; // Deep pink break; case 'church': previewGraphics.tint = 0xFFD700; // Gold break; case 'mortar': previewGraphics.tint = 0x8B4513; // Saddle brown break; default: previewGraphics.tint = 0xAAAAAA; } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) { validGridPlacement = false; } else { for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (!cell || cell.type !== 0) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) { continue; } // Only check non-flying enemies, flying enemies can pass over towers if (!enemy.isFlying) { if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) { self.blockedByEnemy = true; break; } } } } } self.canPlace = validGridPlacement && !self.blockedByEnemy; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; self.checkPlacement(); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 325; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 700; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; // Get display name for tower type var displayName = self.tower.id; switch (self.tower.id) { case 'default': displayName = 'Bow'; break; case 'rapid': displayName = 'Crossbow'; break; case 'splash': displayName = 'Cannon'; break; case 'slow': displayName = 'Ice Wizard'; break; case 'farm': displayName = 'Farm'; break; case 'laser': displayName = 'Laser Base Alpha'; break; case 'inferno': displayName = 'Inferno'; break; case 'wizard': displayName = 'Tricky Wizard'; break; case 'church': displayName = 'Church'; break; case 'mortar': displayName = 'Mortar'; break; } var towerTypeText = new Text2(displayName + ' 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(self.tower.id === 'inferno' ? 'Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + (self.tower.damage + self.tower.level * 3) + ' DPS\nRange: ' + (self.tower.getRange() / CELL_SIZE).toFixed(1) + ' tiles' : self.tower.id === 'church' ? 'Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nLives per wave: ' + self.tower.level + '\nNo combat ability' : '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); // Special ability button var specialButton = new Container(); buttonsContainer.addChild(specialButton); var specialBackground = specialButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); specialBackground.width = 500; specialBackground.height = 150; var hasAbility = self.tower.hasSpecialAbility; var abilityName = self.tower.getSpecialAbilityName(); var canUnlock = self.tower.level >= 3 && !hasAbility; var abilityCost = canUnlock ? self.tower.getSpecialAbilityCost() : 0; var lockText = self.tower.level < 3 ? 'Locked (Lv 3)' : canUnlock ? 'Buy: ' + abilityCost + ' gold' : 'Locked'; specialBackground.tint = hasAbility ? 0x9400D3 : canUnlock ? gold >= abilityCost ? 0x00AA00 : 0x888888 : 0x444444; var specialText = new Text2(hasAbility ? abilityName : lockText, { size: 60, fill: 0xFFFFFF, weight: 800 }); specialText.anchor.set(0.5, 0.5); specialButton.addChild(specialText); 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); // Targeting mode button var targetingButton = new Container(); buttonsContainer.addChild(targetingButton); var targetingBackground = targetingButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); targetingBackground.width = 500; targetingBackground.height = 150; targetingBackground.tint = 0x0088FF; var targetingModeNames = { 'first': 'First', 'last': 'Last', 'strong': 'Strong', 'crowd': 'Crowd' }; var targetingText = new Text2('Target: ' + targetingModeNames[self.tower.targetingMode], { size: 60, fill: 0xFFFFFF, weight: 800 }); targetingText.anchor.set(0.5, 0.5); targetingButton.addChild(targetingText); upgradeButton.y = -210; specialButton.y = -70; targetingButton.y = 70; sellButton.y = 210; 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) { LK.getSound('button_click').play(); 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)); } if (self.tower.id === 'inferno') { statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + (self.tower.damage + self.tower.level * 3) + ' DPS\nRange: ' + (self.tower.getRange() / CELL_SIZE).toFixed(1) + ' tiles'); } else if (self.tower.id === 'church') { statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nLives per wave: ' + self.tower.level + '\nNo combat ability'); } else { statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); } // Update special ability button when upgrading if (self.tower.hasSpecialAbility && !hasAbility) { specialBackground.tint = 0x9400D3; specialText.setText(self.tower.getSpecialAbilityName()); } 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 }); } }); } }; specialButton.down = function () { if (self.tower.hasSpecialAbility) { var notification = game.addChild(new Notification("Special Ability: " + self.tower.getSpecialAbilityName())); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (self.tower.level >= 3) { // Try to purchase special ability if (self.tower.unlockSpecialAbility()) { // Update button appearance after successful purchase specialBackground.tint = 0x9400D3; specialText.setText(self.tower.getSpecialAbilityName()); } } else { var notification = game.addChild(new Notification("Reach level 3 to unlock special ability!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; targetingButton.down = function () { // Cycle through targeting modes var modes = ['first', 'last', 'strong', 'crowd']; var currentIndex = modes.indexOf(self.tower.targetingMode); var nextIndex = (currentIndex + 1) % modes.length; self.tower.targetingMode = modes[nextIndex]; var targetingModeNames = { 'first': 'First', 'last': 'Last', 'strong': 'Strong', 'crowd': 'Crowd' }; targetingText.setText('Target: ' + targetingModeNames[self.tower.targetingMode]); // Visual feedback tween(targetingButton, { scaleX: 1.1, scaleY: 1.1 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(targetingButton, { 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(); // Update laser walls if we removed a laser tower if (self.tower.id === 'laser') { updateLaserWalls(); } 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; } } else { // 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); } } // Update special ability button if (!self.tower.hasSpecialAbility && self.tower.level >= 3) { var abilityCost = self.tower.getSpecialAbilityCost(); var canAffordAbility = gold >= abilityCost; specialBackground.tint = canAffordAbility ? 0x00AA00 : 0x888888; var newSpecialText = 'Buy: ' + abilityCost + ' gold'; if (specialText.text !== newSpecialText) { specialText.setText(newSpecialText); } } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; LK.getSound('wave_start').play(); } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (i === 5) { block.tint = 0x00FFFF; waveType = "Flying Horde"; enemyType = "flying_horde"; enemyCount = 3; // Few spawners that create many } else if (i === 6) { block.tint = 0x8B4513; waveType = "Mole Digger"; enemyType = "mole"; enemyCount = 8; } else if (i === 7) { block.tint = 0xFFFFFF; waveType = "Mixed"; enemyType = "mixed"; enemyCount = 12; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0xFFFF00; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Boss Normal"; break; case 'fast': block.tint = 0x00AAFF; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xAA0000; waveType = "Boss Immune"; break; case 'flying': block.tint = 0xFFFF00; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the color of the wave type switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; break; case 'fast': block.tint = 0x00AAFF; break; case 'immune': block.tint = 0xAA0000; break; case 'flying': block.tint = 0xFFFF00; break; default: block.tint = 0xFF0000; break; } } else if (i + 1 > 10) { // After wave 10, all non-boss waves are completely random var randomWaveTypes = [{ type: 'normal', name: 'Normal', tint: 0xAAAAAA, count: 10 }, { type: 'fast', name: 'Fast', tint: 0x00AAFF, count: 10 }, { type: 'immune', name: 'Immune', tint: 0xAA0000, count: 10 }, { type: 'flying', name: 'Flying', tint: 0xFFFF00, count: 10 }, { type: 'swarm', name: 'Swarm', tint: 0xFF00FF, count: 30 }, { type: 'flying_horde', name: 'Flying Horde', tint: 0x00FFFF, count: 3 }, { type: 'mole', name: 'Mole Digger', tint: 0x8B4513, count: 8 }, { type: 'mixed', name: 'Mixed', tint: 0xFFFFFF, count: 12 }]; var randomChoice = randomWaveTypes[Math.floor(Math.random() * randomWaveTypes.length)]; enemyType = randomChoice.type; waveType = randomChoice.name; block.tint = randomChoice.tint; enemyCount = randomChoice.count; } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if ((i + 1) % 8 === 0) { // Every 8th non-boss wave is flying horde block.tint = 0x00FFFF; waveType = "Flying Horde"; enemyType = "flying_horde"; enemyCount = 3; } else if ((i + 1) % 9 === 0) { // Every 9th non-boss wave is mole digger block.tint = 0x8B4513; waveType = "Mole Digger"; enemyType = "mole"; enemyCount = 8; } else if ((i + 1) % 6 === 0) { // Every 6th non-boss wave is mixed block.tint = 0xFFFFFF; waveType = "Mixed"; enemyType = "mixed"; enemyCount = 12; } 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(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; for (var i = 0; i < totalWaves + 1; i++) { var marker = self.waveMarkers[i]; if (i === 0) { continue; } var block = marker.children[0]; if (i - 1 < currentWave) { block.alpha = .5; } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { waveTimer++; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; if (currentWave != 1) { var waveType = self.getWaveTypeName(currentWave); var enemyCount = self.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } } }; self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 325 }, { 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 laserWalls = []; var selectedTower = null; var gold = 100; 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 // Cheat variables var unlimitedMoneyCheat = false; var oneHitKillCheat = false; var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); } function setGold(value) { if (unlimitedMoneyCheat) { gold = 999999; } else { gold = value; } updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY) { var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 5; switch (towerType) { case 'rapid': cost = 15; break; case 'sniper': cost = 25; break; case 'splash': cost = 35; break; case 'slow': cost = 45; break; case 'farm': cost = 30; break; case 'laser': cost = 40; break; case 'inferno': cost = 50; break; case 'wizard': cost = 40; break; case 'church': cost = 60; break; case 'mortar': cost = 55; break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function updateLaserWalls() { // Remove all existing laser walls for (var i = laserWalls.length - 1; i >= 0; i--) { game.removeChild(laserWalls[i]); } laserWalls = []; // Group laser towers by row var laserTowersByRow = {}; for (var i = 0; i < towers.length; i++) { if (towers[i].id === 'laser') { var row = towers[i].gridY; if (!laserTowersByRow[row]) { laserTowersByRow[row] = []; } laserTowersByRow[row].push(towers[i]); } } // Create laser walls for rows with 2 or more laser towers for (var row in laserTowersByRow) { var rowTowers = laserTowersByRow[row]; if (rowTowers.length >= 2) { // Sort towers by x position rowTowers.sort(function (a, b) { return a.x - b.x; }); // Create walls between adjacent towers for (var i = 0; i < rowTowers.length - 1; i++) { var wall = new LaserWall(rowTowers[i], rowTowers[i + 1]); game.addChild(wall); laserWalls.push(wall); } } } } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); LK.getSound('place_tower').play(); grid.pathFind(); grid.renderDebug(); // Update laser walls if we placed a laser tower if (towerType === 'laser') { updateLaserWalls(); } return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; LK.getSound('error').play(); return false; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } // Check if clicking on a class button for (var i = 0; i < classButtons.length; i++) { var button = classButtons[i]; if (x >= button.x - button.width / 2 && x <= button.x + button.width / 2 && y >= button.y - button.height / 2 && y <= button.y + button.height / 2) { // Class button was clicked, let it handle the event 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.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; 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); // Tower classes configuration var towerClasses = { 'Single Damage': { towers: ['default', 'rapid', 'sniper', 'inferno'], color: 0x4488FF }, 'Area Damage': { towers: ['splash', 'laser', 'mortar'], color: 0xFF8844 }, 'Weakening': { towers: ['slow', 'wizard'], color: 0x8844FF }, 'Income/Lives': { towers: ['farm', 'church'], color: 0x44FF88 } }; var sourceTowers = []; var classButtons = []; var currentClass = null; var towerSelectionContainer = new Container(); game.addChild(towerSelectionContainer); // Create class buttons var classButtonWidth = 400; var classButtonHeight = 80; var classButtonSpacing = 20; var classNames = Object.keys(towerClasses); var totalClassWidth = classNames.length * classButtonWidth + (classNames.length - 1) * classButtonSpacing; var classStartX = 2048 / 2 - totalClassWidth / 2 + classButtonWidth / 2; var classButtonY = 2732 - CELL_SIZE * 3 - 180; for (var i = 0; i < classNames.length; i++) { var className = classNames[i]; var classData = towerClasses[className]; var classButton = new Container(); classButton.className = className; classButton.classData = classData; var buttonBg = classButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBg.width = classButtonWidth; buttonBg.height = classButtonHeight; buttonBg.tint = classData.color; buttonBg.alpha = 0.7; var buttonText = new Text2(className, { size: 40, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); classButton.addChild(buttonText); classButton.x = classStartX + i * (classButtonWidth + classButtonSpacing); classButton.y = classButtonY; classButton.down = function () { selectTowerClass(this.className); }; towerSelectionContainer.addChild(classButton); classButtons.push(classButton); } // Function to select a tower class function selectTowerClass(className) { // Update class button appearances for (var i = 0; i < classButtons.length; i++) { var button = classButtons[i]; var bg = button.children[0]; if (button.className === className) { bg.alpha = 1.0; bg.tint = button.classData.color; } else { bg.alpha = 0.3; bg.tint = 0x666666; } } // Clear existing source towers for (var i = 0; i < sourceTowers.length; i++) { towerLayer.removeChild(sourceTowers[i]); } sourceTowers = []; // Create towers for selected class currentClass = className; var classData = towerClasses[className]; var towerTypes = classData.towers; var towerSpacing = 300; 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); } } // Select first class by default selectTowerClass(classNames[0]); // Play theme music LK.playMusic('game_theme'); // Create manual button var manualButton = new Container(); var manualBg = manualButton.attachAsset('manualButton', { anchorX: 0.5, anchorY: 0.5 }); manualBg.tint = 0x4CAF50; var manualText = new Text2('?', { size: 70, fill: 0xFFFFFF, weight: 800 }); manualText.anchor.set(0.5, 0.5); manualButton.addChild(manualText); manualButton.x = 2048 - 80; manualButton.y = 150; manualButton.down = function () { var manual = new Manual(); game.addChild(manual); }; game.addChild(manualButton); // Create cheat button var cheatButton = new Container(); var cheatBg = cheatButton.attachAsset('manualButton', { anchorX: 0.5, anchorY: 0.5 }); cheatBg.tint = 0xFF0000; var cheatText = new Text2('C', { size: 70, fill: 0xFFFFFF, weight: 800 }); cheatText.anchor.set(0.5, 0.5); cheatButton.addChild(cheatText); cheatButton.x = 2048 - 200; cheatButton.y = 150; cheatButton.down = function () { var cheatMenu = new CheatMenu(); game.addChild(cheatMenu); }; game.addChild(cheatButton); sourceTower = null; enemiesToSpawn = 10; game.update = function () { if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; if (isBossWave && waveType !== 'swarm') { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; LK.getSound('boss_spawn').play(); } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy; if (waveType === 'mixed') { // Mixed wave spawns random enemy types var mixedTypes = ['normal', 'fast', 'immune', 'flying', 'swarm', 'mole']; var randomType = mixedTypes[Math.floor(Math.random() * mixedTypes.length)]; enemy = new Enemy(randomType); } else { 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; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // All enemy types now spawn in the middle 6 tiles at the top spacing var gridWidth = 24; var midPoint = Math.floor(gridWidth / 2); // 12 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading enemy.cellX = spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; // Give income from all farm towers var totalFarmIncome = 0; for (var i = 0; i < towers.length; i++) { if (towers[i].id === 'farm') { // Base income 10, +15 per level var farmIncome = 10 + (towers[i].level - 1) * 15; totalFarmIncome += farmIncome; } } if (totalFarmIncome > 0) { setGold(gold + totalFarmIncome); var notification = game.addChild(new Notification("Farm income: +" + totalFarmIncome + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 250; } // Give lives from all church towers var totalChurchLives = 0; var hasDivineIntervention = false; for (var i = 0; i < towers.length; i++) { if (towers[i].id === 'church') { // Church gives 1 life per wave at level 1, +1 per level upgrade totalChurchLives += towers[i].level; // Level 1 = 1 life, Level 2 = 2 lives, etc. // Check for divine intervention ability if (towers[i].hasSpecialAbility && Math.random() < 0.1) { // 10% chance hasDivineIntervention = true; } } } if (totalChurchLives > 0) { lives += totalChurchLives; updateUI(); var notification = game.addChild(new Notification("Church blessing: +" + totalChurchLives + " lives!")); notification.x = 2048 / 2; notification.y = grid.height - 300; } // Handle divine intervention if (hasDivineIntervention && currentWave < totalWaves) { // Skip the next wave and give 350 gold currentWave++; waveTimer = 0; setGold(gold + 350); var notification = game.addChild(new Notification("Divine Intervention! Wave skipped +350 gold!")); notification.x = 2048 / 2; notification.y = grid.height - 350; LK.getSound('divine_intervention').play(); } // Check for laser tower pairs and create/update laser walls updateLaserWalls(); } } 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); // Check if killed by a tower in range of a farm with special ability var farmBonus = 0; for (var i = 0; i < towers.length; i++) { if (towers[i].id === 'farm' && towers[i].hasSpecialAbility) { // Check if any tower that could have killed this enemy is in range of the farm var farmX = towers[i].x; var farmY = towers[i].y; var farmRange = 3 * CELL_SIZE; // Farm boost range for (var j = 0; j < towers.length; j++) { if (towers[j].id !== 'farm') { var dx = towers[j].x - farmX; var dy = towers[j].y - farmY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= farmRange) { // Check if this tower could have killed the enemy var towerToEnemyDx = enemy.x - towers[j].x; var towerToEnemyDy = enemy.y - towers[j].y; var towerToEnemyDist = Math.sqrt(towerToEnemyDx * towerToEnemyDx + towerToEnemyDy * towerToEnemyDy); if (towerToEnemyDist <= towers[j].getRange()) { farmBonus = Math.ceil(goldEarned * 0.25); // 25% bonus break; } } } } if (farmBonus > 0) break; } } var totalGold = goldEarned + farmBonus; var goldIndicator = new GoldIndicator(totalGold, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + totalGold); LK.getSound('enemy_death').play(); LK.getSound('gold_collect').play(); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // 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) { LK.getSound('game_over').play(); LK.stopMusic(); LK.showGameOver(); } } } 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(); } // Update all laser walls for (var i = 0; i < laserWalls.length; i++) { laserWalls[i].update(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.getSound('victory').play(); LK.stopMusic(); LK.showYouWin(); } };
===================================================================
--- original.js
+++ change.js
@@ -27,10 +27,8 @@
if (distance < self.speed) {
// Create explosion effect
var mortarEffect = new EffectIndicator(self.targetX, self.targetY, 'mortar');
game.addChild(mortarEffect);
- // Play explosion sound
- LK.getSound('explosion').play();
// Area damage
var explosionRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
@@ -63,12 +61,11 @@
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) {
- // Play hit sound
- LK.getSound('enemy_hit').play();
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
+ LK.getSound('enemy_hit').play();
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
@@ -77,10 +74,8 @@
if (self.type === 'splash') {
// Create visual splash effect
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
- // Play explosion sound for splash
- LK.getSound('explosion').play();
// Splash damage to nearby enemies
var splashRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
@@ -1254,9 +1249,8 @@
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(upgradeCost * 3.5 / 2);
}
if (gold >= upgradeCost) {
- LK.getSound('tower_upgrade').play();
setGold(gold - upgradeCost);
self.level++;
self.damage = 18 + self.level * 24; // Increased base by 2 and maintain scaling
// Unlock special ability at level 3
@@ -1318,17 +1312,20 @@
// Check if enemy is within the laser's horizontal range
if (enemy.x >= self.x && enemy.x <= self.x + laserGraphics.width) {
// Deal damage every 30 ticks (0.5 seconds)
if (LK.ticks % 30 === 0) {
- LK.getSound('laser_beam').play();
enemy.health -= oneHitKillCheat ? 999999 : self.damage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Visual effect
LK.effects.flashObject(enemy, 0xFF0088, 100);
+ // Play laser sound occasionally
+ if (LK.ticks % 120 === 0) {
+ LK.getSound('laser_beam').play();
+ }
}
}
}
}
@@ -1789,9 +1786,8 @@
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves) {
- LK.getSound('button_click').play();
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
@@ -2263,11 +2259,11 @@
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
}
if (gold >= upgradeCost) {
- LK.getSound('tower_upgrade').play();
setGold(gold - upgradeCost);
self.level++;
+ LK.getSound('tower_upgrade').play();
// Special ability is unlocked separately at level 3 (no longer automatic)
// Special ability unlocking is now handled by separate purchase button
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
@@ -2526,10 +2522,8 @@
// Deal damage every 10 ticks
self.infernoDamageTimer++;
if (self.infernoDamageTimer >= 10) {
self.infernoDamageTimer = 0;
- // Play inferno beam sound
- LK.getSound('inferno_beam').play();
// Calculate damage based on level
var damagePerTick = oneHitKillCheat ? 999999 : self.damage + self.level * 3;
self.targetEnemy.health -= damagePerTick;
if (self.targetEnemy.health <= 0) {
@@ -2667,9 +2661,8 @@
// Handle mortar quad shot ability
if (self.id === 'mortar' && self.hasSpecialAbility) {
// Fire 4 projectiles at once
if (self.targetEnemy) {
- LK.getSound('mortar_launch').play();
for (var i = 0; i < 4; i++) {
var offsetAngle = (i - 1.5) * 0.3; // Spread the shots
var targetX = self.targetEnemy.x + Math.cos(offsetAngle) * 50;
var targetY = self.targetEnemy.y + Math.sin(offsetAngle) * 50;
@@ -2740,19 +2733,23 @@
var specificBulletGraphics = bullet.attachAsset(bulletAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
- // Play appropriate sound effect
- if (self.id === 'slow') {
- LK.getSound('ice_freeze').play();
+ game.addChild(bullet);
+ bullets.push(bullet);
+ self.targetEnemy.bulletsTargetingThis.push(bullet);
+ // Play shooting sound based on tower type
+ if (self.id === 'sniper') {
+ LK.getSound('tower_shoot').play();
+ } else if (self.id === 'splash' || self.id === 'mortar') {
+ LK.getSound('explosion').play();
+ } else if (self.id === 'slow') {
+ LK.getSound('freeze').play();
} else if (self.id === 'wizard') {
- LK.getSound('spell_cast').play();
+ LK.getSound('charm').play();
} else {
LK.getSound('tower_shoot').play();
}
- game.addChild(bullet);
- bullets.push(bullet);
- self.targetEnemy.bulletsTargetingThis.push(bullet);
// --- Fire recoil effect for gunContainer ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
x: true,
@@ -3138,8 +3135,9 @@
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
+ LK.getSound('button_click').play();
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;
@@ -3390,10 +3388,8 @@
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
if (!self.gameStarted) {
- LK.getSound('button_click').play();
- LK.playMusic('theme_music');
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
@@ -3404,8 +3400,9 @@
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
+ LK.getSound('wave_start').play();
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
@@ -3782,10 +3779,8 @@
/****
* Game Code
****/
-// Background music
-// Sound effects
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
@@ -3990,14 +3985,14 @@
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
- LK.getSound('tower_place').play();
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
+ LK.getSound('place_tower').play();
grid.pathFind();
grid.renderDebug();
// Update laser walls if we placed a laser tower
if (towerType === 'laser') {
@@ -4007,8 +4002,9 @@
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
+ LK.getSound('error').play();
return false;
}
}
game.down = function (x, y, obj) {
@@ -4185,9 +4181,8 @@
classButton.addChild(buttonText);
classButton.x = classStartX + i * (classButtonWidth + classButtonSpacing);
classButton.y = classButtonY;
classButton.down = function () {
- LK.getSound('button_click').play();
selectTowerClass(this.className);
};
towerSelectionContainer.addChild(classButton);
classButtons.push(classButton);
@@ -4227,8 +4222,10 @@
}
}
// Select first class by default
selectTowerClass(classNames[0]);
+// Play theme music
+LK.playMusic('game_theme');
// Create manual button
var manualButton = new Container();
var manualBg = manualButton.attachAsset('manualButton', {
anchorX: 0.5,
@@ -4244,9 +4241,8 @@
manualButton.addChild(manualText);
manualButton.x = 2048 - 80;
manualButton.y = 150;
manualButton.down = function () {
- LK.getSound('button_click').play();
var manual = new Manual();
game.addChild(manual);
};
game.addChild(manualButton);
@@ -4266,9 +4262,8 @@
cheatButton.addChild(cheatText);
cheatButton.x = 2048 - 200;
cheatButton.y = 150;
cheatButton.down = function () {
- LK.getSound('button_click').play();
var cheatMenu = new CheatMenu();
game.addChild(cheatMenu);
};
game.addChild(cheatButton);
@@ -4285,18 +4280,13 @@
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;
- // Play boss music and sound
- LK.getSound('boss_appear').play();
- LK.playMusic('boss_music');
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
- } else {
- // Play wave start sound for non-boss waves
- LK.getSound('wave_start').play();
+ LK.getSound('boss_spawn').play();
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy;
@@ -4373,12 +4363,8 @@
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
- // Resume theme music after boss wave
- if ((currentWave - 1) % 10 === 0 && currentWave > 1) {
- LK.playMusic('theme_music');
- }
// Give income from all farm towers
var totalFarmIncome = 0;
for (var i = 0; i < towers.length; i++) {
if (towers[i].id === 'farm') {
@@ -4422,8 +4408,9 @@
setGold(gold + 350);
var notification = game.addChild(new Notification("Divine Intervention! Wave skipped +350 gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 350;
+ LK.getSound('divine_intervention').play();
}
// Check for laser tower pairs and create/update laser walls
updateLaserWalls();
}
@@ -4467,21 +4454,19 @@
}
var totalGold = goldEarned + farmBonus;
var goldIndicator = new GoldIndicator(totalGold, enemy.x, enemy.y);
game.addChild(goldIndicator);
- LK.getSound('coin_collect').play();
setGold(gold + totalGold);
+ LK.getSound('enemy_death').play();
+ LK.getSound('gold_collect').play();
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Add a notification for boss defeat
if (enemy.isBoss) {
- LK.getSound('enemy_death').play();
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
- } else {
- LK.getSound('enemy_death').play();
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
@@ -4512,8 +4497,10 @@
enemies.splice(a, 1);
lives = Math.max(0, lives - 1);
updateUI();
if (lives <= 0) {
+ LK.getSound('game_over').play();
+ LK.stopMusic();
LK.showGameOver();
}
}
}
@@ -4536,7 +4523,9 @@
for (var i = 0; i < laserWalls.length; i++) {
laserWalls[i].update();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
+ LK.getSound('victory').play();
+ LK.stopMusic();
LK.showYouWin();
}
};
\ No newline at end of file
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
Wooden Guard Tower. In-Game asset. 2d. High contrast. No shadows
Bow: make it face 90 degrees In-Game asset. 2d. High contrast. No shadows
Wooden Tower base like the ones the Cannons in Clash of Clans have. In-Game asset. 2d. High contrast. No shadows. Topdown
Cannon without wheels or base, just the cannon. In-Game asset. 2d. High contrast. No shadows. Topdown
Crossbow rotated 90 degrees. In-Game asset. 2d. High contrast. No shadows
Sniper rifle rotated 90 degrees. In-Game asset. 2d. High contrast. No shadows
Ice tower. In-Game asset. 2d. High contrast. No shadows
Ice staff. In-Game asset. 2d. High contrast. No shadows
Windmill. In-Game asset. 2d. High contrast. No shadows
Laser pointer without pointing a laser. In-Game asset. 2d. High contrast. No shadows. Topdown
Laser projectile In-Game asset. 2d. High contrast. No shadows
Orc holding a small axe. In-Game asset. 2d. High contrast. No shadows
Orc with a big wooden shield full of spikes. In-Game asset. 2d. High contrast. No shadows
Orc in a wooden helicopter. In-Game asset. 2d. High contrast. No shadows
Spider. In-Game asset. 2d. High contrast. No shadows
One minion from Clash Royale. In-Game asset. 2d. High contrast. No shadows
Mole with a minerer's hat and a pickaxe. In-Game asset. 2d. High contrast. No shadows
Wolf. In-Game asset. 2d. High contrast. No shadows
Dark magma tower. In-Game asset. 2d. High contrast. No shadows. Topdown
Add outlines
Magician's staff. In-Game asset. 2d. High contrast. No shadows
Mortar from Clash Royale without base, just the Mortar. In-Game asset. 2d. High contrast. No shadows. Topdown
Tophat house. In-Game asset. 2d. High contrast. No shadows
Church. In-Game asset. 2d. High contrast. No shadows
tower_upgrade
Sound effect
enemy_hit
Sound effect
enemy_death
Sound effect
wave_start
Sound effect
place_tower
Sound effect
boss_spawn
Sound effect
game_over
Sound effect
victory
Sound effect
laser_beam
Sound effect
game_theme
Music
explosion
Sound effect
freeze
Sound effect
divine_intervention
Sound effect
gold_collect
Sound effect