User prompt
position the crystal text in the same recent area of gold text in left side with same space between score and the middle but from the left and the crystal icon beside it from the left.
User prompt
position the crystal text in the same recent area of gold text and the crystal icon beside it from the left.
User prompt
position the crystal text in the same area of gold and the crystal icon beside it from the left.
User prompt
position the crystal text in the same area of gold and the crystal icon beside it from the left.
User prompt
Please fix the bug: 'Uncaught ReferenceError: goldText is not defined' in or related to this line: 'goldText.visible = true;' Line Number: 3107
User prompt
Change 'Gold' to 'Crystal', change text to pink, Create asset 'Crystal' as an Icon beside the 'Crystal' text from the left make its size 100x100px.
User prompt
Change the debug visualization arrows to pink color
User prompt
Change game healthbra to dark pink color and its text to purple, Score text from red to blue sky color.
User prompt
change the mute buttons size to 100x100px.
User prompt
change the mute buttons size to 100x100px.
User prompt
Add 'Mutebutton2' as frame animation when music off change from 'Mutebutton1' to 'Mutebutton2' to make off any music in the game. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add the 'Mutebutton1' when music is on, position it in top right corner before area 50x50px from corner.
User prompt
Change the walls of debug visualization to be with an asset to change it's look with an image named the asset 'DebugWall'.
User prompt
The lines are not like arrows and the color not violet it still orange ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (1 edits merged)
Please save this source code
User prompt
change the direction of lines pointing up to point down. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Fix the debug visualization animation it goes smaller each time i place tower and its pointing up and diagonal up left and right. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make the Flamesound of burning repeated wile burning enemies.
User prompt
okay then do it with animation orange lines. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Remove the gumStuckEffect after 4 sec. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add Bossmusic 1-2-3-4-5 to the waves of boss 10-20-30-40-50.
User prompt
Add Gamemusic5 from wave41 to wave49.
User prompt
Add Gamemusic4 from wave31 to wave39.
User prompt
Add gamemusic3 from wave21 to wave29.
User prompt
Add gamemusic2 from wave11 to wave19.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } // Check if bullet has exceeded its source tower's range (for all tower types) if (self.sourceTower) { var towerDx = self.x - self.sourceTower.x; var towerDy = self.y - self.sourceTower.y; var distanceFromTower = Math.sqrt(towerDx * towerDx + towerDy * towerDy); if (distanceFromTower > self.sourceTower.getRange()) { 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 with resistance scaling var actualDamage = self.damage; if (self.targetEnemy.damageResistance) { actualDamage = self.damage * self.targetEnemy.damageResistance; } self.targetEnemy.health -= actualDamage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'cannon') { // Create fire explosion effect for cannon bullets var explosionEffect = new Container(); explosionEffect.x = self.targetEnemy.x; explosionEffect.y = self.targetEnemy.y; game.addChild(explosionEffect); // Create multiple fire particles for the explosion for (var fireParticle = 0; fireParticle < 8; fireParticle++) { var particle = explosionEffect.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); particle.width = particle.height = 20 + Math.random() * 30; particle.tint = fireParticle % 2 === 0 ? 0xFF4500 : 0xFF8C00; // Orange-red fire colors particle.alpha = 0.8; particle.blendMode = 1; // Add blend mode for fire effect particle.x = (Math.random() - 0.5) * 40; particle.y = (Math.random() - 0.5) * 40; particle.scaleX = 0.1; particle.scaleY = 0.1; // Animate each particle exploding outward tween(particle, { scaleX: 1.5, scaleY: 1.5, x: particle.x + (Math.random() - 0.5) * 60, y: particle.y + (Math.random() - 0.5) * 60, alpha: 0 }, { duration: 300 + Math.random() * 200, easing: tween.easeOut }); } // Create central explosion flash var centralFlash = explosionEffect.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); centralFlash.width = centralFlash.height = 80; centralFlash.tint = 0xFFFFAA; // Bright yellow-white center centralFlash.alpha = 1; centralFlash.blendMode = 1; centralFlash.scaleX = 0.1; centralFlash.scaleY = 0.1; // Animate central flash tween(centralFlash, { scaleX: 2, scaleY: 2, alpha: 0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { explosionEffect.destroy(); } }); } else if (self.type === 'Rocket') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Splash damage to nearby enemies - 200x200 area means 100 pixel radius var splashRadius = 100; 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 === 'gumbomb') { // Prevent gum effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual gumbomb explosion effect var gumEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'gumbomb'); game.addChild(gumEffect); // Apply gum stuck effect - enemies become completely immobilized if (!self.targetEnemy.gumStuck) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed = 0; // Completely stuck self.targetEnemy.gumStuck = true; self.targetEnemy.gumDuration = 240; // 4 seconds at 60 FPS // Create purple gum effect beneath the enemy var gumStuckGraphics = LK.getAsset('gumStuckEffect', { anchorX: 0.5, anchorY: 0.5 }); gumStuckGraphics.x = self.targetEnemy.x; gumStuckGraphics.y = self.targetEnemy.y + self.targetEnemy.children[0].height / 2; gumStuckGraphics.alpha = 0; gumStuckGraphics.scaleX = 0.1; gumStuckGraphics.scaleY = 0.1; self.targetEnemy.gumStuckEffect = gumStuckGraphics; enemyLayerBottom.addChildAt(gumStuckGraphics, 0); // Explosion animation - scale up and fade in quickly tween(gumStuckGraphics, { alpha: 0.7, scaleX: 1.2, scaleY: 1.2 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { // Settle to normal size tween(gumStuckGraphics, { scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.elasticOut }); // Set up tween to destroy gum area after 4 seconds tween(gumStuckGraphics, { alpha: 0.7 }, { duration: 4000, onFinish: function onFinish() { if (gumStuckGraphics && gumStuckGraphics.parent) { game.removeChild(gumStuckGraphics); } } }); } }); } else { self.targetEnemy.gumDuration = 240; // Reset duration to 4 seconds } } } else if (self.type === 'Toxin') { // Prevent toxin effect on immune enemies if (!self.targetEnemy.isImmune) { // Create ToxinBomb explosion effect using Toxin asset var explosionEffect = new Container(); explosionEffect.x = self.targetEnemy.x; explosionEffect.y = self.targetEnemy.y; game.addChild(explosionEffect); // Create the main toxin explosion using Toxin asset var toxinExplosion = explosionEffect.attachAsset('Toxin', { anchorX: 0.5, anchorY: 0.5 }); toxinExplosion.width = toxinExplosion.height = 150; toxinExplosion.tint = 0x00FFAA; // Green toxin color toxinExplosion.alpha = 0.9; toxinExplosion.scaleX = 0.1; toxinExplosion.scaleY = 0.1; // Animate the toxin explosion expanding and fading tween(toxinExplosion, { scaleX: 1.8, scaleY: 1.8, alpha: 0.3 }, { duration: 400, easing: tween.easeOut }); // Create additional smaller toxin particles around the main explosion for (var toxinParticle = 0; toxinParticle < 6; toxinParticle++) { var particle = explosionEffect.attachAsset('Toxin', { anchorX: 0.5, anchorY: 0.5 }); particle.width = particle.height = 40 + Math.random() * 30; particle.tint = 0x00FFAA; // Consistent green toxin color particle.alpha = 0.7; particle.x = (Math.random() - 0.5) * 80; particle.y = (Math.random() - 0.5) * 80; particle.scaleX = 0.1; particle.scaleY = 0.1; // Animate each particle expanding outward tween(particle, { scaleX: 1.2, scaleY: 1.2, x: particle.x + (Math.random() - 0.5) * 100, y: particle.y + (Math.random() - 0.5) * 100, alpha: 0 }, { duration: 500 + Math.random() * 200, easing: tween.easeOut }); } // Destroy the explosion effect after animation completes tween(explosionEffect, { alpha: 1 }, { duration: 600, onFinish: function onFinish() { explosionEffect.destroy(); } }); // Apply toxin effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } } // Flame tower does not use bullets, so destroy any 'flame' type bullets immediately if (self.type === 'flame') { self.destroy(); } else { self.destroy(); } } else { var angle = Math.atan2(dy, dx); // Rotate bullet to face movement direction if (self.children[0]) { // Check if this is a RifleBullet which needs rotation offset var rotationOffset = 0; if (self.type === 'rifle') { rotationOffset = Math.PI / 2; // Compensate for upward-pointing asset } var targetAngle = angle + rotationOffset; if (self.children[0].targetRotation === undefined) { self.children[0].targetRotation = targetAngle; self.children[0].rotation = targetAngle; } else { if (Math.abs(targetAngle - self.children[0].targetRotation) > 0.05) { tween.stop(self.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = self.children[0].rotation; var angleDiff = targetAngle - 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; } self.children[0].targetRotation = targetAngle; tween(self.children[0], { rotation: currentRotation + angleDiff }, { duration: 100, easing: tween.easeOut }); } } } 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('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); cellGraphics.tint = 0x880000; return; } numberLabel.visible = true; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0xe14de9; // pink color with transparency cellGraphics.alpha = 0.5; // Almost max transparent } else { cellGraphics.tint = 0x4db3e9; // Sky blue color with transparency cellGraphics.alpha = 0.5; // Almost max transparent } while (debugArrows.length > data.targets.length) { self.removeChild(debugArrows.pop()); } for (var a = 0; a < data.targets.length; a++) { var destination = data.targets[a]; var ox = destination.x - data.x; var oy = destination.y - data.y; var angle = Math.atan2(oy, ox); if (!debugArrows[a]) { debugArrows[a] = LK.getAsset('arrow', { anchorX: -.5, anchorY: 0.5 }); debugArrows[a].alpha = .5; self.addChildAt(debugArrows[a], 1); } debugArrows[a].rotation = angle; } break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xff58f7; cellGraphics.alpha = 0.5; // Almost max transparent numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0xff8400; // Blue sky color cellGraphics.alpha = 0.5; // Almost max transparent numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; 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 'gumbomb': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE * 2; // Larger explosion effect break; case 'toxin': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'flame': effectGraphics.tint = 0xFF8800; // Mix of yellow, orange, red effectGraphics.width = effectGraphics.height = CELL_SIZE; 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; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type - increased health switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 45; // Increased from 30 break; case 'immune': self.isImmune = true; self.maxHealth = 40; // Increased from 25 break; case 'flying': self.isFlying = true; self.maxHealth = 40; // Increased from 25 break; case 'swarm': self.maxHealth = 25; // Increased from 15 break; case 'normal': default: self.maxHealth = 35; // Increased from 20 break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 8x health and are larger self.maxHealth *= 8; // Slightly slower speed for bosses but not as much self.speed = self.speed * 0.8; } // Apply wave-based difficulty scaling every 5 waves var difficultyWaves = Math.floor((currentWave - 1) / 5); if (difficultyWaves > 0) { // Increase health by 75% every 5 waves (was 50%) var healthMultiplier = 1 + difficultyWaves * 0.75; self.maxHealth = Math.round(self.maxHealth * healthMultiplier); // Increase speed by 30% every 5 waves (was 20%) var speedMultiplier = 1 + difficultyWaves * 0.3; self.speed = self.speed * speedMultiplier; // Add damage resistance - enemies take 10% less damage every 5 waves self.damageResistance = 1 - Math.min(0.5, difficultyWaves * 0.1); // Cap at 50% resistance } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } // For flying enemies, use different assets based on wave progression if (self.type === 'flying') { // Use different flying enemy assets based on wave number var waveGroup = Math.floor((currentWave - 1) / 10); // Every 10 waves change flying enemy type var flyingAssets = ['Firo', 'Eye', 'Electro']; var assetIndex = waveGroup % flyingAssets.length; assetId = flyingAssets[assetIndex]; } else { // For ground enemies, use different Cyborg assets based on wave number var waveGroup = Math.floor((currentWave - 1) / 10); // Every 10 waves change ground enemy type var groundAssets = ['Cyborg', 'Cyborg2', 'Cyborg3']; var assetIndex = waveGroup % groundAssets.length; if (self.type === 'normal') { assetId = groundAssets[assetIndex]; } else if (self.type === 'fast') { // For fast enemies, use Robot1, Spider1, and Spider2 assets based on wave progression var waveGroup = Math.floor((currentWave - 1) / 10); // Every 10 waves change fast enemy type var fastAssets = ['Robot1', 'Spider1', 'Spider2']; var assetIndex = waveGroup % fastAssets.length; assetId = fastAssets[assetIndex]; } else if (self.type === 'immune') { // For immune enemies, use Big_Robot1, Big_Robot2, and Big_Robot3 assets based on wave progression var waveGroup = Math.floor((currentWave - 1) / 10); // Every 10 waves change immune enemy type var immuneAssets = ['Big_Robot1', 'Big_Robot2', 'Big_Robot3']; var assetIndex = waveGroup % immuneAssets.length; assetId = immuneAssets[assetIndex]; } else if (self.type === 'swarm') { // For swarm enemies, use CyberSnake1, CyberSnake2, and CyberSnake3 assets based on wave progression var waveGroup = Math.floor((currentWave - 1) / 10); // Every 10 waves change swarm enemy type var swarmAssets = ['CyberSnake1', 'CyberSnake2', 'CyberSnake3']; var assetIndex = waveGroup % swarmAssets.length; assetId = swarmAssets[assetIndex]; } else { // For other non-normal ground enemies, still use the original prefixed system but could be enhanced later 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 - use the same asset as the main enemy var shadowAssetId = assetId; // For flying enemies, make sure we use the correct asset for the shadow if (self.type === 'flying') { var waveGroup = Math.floor((currentWave - 1) / 10); var flyingAssets = ['Firo', 'Eye', 'Electro']; var assetIndex = waveGroup % flyingAssets.length; shadowAssetId = flyingAssets[assetIndex]; } var shadowGraphics = self.shadow.attachAsset(shadowAssetId, { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.3; // 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 = 0xffff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed, gum stuck, or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.gumStuck = false; if (self.gumStuckEffect && self.gumStuckEffect.parent) { game.removeChild(self.gumStuckEffect); } self.gumStuckEffect = null; self.poisoned = false; self.poisonEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle gum stuck effect if (self.gumStuck) { // Visual indication of gum stuck status if (!self.gumStuckEffect) { self.gumStuckEffect = true; } self.gumDuration--; if (self.gumDuration <= 0) { self.speed = self.originalSpeed; self.gumStuck = false; self.gumStuckEffect = false; // Remove gum effect visual with fade out animation if (self.gumStuckEffect && self.gumStuckEffect.parent) { tween(self.gumStuckEffect, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { if (self.gumStuckEffect && self.gumStuckEffect.parent) { game.removeChild(self.gumStuckEffect); } self.gumStuckEffect = null; } }); } else { self.gumStuckEffect = null; } // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } else { // Update gum effect position to follow enemy if (self.gumStuckEffect && self.gumStuckEffect.parent) { self.gumStuckEffect.x = self.x; self.gumStuckEffect.y = self.y + enemyGraphics.height / 2; } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { var actualPoisonDamage = self.poisonDamage; if (self.damageResistance) { actualPoisonDamage = self.poisonDamage * self.damageResistance; } self.health -= actualPoisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && (self.slowed || self.gumStuck)) { // Combine poison (0x00FFAA) and slow/gum (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed || self.gumStuck) { 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.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { if (cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("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 = 2732 / 2; } }; 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: 65, fill: 0xFF69B4, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 60; notificationGraphics.height = notificationText.height + 40; 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 || 'cannon'; // Use specific tower asset based on type var towerAssetId = 'CannonTower'; // default switch (self.towerType) { case 'rifle': towerAssetId = 'RifleTower'; break; case 'flame': towerAssetId = 'FlameTower'; break; case 'Rocket': towerAssetId = 'RocketTower'; break; case 'gumbomb': towerAssetId = 'GumBombTower'; break; case 'Toxin': towerAssetId = 'ToxinTower'; break; case 'cannon': default: towerAssetId = 'CannonTower'; break; } // 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 as we're using specific tower assets var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); // Add tower type label var displayName = self.towerType === 'rifle' ? 'Rifle' : self.towerType === 'Toxin' ? 'Toxinbomb' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1); // Set color based on tower type var towerColor = 0xFFFFFF; // default white switch (self.towerType) { case 'cannon': towerColor = 0xFF0000; // Red break; case 'rifle': towerColor = 0x00FF00; // Green break; case 'flame': towerColor = 0x800080; // Purple break; case 'Rocket': towerColor = 0x000000; // Black break; case 'gumbomb': towerColor = 0x0000FF; // Blue break; case 'Toxin': towerColor = 0xFFFF00; // Yellow break; } var typeLabel = new Text2(displayName, { size: 50, fill: towerColor, 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 || 'cannon'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case 'flame': // Flame: base 5, +0.8 per level, but final upgrade gets a huge boost if (self.level === self.maxLevel) { return 12 * CELL_SIZE; // Significantly increased range for max level } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case 'Rocket': // Rocket: base range higher than sniper, +0.8 per level if (self.level === self.maxLevel) { return 14 * CELL_SIZE; // Higher than sniper's max range } return (6 + (self.level - 1) * 0.8) * CELL_SIZE; case 'rifle': // Rifle: base 3 (default), +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; case 'gumbomb': // GumBomb: base 3.5, +0.5 per level return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'Toxin': // Toxin: base 3.2, +0.5 per level return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; case 'cannon': // Cannon: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; break; 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 = 7; self.lastFired = 0; self.targetEnemy = null; self.flameEffect = null; // Track active flame effect for flame tower self.flameAnimation = null; // Track flame animation visual switch (self.id) { case 'rifle': self.fireRate = Math.floor(60 / 3.5); // 3.5 shots per second = 60/3.5 = 17 frames self.damage = 5; self.range = 3 * CELL_SIZE; // Default tower range self.bulletSpeed = 7; break; case 'flame': self.fireRate = 120; // 0.5 shots per second self.damage = 9; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'Rocket': self.fireRate = Math.floor(60 / 1.0); // 1.0 shots per second = 60 frames self.damage = 13; self.range = 2 * CELL_SIZE; self.bulletSpeed = 8; break; case 'gumbomb': self.fireRate = 50; self.damage = 3; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'Toxin': self.fireRate = 70; self.damage = 18; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 5; break; } // Use specific tower asset based on type var towerAssetId = 'CannonTower'; // default switch (self.id) { case 'rifle': towerAssetId = 'RifleTower'; break; case 'flame': towerAssetId = 'FlameTower'; break; case 'Rocket': towerAssetId = 'RocketTower'; break; case 'gumbomb': towerAssetId = 'GumBombTower'; break; case 'Toxin': towerAssetId = 'ToxinTower'; break; case 'cannon': default: towerAssetId = 'CannonTower'; break; } var baseGraphics = self.attachAsset(towerAssetId, { anchorX: 0.5, anchorY: 0.5 }); // No need to tint as we're using specific tower assets var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = 30; 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 = dotSpacing * (i + 1) - baseGraphics.width / 2; dot.y = -CELL_SIZE * 1.2; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); var weaponAssetId = 'Weapon1'; // default switch (self.id) { case 'rifle': weaponAssetId = 'Weapon2'; break; case 'flame': weaponAssetId = 'Weapon3'; break; case 'Rocket': weaponAssetId = 'Weapon4'; break; case 'gumbomb': weaponAssetId = 'Weapon5'; break; case 'Toxin': weaponAssetId = 'Weapon6'; break; case 'cannon': default: weaponAssetId = 'Weapon1'; break; } var gunGraphics = gunContainer.attachAsset(weaponAssetId, { 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 = 0xff6600; } else { switch (self.id) { case 'rifle': towerLevelIndicator.tint = 0xffffff; break; case 'flame': towerLevelIndicator.tint = 0xffffff; break; case 'Rocket': towerLevelIndicator.tint = 0xffffff; break; case 'gumbomb': towerLevelIndicator.tint = 0xffffff; break; case 'Toxin': towerLevelIndicator.tint = 0xffffff; break; default: towerLevelIndicator.tint = 0xffffff; } } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rifle') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(4, 12 - self.level * 3); // double the effect, starting from 12 self.damage = 8 + self.level * 8; // double the effect, starting from 8 self.bulletSpeed = 7 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(6, 12 - self.level * 1); // Rifle gets faster with upgrades, starting from 12 self.damage = 8 + self.level * 2; // Starting from 8 damage self.bulletSpeed = 7 + self.level * 0.7; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range if (distance <= self.getRange()) { // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { self.targetEnemy = self.findTarget(); if (!self.targetEnemy) { if (self.flameAnimation) { self.flameAnimation.destroy(); self.flameAnimation = null; } if (self.flameEffect) { self.flameEffect.destroy(); self.flameEffect = null; } } if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); // Smooth gun rotation using tween if (gunContainer.targetRotation === undefined) { gunContainer.targetRotation = angle; gunContainer.rotation = angle; } else { if (Math.abs(angle - gunContainer.targetRotation) > 0.05) { tween.stop(gunContainer, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = gunContainer.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; } gunContainer.targetRotation = angle; tween(gunContainer, { rotation: currentRotation + angleDiff }, { duration: 200, easing: tween.easeOut }); } } // Flame tower has special continuous firing logic if (self.id === 'flame') { // Continuously apply damage if an enemy is targeted if (self.targetEnemy && self.isInRange(self.targetEnemy)) { // Apply damage every frame while the enemy is in range and alive if (self.targetEnemy.health > 0) { var actualDamage = self.damage / 60; // Damage per second (damage/60 FPS) if (self.targetEnemy.damageResistance) { actualDamage = actualDamage * self.targetEnemy.damageResistance; } self.targetEnemy.health -= actualDamage; // Ensure health doesn't go below zero if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // --- Flame Visuals --- var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Create flame animation from the gun if not already active if (!self.flameAnimation) { LK.getSound('Flamesound').play(); self.flameAnimation = new Container(); var flameGraphics = self.flameAnimation.attachAsset('Flame', { anchorX: 0, // Anchor at the base (left) of the flame anchorY: 0.5 // Anchor in the middle vertically }); // The Flame asset needs no rotation as it should extend horizontally from the gun flameGraphics.rotation = 0; flameGraphics.tint = 0xFF8800; gunContainer.addChild(self.flameAnimation); // Position at the front edge of the gun barrel self.flameAnimation.x = gunGraphics.width / 2 + 20; // Set initial scale for appear animation flameGraphics.scale.set(0, 0.6); } // Update the flame animation every frame to stretch and flicker if (self.flameAnimation && self.flameAnimation.children[0]) { var flameGraphics = self.flameAnimation.children[0]; // The flame asset is 300px long (originally height) var flameAssetLength = 300; var targetScaleX = distance / flameAssetLength; // Animate the flame length flameGraphics.scale.x += (targetScaleX - flameGraphics.scale.x) * 0.3; // Use a sinusoidal flicker for a more natural look on the width var flicker = Math.sin(LK.ticks * 0.5) * 0.1; flameGraphics.scale.y = 0.6 + flicker; } // Show visual effect on the enemy if (!self.flameEffect) { self.flameEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'flame'); game.addChild(self.flameEffect); } // Update the position of the flame effect to match the target enemy self.flameEffect.x = self.targetEnemy.x; self.flameEffect.y = self.targetEnemy.y; } else { // Target is dead, destroy flame animation and effect if (self.flameAnimation) { self.flameAnimation.destroy(); self.flameAnimation = null; } if (self.flameEffect) { self.flameEffect.destroy(); self.flameEffect = null; } } } else { // No target enemy in range, destroy flame animation and effect if (self.flameAnimation) { self.flameAnimation.destroy(); self.flameAnimation = null; } if (self.flameEffect) { self.flameEffect.destroy(); self.flameEffect = null; } } } else { // Standard tower firing logic (single bullets) if (self.id === 'flame') { // Manage flame visibility based on fire rate cooldown var flameGraphics = self.flameAnimation ? self.flameAnimation.children[0] : null; if (flameGraphics) { if (LK.ticks - self.lastFired >= self.fireRate * 0.6) { // Show flame for 60% of fire rate flameGraphics.visible = true; } else { flameGraphics.visible = false; } } if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } else { if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } } } }; self.down = function (x, y, obj) { var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // Store reference to source tower for range checking (all tower types) bullet.sourceTower = self; // For Rocket towers, use Rocket asset if (self.id === 'Rocket') { // Replace bullet asset with Rocket asset bullet.removeChild(bullet.children[0]); var rocketGraphics = bullet.attachAsset('Rocket', { anchorX: 0.5, anchorY: 0.5 }); rocketGraphics.tint = 0x00FF00; rocketGraphics.width = 80; rocketGraphics.height = 70; } // For gumbomb tower, pass level for scaling gum effect if (self.id === 'gumbomb') { bullet.sourceTowerLevel = self.level; } // Customize bullet appearance based on tower type switch (self.id) { case 'rifle': // Replace bullet asset with RifleBullet asset bullet.removeChild(bullet.children[0]); var rifleGraphics = bullet.attachAsset('RifleBullet', { anchorX: 0.5, anchorY: 0.5 }); rifleGraphics.tint = 0xFF8000; rifleGraphics.width = 5; rifleGraphics.height = 10; break; // Remove flame bullet type creation as flame tower now applies continuous damage case 'Rocket': // Rocket bullets now use Rocket asset, styling already applied above break; case 'gumbomb': // Replace bullet asset with GumBomb asset bullet.removeChild(bullet.children[0]); var gumbombGraphics = bullet.attachAsset('gumbomb', { anchorX: 0.5, anchorY: 0.5 }); gumbombGraphics.tint = 0x9900FF; gumbombGraphics.width = 35; gumbombGraphics.height = 35; break; case 'Toxin': // Replace bullet asset with ToxinBomb asset bullet.removeChild(bullet.children[0]); var toxinGraphics = bullet.attachAsset('ToxinBomb', { anchorX: 0.5, anchorY: 0.5 }); toxinGraphics.tint = 0xFF00AA; toxinGraphics.width = 35; toxinGraphics.height = 35; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // Play sound for tower shots switch (self.id) { case 'cannon': LK.getSound('Bulletsound').play(); break; case 'rifle': LK.getSound('Riflebulletsound').play(); break; case 'Rocket': LK.getSound('Rocketsound').play(); break; case 'gumbomb': LK.getSound('Gumbombsound').play(); break; case 'Toxin': LK.getSound('Toxinbombsound').play(); break; } // --- 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(); // Play placement sound for cannon tower if (self.id === 'cannon') { LK.getSound('placmentsound1').play(); } else if (self.id === 'rifle') { LK.getSound('placmentsound2').play(); } else if (self.id === 'flame') { LK.getSound('placmentsound3').play(); } else if (self.id === 'Rocket') { LK.getSound('placmentsound4').play(); } else if (self.id === 'gumbomb') { LK.getSound('placmentsound5').play(); } else if (self.id === 'Toxin') { LK.getSound('placmentsound6').play(); } }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'cannon'; 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 'rifle': previewGraphics.tint = 0xFFFFFF; break; case 'flame': previewGraphics.tint = 0xFFFFFF; break; case 'Rocket': previewGraphics.tint = 0xFFFFFF; break; case 'gumbomb': previewGraphics.tint = 0xFFFFFF; break; case 'Toxin': previewGraphics.tint = 0xFFFFFF; break; case 'cannon': previewGraphics.tint = 0xFFFFFF; break; default: previewGraphics.tint = 0xFFFFFF; } 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 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var displayName = self.tower.id === 'rifle' ? 'Rifle' : self.tower.id === 'Toxin' ? 'Toxinbomb' : self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1); 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('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; 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 = 0xFF0000; // 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 = 2732 / 2; } }; 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 = 0xFF0000; waveType = "Easy"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0xFF0000; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xFF0000; waveType = "Unstoppable"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFF0000; waveType = "Air"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF0000; waveType = "Group"; enemyType = "swarm"; enemyCount = 30; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Air"; block.tint = 0xFF0000; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xFF0000; waveType = "Boss Easy"; break; case 'fast': block.tint = 0xFF0000; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xFF0000; waveType = "Boss Unstoppable"; break; case 'flying': block.tint = 0xFF0000; waveType = "Boss Air"; 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 = 0xFF0000; break; case 'fast': block.tint = 0xFF0000; break; case 'immune': block.tint = 0xFF0000; break; case 'flying': block.tint = 0xFF0000; break; default: block.tint = 0xFF0000; break; } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0xFF0000; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xFF0000; waveType = "Unstoppable"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFF0000; waveType = "Air"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF0000; waveType = "Group"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = 0xFF0000; waveType = "Easy"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Add tween animation to transition block color from red to white tween(block, { tint: 0xFFFFFF }, { duration: 2000, easing: tween.easeInOut }); // 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 = 2732 / 2; } } } }; self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ // Game state variables var gameStarted = false; var showingIntro = true; var showingScores = false; var scoreBackground = null; var scoreMenu = null; // High scores management function getHighScores() { return storage.highScores || []; } function addHighScore(score) { var highScores = getHighScores(); highScores.push(score); // Sort in descending order highScores.sort(function (a, b) { return b - a; }); // Keep only top 10 if (highScores.length > 10) { highScores = highScores.slice(0, 10); } storage.highScores = highScores; return highScores; } function showHighScores() { if (showingScores) return; showingScores = true; // Create score background scoreBackground = game.attachAsset('Scorebackground', { anchorX: 0.5, anchorY: 0.5 }); scoreBackground.x = 2048 / 2; scoreBackground.y = 2732 / 2; game.addChild(scoreBackground); // Create score menu container scoreMenu = new Container(); scoreMenu.x = 2048 / 2; scoreMenu.y = 2732 / 2; game.addChild(scoreMenu); // Add title var titleShadow = new Text2("HIGH SCORES", { size: 80, fill: 0x000000, weight: 800 }); titleShadow.anchor.set(0.5, 0.5); titleShadow.x = 4; titleShadow.y = -400 + 4; scoreMenu.addChild(titleShadow); var title = new Text2("HIGH SCORES", { size: 80, fill: 0xFFD700, weight: 800 }); title.anchor.set(0.5, 0.5); title.y = -400; scoreMenu.addChild(title); // Get and display high scores var highScores = getHighScores(); for (var i = 0; i < 10; i++) { var rank = i + 1; var scoreValue = i < highScores.length ? highScores[i] : 0; var yPos = -280 + i * 60; // Score rank and value shadow var scoreShadow = new Text2(rank + ". " + scoreValue, { size: 60, fill: 0x000000, weight: 800 }); scoreShadow.anchor.set(0.5, 0.5); scoreShadow.x = 4; scoreShadow.y = yPos + 4; scoreMenu.addChild(scoreShadow); // Score rank and value var scoreColor = i < 3 ? 0xFFD700 : 0xFFFFFF; // Gold for top 3 var scoreText = new Text2(rank + ". " + scoreValue, { size: 60, fill: scoreColor, weight: 800 }); scoreText.anchor.set(0.5, 0.5); scoreText.y = yPos; scoreMenu.addChild(scoreText); } // Add close button var closeButton = new Container(); var closeButtonBg = closeButton.attachAsset('ScoreButton', { anchorX: 0.5, anchorY: 0.5 }); closeButtonBg.width = 300; closeButtonBg.height = 100; closeButtonBg.tint = 0xAA0000; var closeButtonShadow = new Text2("CLOSE", { size: 50, fill: 0x000000, weight: 800 }); closeButtonShadow.anchor.set(0.5, 0.5); closeButtonShadow.x = 4; closeButtonShadow.y = 4; closeButton.addChild(closeButtonShadow); var closeButtonText = new Text2("CLOSE", { size: 50, fill: 0xFFFFFF, weight: 800 }); closeButtonText.anchor.set(0.5, 0.5); closeButton.addChild(closeButtonText); closeButton.y = 450; scoreMenu.addChild(closeButton); // Close button handler closeButton.down = function () { hideHighScores(); }; } function hideHighScores() { if (!showingScores) return; showingScores = false; if (scoreBackground) { game.removeChild(scoreBackground); scoreBackground = null; } if (scoreMenu) { game.removeChild(scoreMenu); scoreMenu = null; } } // Add intro background image to cover the entire game area var introBackground = game.attachAsset('Introbackground', { anchorX: 0, anchorY: 0, width: 2048, height: 2732 }); // Position background at top-left corner introBackground.x = 0; introBackground.y = 0; // Send background to the back game.addChildAt(introBackground, 0); // Play intro music LK.playMusic('Intromusic'); // Add StartButton on the left side var startButton = game.attachAsset('StartButton', { anchorX: 0.5, anchorY: 0.5 }); startButton.x = 2048 / 2 - 200; // 200px spacing from center startButton.y = 2732 - 200 - 200; // 200px from bottom game.addChild(startButton); // Add ScoreButton on the right side var scoreButton = game.attachAsset('ScoreButton', { anchorX: 0.5, anchorY: 0.5 }); scoreButton.x = 2048 / 2 + 200; // 200px spacing from center scoreButton.y = 2732 - 200 - 200; // 200px from bottom game.addChild(scoreButton); // Add game background image (initially hidden) var gameBackground = game.attachAsset('gamebackground', { anchorX: 0, anchorY: 0, width: 2048, height: 2732 }); gameBackground.x = 0; gameBackground.y = 0; gameBackground.visible = false; game.addChildAt(gameBackground, 0); // Function to start the game function startGame() { showingIntro = false; gameStarted = true; // Hide intro elements introBackground.visible = false; startButton.visible = false; scoreButton.visible = false; // Show game elements gameBackground.visible = true; debugLayer.visible = true; towerLayer.visible = true; enemyLayer.visible = true; goldText.visible = true; healthBarContainer.visible = true; scoreText.visible = true; waveIndicator.visible = true; nextWaveButtonContainer.visible = true; towerPreview.visible = false; // Stop intro music and play game music LK.stopMusic(); LK.playMusic('Gamemusic'); } // Add click handlers for buttons startButton.down = function () { startGame(); }; scoreButton.down = function () { showHighScores(); }; var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; var gold = 100; var lives = 100; var score = 0; var currentWave = 0; var lastWave = 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 healthBarBG = LK.getAsset('healthBarOutline', { anchorX: 0.5, anchorY: 0.5, scaleX: 5, scaleY: 3 }); var healthBarFill = LK.getAsset('healthBar', { anchorX: 0.5, anchorY: 0.5, scaleX: 5, scaleY: 3 }); healthBarFill.tint = 0x00FF00; var healthText = new Text2('100/100', { size: 40, fill: 0xFFFFFF, weight: 800 }); healthText.anchor.set(0.5, 0.5); var healthBarContainer = new Container(); healthBarContainer.addChild(healthBarBG); healthBarContainer.addChild(healthBarFill); healthBarContainer.addChild(healthText); 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(healthBarContainer); LK.gui.top.addChild(scoreText); // Hide UI elements during intro goldText.visible = false; healthBarContainer.visible = false; scoreText.visible = false; healthBarContainer.x = 0; healthBarContainer.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; function updateUI() { goldText.setText('Gold: ' + gold); healthText.setText(lives + '/100'); // Update health bar fill width based on current lives (assuming max 100 lives) var healthPercentage = lives / 100; healthBarFill.scaleX = 5 * healthPercentage; 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); // Hide game elements during intro debugLayer.visible = false; towerLayer.visible = false; enemyLayer.visible = false; 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 = 10; switch (towerType) { case 'rifle': cost = 20; break; case 'flame': cost = 60; break; case 'Rocket': cost = 80; break; case 'gumbomb': cost = 30; break; case 'Toxin': cost = 100; break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.8) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'cannon'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); grid.pathFind(); grid.renderDebug(); return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { // Handle high scores menu during intro if (showingIntro && showingScores) { // Check if click is outside score menu area var menuLeft = 2048 / 2 - 800; var menuRight = 2048 / 2 + 800; var menuTop = 2732 / 2 - 1100; var menuBottom = 2732 / 2 + 1100; if (x < menuLeft || x > menuRight || y < menuTop || y > menuBottom) { hideHighScores(); } return; } // Don't process game input during intro if (showingIntro || !gameStarted) { return; } var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.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) { // Don't process game input during intro if (showingIntro || !gameStarted) { return; } 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) { // Don't process game input during intro if (showingIntro || !gameStarted) { return; } 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; waveIndicator.visible = false; game.addChild(waveIndicator); var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); nextWaveButtonContainer.visible = false; game.addChild(nextWaveButtonContainer); var towerTypes = ['cannon', 'rifle', 'flame', 'Rocket', 'gumbomb', 'Toxin']; var sourceTowers = []; var towerSpacing = 300; // Increase spacing for larger towers var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; game.update = function () { // Only run game logic if the game has started if (!gameStarted || showingIntro) { return; } if (currentWave !== lastWave) { if (currentWave === 11) { LK.playMusic('Gamemusic2'); } else if (currentWave === 20) { LK.playMusic('Gamemusic'); } lastWave = currentWave; } 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 = 2732 / 2; } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy = new Enemy(waveType); // Add enemy to the appropriate layer based on type if (enemy.isFlying) { // Add flying enemy to the top layer enemyLayerTop.addChild(enemy); // If it's a flying enemy, add its shadow to the middle layer if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else { // Add normal/ground enemies to the bottom layer enemyLayerBottom.addChild(enemy); } // Enemy scaling is now handled in Enemy constructor based on currentWave // This ensures consistent scaling every 5 waves for all enemy types // 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; } } 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 - increased rewards var goldEarned = enemy.isBoss ? Math.floor(25 + (enemy.waveNumber - 1) * 1.5) : Math.floor(3 + (enemy.waveNumber - 1) * 0.4); 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 = 2732 / 2; } 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) { // Save high score before showing game over addHighScore(score); 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(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { // Save high score before showing you win addHighScore(score); LK.showYouWin(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
// Check if bullet has exceeded its source tower's range (for all tower types)
if (self.sourceTower) {
var towerDx = self.x - self.sourceTower.x;
var towerDy = self.y - self.sourceTower.y;
var distanceFromTower = Math.sqrt(towerDx * towerDx + towerDy * towerDy);
if (distanceFromTower > self.sourceTower.getRange()) {
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 with resistance scaling
var actualDamage = self.damage;
if (self.targetEnemy.damageResistance) {
actualDamage = self.damage * self.targetEnemy.damageResistance;
}
self.targetEnemy.health -= actualDamage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Apply special effects based on bullet type
if (self.type === 'cannon') {
// Create fire explosion effect for cannon bullets
var explosionEffect = new Container();
explosionEffect.x = self.targetEnemy.x;
explosionEffect.y = self.targetEnemy.y;
game.addChild(explosionEffect);
// Create multiple fire particles for the explosion
for (var fireParticle = 0; fireParticle < 8; fireParticle++) {
var particle = explosionEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
particle.width = particle.height = 20 + Math.random() * 30;
particle.tint = fireParticle % 2 === 0 ? 0xFF4500 : 0xFF8C00; // Orange-red fire colors
particle.alpha = 0.8;
particle.blendMode = 1; // Add blend mode for fire effect
particle.x = (Math.random() - 0.5) * 40;
particle.y = (Math.random() - 0.5) * 40;
particle.scaleX = 0.1;
particle.scaleY = 0.1;
// Animate each particle exploding outward
tween(particle, {
scaleX: 1.5,
scaleY: 1.5,
x: particle.x + (Math.random() - 0.5) * 60,
y: particle.y + (Math.random() - 0.5) * 60,
alpha: 0
}, {
duration: 300 + Math.random() * 200,
easing: tween.easeOut
});
}
// Create central explosion flash
var centralFlash = explosionEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
centralFlash.width = centralFlash.height = 80;
centralFlash.tint = 0xFFFFAA; // Bright yellow-white center
centralFlash.alpha = 1;
centralFlash.blendMode = 1;
centralFlash.scaleX = 0.1;
centralFlash.scaleY = 0.1;
// Animate central flash
tween(centralFlash, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
explosionEffect.destroy();
}
});
} else if (self.type === 'Rocket') {
// Create visual splash effect
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
// Splash damage to nearby enemies - 200x200 area means 100 pixel radius
var splashRadius = 100;
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 === 'gumbomb') {
// Prevent gum effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual gumbomb explosion effect
var gumEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'gumbomb');
game.addChild(gumEffect);
// Apply gum stuck effect - enemies become completely immobilized
if (!self.targetEnemy.gumStuck) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed = 0; // Completely stuck
self.targetEnemy.gumStuck = true;
self.targetEnemy.gumDuration = 240; // 4 seconds at 60 FPS
// Create purple gum effect beneath the enemy
var gumStuckGraphics = LK.getAsset('gumStuckEffect', {
anchorX: 0.5,
anchorY: 0.5
});
gumStuckGraphics.x = self.targetEnemy.x;
gumStuckGraphics.y = self.targetEnemy.y + self.targetEnemy.children[0].height / 2;
gumStuckGraphics.alpha = 0;
gumStuckGraphics.scaleX = 0.1;
gumStuckGraphics.scaleY = 0.1;
self.targetEnemy.gumStuckEffect = gumStuckGraphics;
enemyLayerBottom.addChildAt(gumStuckGraphics, 0);
// Explosion animation - scale up and fade in quickly
tween(gumStuckGraphics, {
alpha: 0.7,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Settle to normal size
tween(gumStuckGraphics, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.elasticOut
});
// Set up tween to destroy gum area after 4 seconds
tween(gumStuckGraphics, {
alpha: 0.7
}, {
duration: 4000,
onFinish: function onFinish() {
if (gumStuckGraphics && gumStuckGraphics.parent) {
game.removeChild(gumStuckGraphics);
}
}
});
}
});
} else {
self.targetEnemy.gumDuration = 240; // Reset duration to 4 seconds
}
}
} else if (self.type === 'Toxin') {
// Prevent toxin effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create ToxinBomb explosion effect using Toxin asset
var explosionEffect = new Container();
explosionEffect.x = self.targetEnemy.x;
explosionEffect.y = self.targetEnemy.y;
game.addChild(explosionEffect);
// Create the main toxin explosion using Toxin asset
var toxinExplosion = explosionEffect.attachAsset('Toxin', {
anchorX: 0.5,
anchorY: 0.5
});
toxinExplosion.width = toxinExplosion.height = 150;
toxinExplosion.tint = 0x00FFAA; // Green toxin color
toxinExplosion.alpha = 0.9;
toxinExplosion.scaleX = 0.1;
toxinExplosion.scaleY = 0.1;
// Animate the toxin explosion expanding and fading
tween(toxinExplosion, {
scaleX: 1.8,
scaleY: 1.8,
alpha: 0.3
}, {
duration: 400,
easing: tween.easeOut
});
// Create additional smaller toxin particles around the main explosion
for (var toxinParticle = 0; toxinParticle < 6; toxinParticle++) {
var particle = explosionEffect.attachAsset('Toxin', {
anchorX: 0.5,
anchorY: 0.5
});
particle.width = particle.height = 40 + Math.random() * 30;
particle.tint = 0x00FFAA; // Consistent green toxin color
particle.alpha = 0.7;
particle.x = (Math.random() - 0.5) * 80;
particle.y = (Math.random() - 0.5) * 80;
particle.scaleX = 0.1;
particle.scaleY = 0.1;
// Animate each particle expanding outward
tween(particle, {
scaleX: 1.2,
scaleY: 1.2,
x: particle.x + (Math.random() - 0.5) * 100,
y: particle.y + (Math.random() - 0.5) * 100,
alpha: 0
}, {
duration: 500 + Math.random() * 200,
easing: tween.easeOut
});
}
// Destroy the explosion effect after animation completes
tween(explosionEffect, {
alpha: 1
}, {
duration: 600,
onFinish: function onFinish() {
explosionEffect.destroy();
}
});
// Apply toxin effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
}
// Flame tower does not use bullets, so destroy any 'flame' type bullets immediately
if (self.type === 'flame') {
self.destroy();
} else {
self.destroy();
}
} else {
var angle = Math.atan2(dy, dx);
// Rotate bullet to face movement direction
if (self.children[0]) {
// Check if this is a RifleBullet which needs rotation offset
var rotationOffset = 0;
if (self.type === 'rifle') {
rotationOffset = Math.PI / 2; // Compensate for upward-pointing asset
}
var targetAngle = angle + rotationOffset;
if (self.children[0].targetRotation === undefined) {
self.children[0].targetRotation = targetAngle;
self.children[0].rotation = targetAngle;
} else {
if (Math.abs(targetAngle - self.children[0].targetRotation) > 0.05) {
tween.stop(self.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = self.children[0].rotation;
var angleDiff = targetAngle - 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;
}
self.children[0].targetRotation = targetAngle;
tween(self.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 100,
easing: tween.easeOut
});
}
}
}
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('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.setText("-");
cellGraphics.tint = 0x880000;
return;
}
numberLabel.visible = true;
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0xe14de9; // pink color with transparency
cellGraphics.alpha = 0.5; // Almost max transparent
} else {
cellGraphics.tint = 0x4db3e9; // Sky blue color with transparency
cellGraphics.alpha = 0.5; // Almost max transparent
}
while (debugArrows.length > data.targets.length) {
self.removeChild(debugArrows.pop());
}
for (var a = 0; a < data.targets.length; a++) {
var destination = data.targets[a];
var ox = destination.x - data.x;
var oy = destination.y - data.y;
var angle = Math.atan2(oy, ox);
if (!debugArrows[a]) {
debugArrows[a] = LK.getAsset('arrow', {
anchorX: -.5,
anchorY: 0.5
});
debugArrows[a].alpha = .5;
self.addChildAt(debugArrows[a], 1);
}
debugArrows[a].rotation = angle;
}
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xff58f7;
cellGraphics.alpha = 0.5; // Almost max transparent
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.tint = 0xff8400; // Blue sky color
cellGraphics.alpha = 0.5; // Almost max transparent
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 1000) / 10);
};
});
// This update method was incorrectly placed here and should be removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
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 'gumbomb':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 2; // Larger explosion effect
break;
case 'toxin':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'flame':
effectGraphics.tint = 0xFF8800; // Mix of yellow, orange, red
effectGraphics.width = effectGraphics.height = CELL_SIZE;
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;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type - increased health
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = 45; // Increased from 30
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 40; // Increased from 25
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 40; // Increased from 25
break;
case 'swarm':
self.maxHealth = 25; // Increased from 15
break;
case 'normal':
default:
self.maxHealth = 35; // Increased from 20
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 8x health and are larger
self.maxHealth *= 8;
// Slightly slower speed for bosses but not as much
self.speed = self.speed * 0.8;
}
// Apply wave-based difficulty scaling every 5 waves
var difficultyWaves = Math.floor((currentWave - 1) / 5);
if (difficultyWaves > 0) {
// Increase health by 75% every 5 waves (was 50%)
var healthMultiplier = 1 + difficultyWaves * 0.75;
self.maxHealth = Math.round(self.maxHealth * healthMultiplier);
// Increase speed by 30% every 5 waves (was 20%)
var speedMultiplier = 1 + difficultyWaves * 0.3;
self.speed = self.speed * speedMultiplier;
// Add damage resistance - enemies take 10% less damage every 5 waves
self.damageResistance = 1 - Math.min(0.5, difficultyWaves * 0.1); // Cap at 50% resistance
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
// For flying enemies, use different assets based on wave progression
if (self.type === 'flying') {
// Use different flying enemy assets based on wave number
var waveGroup = Math.floor((currentWave - 1) / 10); // Every 10 waves change flying enemy type
var flyingAssets = ['Firo', 'Eye', 'Electro'];
var assetIndex = waveGroup % flyingAssets.length;
assetId = flyingAssets[assetIndex];
} else {
// For ground enemies, use different Cyborg assets based on wave number
var waveGroup = Math.floor((currentWave - 1) / 10); // Every 10 waves change ground enemy type
var groundAssets = ['Cyborg', 'Cyborg2', 'Cyborg3'];
var assetIndex = waveGroup % groundAssets.length;
if (self.type === 'normal') {
assetId = groundAssets[assetIndex];
} else if (self.type === 'fast') {
// For fast enemies, use Robot1, Spider1, and Spider2 assets based on wave progression
var waveGroup = Math.floor((currentWave - 1) / 10); // Every 10 waves change fast enemy type
var fastAssets = ['Robot1', 'Spider1', 'Spider2'];
var assetIndex = waveGroup % fastAssets.length;
assetId = fastAssets[assetIndex];
} else if (self.type === 'immune') {
// For immune enemies, use Big_Robot1, Big_Robot2, and Big_Robot3 assets based on wave progression
var waveGroup = Math.floor((currentWave - 1) / 10); // Every 10 waves change immune enemy type
var immuneAssets = ['Big_Robot1', 'Big_Robot2', 'Big_Robot3'];
var assetIndex = waveGroup % immuneAssets.length;
assetId = immuneAssets[assetIndex];
} else if (self.type === 'swarm') {
// For swarm enemies, use CyberSnake1, CyberSnake2, and CyberSnake3 assets based on wave progression
var waveGroup = Math.floor((currentWave - 1) / 10); // Every 10 waves change swarm enemy type
var swarmAssets = ['CyberSnake1', 'CyberSnake2', 'CyberSnake3'];
var assetIndex = waveGroup % swarmAssets.length;
assetId = swarmAssets[assetIndex];
} else {
// For other non-normal ground enemies, still use the original prefixed system but could be enhanced later
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 - use the same asset as the main enemy
var shadowAssetId = assetId;
// For flying enemies, make sure we use the correct asset for the shadow
if (self.type === 'flying') {
var waveGroup = Math.floor((currentWave - 1) / 10);
var flyingAssets = ['Firo', 'Eye', 'Electro'];
var assetIndex = waveGroup % flyingAssets.length;
shadowAssetId = flyingAssets[assetIndex];
}
var shadowGraphics = self.shadow.attachAsset(shadowAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.3; // 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 = 0xffff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
self.update = function () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed, gum stuck, or poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.gumStuck = false;
if (self.gumStuckEffect && self.gumStuckEffect.parent) {
game.removeChild(self.gumStuckEffect);
}
self.gumStuckEffect = null;
self.poisoned = false;
self.poisonEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle gum stuck effect
if (self.gumStuck) {
// Visual indication of gum stuck status
if (!self.gumStuckEffect) {
self.gumStuckEffect = true;
}
self.gumDuration--;
if (self.gumDuration <= 0) {
self.speed = self.originalSpeed;
self.gumStuck = false;
self.gumStuckEffect = false;
// Remove gum effect visual with fade out animation
if (self.gumStuckEffect && self.gumStuckEffect.parent) {
tween(self.gumStuckEffect, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
if (self.gumStuckEffect && self.gumStuckEffect.parent) {
game.removeChild(self.gumStuckEffect);
}
self.gumStuckEffect = null;
}
});
} else {
self.gumStuckEffect = null;
}
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
} else {
// Update gum effect position to follow enemy
if (self.gumStuckEffect && self.gumStuckEffect.parent) {
self.gumStuckEffect.x = self.x;
self.gumStuckEffect.y = self.y + enemyGraphics.height / 2;
}
}
}
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
var actualPoisonDamage = self.poisonDamage;
if (self.damageResistance) {
actualPoisonDamage = self.poisonDamage * self.damageResistance;
}
self.health -= actualPoisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && (self.slowed || self.gumStuck)) {
// Combine poison (0x00FFAA) and slow/gum (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed || self.gumStuck) {
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.currentTarget) {
enemy.currentTarget = cell.targets[0];
}
if (enemy.currentTarget) {
if (cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300;
buttonBackground.height = 100;
buttonBackground.tint = 0x0088FF;
var buttonText = new Text2("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 = 2732 / 2;
}
};
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: 65,
fill: 0xFF69B4,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 60;
notificationGraphics.height = notificationText.height + 40;
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 || 'cannon';
// Use specific tower asset based on type
var towerAssetId = 'CannonTower'; // default
switch (self.towerType) {
case 'rifle':
towerAssetId = 'RifleTower';
break;
case 'flame':
towerAssetId = 'FlameTower';
break;
case 'Rocket':
towerAssetId = 'RocketTower';
break;
case 'gumbomb':
towerAssetId = 'GumBombTower';
break;
case 'Toxin':
towerAssetId = 'ToxinTower';
break;
case 'cannon':
default:
towerAssetId = 'CannonTower';
break;
}
// 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 as we're using specific tower assets
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = -20 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var displayName = self.towerType === 'rifle' ? 'Rifle' : self.towerType === 'Toxin' ? 'Toxinbomb' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
// Set color based on tower type
var towerColor = 0xFFFFFF; // default white
switch (self.towerType) {
case 'cannon':
towerColor = 0xFF0000; // Red
break;
case 'rifle':
towerColor = 0x00FF00; // Green
break;
case 'flame':
towerColor = 0x800080; // Purple
break;
case 'Rocket':
towerColor = 0x000000; // Black
break;
case 'gumbomb':
towerColor = 0x0000FF; // Blue
break;
case 'Toxin':
towerColor = 0xFFFF00; // Yellow
break;
}
var typeLabel = new Text2(displayName, {
size: 50,
fill: towerColor,
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 || 'cannon';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// Standardized method to get the current range of the tower
self.getRange = function () {
// Always calculate range based on tower type and level
switch (self.id) {
case 'flame':
// Flame: base 5, +0.8 per level, but final upgrade gets a huge boost
if (self.level === self.maxLevel) {
return 12 * CELL_SIZE; // Significantly increased range for max level
}
return (5 + (self.level - 1) * 0.8) * CELL_SIZE;
case 'Rocket':
// Rocket: base range higher than sniper, +0.8 per level
if (self.level === self.maxLevel) {
return 14 * CELL_SIZE; // Higher than sniper's max range
}
return (6 + (self.level - 1) * 0.8) * CELL_SIZE;
case 'rifle':
// Rifle: base 3 (default), +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'gumbomb':
// GumBomb: base 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'Toxin':
// Toxin: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'cannon':
// Cannon: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
break;
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 = 7;
self.lastFired = 0;
self.targetEnemy = null;
self.flameEffect = null; // Track active flame effect for flame tower
self.flameAnimation = null; // Track flame animation visual
switch (self.id) {
case 'rifle':
self.fireRate = Math.floor(60 / 3.5); // 3.5 shots per second = 60/3.5 = 17 frames
self.damage = 5;
self.range = 3 * CELL_SIZE; // Default tower range
self.bulletSpeed = 7;
break;
case 'flame':
self.fireRate = 120; // 0.5 shots per second
self.damage = 9;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'Rocket':
self.fireRate = Math.floor(60 / 1.0); // 1.0 shots per second = 60 frames
self.damage = 13;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 8;
break;
case 'gumbomb':
self.fireRate = 50;
self.damage = 3;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'Toxin':
self.fireRate = 70;
self.damage = 18;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
// Use specific tower asset based on type
var towerAssetId = 'CannonTower'; // default
switch (self.id) {
case 'rifle':
towerAssetId = 'RifleTower';
break;
case 'flame':
towerAssetId = 'FlameTower';
break;
case 'Rocket':
towerAssetId = 'RocketTower';
break;
case 'gumbomb':
towerAssetId = 'GumBombTower';
break;
case 'Toxin':
towerAssetId = 'ToxinTower';
break;
case 'cannon':
default:
towerAssetId = 'CannonTower';
break;
}
var baseGraphics = self.attachAsset(towerAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
// No need to tint as we're using specific tower assets
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = 30;
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 = dotSpacing * (i + 1) - baseGraphics.width / 2;
dot.y = -CELL_SIZE * 1.2;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
var weaponAssetId = 'Weapon1'; // default
switch (self.id) {
case 'rifle':
weaponAssetId = 'Weapon2';
break;
case 'flame':
weaponAssetId = 'Weapon3';
break;
case 'Rocket':
weaponAssetId = 'Weapon4';
break;
case 'gumbomb':
weaponAssetId = 'Weapon5';
break;
case 'Toxin':
weaponAssetId = 'Weapon6';
break;
case 'cannon':
default:
weaponAssetId = 'Weapon1';
break;
}
var gunGraphics = gunContainer.attachAsset(weaponAssetId, {
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 = 0xff6600;
} else {
switch (self.id) {
case 'rifle':
towerLevelIndicator.tint = 0xffffff;
break;
case 'flame':
towerLevelIndicator.tint = 0xffffff;
break;
case 'Rocket':
towerLevelIndicator.tint = 0xffffff;
break;
case 'gumbomb':
towerLevelIndicator.tint = 0xffffff;
break;
case 'Toxin':
towerLevelIndicator.tint = 0xffffff;
break;
default:
towerLevelIndicator.tint = 0xffffff;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// Make last upgrade level extra expensive
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
if (self.id === 'rifle') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(4, 12 - self.level * 3); // double the effect, starting from 12
self.damage = 8 + self.level * 8; // double the effect, starting from 8
self.bulletSpeed = 7 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(6, 12 - self.level * 1); // Rifle gets faster with upgrades, starting from 12
self.damage = 8 + self.level * 2; // Starting from 8 damage
self.bulletSpeed = 7 + self.level * 0.7;
}
} else {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for all other towers (double the effect)
self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect
self.damage = 10 + self.level * 20; // double the effect
self.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is in range
if (distance <= self.getRange()) {
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, use the original path-based targeting
// Get the cell for this enemy
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Use the cell's score (distance to exit) for prioritization
// Lower score means closer to exit
if (cell.score < closestScore) {
closestScore = cell.score;
closestEnemy = enemy;
}
}
}
}
}
if (!closestEnemy) {
self.targetEnemy = null;
}
return closestEnemy;
};
self.update = function () {
self.targetEnemy = self.findTarget();
if (!self.targetEnemy) {
if (self.flameAnimation) {
self.flameAnimation.destroy();
self.flameAnimation = null;
}
if (self.flameEffect) {
self.flameEffect.destroy();
self.flameEffect = null;
}
}
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
// Smooth gun rotation using tween
if (gunContainer.targetRotation === undefined) {
gunContainer.targetRotation = angle;
gunContainer.rotation = angle;
} else {
if (Math.abs(angle - gunContainer.targetRotation) > 0.05) {
tween.stop(gunContainer, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = gunContainer.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;
}
gunContainer.targetRotation = angle;
tween(gunContainer, {
rotation: currentRotation + angleDiff
}, {
duration: 200,
easing: tween.easeOut
});
}
}
// Flame tower has special continuous firing logic
if (self.id === 'flame') {
// Continuously apply damage if an enemy is targeted
if (self.targetEnemy && self.isInRange(self.targetEnemy)) {
// Apply damage every frame while the enemy is in range and alive
if (self.targetEnemy.health > 0) {
var actualDamage = self.damage / 60; // Damage per second (damage/60 FPS)
if (self.targetEnemy.damageResistance) {
actualDamage = actualDamage * self.targetEnemy.damageResistance;
}
self.targetEnemy.health -= actualDamage;
// Ensure health doesn't go below zero
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// --- Flame Visuals ---
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Create flame animation from the gun if not already active
if (!self.flameAnimation) {
LK.getSound('Flamesound').play();
self.flameAnimation = new Container();
var flameGraphics = self.flameAnimation.attachAsset('Flame', {
anchorX: 0,
// Anchor at the base (left) of the flame
anchorY: 0.5 // Anchor in the middle vertically
});
// The Flame asset needs no rotation as it should extend horizontally from the gun
flameGraphics.rotation = 0;
flameGraphics.tint = 0xFF8800;
gunContainer.addChild(self.flameAnimation);
// Position at the front edge of the gun barrel
self.flameAnimation.x = gunGraphics.width / 2 + 20;
// Set initial scale for appear animation
flameGraphics.scale.set(0, 0.6);
}
// Update the flame animation every frame to stretch and flicker
if (self.flameAnimation && self.flameAnimation.children[0]) {
var flameGraphics = self.flameAnimation.children[0];
// The flame asset is 300px long (originally height)
var flameAssetLength = 300;
var targetScaleX = distance / flameAssetLength;
// Animate the flame length
flameGraphics.scale.x += (targetScaleX - flameGraphics.scale.x) * 0.3;
// Use a sinusoidal flicker for a more natural look on the width
var flicker = Math.sin(LK.ticks * 0.5) * 0.1;
flameGraphics.scale.y = 0.6 + flicker;
}
// Show visual effect on the enemy
if (!self.flameEffect) {
self.flameEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'flame');
game.addChild(self.flameEffect);
}
// Update the position of the flame effect to match the target enemy
self.flameEffect.x = self.targetEnemy.x;
self.flameEffect.y = self.targetEnemy.y;
} else {
// Target is dead, destroy flame animation and effect
if (self.flameAnimation) {
self.flameAnimation.destroy();
self.flameAnimation = null;
}
if (self.flameEffect) {
self.flameEffect.destroy();
self.flameEffect = null;
}
}
} else {
// No target enemy in range, destroy flame animation and effect
if (self.flameAnimation) {
self.flameAnimation.destroy();
self.flameAnimation = null;
}
if (self.flameEffect) {
self.flameEffect.destroy();
self.flameEffect = null;
}
}
} else {
// Standard tower firing logic (single bullets)
if (self.id === 'flame') {
// Manage flame visibility based on fire rate cooldown
var flameGraphics = self.flameAnimation ? self.flameAnimation.children[0] : null;
if (flameGraphics) {
if (LK.ticks - self.lastFired >= self.fireRate * 0.6) {
// Show flame for 60% of fire rate
flameGraphics.visible = true;
} else {
flameGraphics.visible = false;
}
}
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
} else {
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
game.addChild(rangeIndicator);
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// Store reference to source tower for range checking (all tower types)
bullet.sourceTower = self;
// For Rocket towers, use Rocket asset
if (self.id === 'Rocket') {
// Replace bullet asset with Rocket asset
bullet.removeChild(bullet.children[0]);
var rocketGraphics = bullet.attachAsset('Rocket', {
anchorX: 0.5,
anchorY: 0.5
});
rocketGraphics.tint = 0x00FF00;
rocketGraphics.width = 80;
rocketGraphics.height = 70;
}
// For gumbomb tower, pass level for scaling gum effect
if (self.id === 'gumbomb') {
bullet.sourceTowerLevel = self.level;
}
// Customize bullet appearance based on tower type
switch (self.id) {
case 'rifle':
// Replace bullet asset with RifleBullet asset
bullet.removeChild(bullet.children[0]);
var rifleGraphics = bullet.attachAsset('RifleBullet', {
anchorX: 0.5,
anchorY: 0.5
});
rifleGraphics.tint = 0xFF8000;
rifleGraphics.width = 5;
rifleGraphics.height = 10;
break;
// Remove flame bullet type creation as flame tower now applies continuous damage
case 'Rocket':
// Rocket bullets now use Rocket asset, styling already applied above
break;
case 'gumbomb':
// Replace bullet asset with GumBomb asset
bullet.removeChild(bullet.children[0]);
var gumbombGraphics = bullet.attachAsset('gumbomb', {
anchorX: 0.5,
anchorY: 0.5
});
gumbombGraphics.tint = 0x9900FF;
gumbombGraphics.width = 35;
gumbombGraphics.height = 35;
break;
case 'Toxin':
// Replace bullet asset with ToxinBomb asset
bullet.removeChild(bullet.children[0]);
var toxinGraphics = bullet.attachAsset('ToxinBomb', {
anchorX: 0.5,
anchorY: 0.5
});
toxinGraphics.tint = 0xFF00AA;
toxinGraphics.width = 35;
toxinGraphics.height = 35;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// Play sound for tower shots
switch (self.id) {
case 'cannon':
LK.getSound('Bulletsound').play();
break;
case 'rifle':
LK.getSound('Riflebulletsound').play();
break;
case 'Rocket':
LK.getSound('Rocketsound').play();
break;
case 'gumbomb':
LK.getSound('Gumbombsound').play();
break;
case 'Toxin':
LK.getSound('Toxinbombsound').play();
break;
}
// --- 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();
// Play placement sound for cannon tower
if (self.id === 'cannon') {
LK.getSound('placmentsound1').play();
} else if (self.id === 'rifle') {
LK.getSound('placmentsound2').play();
} else if (self.id === 'flame') {
LK.getSound('placmentsound3').play();
} else if (self.id === 'Rocket') {
LK.getSound('placmentsound4').play();
} else if (self.id === 'gumbomb') {
LK.getSound('placmentsound5').play();
} else if (self.id === 'Toxin') {
LK.getSound('placmentsound6').play();
}
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'cannon';
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 'rifle':
previewGraphics.tint = 0xFFFFFF;
break;
case 'flame':
previewGraphics.tint = 0xFFFFFF;
break;
case 'Rocket':
previewGraphics.tint = 0xFFFFFF;
break;
case 'gumbomb':
previewGraphics.tint = 0xFFFFFF;
break;
case 'Toxin':
previewGraphics.tint = 0xFFFFFF;
break;
case 'cannon':
previewGraphics.tint = 0xFFFFFF;
break;
default:
previewGraphics.tint = 0xFFFFFF;
}
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 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
var displayName = self.tower.id === 'rifle' ? 'Rifle' : self.tower.id === 'Toxin' ? 'Toxinbomb' : self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1);
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('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' gold');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 0;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
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 = 0xFF0000;
// 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 = 2732 / 2;
}
};
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 = 0xFF0000;
waveType = "Easy";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0xFF0000;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xFF0000;
waveType = "Unstoppable";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFF0000;
waveType = "Air";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF0000;
waveType = "Group";
enemyType = "swarm";
enemyCount = 30;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['normal', 'fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Air";
block.tint = 0xFF0000;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xFF0000;
waveType = "Boss Easy";
break;
case 'fast':
block.tint = 0xFF0000;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0xFF0000;
waveType = "Boss Unstoppable";
break;
case 'flying':
block.tint = 0xFF0000;
waveType = "Boss Air";
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 = 0xFF0000;
break;
case 'fast':
block.tint = 0xFF0000;
break;
case 'immune':
block.tint = 0xFF0000;
break;
case 'flying':
block.tint = 0xFF0000;
break;
default:
block.tint = 0xFF0000;
break;
}
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0xFF0000;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xFF0000;
waveType = "Unstoppable";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFF0000;
waveType = "Air";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF0000;
waveType = "Group";
enemyType = "swarm";
enemyCount = 30;
} else {
block.tint = 0xFF0000;
waveType = "Easy";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Add tween animation to transition block color from red to white
tween(block, {
tint: 0xFFFFFF
}, {
duration: 2000,
easing: tween.easeInOut
});
// 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 = 2732 / 2;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
// Game state variables
var gameStarted = false;
var showingIntro = true;
var showingScores = false;
var scoreBackground = null;
var scoreMenu = null;
// High scores management
function getHighScores() {
return storage.highScores || [];
}
function addHighScore(score) {
var highScores = getHighScores();
highScores.push(score);
// Sort in descending order
highScores.sort(function (a, b) {
return b - a;
});
// Keep only top 10
if (highScores.length > 10) {
highScores = highScores.slice(0, 10);
}
storage.highScores = highScores;
return highScores;
}
function showHighScores() {
if (showingScores) return;
showingScores = true;
// Create score background
scoreBackground = game.attachAsset('Scorebackground', {
anchorX: 0.5,
anchorY: 0.5
});
scoreBackground.x = 2048 / 2;
scoreBackground.y = 2732 / 2;
game.addChild(scoreBackground);
// Create score menu container
scoreMenu = new Container();
scoreMenu.x = 2048 / 2;
scoreMenu.y = 2732 / 2;
game.addChild(scoreMenu);
// Add title
var titleShadow = new Text2("HIGH SCORES", {
size: 80,
fill: 0x000000,
weight: 800
});
titleShadow.anchor.set(0.5, 0.5);
titleShadow.x = 4;
titleShadow.y = -400 + 4;
scoreMenu.addChild(titleShadow);
var title = new Text2("HIGH SCORES", {
size: 80,
fill: 0xFFD700,
weight: 800
});
title.anchor.set(0.5, 0.5);
title.y = -400;
scoreMenu.addChild(title);
// Get and display high scores
var highScores = getHighScores();
for (var i = 0; i < 10; i++) {
var rank = i + 1;
var scoreValue = i < highScores.length ? highScores[i] : 0;
var yPos = -280 + i * 60;
// Score rank and value shadow
var scoreShadow = new Text2(rank + ". " + scoreValue, {
size: 60,
fill: 0x000000,
weight: 800
});
scoreShadow.anchor.set(0.5, 0.5);
scoreShadow.x = 4;
scoreShadow.y = yPos + 4;
scoreMenu.addChild(scoreShadow);
// Score rank and value
var scoreColor = i < 3 ? 0xFFD700 : 0xFFFFFF; // Gold for top 3
var scoreText = new Text2(rank + ". " + scoreValue, {
size: 60,
fill: scoreColor,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
scoreText.y = yPos;
scoreMenu.addChild(scoreText);
}
// Add close button
var closeButton = new Container();
var closeButtonBg = closeButton.attachAsset('ScoreButton', {
anchorX: 0.5,
anchorY: 0.5
});
closeButtonBg.width = 300;
closeButtonBg.height = 100;
closeButtonBg.tint = 0xAA0000;
var closeButtonShadow = new Text2("CLOSE", {
size: 50,
fill: 0x000000,
weight: 800
});
closeButtonShadow.anchor.set(0.5, 0.5);
closeButtonShadow.x = 4;
closeButtonShadow.y = 4;
closeButton.addChild(closeButtonShadow);
var closeButtonText = new Text2("CLOSE", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
closeButtonText.anchor.set(0.5, 0.5);
closeButton.addChild(closeButtonText);
closeButton.y = 450;
scoreMenu.addChild(closeButton);
// Close button handler
closeButton.down = function () {
hideHighScores();
};
}
function hideHighScores() {
if (!showingScores) return;
showingScores = false;
if (scoreBackground) {
game.removeChild(scoreBackground);
scoreBackground = null;
}
if (scoreMenu) {
game.removeChild(scoreMenu);
scoreMenu = null;
}
}
// Add intro background image to cover the entire game area
var introBackground = game.attachAsset('Introbackground', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732
});
// Position background at top-left corner
introBackground.x = 0;
introBackground.y = 0;
// Send background to the back
game.addChildAt(introBackground, 0);
// Play intro music
LK.playMusic('Intromusic');
// Add StartButton on the left side
var startButton = game.attachAsset('StartButton', {
anchorX: 0.5,
anchorY: 0.5
});
startButton.x = 2048 / 2 - 200; // 200px spacing from center
startButton.y = 2732 - 200 - 200; // 200px from bottom
game.addChild(startButton);
// Add ScoreButton on the right side
var scoreButton = game.attachAsset('ScoreButton', {
anchorX: 0.5,
anchorY: 0.5
});
scoreButton.x = 2048 / 2 + 200; // 200px spacing from center
scoreButton.y = 2732 - 200 - 200; // 200px from bottom
game.addChild(scoreButton);
// Add game background image (initially hidden)
var gameBackground = game.attachAsset('gamebackground', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732
});
gameBackground.x = 0;
gameBackground.y = 0;
gameBackground.visible = false;
game.addChildAt(gameBackground, 0);
// Function to start the game
function startGame() {
showingIntro = false;
gameStarted = true;
// Hide intro elements
introBackground.visible = false;
startButton.visible = false;
scoreButton.visible = false;
// Show game elements
gameBackground.visible = true;
debugLayer.visible = true;
towerLayer.visible = true;
enemyLayer.visible = true;
goldText.visible = true;
healthBarContainer.visible = true;
scoreText.visible = true;
waveIndicator.visible = true;
nextWaveButtonContainer.visible = true;
towerPreview.visible = false;
// Stop intro music and play game music
LK.stopMusic();
LK.playMusic('Gamemusic');
}
// Add click handlers for buttons
startButton.down = function () {
startGame();
};
scoreButton.down = function () {
showHighScores();
};
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
var gold = 100;
var lives = 100;
var score = 0;
var currentWave = 0;
var lastWave = 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 healthBarBG = LK.getAsset('healthBarOutline', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 5,
scaleY: 3
});
var healthBarFill = LK.getAsset('healthBar', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 5,
scaleY: 3
});
healthBarFill.tint = 0x00FF00;
var healthText = new Text2('100/100', {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
healthText.anchor.set(0.5, 0.5);
var healthBarContainer = new Container();
healthBarContainer.addChild(healthBarBG);
healthBarContainer.addChild(healthBarFill);
healthBarContainer.addChild(healthText);
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(healthBarContainer);
LK.gui.top.addChild(scoreText);
// Hide UI elements during intro
goldText.visible = false;
healthBarContainer.visible = false;
scoreText.visible = false;
healthBarContainer.x = 0;
healthBarContainer.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
function updateUI() {
goldText.setText('Gold: ' + gold);
healthText.setText(lives + '/100');
// Update health bar fill width based on current lives (assuming max 100 lives)
var healthPercentage = lives / 100;
healthBarFill.scaleX = 5 * healthPercentage;
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);
// Hide game elements during intro
debugLayer.visible = false;
towerLayer.visible = false;
enemyLayer.visible = false;
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 = 10;
switch (towerType) {
case 'rifle':
cost = 20;
break;
case 'flame':
cost = 60;
break;
case 'Rocket':
cost = 80;
break;
case 'gumbomb':
cost = 30;
break;
case 'Toxin':
cost = 100;
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.8) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'cannon');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
grid.pathFind();
grid.renderDebug();
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
// Handle high scores menu during intro
if (showingIntro && showingScores) {
// Check if click is outside score menu area
var menuLeft = 2048 / 2 - 800;
var menuRight = 2048 / 2 + 800;
var menuTop = 2732 / 2 - 1100;
var menuBottom = 2732 / 2 + 1100;
if (x < menuLeft || x > menuRight || y < menuTop || y > menuBottom) {
hideHighScores();
}
return;
}
// Don't process game input during intro
if (showingIntro || !gameStarted) {
return;
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.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) {
// Don't process game input during intro
if (showingIntro || !gameStarted) {
return;
}
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) {
// Don't process game input during intro
if (showingIntro || !gameStarted) {
return;
}
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;
waveIndicator.visible = false;
game.addChild(waveIndicator);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200;
nextWaveButton.y = 2732 - 100 + 20;
nextWaveButtonContainer.addChild(nextWaveButton);
nextWaveButtonContainer.visible = false;
game.addChild(nextWaveButtonContainer);
var towerTypes = ['cannon', 'rifle', 'flame', 'Rocket', 'gumbomb', 'Toxin'];
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
game.update = function () {
// Only run game logic if the game has started
if (!gameStarted || showingIntro) {
return;
}
if (currentWave !== lastWave) {
if (currentWave === 11) {
LK.playMusic('Gamemusic2');
} else if (currentWave === 20) {
LK.playMusic('Gamemusic');
}
lastWave = currentWave;
}
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 = 2732 / 2;
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy = new Enemy(waveType);
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Enemy scaling is now handled in Enemy constructor based on currentWave
// This ensures consistent scaling every 5 waves for all enemy types
// 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;
}
}
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 - increased rewards
var goldEarned = enemy.isBoss ? Math.floor(25 + (enemy.waveNumber - 1) * 1.5) : Math.floor(3 + (enemy.waveNumber - 1) * 0.4);
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 = 2732 / 2;
}
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) {
// Save high score before showing game over
addHighScore(score);
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();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
// Save high score before showing you win
addHighScore(score);
LK.showYouWin();
}
};
Pack top square of tower facing the screen to use it as a grid. future. seen from above. HD colors.
Make it blue
different cyber flying enemy from the front side facing camera. HD colors. separated In-Game asset. 2d. High contrast. No shadows
different cyber electro flying enemy from the front side facing camera. HD colors. separated In-Game asset. 2d. High contrast. No shadows
different cyborg electro enemy from the front side facing camera. HD colors. separated In-Game asset. 2d. High contrast. No shadows
different cyber small enemy from the front side facing camera. HD colors. separated In-Game asset. 2d. High contrast. No shadows
different cyber spider small enemy from the front side facing camera. HD colors. separated In-Game asset. 2d. High contrast. No shadows
different cyber big Robot enemy from the front side facing camera. HD colors. separated In-Game asset. 2d. High contrast. No shadows
different big cyber snake Robot enemy from the front side facing camera. HD colors. separated In-Game asset. 2d. High contrast. No shadows
Fullscreen modern App Store landscape banner, 16:9, high definition, HD colors. for a future game of tower defense with cyberpunk and abstract style titled "Cyber Towers" without description "Defend the exit way in final path from different enemies Robots, cyborgs, cyber robot snakes, cyber spiders, flying cyber robots, by placing towers each tower have different weapon then the others, cannon, rifle, flame, rocket, gum bomb, toxin bomb. don't let them pass through the path!". with text on the middle of the banner "Cyber Towers"!
placmentsound1
Sound effect
placmentsound2
Sound effect
placmentsound3
Sound effect
placmentsound4
Sound effect
placmentsound5
Sound effect
placmentsound6
Sound effect
Gamemusic
Music
Intromusic
Music
Bulletsound
Sound effect
Riflebulletsound
Sound effect
Flamesound
Sound effect
Rocketsound
Sound effect
Gumbombsound
Sound effect
Toxinbombsound
Sound effect
Gamemusic2
Music
gamemusic3
Music
Gamemusic4
Music
Gamemusic5
Music
Bossmusic1
Music
Bossmusic2
Music
Bossmusic3
Music
Bossmusic4
Music
Bossmusic5
Music