Code edit (1 edits merged)
Please save this source code
User prompt
Add 'Crystal' asset to the game on the top left corner. don't change anything in other objects!.
Code edit (1 edits merged)
Please save this source code
User prompt
Add 'Crystal' asset to the game on the top left corner. don't change anything in other objects!.
User prompt
Add 'Crystal' on the top left corner
User prompt
Add 'Crystal' on the top left corner
User prompt
Add 'Crystal' asset beside the crystal text from the left side.
User prompt
Please fix the bug: 'goldText is not defined' in or related to this line: 'goldText.anchor.set(0.5, 0.5);' Line Number: 3168
User prompt
Change 'Gold' to 'Crystal', change text to pink.
User prompt
Change 'Gold' to 'Crystal', change text to pink.
User prompt
Make the crystal text with its value 200x200px spacing from from the middle on the left side as the score
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 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'.
/**** * 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 }, { duration: 4000, onFinish: function onFinish() { if (gumStuckGraphics && gumStuckGraphics.parent) { gumStuckGraphics.parent.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 CrystalIndicator = 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 crystalText = new Text2("+" + value, { size: 45, fill: 0xFF69B4, weight: 800 }); crystalText.anchor.set(0.5, 0.5); self.addChild(crystalText); 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 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 = []; // Numbers removed - using animated orange lines only 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(); cellGraphics.tint = 0x880000; return; } 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; // Change lines pointing up to point down if (oy < 0) { oy = -oy; // Flip the y direction for upward pointing lines } var angle = Math.atan2(oy, ox); if (!debugArrows[a]) { // Create arrow container debugArrows[a] = new Container(); // Create arrow shaft using shape asset var arrowShaft = LK.getAsset('cell', { anchorX: 0, anchorY: 0.5 }); arrowShaft.width = 40; arrowShaft.height = 8; arrowShaft.tint = 0xFF69B4; // Pink color arrowShaft.alpha = 0.8; debugArrows[a].addChild(arrowShaft); // Create arrow head using shape asset var arrowHead = LK.getAsset('cell', { anchorX: 0, anchorY: 0.5 }); arrowHead.width = 20; arrowHead.height = 16; arrowHead.tint = 0xFF69B4; // Pink color arrowHead.alpha = 0.8; arrowHead.x = 35; // Position at end of shaft arrowHead.y = 0; debugArrows[a].addChild(arrowHead); self.addChildAt(debugArrows[a], 1); } // Fix rotation calculation - ensure correct direction mapping debugArrows[a].rotation = angle; // Position the line at center of cell for better visibility debugArrows[a].x = 0; debugArrows[a].y = 0; // Animate the violet arrow with flowing effect var arrow = debugArrows[a]; arrow.animationPhase = (arrow.animationPhase || 0) + 0.1; // Create pulsing alpha effect for flowing animation var pulseAlpha = 0.4 + 0.4 * Math.sin(arrow.animationPhase); arrow.alpha = pulseAlpha; // Create flowing effect by animating scale - maintain consistent base scale var flowScale = 1.0 + 0.3 * Math.sin(arrow.animationPhase * 1.2); arrow.scaleX = flowScale; // Reset scale Y to ensure proper aspect ratio arrow.scaleY = 1.0; } break; } case 1: { self.removeArrows(); // Replace the cell with DebugWall image asset if (cellGraphics) { self.removeChild(cellGraphics); } cellGraphics = self.attachAsset('DebugWall', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.alpha = 0.9; // Make wall more visible break; } case 3: { self.removeArrows(); cellGraphics.tint = 0xff8400; // Blue sky color cellGraphics.alpha = 0.5; // Almost max transparent break; } } }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case '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 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 = crystal >= 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 (crystal >= upgradeCost) { setCrystal(crystal - 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 crystal 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) { 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); // Initialize flame sound timer self.flameSoundTimer = 0; } // Play flame sound repeatedly while burning if (!self.flameSoundTimer) { self.flameSoundTimer = 0; } self.flameSoundTimer++; // Play flame sound every 30 frames (0.5 seconds at 60 FPS) while actively burning if (self.flameSoundTimer % 30 === 0) { LK.getSound('Flamesound').play(); } // 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 = crystal >= 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 = crystal >= 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 : crystal >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' crystal', { 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 + ' crystal', { 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 + ' crystal'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' crystal'); 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); setCrystal(crystal + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " crystal!")); 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 = crystal >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' crystal'; 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 only if music is on if (musicOn) { 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; crystalIcon.visible = true; crystalText.visible = true; healthBarContainer.visible = true; scoreText.visible = true; waveIndicator.visible = true; nextWaveButtonContainer.visible = true; towerPreview.visible = false; // Stop intro music and play game music only if music is on LK.stopMusic(); if (musicOn) { 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 crystal = 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 crystalText = new Text2('Crystal: ' + crystal, { size: 60, fill: 0xFF69B4, weight: 800 }); crystalText.anchor.set(0.5, 0.5); var crystalIcon = LK.getAsset('Crystal', { anchorX: 0.5, anchorY: 0.5, width: 100, height: 100 }); 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 = 0xFF1493; var healthText = new Text2('100/100', { size: 40, fill: 0x800080, 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: 0x87CEEB, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(crystalIcon); LK.gui.top.addChild(crystalText); LK.gui.top.addChild(healthBarContainer); LK.gui.top.addChild(scoreText); // Hide UI elements during intro crystalIcon.visible = false; crystalText.visible = false; healthBarContainer.visible = false; scoreText.visible = false; healthBarContainer.x = 0; healthBarContainer.y = topMargin; // crystal text and icon are positioned in updateUI scoreText.x = spacing; scoreText.y = topMargin; // Add mute button to top right corner with music state tracking var musicOn = true; // Track current music state var muteButton = LK.getAsset('Mutebutton1', { anchorX: 1.0, anchorY: 0.0, width: 100, height: 100 }); muteButton.x = 2048 - 50; // 50px from right edge muteButton.y = 50; // 50px from top edge game.addChild(muteButton); // Add mute button click handler muteButton.down = function () { musicOn = !musicOn; // Toggle music state if (musicOn) { // Music is now on - show Mutebutton1 and resume music game.removeChild(muteButton); muteButton = LK.getAsset('Mutebutton1', { anchorX: 1.0, anchorY: 0.0, width: 100, height: 100 }); muteButton.x = 2048 - 50; muteButton.y = 50; game.addChild(muteButton); // Animate button appearance muteButton.alpha = 0; tween(muteButton, { alpha: 1 }, { duration: 200, easing: tween.easeOut }); // Resume appropriate music based on game state if (showingIntro) { LK.playMusic('Intromusic'); } else if (gameStarted) { // Resume game music based on current wave if (currentWave === 10) { LK.playMusic('Bossmusic1'); } else if (currentWave >= 11 && currentWave < 20) { LK.playMusic('Gamemusic2'); } else if (currentWave === 20) { LK.playMusic('Bossmusic2'); } else if (currentWave >= 21 && currentWave < 30) { LK.playMusic('gamemusic3'); } else if (currentWave === 30) { LK.playMusic('Bossmusic3'); } else if (currentWave >= 31 && currentWave < 40) { LK.playMusic('Gamemusic4'); } else if (currentWave === 40) { LK.playMusic('Bossmusic4'); } else if (currentWave >= 41 && currentWave < 50) { LK.playMusic('Gamemusic5'); } else if (currentWave === 50) { LK.playMusic('Bossmusic5'); } else { LK.playMusic('Gamemusic'); } } } else { // Music is now off - show Mutebutton2 and stop music game.removeChild(muteButton); muteButton = LK.getAsset('Mutebutton2', { anchorX: 1.0, anchorY: 0.0, width: 100, height: 100 }); muteButton.x = 2048 - 50; muteButton.y = 50; game.addChild(muteButton); // Animate button appearance muteButton.alpha = 0; tween(muteButton, { alpha: 1 }, { duration: 200, easing: tween.easeOut }); // Stop all music LK.stopMusic(); } // Re-assign the click handler to the new button muteButton.down = arguments.callee; }; function updateUI() { crystalText.setText('Crystal: ' + crystal); // Position crystal text centered at -spacing, and the icon to its left. crystalText.x = -spacing; crystalIcon.x = crystalText.x - crystalText.width / 2 - 10 - crystalIcon.width / 2; crystalIcon.y = topMargin; crystalText.y = topMargin; 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 setCrystal(value) { crystal = 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 (crystal >= towerCost) { var tower = new Tower(towerType || 'cannon'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setCrystal(crystal - towerCost); grid.pathFind(); grid.renderDebug(); return true; } else { var notification = game.addChild(new Notification("Not enough crystal!")); 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 (musicOn) { // Only play music if music is enabled if (currentWave === 10) { LK.playMusic('Bossmusic1'); } else if (currentWave === 11) { LK.playMusic('Gamemusic2'); } else if (currentWave === 20) { LK.playMusic('Bossmusic2'); } else if (currentWave === 21) { LK.playMusic('gamemusic3'); } else if (currentWave === 30) { LK.playMusic('Bossmusic3'); } else if (currentWave === 31) { LK.playMusic('Gamemusic4'); } else if (currentWave === 40) { LK.playMusic('Bossmusic4'); } else if (currentWave === 41) { LK.playMusic('Gamemusic5'); } else if (currentWave === 50) { LK.playMusic('Bossmusic5'); } } 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 crystal and score - increased rewards var crystalEarned = enemy.isBoss ? Math.floor(25 + (enemy.waveNumber - 1) * 1.5) : Math.floor(3 + (enemy.waveNumber - 1) * 0.4); var crystalIndicator = new CrystalIndicator(crystalEarned, enemy.x, enemy.y); game.addChild(crystalIndicator); setCrystal(crystal + crystalEarned); // 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! +" + crystalEarned + " crystal!")); 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(); } };
===================================================================
--- original.js
+++ change.js
@@ -3080,9 +3080,9 @@
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
-var crystalText = new Text2(String(crystal), {
+var crystalText = new Text2('Crystal: ' + crystal, {
size: 60,
fill: 0xFF69B4,
weight: 800
});
@@ -3226,10 +3226,10 @@
// Re-assign the click handler to the new button
muteButton.down = arguments.callee;
};
function updateUI() {
- crystalText.setText(String(crystal));
- // Position crystal text at -spacing and icon to its left
+ crystalText.setText('Crystal: ' + crystal);
+ // Position crystal text centered at -spacing, and the icon to its left.
crystalText.x = -spacing;
crystalIcon.x = crystalText.x - crystalText.width / 2 - 10 - crystalIcon.width / 2;
crystalIcon.y = topMargin;
crystalText.y = topMargin;
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