User prompt
Now there's a new upgrade button that, when bought, it's respective tower gains a special ability! (Archer: 25% chance to shoot 5 bullets in quick succession; Crossbow: Has a 10% chance to make enemies bleed (enemies with bleed take 5 damage every second), Sniper: gains infinite range; Inferno: can target up to 3 enemies with its beam; Cannon: Firerate and blast radius triple, Laser wall: can hit flyings and underground moles; Mortar: Shoots 4 projectiles at once; Ice wizard: slow effect becomes permanent; Tricky Wizard: Confused effect is replaced with a charmed effect that lets enemies fight for you for 5 seconds; Farm: towers in range of the farm give you extra money when killing enemies; Church: Has a chance to summon God to make you skip a wave and still gain 350 gold!)
User prompt
Special ability is now a separate upgrade
User prompt
Nowz when a tower reaches level 6, it gains a special ability! (Archer: 25% chance to shoot 5 bullets in quick succession; Crossbow: Has a 10% chance to make enemies bleed (enemies with bleed take 5 damage every second), Sniper: gains infinite range; Inferno: can target up to 3 enemies with its beam; Cannon: Firerate and blast radius triple, Laser wall: can hit flyings and underground moles; Mortar: Shoots 4 projectiles at once; Ice wizard: slow effect becomes permanent; Tricky Wizard: Confused effect is replaced with a charmed effect that lets enemies fight for you for 5 seconds; Farm: towers in range of the farm give you extra money when killing enemies; Church: Has a chance to summon God to make you skip a wave and still gain 350 gold!)
User prompt
Leveling up church does nothing: Fix that
User prompt
Add new towers: Inferno Tower (drains enemy's health with a beam. Type: Single damage), Tricky Wizard (his magic has a 50% chance of Confusing enemies: confused enemies move towards towers to take as much damage as possible. Type: Weakening), Church (Gives you lives after every wave. Type: Income/Lives), Mortar (has the highest range and great area damage. It can't hit enemies that are close to it. Type: Area damage)
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'x')' in or related to this line: 'var localPos = closeButton.toLocal(obj.position);' Line Number: 1426
User prompt
Make the manual screen scrollable on mobile
User prompt
Make the manual screen at the center of the screen
User prompt
Add new thingy: Manual! See your towers and enemies' stats and how they work
User prompt
New enemy types only appear once: Fix that by making all waves after wave 10 Completely random types of waves (excluding boss) except the multiples of 10 that always use Boss
User prompt
Increase starting gold from 80 to 100
User prompt
Add assets for new enemies
User prompt
Add new enemies: Flying Horde (spawn a lot of weak flying units!), Mole digger (uses its shovel to hide underground for 1.5 seconds every 5 seconds: while hidden it moves twice as fast and cannot be targeted), Mixed (spawn a various mix of random enemies) ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make it so that there are buttons for classes of towers (classes: Single damage: Bow, Crossbow, Sniper; Area Damage: Cannon, Laser Base Alpha; Weakening: Ice wizard; Income/Lives: Farm)
User prompt
Increase Laser wall damage by two
User prompt
Increase Laser wall damage by ONE
User prompt
Double Laser wall damage
User prompt
Add targeting options for towers: First (attacks the enemy closest to the base), Last (attacks the enemy Farthest from the base), Strong (attacks the enemy with the most HP), Crowd (attacks the most tightly packed swarm of enemies)
User prompt
Quadruple Laser Base Alpha's Laser wall damage but make it completely useless against flyings
User prompt
Increase FPS by hiding the arrows and numbers on tiles
User prompt
Make the game a lot more smooth ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make farms cost 30
User prompt
Add new towers: Farm (the farm gives you some passive money after every wave. Upgrading it makes it make more money), Laser Base Alpha (place 2 of these in the same horizontal row and they will make a Laser Wall that damages enemies that touch it. Upgrading the Laser wall (the tower cannot be upgraded, but the wall can) makes it deal more damage)
User prompt
Default tower gets renamed into "bow tower", Rapid into "Crossbow tower", Splash gets renamed into "Cannon", Slow gets renamed into "Ice Wizard tower", and poison gets removed entirely
User prompt
Add assets for each tower tile and add different assets for each tower's defense/projectile
/**** * 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 -= 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; 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 // Special ability cannon has triple blast radius var splashRadius = CELL_SIZE * 1.5; if (self.sourceTowerHasSpecial) { splashRadius = CELL_SIZE * 4.5; // Triple radius } for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'rapid') { // Special ability crossbow has 10% chance to cause bleed if (self.sourceTowerHasSpecial && Math.random() < 0.1 && !self.targetEnemy.isImmune) { self.targetEnemy.isBleeding = true; self.targetEnemy.bleedDuration = 300; // 5 seconds at 60 FPS self.targetEnemy.bleedTimer = 0; // Visual effect for bleed var bleedEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'bleed'); game.addChild(bleedEffect); } } 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 // Special ability ice wizard makes slow permanent if (self.sourceTowerHasSpecial) { 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 } } else { // 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') { // Special ability: Charm effect instead of confusion if (self.sourceTowerHasSpecial) { if (Math.random() < 0.5 && !self.targetEnemy.isCharmed && !self.targetEnemy.isImmune) { self.targetEnemy.isCharmed = true; self.targetEnemy.charmDuration = 300; // 5 seconds at 60 FPS self.targetEnemy.charmSource = 'player'; // Visual effect for charm var charmEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'charm'); game.addChild(charmEffect); } } else { // 50% chance to confuse enemy 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); } } } 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 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 'bleed': effectGraphics.tint = 0x8B0000; effectGraphics.width = effectGraphics.height = CELL_SIZE * 0.8; break; case 'charm': effectGraphics.tint = 0xFFD700; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.3; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .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 charm effect if (self.isCharmed && !self.isImmune) { self.charmDuration--; if (self.charmDuration <= 0) { self.isCharmed = false; self.charmSource = null; // Reset pathfinding when charm ends self.currentTarget = undefined; } // Charmed enemies attack other enemies if (LK.ticks % 30 === 0) { // Attack every 0.5 seconds var nearestEnemy = null; var nearestDist = Infinity; for (var i = 0; i < enemies.length; i++) { if (enemies[i] !== self && !enemies[i].isCharmed) { var dx = enemies[i].x - self.x; var dy = enemies[i].y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < CELL_SIZE * 2 && dist < nearestDist) { nearestDist = dist; nearestEnemy = enemies[i]; } } } if (nearestEnemy) { nearestEnemy.health -= 20; // Charmed enemies deal 20 damage if (nearestEnemy.health <= 0) { nearestEnemy.health = 0; } else { nearestEnemy.healthBar.width = nearestEnemy.health / nearestEnemy.maxHealth * 70; } LK.effects.flashObject(nearestEnemy, 0xFFD700, 200); } } } // 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 bleed effect if (self.isBleeding && !self.isImmune) { self.bleedDuration--; self.bleedTimer++; // Take 5 damage every second (60 ticks) if (self.bleedTimer >= 60) { self.bleedTimer = 0; self.health -= 5; if (self.health <= 0) { self.health = 0; } else { self.healthBar.width = self.health / self.maxHealth * 70; } // Visual effect LK.effects.flashObject(self, 0x8B0000, 200); } if (self.bleedDuration <= 0) { self.isBleeding = false; self.bleedTimer = 0; } } // 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 = 0xFFD700; // Gold 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 stay in place and attack nearby enemies enemy.currentTarget = null; // Don't move } 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; self.hasSpecialAbility = false; // 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 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.applySpecialAbility = function () { if (self.hasSpecialAbility || self.level < self.maxLevel) { return false; } // Cost for special ability is 200 gold (5x base cost of 40) var specialAbilityCost = 200; if (gold >= specialAbilityCost) { setGold(gold - specialAbilityCost); self.hasSpecialAbility = true; // Visual feedback tween(laserGraphics, { scaleY: 2, 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 for special ability!")); notification.x = 2048 / 2; notification.y = grid.height - 50; 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 has special ability if (enemy.isFlying && !self.hasSpecialAbility) { continue; } // Skip underground moles unless laser 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 -= 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); } } } } // 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); // Special ability button (only shown when laser is max level) var specialButton = null; if (self.laserWall.level >= self.laserWall.maxLevel && !self.laserWall.hasSpecialAbility) { specialButton = new Container(); buttonsContainer.addChild(specialButton); var specialBackground = specialButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); specialBackground.width = 500; specialBackground.height = 150; var specialCost = 200; // 5x base cost of 40 specialBackground.tint = gold >= specialCost ? 0xFFD700 : 0x888888; var specialText = new Text2('Special Ability: ' + specialCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); specialText.anchor.set(0.5, 0.5); specialButton.addChild(specialText); specialButton.down = function () { if (self.laserWall.applySpecialAbility()) { // Update menu self.destroy(); selectedTower = null; } }; } // Adjust button positions if (specialButton) { upgradeButton.y = -170; specialButton.y = 0; sellButton.y = 170; } else { 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.hasSpecialAbility = false; // New property for special ability self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; self.targetingMode = 'first'; // Options: 'first', 'last', 'strong', 'crowd' // 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 20 * 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.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type 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 === 'splash') { self.fireRate = Math.max(50, 75 - self.level * 5); self.damage = 15 + self.level * 4; self.bulletSpeed = 4 + self.level * 0.3; } 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.applySpecialAbility = function () { if (self.hasSpecialAbility || self.level < self.maxLevel) { return false; } // Cost for special ability is 5x the base tower cost var specialAbilityCost = getTowerCost(self.id) * 5; if (gold >= specialAbilityCost) { setGold(gold - specialAbilityCost); self.hasSpecialAbility = true; // Apply special ability effects based on tower type if (self.id === 'splash') { // Cannon: Triple fire rate and blast radius self.fireRate = Math.max(25, 75 / 3); // Triple fire rate } // Visual feedback tween(self, { scaleX: 1.2, scaleY: 1.2 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeIn }); } }); 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; } }; 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; } 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') { // Special ability inferno can target up to 3 enemies var maxTargets = self.hasSpecialAbility ? 3 : 1; var infernoTargets = []; // Find all enemies in range var enemiesInRange = []; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.getRange() && !enemy.isHidden) { enemiesInRange.push({ enemy: enemy, distance: distance }); } } // Sort by distance and take up to maxTargets enemiesInRange.sort(function (a, b) { return a.distance - b.distance; }); for (var i = 0; i < Math.min(maxTargets, enemiesInRange.length); i++) { infernoTargets.push(enemiesInRange[i].enemy); } // Create additional beams for level 6 if (!self.infernoBeams) { self.infernoBeams = []; for (var i = 0; i < 2; i++) { // 2 additional beams for total of 3 var beam = self.attachAsset('infernoBeam', { anchorX: 0, anchorY: 0.5 }); beam.visible = false; beam.alpha = 0.8; self.infernoBeams.push(beam); } } // Update all beams for (var i = 0; i < maxTargets; i++) { var beam = i === 0 ? self.infernoBeam : self.infernoBeams[i - 1]; if (i < infernoTargets.length) { var target = infernoTargets[i]; beam.visible = true; var dx = target.x - self.x; var dy = target.y - self.y; var angle = Math.atan2(dy, dx); var distance = Math.sqrt(dx * dx + dy * dy); beam.rotation = angle; beam.width = distance; if (i === 0) { gunContainer.rotation = angle; } } else { beam.visible = false; } } // Deal damage to all targets if (infernoTargets.length > 0) { self.infernoDamageTimer++; if (self.infernoDamageTimer >= 10) { self.infernoDamageTimer = 0; var damagePerTick = self.damage + self.level * 3; for (var i = 0; i < infernoTargets.length; i++) { var target = infernoTargets[i]; target.health -= damagePerTick; if (target.health <= 0) { target.health = 0; } else { target.healthBar.width = target.health / target.maxHealth * 70; } LK.effects.flashObject(target, 0xFF4500, 100); } } } else { self.infernoBeam.visible = false; if (self.infernoBeams) { for (var i = 0; i < self.infernoBeams.length; i++) { self.infernoBeams[i].visible = false; } } self.infernoDamageTimer = 0; } 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; 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 - 275 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { // Check for special ability - Archer rapid fire var bulletCount = 1; if (self.id === 'default' && self.hasSpecialAbility && Math.random() < 0.25) { bulletCount = 5; // 25% chance to shoot 5 bullets } // For special ability mortar, always shoot 4 projectiles if (self.id === 'mortar' && self.hasSpecialAbility) { bulletCount = 4; } for (var b = 0; b < bulletCount; b++) { // Add slight delay between bullets for rapid fire effect LK.setTimeout(function () { if (!self.targetEnemy || !self.targetEnemy.parent) return; var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // Pass tower level and special ability status for all towers bullet.sourceTowerLevel = self.level; bullet.sourceTowerHasSpecial = self.hasSpecialAbility; // For mortar, store target position instead of enemy if (self.id === 'mortar') { // For special ability, spread the projectiles slightly var spread = 0; if (self.hasSpecialAbility && bulletCount > 1) { spread = (b - 1.5) * CELL_SIZE * 0.5; // Spread projectiles } bullet.targetX = self.targetEnemy.x + spread * (Math.random() - 0.5); bullet.targetY = self.targetEnemy.y + spread * (Math.random() - 0.5); 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); if (self.targetEnemy) { self.targetEnemy.bulletsTargetingThis.push(bullet); } }, b * 100); // 100ms delay between each bullet for rapid fire } // --- 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 + 275; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 600; 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); 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); // Special ability button (only shown when tower is max level) var specialButton = null; if (self.tower.level >= self.tower.maxLevel && !self.tower.hasSpecialAbility) { specialButton = new Container(); buttonsContainer.addChild(specialButton); var specialBackground = specialButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); specialBackground.width = 500; specialBackground.height = 150; var specialCost = getTowerCost(self.tower.id) * 5; specialBackground.tint = gold >= specialCost ? 0xFFD700 : 0x888888; var specialText = new Text2('Special Ability: ' + specialCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); specialText.anchor.set(0.5, 0.5); specialButton.addChild(specialText); specialButton.down = function () { if (self.tower.applySpecialAbility()) { // Update menu self.destroy(); selectedTower = null; grid.renderDebug(); } }; } // Adjust button positions based on whether special ability button exists if (specialButton) { upgradeButton.y = -255; specialButton.y = -85; targetingButton.y = 85; sellButton.y = 255; } else { upgradeButton.y = -170; targetingButton.y = 0; sellButton.y = 170; } var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } 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'); } 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 }); } }); } }; 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; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- 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 + 275 }, { 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 var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); } function setGold(value) { gold = value; updateUI(); } 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); 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; 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]); // 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); sourceTower = null; enemiesToSpawn = 10; game.update = function () { if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; if (isBossWave && waveType !== 'swarm') { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy; 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 hasSpecialChurch = 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. if (towers[i].hasSpecialAbility) { hasSpecialChurch = 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; } // Special ability church has a chance to summon God to skip the wave if (hasSpecialChurch && Math.random() < 0.1) { // 10% chance // Skip to next wave and give gold reward setGold(gold + 350); var notification = game.addChild(new Notification("✨ DIVINE INTERVENTION! Wave skipped! +350 gold! ✨")); notification.x = 2048 / 2; notification.y = grid.height - 350; // Clear any remaining enemies from this wave for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i].waveNumber === currentWave) { // Remove enemy without giving gold/score if (enemies[i].isFlying && enemies[i].shadow) { enemyLayerMiddle.removeChild(enemies[i].shadow); enemies[i].shadow = null; } if (enemies[i].isFlying) { enemyLayerTop.removeChild(enemies[i]); } else { enemyLayerBottom.removeChild(enemies[i]); } enemies.splice(i, 1); } } } // 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 for special ability farms in range for bonus gold var farmBonus = 0; for (var f = 0; f < towers.length; f++) { if (towers[f].id === 'farm' && towers[f].hasSpecialAbility) { // Check if any tower is in range of this farm var farmRange = 3 * CELL_SIZE; // Farm influence range for (var t = 0; t < towers.length; t++) { if (towers[t] !== towers[f]) { var dx = towers[t].x - towers[f].x; var dy = towers[t].y - towers[f].y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist <= farmRange) { // Check if this tower was involved in killing the enemy if (towers[t].isInRange && towers[t].isInRange(enemy)) { farmBonus += Math.floor(goldEarned * 0.25); // 25% bonus per farm break; // Only count each farm once } } } } } } goldEarned += farmBonus; var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // 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.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.showYouWin(); } };
===================================================================
--- original.js
+++ change.js
@@ -74,11 +74,11 @@
// Create visual splash effect
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
// Splash damage to nearby enemies
- // Level 6 cannon has triple blast radius
+ // Special ability cannon has triple blast radius
var splashRadius = CELL_SIZE * 1.5;
- if (self.sourceTowerLevel === 6) {
+ if (self.sourceTowerHasSpecial) {
splashRadius = CELL_SIZE * 4.5; // Triple radius
}
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
@@ -97,10 +97,10 @@
}
}
}
} else if (self.type === 'rapid') {
- // Level 6 crossbow has 10% chance to cause bleed
- if (self.sourceTowerLevel === 6 && Math.random() < 0.1 && !self.targetEnemy.isImmune) {
+ // Special ability crossbow has 10% chance to cause bleed
+ if (self.sourceTowerHasSpecial && Math.random() < 0.1 && !self.targetEnemy.isImmune) {
self.targetEnemy.isBleeding = true;
self.targetEnemy.bleedDuration = 300; // 5 seconds at 60 FPS
self.targetEnemy.bleedTimer = 0;
// Visual effect for bleed
@@ -113,10 +113,10 @@
// Create visual slow effect
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
// Apply slow effect
- // Level 6 ice wizard makes slow permanent
- if (self.sourceTowerLevel === 6) {
+ // Special ability ice wizard makes slow permanent
+ if (self.sourceTowerHasSpecial) {
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 0.2; // 80% slow
self.targetEnemy.slowed = true;
@@ -145,10 +145,10 @@
// 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') {
- // Level 6: Charm effect instead of confusion
- if (self.sourceTowerLevel === 6) {
+ // Special ability: Charm effect instead of confusion
+ if (self.sourceTowerHasSpecial) {
if (Math.random() < 0.5 && !self.targetEnemy.isCharmed && !self.targetEnemy.isImmune) {
self.targetEnemy.isCharmed = true;
self.targetEnemy.charmDuration = 300; // 5 seconds at 60 FPS
self.targetEnemy.charmSource = 'player';
@@ -1083,8 +1083,9 @@
self.tower2 = tower2;
self.damage = 18; // Base damage per tick (increased by 2)
self.level = 1;
self.maxLevel = 6;
+ self.hasSpecialAbility = false;
// Create laser visual
var laserGraphics = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
@@ -1176,8 +1177,42 @@
}
}
return false;
};
+ self.applySpecialAbility = function () {
+ if (self.hasSpecialAbility || self.level < self.maxLevel) {
+ return false;
+ }
+ // Cost for special ability is 200 gold (5x base cost of 40)
+ var specialAbilityCost = 200;
+ if (gold >= specialAbilityCost) {
+ setGold(gold - specialAbilityCost);
+ self.hasSpecialAbility = true;
+ // Visual feedback
+ tween(laserGraphics, {
+ scaleY: 2,
+ 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 for special ability!"));
+ notification.x = 2048 / 2;
+ notification.y = grid.height - 50;
+ 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));
@@ -1187,14 +1222,14 @@
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 is level 6
- if (enemy.isFlying && self.level < 6) {
+ // Skip flying enemies unless laser has special ability
+ if (enemy.isFlying && !self.hasSpecialAbility) {
continue;
}
- // Skip underground moles unless laser is level 6
- if (enemy.isHidden && self.level < 6) {
+ // Skip underground moles unless laser 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) {
@@ -1313,10 +1348,45 @@
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
- upgradeButton.y = -85;
- sellButton.y = 85;
+ // Special ability button (only shown when laser is max level)
+ var specialButton = null;
+ if (self.laserWall.level >= self.laserWall.maxLevel && !self.laserWall.hasSpecialAbility) {
+ specialButton = new Container();
+ buttonsContainer.addChild(specialButton);
+ var specialBackground = specialButton.attachAsset('notification', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ specialBackground.width = 500;
+ specialBackground.height = 150;
+ var specialCost = 200; // 5x base cost of 40
+ specialBackground.tint = gold >= specialCost ? 0xFFD700 : 0x888888;
+ var specialText = new Text2('Special Ability: ' + specialCost + ' gold', {
+ size: 60,
+ fill: 0xFFFFFF,
+ weight: 800
+ });
+ specialText.anchor.set(0.5, 0.5);
+ specialButton.addChild(specialText);
+ specialButton.down = function () {
+ if (self.laserWall.applySpecialAbility()) {
+ // Update menu
+ self.destroy();
+ selectedTower = null;
+ }
+ };
+ }
+ // Adjust button positions
+ if (specialButton) {
+ upgradeButton.y = -170;
+ specialButton.y = 0;
+ sellButton.y = 170;
+ } else {
+ upgradeButton.y = -85;
+ sellButton.y = 85;
+ }
// Close button
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
@@ -1813,8 +1883,9 @@
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
+ self.hasSpecialAbility = false; // New property for special ability
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
self.targetingMode = 'first'; // Options: 'first', 'last', 'strong', 'crowd'
@@ -1822,10 +1893,10 @@
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 at level 6
- if (self.level === self.maxLevel) {
+ // Sniper: base 5, +0.8 per level, infinite range with special ability
+ if (self.hasSpecialAbility) {
return 20 * CELL_SIZE; // Effectively infinite range
}
return (5 + (self.level - 1) * 0.8) * CELL_SIZE;
case 'splash':
@@ -2113,18 +2184,11 @@
self.damage = 10 + self.level * 5;
self.bulletSpeed = 6 + self.level * 0.5;
}
} else if (self.id === 'splash') {
- if (self.level === self.maxLevel) {
- // Triple fire rate and increased damage for level 6
- self.fireRate = Math.max(25, 75 / 3); // Triple fire rate
- self.damage = 15 + self.level * 15;
- self.bulletSpeed = 4 + self.level * 1.0;
- } else {
- self.fireRate = Math.max(50, 75 - self.level * 5);
- self.damage = 15 + self.level * 4;
- self.bulletSpeed = 4 + self.level * 0.3;
- }
+ self.fireRate = Math.max(50, 75 - self.level * 5);
+ self.damage = 15 + self.level * 4;
+ self.bulletSpeed = 4 + self.level * 0.3;
} 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;
@@ -2176,8 +2240,47 @@
}
}
return false;
};
+ self.applySpecialAbility = function () {
+ if (self.hasSpecialAbility || self.level < self.maxLevel) {
+ return false;
+ }
+ // Cost for special ability is 5x the base tower cost
+ var specialAbilityCost = getTowerCost(self.id) * 5;
+ if (gold >= specialAbilityCost) {
+ setGold(gold - specialAbilityCost);
+ self.hasSpecialAbility = true;
+ // Apply special ability effects based on tower type
+ if (self.id === 'splash') {
+ // Cannon: Triple fire rate and blast radius
+ self.fireRate = Math.max(25, 75 / 3); // Triple fire rate
+ }
+ // Visual feedback
+ tween(self, {
+ scaleX: 1.2,
+ scaleY: 1.2
+ }, {
+ duration: 200,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ tween(self, {
+ scaleX: 1,
+ scaleY: 1
+ }, {
+ duration: 200,
+ easing: tween.easeIn
+ });
+ }
+ });
+ 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;
+ }
+ };
self.findTarget = function () {
var enemiesInRange = [];
// First, collect all enemies in range
for (var i = 0; i < enemies.length; i++) {
@@ -2327,10 +2430,10 @@
return;
}
// Inferno tower uses continuous beam
if (self.id === 'inferno') {
- // Level 6 inferno can target up to 3 enemies
- var maxTargets = self.level === 6 ? 3 : 1;
+ // Special ability inferno can target up to 3 enemies
+ var maxTargets = self.hasSpecialAbility ? 3 : 1;
var infernoTargets = [];
// Find all enemies in range
var enemiesInRange = [];
for (var i = 0; i < enemies.length; i++) {
@@ -2503,15 +2606,15 @@
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
- // Check for level 6 special ability - Archer rapid fire
+ // Check for special ability - Archer rapid fire
var bulletCount = 1;
- if (self.id === 'default' && self.level === 6 && Math.random() < 0.25) {
+ if (self.id === 'default' && self.hasSpecialAbility && Math.random() < 0.25) {
bulletCount = 5; // 25% chance to shoot 5 bullets
}
- // For level 6 mortar, always shoot 4 projectiles
- if (self.id === 'mortar' && self.level === 6) {
+ // For special ability mortar, always shoot 4 projectiles
+ if (self.id === 'mortar' && self.hasSpecialAbility) {
bulletCount = 4;
}
for (var b = 0; b < bulletCount; b++) {
// Add slight delay between bullets for rapid fire effect
@@ -2521,15 +2624,16 @@
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
- // Pass tower level for all towers to enable special abilities
+ // Pass tower level and special ability status for all towers
bullet.sourceTowerLevel = self.level;
+ bullet.sourceTowerHasSpecial = self.hasSpecialAbility;
// For mortar, store target position instead of enemy
if (self.id === 'mortar') {
- // For level 6, spread the projectiles slightly
+ // For special ability, spread the projectiles slightly
var spread = 0;
- if (self.level === 6 && bulletCount > 1) {
+ if (self.hasSpecialAbility && bulletCount > 1) {
spread = (b - 1.5) * CELL_SIZE * 0.5; // Spread projectiles
}
bullet.targetX = self.targetEnemy.x + spread * (Math.random() - 0.5);
bullet.targetY = self.targetEnemy.y + spread * (Math.random() - 0.5);
@@ -2895,11 +2999,48 @@
weight: 800
});
targetingText.anchor.set(0.5, 0.5);
targetingButton.addChild(targetingText);
- upgradeButton.y = -170;
- targetingButton.y = 0;
- sellButton.y = 170;
+ // Special ability button (only shown when tower is max level)
+ var specialButton = null;
+ if (self.tower.level >= self.tower.maxLevel && !self.tower.hasSpecialAbility) {
+ specialButton = new Container();
+ buttonsContainer.addChild(specialButton);
+ var specialBackground = specialButton.attachAsset('notification', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ specialBackground.width = 500;
+ specialBackground.height = 150;
+ var specialCost = getTowerCost(self.tower.id) * 5;
+ specialBackground.tint = gold >= specialCost ? 0xFFD700 : 0x888888;
+ var specialText = new Text2('Special Ability: ' + specialCost + ' gold', {
+ size: 60,
+ fill: 0xFFFFFF,
+ weight: 800
+ });
+ specialText.anchor.set(0.5, 0.5);
+ specialButton.addChild(specialText);
+ specialButton.down = function () {
+ if (self.tower.applySpecialAbility()) {
+ // Update menu
+ self.destroy();
+ selectedTower = null;
+ grid.renderDebug();
+ }
+ };
+ }
+ // Adjust button positions based on whether special ability button exists
+ if (specialButton) {
+ upgradeButton.y = -255;
+ specialButton.y = -85;
+ targetingButton.y = 85;
+ sellButton.y = 255;
+ } else {
+ upgradeButton.y = -170;
+ targetingButton.y = 0;
+ sellButton.y = 170;
+ }
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
@@ -4094,15 +4235,15 @@
notification.y = grid.height - 250;
}
// Give lives from all church towers
var totalChurchLives = 0;
- var hasLevel6Church = false;
+ var hasSpecialChurch = 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.
- if (towers[i].level === 6) {
- hasLevel6Church = true;
+ if (towers[i].hasSpecialAbility) {
+ hasSpecialChurch = true;
}
}
}
if (totalChurchLives > 0) {
@@ -4111,10 +4252,10 @@
var notification = game.addChild(new Notification("Church blessing: +" + totalChurchLives + " lives!"));
notification.x = 2048 / 2;
notification.y = grid.height - 300;
}
- // Level 6 church has a chance to summon God to skip the wave
- if (hasLevel6Church && Math.random() < 0.1) {
+ // Special ability church has a chance to summon God to skip the wave
+ if (hasSpecialChurch && Math.random() < 0.1) {
// 10% chance
// Skip to next wave and give gold reward
setGold(gold + 350);
var notification = game.addChild(new Notification("✨ DIVINE INTERVENTION! Wave skipped! +350 gold! ✨"));
@@ -4149,12 +4290,12 @@
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 for level 6 farms in range for bonus gold
+ // Check for special ability farms in range for bonus gold
var farmBonus = 0;
for (var f = 0; f < towers.length; f++) {
- if (towers[f].id === 'farm' && towers[f].level === 6) {
+ if (towers[f].id === 'farm' && towers[f].hasSpecialAbility) {
// Check if any tower is in range of this farm
var farmRange = 3 * CELL_SIZE; // Farm influence range
for (var t = 0; t < towers.length; t++) {
if (towers[t] !== towers[f]) {
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