Code edit (2 edits merged)
Please save this source code
User prompt
Add damage sounds when enemies get hit by towers shots Bigrobots_damage_sound1 for Big_robot 1 2 3, Cyborg_damage_sound1 1 2 3, Cybersnake_damage_sound1 for Cybersnake 1 2 3, Eye_damage_sound1, Firo_damage_sound1, Electro_damage_sound1, Robot_damage_sound1.
User prompt
Add sounds useabilitysound1 for ability use button, Abilitybuttonsound1 for ability button, Startbuttonsound1 for start button in the intro.
User prompt
use closebuttonsound1 for all close buttons.
User prompt
prevent any clicks on intro buttons when in tutorial screen.
User prompt
prevent any clicks on intro buttons when in tutorial screen.
User prompt
Start the electronic text animation in the tablet when the girl animation is started.
User prompt
More to the bottom for the tutorial close button.
User prompt
position the tutorial close button on the middle max bottom of its background
Code edit (1 edits merged)
Please save this source code
User prompt
Increase spacing between tutorial types 3-4 and 6-7
User prompt
Do more spacing between type3 and type4, between type6 and type7.
User prompt
Make the description texts of types bigger
User prompt
Lower the tutorial types from "How to play" text by 50px.
User prompt
Reduce spacing between Type 1 and Type 2, between Type 5 and Type 6.
User prompt
Fix spacing between types of the tutorial
User prompt
Fix spacing between types of the tutorial
User prompt
Fix spacing between types of the tutorial
User prompt
move the button down a bit more from center
User prompt
move the button down a bit more from center
User prompt
Fix spacing between each type
User prompt
Reposition the main menu buttons to be below the center of the screen and add more spacing between them.
User prompt
Reposition the main menu buttons to be below the center of the screen and add more spacing between them.
User prompt
position the buttons bellow the center of the screen and with spacing between it
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var AbilitiesMenu = Container.expand(function () { var self = Container.call(this); var menuBackground = self.attachAsset('AbilitiesBackground', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.alpha = 0.95; var closeButton = self.attachAsset('Abilitiesclosebutton', { anchorX: 0.5, anchorY: 0.5 }); closeButton.x = 33; closeButton.y = menuBackground.height / 2 - 60; closeButton.down = function () { hideAbilities(); }; var gridContainer = new Container(); self.addChild(gridContainer); var gridCols = 3; var gridRows = 3; var cellWidth = 350; var cellHeight = 300; var padding = 50; var gridTotalWidth = gridCols * cellWidth + (gridCols - 1) * padding; var gridTotalHeight = gridRows * cellHeight + (gridRows - 1) * padding; // Center the grid horizontally and vertically within the background, then move 160px right and 50px down gridContainer.x = -gridTotalWidth / 2 + 210; // 150 + 50 + 10 = 210 gridContainer.y = -gridTotalHeight / 2; // -50 + 50 = 0 var title = new Text2("Abilities", { size: 80, fill: 0xff7b00, weight: 800 }); title.anchor.set(0.5, 0.5); title.x = 43; title.y = -menuBackground.height / 2 + 80; self.addChild(title); // Create description tooltip that will be shared by all cells var descriptionTooltip = null; for (var row = 0; row < gridRows; row++) { for (var col = 0; col < gridCols; col++) { (function (r, c) { var abilityIndex = r * gridCols + c; var abilityId = 'ability_' + abilityIndex; var abilityCost = (abilityIndex + 1) * 100; var abilityDef = abilityDefinitions[abilityId]; var cell = new Container(); cell.x = c * (cellWidth + padding); cell.y = r * (cellHeight + padding); gridContainer.addChild(cell); // Set initial animation state for entrance effect cell.alpha = 0; cell.scaleX = 0.3; cell.scaleY = 0.3; cell.y += 50; // Start slightly below final position // Calculate staggered delay based on position (diagonal wave effect) var animationDelay = (r + c) * 100; // 100ms delay per diagonal step // Animate entrance with staggered timing tween(cell, { alpha: 1, scaleX: 1, scaleY: 1, y: r * (cellHeight + padding) }, { duration: 400, delay: animationDelay, easing: tween.easeOut }); var abilityIcon = cell.attachAsset(abilityDef.icon, { anchorX: 0.5, anchorY: 0.5 }); abilityIcon.y = 50; abilityIcon.tint = abilityDef.color; // Add ability name var abilityName = new Text2(abilityDef.name, { size: 48, fill: 0x00ffb3, weight: 600 }); abilityName.anchor.set(0.5, 0.5); abilityName.y = 50; // Center in the middle of the ability icon cell.addChild(abilityName); // Position buttons below the icon var buttonY = 250; // --- Use Button --- var useButton = new Container(); useButton.y = buttonY; cell.addChild(useButton); var useBg = useButton.attachAsset('ScoreButton', { anchorX: 0.5, anchorY: 0.5, width: 200, height: 100 }); var useText = new Text2("USE\n" + abilityCost + " C", { size: 36, fill: 0xffffff, align: 'center', weight: 600 }); useText.anchor.set(0.5, 0.5); useButton.addChild(useText); // --- Button Handlers --- useButton.down = function () { var canUseNow = gameStarted && !showingIntro && canStartWaves; if (!canUseNow) { var notification = game.addChild(new Notification("Cannot use abilities until game starts!")); return; } if (crystals >= abilityCost) { setCrystals(crystals - abilityCost); abilityDef.effect(); hideAbilities(); } else { var notification = game.addChild(new Notification("Not enough crystals!")); } }; // --- Update visuals based on state --- cell.update = function () { var canUseNow = gameStarted && !showingIntro && canStartWaves; var canAfford = crystals >= abilityCost; if (canUseNow) { useBg.tint = canAfford ? 0x00ff00 : 0xff0000; // Green if affordable, red if not } else { useBg.tint = 0x808080; // Gray if not usable } abilityIcon.alpha = canAfford ? 1.0 : 0.6; }; cell.update(); // Initial call to set state // Hover functionality removed - descriptions only show on click cell.down = function () { // Show bigger description notification when clicking on cell var descriptionNotification = game.addChild(new Container()); var notificationBg = descriptionNotification.attachAsset('notification3', { anchorX: 0.5, anchorY: 0.5 }); notificationBg.tint = 0x000000; notificationBg.alpha = 0.9; var descText = new Text2(abilityDef.description, { size: 48, fill: 0xffffff, weight: 600, align: 'center' }); descText.anchor.set(0.5, 0.5); descriptionNotification.addChild(descText); descriptionNotification.x = 2048 / 2; descriptionNotification.y = 2732 / 2; descriptionNotification.alpha = 0; descriptionNotification.scaleX = 0.5; descriptionNotification.scaleY = 0.5; // Animate in tween(descriptionNotification, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeOut }); // Auto hide after 3 seconds LK.setTimeout(function () { tween(descriptionNotification, { alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { descriptionNotification.destroy(); } }); }, 3000); }; })(row, col); } } self.update = function () { for (var i = 0; i < gridContainer.children.length; i++) { var cell = gridContainer.children[i]; if (cell.update) { cell.update(); } } }; // Tooltip functionality removed return self; }); 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 { // Update enemy health bar using stored original width var healthPercentage = self.targetEnemy.health / self.targetEnemy.maxHealth; self.targetEnemy.healthBar.width = self.targetEnemy.healthBarOriginalWidth * healthPercentage; } // 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 { // Update enemy health bar using stored original width var healthPercentage = otherEnemy.health / otherEnemy.maxHealth; otherEnemy.healthBar.width = otherEnemy.healthBarOriginalWidth * healthPercentage; } } } } } 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 === 'ToxinBomb') { // Prevent toxin effect on immune enemies if (!self.targetEnemy.isImmune) { // Create ToxinBombEffect explosion effect using ToxinBombEffect asset var explosionEffect = new Container(); explosionEffect.x = self.targetEnemy.x; explosionEffect.y = self.targetEnemy.y; game.addChild(explosionEffect); // Create the main toxin explosion using ToxinBombEffect asset var toxinExplosion = explosionEffect.attachAsset('ToxinBombEffect', { anchorX: 0.5, anchorY: 0.5 }); toxinExplosion.width = toxinExplosion.height = 150; toxinExplosion.tint = 0x00FFb3; // 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('ToxinBombEffect', { anchorX: 0.5, anchorY: 0.5 }); particle.width = particle.height = 40 + Math.random() * 30; particle.tint = 0x00FFb3; // 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: 0xff0080, 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 = 0xff0080; 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 = 0xff0080; 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 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 = 0xff0080; // pink cellGraphics.alpha = 5; 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 = 0x00FFb3; 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('EnemyHealthBarOutline', { anchorX: 0.5, anchorY: 0.5 }); var healthBar = self.attachAsset('Enemyhealthbar', { anchorX: 0, anchorY: 0.5 }); healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBar.x = -healthBar.width / 2; self.healthBar = healthBar; self.healthBarOriginalWidth = healthBar.width; // Store original width self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } else { // Update health bar width based on current health var healthPercentage = self.health / self.maxHealth; self.healthBar.width = self.healthBarOriginalWidth * healthPercentage; } // 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; } // Update enemy health bar using stored original width var healthPercentage = self.health / self.maxHealth; self.healthBar.width = self.healthBarOriginalWidth * healthPercentage; } 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 } } } // Handle weakness effect if (self.weakened) { self.weakenDuration--; if (self.weakenDuration <= 0) { self.weakened = false; self.damageResistance = self.originalDamageResistance || 1; } } // Handle dust slow effect if (self.dustSlowed) { self.dustSlowDuration--; if (self.dustSlowDuration <= 0) { self.dustSlowed = false; self.speed = self.originalSpeed; } } // Handle drowning effect if (self.drowning) { // Apply drowning damage every frame self.health -= self.drownDamage; if (self.health <= 0) { self.health = 0; } else { var healthPercentage = self.health / self.maxHealth; self.healthBar.width = self.healthBarOriginalWidth * healthPercentage; } self.drownDuration--; if (self.drownDuration <= 0) { self.drowning = false; } } // Handle burning effect if (self.burning) { // Apply burn damage every frame self.health -= self.burnDamage; if (self.health <= 0) { self.health = 0; } else { var healthPercentage = self.health / self.maxHealth; self.healthBar.width = self.healthBarOriginalWidth * healthPercentage; } self.burnDuration--; if (self.burnDuration <= 0) { self.burning = false; } } // Handle freeze effect if (self.frozen) { self.freezeDuration--; if (self.freezeDuration <= 0) { self.frozen = false; self.speed = self.originalSpeed; // Add ice shattering effect (function (enemy) { var shatterContainer = new Container(); shatterContainer.x = enemy.x; shatterContainer.y = enemy.y; game.addChild(shatterContainer); // Add to game layer, not enemy for (var s = 0; s < 12; s++) { (function () { var shard = shatterContainer.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5, tint: 0xB0E0E6, // Powder blue width: 15 + Math.random() * 10, height: 5 + Math.random() * 5, rotation: Math.random() * Math.PI * 2, alpha: 1.0 }); var angle = Math.random() * Math.PI * 2; var distance = 40 + Math.random() * 30; tween(shard, { x: Math.cos(angle) * distance, y: Math.sin(angle) * distance, alpha: 0, rotation: shard.rotation + (Math.random() - 0.5) * Math.PI }, { duration: 300 + Math.random() * 200, easing: tween.easeOut, onFinish: function onFinish() { if (shard.parent) { shard.destroy(); } } }); })(); } LK.setTimeout(function () { if (shatterContainer.parent) { shatterContainer.destroy(); } }, 600); })(self); } } // Handle ultimate slow effect if (self.ultimateSlowed) { self.ultimateSlowDuration--; if (self.ultimateSlowDuration <= 0) { self.ultimateSlowed = false; self.speed = self.originalSpeed; } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.frozen) { enemyGraphics.tint = 0x00FFFF; // Cyan for frozen } else if (self.drowning) { enemyGraphics.tint = 0x0080FF; // Blue for drowning } else if (self.burning) { // Tint is handled by the looping animation in the ability effect } else if (self.dustSlowed) { enemyGraphics.tint = 0x8B4513; // Brown for dust storm } else if (self.weakened) { enemyGraphics.tint = 0xFF4500; // Orange-red for weakened } else if (self.poisoned && (self.slowed || self.gumStuck)) { // Combine poison (0x00FFb3) 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 = 0x00FFb3; } else if (self.slowed || self.gumStuck) { enemyGraphics.tint = 0xff0080; } 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 = 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('Notification1', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0xffffff; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xff0080, 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 = 0x00ffb3; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0xffffff; 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!")); } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('Notification1', { anchorX: 0.5, anchorY: 0.5 }); var isSmallNotification = message === "Cannot build here!"; // Set fixed size to 1700x600px notificationGraphics.width = 1700; notificationGraphics.height = 600; if (isSmallNotification) { notificationGraphics.width = 1100; notificationGraphics.height = 300; } // Parse message to extract wave info for two-line formatting var displayMessage = message; var isWaveMessage = false; var waveInfo = ""; var waveDetails = ""; // Check if this is a wave message if (message.includes("Wave ") && (message.includes("incoming!") || message.includes("activated!"))) { isWaveMessage = true; // Extract wave number and type var waveMatch = message.match(/Wave (\d+) \(([^)]+)\)/); if (waveMatch) { var waveNumber = waveMatch[1]; var waveTypeInfo = waveMatch[2]; waveInfo = "Wave " + waveNumber; waveDetails = waveTypeInfo; displayMessage = waveInfo + "\n" + waveDetails; } } var notificationText = new Text2("", { size: isWaveMessage ? 90 : 90, fill: 0xff0080, weight: 800, align: 'center' }); notificationText.anchor.set(0.5, 0.5); self.addChild(notificationText); // Position notification in center of screen self.x = 2048 / 2; self.y = 2732 / 2; self.alpha = 0; self.scaleX = 0.3; self.scaleY = 0.3; // Electronic text animation variables var currentCharIndex = 0; var displayText = ""; var electronicTimer = 0; var charDelay = 4; // frames between characters for electronic typing effect var isAnimatingText = true; // Add electronic glitch effect var glitchTimer = 0; var originalMessage = displayMessage; // Entrance animation - scale up and fade in tween(self, { alpha: 1, scaleX: 1.1, scaleY: 1.1 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { // Settle to normal size tween(self, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); var fadeOutTime = 240; self.update = function () { // Electronic text animation if (isAnimatingText && electronicTimer % charDelay === 0) { if (currentCharIndex < originalMessage.length) { displayText += originalMessage[currentCharIndex]; notificationText.setText(displayText); currentCharIndex++; } else { isAnimatingText = false; } } electronicTimer++; // Add subtle glitch effect after text is complete if (!isAnimatingText) { glitchTimer++; if (glitchTimer % 60 === 0) { // Glitch every second // Briefly change one random character if (Math.random() < 0.1) { var glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?`~"; var randomChar = glitchChars[Math.floor(Math.random() * glitchChars.length)]; var glitchIndex = Math.floor(Math.random() * displayText.length); var glitchedText = displayText.substring(0, glitchIndex) + randomChar + displayText.substring(glitchIndex + 1); notificationText.setText(glitchedText); // Restore original text after 3 frames LK.setTimeout(function () { notificationText.setText(displayText); }, 50); } } } if (fadeOutTime > 0) { fadeOutTime--; if (fadeOutTime <= 60) { // Exit animation - scale down and fade out if (!self.exitAnimStarted) { self.exitAnimStarted = true; tween(self, { alpha: 0, scaleX: 0.8, scaleY: 0.8 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } } } else if (!self.exitAnimStarted) { self.destroy(); } }; return self; }); var Ship = Container.expand(function (type) { var self = Container.call(this); self.type = type || 1; // Ship type 1, 2, or 3 self.speed = 2 + Math.random() * 2; // Random speed between 2-4 self.direction = Math.random() < 0.5 ? 1 : -1; // 1 for left to right, -1 for right to left // Select ship asset based on type var shipAsset = 'Ship' + self.type; var shipGraphics = self.attachAsset(shipAsset, { anchorX: 0.5, anchorY: 0.5 }); // Play ship sound based on ship type var shipSound = 'Ship' + self.type + 'sound'; game.playSoundWithVolume(shipSound); // Flip ship if moving right to left if (self.direction === -1) { shipGraphics.scaleX = -1; } // Calculate game field boundaries var gridLeft = 150; // grid.x value var gridWidth = 24 * 76; // 24 cells * CELL_SIZE var gridRight = gridLeft + gridWidth; // 150 + 1824 = 1974 // Calculate vertical boundaries based on grid position var gridTop = 200 - 76 * 4; // grid.y value (200 - CELL_SIZE * 4) var gridHeight = (29 + 6) * 76; // (29 + 6) cells * CELL_SIZE var gridBottom = gridTop + gridHeight; // Random Y position within the game field boundaries self.y = gridTop + Math.random() * gridHeight; // Set starting X position based on direction, spawning from field edges if (self.direction === 1) { // Moving left to right, start from left edge of field self.x = gridLeft - shipGraphics.width / 2; } else { // Moving right to left, start from right edge of field self.x = gridRight + shipGraphics.width / 2; } self.update = function () { self.x += self.speed * self.direction; // Check if ship has moved off the opposite field edge if (self.direction === 1 && self.x > gridRight + shipGraphics.width) { self.destroy(); } else if (self.direction === -1 && self.x < gridLeft - shipGraphics.width) { self.destroy(); } // Initialize sound timer if not set if (self.soundTimer === undefined) { self.soundTimer = 0; } // Increment sound timer self.soundTimer++; // Play ship sound every 120 frames (2 seconds at 60 FPS) if (self.soundTimer % 120 === 0) { var shipSound = 'Ship' + self.type + 'sound'; game.playSoundWithVolume(shipSound); } }; 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 'ToxinBomb': towerAssetId = 'ToxinBombTower'; 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 === 'ToxinBomb' ? '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 = 0xff0080; break; case 'rifle': towerColor = 0xff0080; break; case 'flame': towerColor = 0xff0080; break; case 'Rocket': towerColor = 0xff0080; break; case 'gumbomb': towerColor = 0xff0080; break; case 'ToxinBomb': towerColor = 0xff0080; 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: 0x00ffb3, 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 = crystals >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var SpeakingGirl = Container.expand(function () { var self = Container.call(this); var girlAsset1 = self.attachAsset('Girl1', { anchorX: 0.5, anchorY: 0.5, width: 500, height: 500 }); var girlAsset2 = self.attachAsset('Girl2', { anchorX: 0.5, anchorY: 0.5, width: 500, height: 500 }); var girlAsset3 = self.attachAsset('Girl3', { anchorX: 0.5, anchorY: 0.5, width: 500, height: 500 }); var allGirlAssets = [girlAsset1, girlAsset2, girlAsset3]; // Girl1 is index 0, Girl2 is 1, Girl3 is 2 var animationSequence = [0, 1, 0, 2]; var currentFrameIndexInSequence = 0; var animationInterval = null; var hasStartedAnimation = false; function setFrame(assetIndex) { for (var i = 0; i < allGirlAssets.length; i++) { allGirlAssets[i].visible = i === assetIndex; } } function animate() { currentFrameIndexInSequence = (currentFrameIndexInSequence + 1) % animationSequence.length; var assetIndexToShow = animationSequence[currentFrameIndexInSequence]; setFrame(assetIndexToShow); } function startSpeakingAnimation() { if (!hasStartedAnimation) { hasStartedAnimation = true; animationInterval = LK.setInterval(animate, 300); } } // Start at right edge of screen, move to left side self.x = 2048 - 250; // Start from right edge (250 = half of 500px width) self.y = 2732 - 250; // 250 = half of 500px height, so bottom edge inside setFrame(0); // Start with Girl1 // Move from right to left side tween(self, { x: 0 + 250 // Move to left edge inside screen }, { duration: 2000, // 2 second movement easing: tween.easeOut, onFinish: function onFinish() { // Start animation when reaching left side startSpeakingAnimation(); } }); var originalDestroy = self.destroy; self.destroy = function () { if (animationInterval) { LK.clearInterval(animationInterval); } tween.stop(self); originalDestroy.call(self); }; return self; }); var Tablet = Container.expand(function () { var self = Container.call(this); var tabletGraphics = self.attachAsset('Tablet', { anchorX: 0.5, anchorY: 0.5 }); // Position tablet in center of screen self.x = 2048 / 2; self.y = 2732 / 2; // Message to display with electronic character animation - formatted with more lines var message = "Hi! I'm the high commander:\n\nWe shall begin a war with\nthe enemies, nothing will\nstand in our ways,\n\nwe will destroy them and\nwe prove our supremacy,\n\nwe will not rest until\nvictory is ours!"; // Create text with 100px spacing from sides var messageText = new Text2("", { size: 80, fill: 0xff0080, weight: 800 }); messageText.anchor.set(0.5, 0.5); self.addChild(messageText); // Animation variables var currentChar = 0; var displayText = ""; var animationTimer = 0; var charDelay = 3; // Frames between characters (electronic typing effect) var isAnimating = true; self.update = function () { if (isAnimating && animationTimer % charDelay === 0) { if (currentChar < message.length) { displayText += message[currentChar]; messageText.setText(displayText); currentChar++; } else { isAnimating = false; // Auto-destroy after message is complete and a short delay tween(self, { alpha: 0 }, { duration: 1000, delay: 3000, // Wait 3 seconds after text is complete easing: tween.easeOut, onFinish: function onFinish() { // Remove girl character when tablet is destroyed if (girlCharacter && girlCharacter.parent) { girlCharacter.destroy(); girlCharacter = null; } self.destroy(); // Show game started notification var notification = game.addChild(new Notification("Game started!\nPrepare for battle!")); var originalDestroy = notification.destroy; notification.destroy = function () { canStartWaves = true; canPlaceTowers = true; originalDestroy.call(this); }; } }); } } animationTimer++; }; 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 'ToxinBomb': // ToxinBomb: 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 'ToxinBomb': 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 'ToxinBomb': towerAssetId = 'ToxinBombTower'; 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 'ToxinBomb': 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 = 0xff0080; } 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 'ToxinBomb': 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 (crystals >= upgradeCost) { setCrystals(crystals - 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 crystals 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 () { // Handle tower boost effect if (self.boosted) { self.boostDuration--; if (self.boostDuration <= 0) { self.boosted = false; self.damage = self.originalDamage; } } 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 { // Update enemy health bar using stored original width var healthPercentage = self.targetEnemy.health / self.targetEnemy.maxHealth; self.targetEnemy.healthBar.width = self.targetEnemy.healthBarOriginalWidth * healthPercentage; } // --- 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) { game.playSoundWithVolume('Flamesound'); } // 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; // Center the menu on screen using asset height tween(upgradeMenu, { y: 2732 / 2 }, { 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 = 0xff0000; 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 = 0xffd900; 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 = 0xff0080; gumbombGraphics.width = 35; gumbombGraphics.height = 35; break; case 'ToxinBomb': // Replace bullet asset with ToxinBomb asset bullet.removeChild(bullet.children[0]); var toxinGraphics = bullet.attachAsset('ToxinBomb', { anchorX: 0.5, anchorY: 0.5 }); toxinGraphics.tint = 0x01df01; 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': game.playSoundWithVolume('Bulletsound'); break; case 'rifle': game.playSoundWithVolume('Riflebulletsound'); break; case 'Rocket': game.playSoundWithVolume('Rocketsound'); break; case 'gumbomb': game.playSoundWithVolume('Gumbombsound'); break; case 'ToxinBomb': game.playSoundWithVolume('Toxinbombsound'); 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') { game.playSoundWithVolume('placmentsound1'); } else if (self.id === 'rifle') { game.playSoundWithVolume('placmentsound2'); } else if (self.id === 'flame') { game.playSoundWithVolume('placmentsound3'); } else if (self.id === 'Rocket') { game.playSoundWithVolume('placmentsound4'); } else if (self.id === 'gumbomb') { game.playSoundWithVolume('placmentsound5'); } else if (self.id === 'ToxinBomb') { game.playSoundWithVolume('placmentsound6'); } }; 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 = crystals >= 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 'ToxinBomb': previewGraphics.tint = 0xFFFFFF; break; case 'cannon': previewGraphics.tint = 0xFFFFFF; break; default: previewGraphics.tint = 0xFFFFFF; } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xffffff; } }; 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 = crystals >= 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; var menuBackground = self.attachAsset('Notification2', { anchorX: 0.5, anchorY: 0.5 }); // Use asset dimensions instead of hardcoded values menuBackground.tint = 0xffffff; menuBackground.alpha = 0.9; // Set starting position offscreen using actual asset height self.y = 2732 + menuBackground.height / 2; // Define all elements var displayName = self.tower.id === 'rifle' ? 'Rifle' : self.tower.id === 'ToxinBomb' ? 'Toxinbomb' : self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1); var fullTowerName = displayName + ' Tower'; var towerTypeText = new Text2('', { size: 80, fill: 0xff0080, weight: 800 }); var currentNameText = ''; var fullStatsText = 'Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'; var statsText = new Text2('', { size: 70, fill: 0xff0080, weight: 400 }); var currentStatsText = ''; // Animation variables var nameAnimation = { text: fullTowerName, index: 0, timer: 0 }; var statsAnimation = { text: fullStatsText, index: 0, timer: 0 }; var animationPhase = 'name'; // name, stats, hold var holdTimer = 0; var holdDuration = 120; // 2 seconds var animationDelay = 2; // frames var upgradeButton = new Container(); var buttonBackground = upgradeButton.attachAsset('Upgradebutton', { anchorX: 0.5, anchorY: 0.5 }); 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 : crystals >= upgradeCost ? 0x00ff00 : 0xff0000; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' crystal', { size: 60, fill: 0xff0080, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); var sellButtonBackground = sellButton.attachAsset('Sellbutton', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.tint = 0xffffff; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' crystal', { size: 60, fill: 0xff0080, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); var closeButton = new Container(); var closeBackground = closeButton.attachAsset('Notification1', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0x00ffb3; var closeText = new Text2('X', { size: 68, fill: 0xff0080, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); // Add all elements to self self.addChild(statsText); self.addChild(towerTypeText); self.addChild(upgradeButton); self.addChild(sellButton); self.addChild(closeButton); // Position all elements // Tower info towerTypeText.anchor.set(0.5, 0.5); towerTypeText.x = 0; towerTypeText.y = -175; statsText.anchor.set(0.5, 0.5); statsText.x = 0; statsText.y = 10; // Buttons var buttonYPos = menuBackground.height / 2 - 100 - buttonBackground.height / 2; upgradeButton.y = buttonYPos; sellButton.y = buttonYPos; var buttonSpacing = 20; sellButton.x = -sellButtonBackground.width / 2 - buttonSpacing; upgradeButton.x = buttonBackground.width / 2 + buttonSpacing; // Close button closeButton.x = menuBackground.width / 2 - 100; closeButton.y = -menuBackground.height / 2 + 250; 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)); } // Update stats text and restart animation fullStatsText = 'Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'; statsAnimation.text = fullStatsText; statsAnimation.index = 0; statsAnimation.timer = 0; statsText.setText(''); currentStatsText = ''; animationPhase = 'name'; nameAnimation.index = 0; nameAnimation.timer = 0; towerTypeText.setText(''); currentNameText = ''; 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); setCrystals(crystals + 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 () { // Text animation if (animationPhase === 'name') { nameAnimation.timer++; if (nameAnimation.timer % animationDelay === 0 && nameAnimation.index < nameAnimation.text.length) { currentNameText += nameAnimation.text[nameAnimation.index]; towerTypeText.setText(currentNameText); nameAnimation.index++; } if (nameAnimation.index >= nameAnimation.text.length) { animationPhase = 'stats'; } } else if (animationPhase === 'stats') { statsAnimation.timer++; if (statsAnimation.timer % animationDelay === 0 && statsAnimation.index < statsAnimation.text.length) { currentStatsText += statsAnimation.text[statsAnimation.index]; statsText.setText(currentStatsText); statsAnimation.index++; } if (statsAnimation.index >= statsAnimation.text.length) { animationPhase = 'hold'; holdTimer = 0; } } else if (animationPhase === 'hold') { holdTimer++; if (holdTimer >= holdDuration) { animationPhase = 'name'; nameAnimation.index = 0; nameAnimation.timer = 0; towerTypeText.setText(''); currentNameText = ''; statsAnimation.index = 0; statsAnimation.timer = 0; statsText.setText(''); currentStatsText = ''; } } if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0xffffff; } 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 = crystals >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00ff00 : 0xff0000; 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('Notification1', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0xffffff; // 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: 0xff0080, 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 && canStartWaves) { 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!\nWave 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('Notification1', { 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 = 0xffffff; waveType = "Easy"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0xffffff; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xffffff; waveType = "Unstoppable"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xffffff; waveType = "Air"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xffffff; 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 = 0xffffff; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xffffff; waveType = "Boss Easy"; break; case 'fast': block.tint = 0xffffff; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xffffff; waveType = "Boss Unstoppable"; break; case 'flying': block.tint = 0xffffff; 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 = 0xffffff; break; case 'fast': block.tint = 0xffffff; break; case 'immune': block.tint = 0xffffff; break; case 'flying': block.tint = 0xffffff; break; default: block.tint = 0xffffff; break; } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0xffffff; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xffffff; waveType = "Unstoppable"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xffffff; waveType = "Air"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xffffff; waveType = "Group"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = 0xffffff; 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; self.waveDisplayNames = self.waveDisplayNames || []; self.waveDisplayNames[i] = waveType; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2("Wave " + (i + 1), { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 0 + 2; waveTypeShadow.y = -20 + 2; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2("Wave " + (i + 1), { size: 56, fill: 0xff0080, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.x = 0; waveTypeText.y = -20; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2(waveType, { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(0.5, 0.5); waveNumShadow.x = 0 + 2; waveNumShadow.y = 20 + 2; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2(waveType, { size: 48, fill: 0xff0080, weight: 800 }); waveNum.anchor.set(0.5, 0.5); waveNum.x = 0; waveNum.y = 20; 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) { if (waveNumber < 1 || waveNumber > totalWaves) { return "Normal"; } // Return the stored display name return self.waveDisplayNames[waveNumber - 1] || "Normal"; }; 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 = 0x00ffb3; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0x00ffb3; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0x00ffb3; 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 = 0x00ffb3; 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; }); var WeatherSystem = Container.expand(function () { var self = Container.call(this); self.currentWeather = null; self.weatherParticles = []; self.weatherOverlay = null; self.lastWeatherType = null; // Weather types: sunny, rainy, snowy, foggy, stormy self.getWeatherType = function (waveNumber) { if (waveNumber < 1) return 'sunny'; var weatherCycle = Math.floor((waveNumber - 1) / 10) % 5; var weatherTypes = ['sunny', 'rainy', 'snowy', 'foggy', 'stormy']; return weatherTypes[weatherCycle]; }; self.update = function () { var weatherType = self.getWeatherType(currentWave); // Change weather if needed if (weatherType !== self.currentWeather) { self.clearWeather(); self.currentWeather = weatherType; self.applyWeather(weatherType); } // Update particles for (var i = self.weatherParticles.length - 1; i >= 0; i--) { var particle = self.weatherParticles[i]; if (particle && particle.update) { particle.update(); } if (!particle || !particle.parent || particle.shouldRemove) { if (particle && particle.parent) { particle.destroy(); } self.weatherParticles.splice(i, 1); } } // Spawn new particles based on weather if (self.currentWeather === 'rainy') { if (Math.random() < 0.3) { // 30% chance per frame self.spawnRainDrop(); } } else if (self.currentWeather === 'snowy') { if (Math.random() < 0.2) { // 20% chance per frame self.spawnSnowflake(); } } else if (self.currentWeather === 'stormy') { if (Math.random() < 0.35) { // 35% chance per frame self.spawnStormParticle(); } if (Math.random() < 0.01) { // 1% chance for lightning self.createLightning(); } } }; self.clearWeather = function () { // Clear all particles for (var i = 0; i < self.weatherParticles.length; i++) { if (self.weatherParticles[i] && self.weatherParticles[i].parent) { self.weatherParticles[i].destroy(); } } self.weatherParticles = []; // Remove overlay if (self.weatherOverlay) { tween(self.weatherOverlay, { alpha: 0 }, { duration: 1000, onFinish: function onFinish() { if (self.weatherOverlay && self.weatherOverlay.parent) { self.weatherOverlay.destroy(); } self.weatherOverlay = null; } }); } }; self.applyWeather = function (weatherType) { switch (weatherType) { case 'sunny': self.applySunnyWeather(); break; case 'rainy': self.applyRainyWeather(); break; case 'snowy': self.applySnowyWeather(); break; case 'foggy': self.applyFoggyWeather(); break; case 'stormy': self.applyStormyWeather(); break; } }; self.applySunnyWeather = function () { // Create sun rays effect for (var i = 0; i < 8; i++) { (function () { var sunRay = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0, width: 150, height: 2048, tint: 0xFFFF88, alpha: 0 }); sunRay.x = 300 + i * 200; sunRay.y = -500; sunRay.rotation = 0.3 + i * 0.1; sunRay.blendMode = 1; tween(sunRay, { alpha: 0.15 }, { duration: 2000, easing: tween.easeOut }); // Gentle animation var _animateSunRay = function animateSunRay() { tween(sunRay, { alpha: 0.1 + Math.random() * 0.1, rotation: sunRay.rotation + (Math.random() - 0.5) * 0.1 }, { duration: 3000 + Math.random() * 2000, easing: tween.easeInOut, onFinish: _animateSunRay }); }; _animateSunRay(); self.weatherParticles.push(sunRay); })(); } // Add warm overlay self.weatherOverlay = self.attachAsset('cell', { width: 2048, height: 2732, tint: 0xFFFF00, alpha: 0 }); tween(self.weatherOverlay, { alpha: 0.05 }, { duration: 2000 }); }; self.applyRainyWeather = function () { // Add dark overlay self.weatherOverlay = self.attachAsset('cell', { width: 2048, height: 2732, tint: 0x4444AA, alpha: 0 }); tween(self.weatherOverlay, { alpha: 0.2 }, { duration: 2000 }); }; self.spawnRainDrop = function () { var rainDrop = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5, width: 3, height: 20, tint: 0x6699FF, alpha: 0.6, x: Math.random() * 2048, y: -50 }); rainDrop.velocity = 15 + Math.random() * 10; rainDrop.update = function () { rainDrop.y += rainDrop.velocity; if (rainDrop.y > 2782) { rainDrop.shouldRemove = true; // Create splash effect var splash = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, width: 20, height: 10, tint: 0x6699FF, alpha: 0.5, x: rainDrop.x, y: 2732 }); tween(splash, { width: 40, height: 20, alpha: 0 }, { duration: 300, onFinish: function onFinish() { if (splash.parent) splash.destroy(); } }); } }; self.weatherParticles.push(rainDrop); }; self.applySnowyWeather = function () { // Add light blue overlay self.weatherOverlay = self.attachAsset('cell', { width: 2048, height: 2732, tint: 0xCCDDFF, alpha: 0 }); tween(self.weatherOverlay, { alpha: 0.15 }, { duration: 2000 }); }; self.spawnSnowflake = function () { var snowflake = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, width: 8 + Math.random() * 8, height: 8 + Math.random() * 8, tint: 0xFFFFFF, alpha: 0.8, x: Math.random() * 2048, y: -50 }); snowflake.velocity = 2 + Math.random() * 2; snowflake.swaySpeed = 0.02 + Math.random() * 0.02; snowflake.swayAmount = 30 + Math.random() * 20; snowflake.baseX = snowflake.x; snowflake.time = Math.random() * Math.PI * 2; snowflake.update = function () { snowflake.y += snowflake.velocity; snowflake.time += snowflake.swaySpeed; snowflake.x = snowflake.baseX + Math.sin(snowflake.time) * snowflake.swayAmount; snowflake.rotation += 0.02; if (snowflake.y > 2782) { snowflake.shouldRemove = true; } }; self.weatherParticles.push(snowflake); }; self.applyFoggyWeather = function () { // Create multiple fog layers for (var i = 0; i < 5; i++) { (function (index) { var fog = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, width: 800, height: 600, tint: 0xCCCCCC, alpha: 0, x: Math.random() * 2048, y: Math.random() * 2732 }); fog.blendMode = 1; tween(fog, { alpha: 0.3 }, { duration: 3000, easing: tween.easeOut }); // Animate fog movement var _animateFog = function animateFog() { var targetX = Math.random() * 2048; var targetY = Math.random() * 2732; tween(fog, { x: targetX, y: targetY, scaleX: 0.8 + Math.random() * 0.4, scaleY: 0.8 + Math.random() * 0.4 }, { duration: 20000 + Math.random() * 10000, easing: tween.easeInOut, onFinish: _animateFog }); }; _animateFog(); self.weatherParticles.push(fog); })(); } }; self.applyStormyWeather = function () { // Add dark storm overlay self.weatherOverlay = self.attachAsset('cell', { width: 2048, height: 2732, tint: 0x333366, alpha: 0 }); tween(self.weatherOverlay, { alpha: 0.3 }, { duration: 2000 }); }; self.spawnStormParticle = function () { // Mix of heavy rain and wind particles var particle = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5, width: 4, height: 25, tint: 0x8899CC, alpha: 0.7, x: Math.random() * 2248 - 100, // Can start off-screen y: -50, rotation: -0.3 // Angled rain }); particle.velocityX = 5 + Math.random() * 3; particle.velocityY = 20 + Math.random() * 10; particle.update = function () { particle.x += particle.velocityX; particle.y += particle.velocityY; if (particle.y > 2782 || particle.x > 2148) { particle.shouldRemove = true; } }; self.weatherParticles.push(particle); }; self.createLightning = function () { // Flash effect var flash = self.attachAsset('cell', { width: 2048, height: 2732, tint: 0xFFFFFF, alpha: 0 }); tween(flash, { alpha: 0.6 }, { duration: 50, onFinish: function onFinish() { tween(flash, { alpha: 0 }, { duration: 100, onFinish: function onFinish() { if (flash.parent) flash.destroy(); } }); } }); // Lightning bolt var bolt = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0, width: 10, height: 2732, tint: 0xCCCCFF, alpha: 0, x: 200 + Math.random() * 1648, y: 0 }); bolt.blendMode = 1; // Create jagged lightning effect var segments = 8; for (var i = 0; i < segments; i++) { (function (index) { var segment = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5, width: 5 + Math.random() * 15, height: 2732 / segments, tint: 0xFFFFFF, alpha: 0, x: bolt.x + (Math.random() - 0.5) * 100, y: index * (2732 / segments) }); segment.blendMode = 1; tween(segment, { alpha: 0.9 }, { duration: 50, delay: index * 10, onFinish: function onFinish() { tween(segment, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { if (segment.parent) segment.destroy(); } }); } }); })(); } tween(bolt, { alpha: 0.8 }, { duration: 50, onFinish: function onFinish() { tween(bolt, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { if (bolt.parent) bolt.destroy(); } }); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x00ffdd }); /**** * Game Code ****/ // Initialize sound system FIRST before any other game code game.soundVolume = storage.soundVolume !== undefined ? storage.soundVolume : 1.0; // Load from storage or default to full volume // Create the sound playing helper function game.playSoundWithVolume = function (soundName) { var sound = LK.getSound(soundName); if (sound) { // Get the original volume from the asset definition var originalVolume = 1.0; // Default volume switch (soundName) { case 'Bulletsound': originalVolume = 0.3; break; case 'Flamesound': originalVolume = 0.5; break; case 'Gumbombsound': originalVolume = 0.4; break; case 'Riflebulletsound': originalVolume = 0.3; break; case 'Rocketsound': originalVolume = 0.3; break; case 'Ship1sound': originalVolume = 1.0; break; case 'Ship2sound': originalVolume = 1.0; break; case 'Ship3sound': originalVolume = 0.2; break; case 'Toxinbombsound': originalVolume = 1.0; break; case 'placmentsound1': case 'placmentsound2': case 'placmentsound3': case 'placmentsound4': case 'placmentsound5': case 'placmentsound6': originalVolume = 1.0; break; } // Apply both the original volume and the user's sound volume setting var finalVolume = originalVolume * game.soundVolume; // Play the sound with the calculated volume sound.play({ volume: finalVolume }); } }; // Game state variables // Define all available abilities with their effects var abilityDefinitions = { 'ability_0': { name: 'Healing Wave', description: 'Heal 25% of max health', icon: 'Ability1', color: 0x00FF00, effect: function effect() { lives = Math.min(100, lives + 25); updateUI(); var notification = game.addChild(new Notification("Healing Wave activated! +25 health")); // Add a green screen flash for healing var flash = LK.getAsset('cell', { width: 2048, height: 2732, x: 0, y: 0, alpha: 0, tint: 0x00FF00 }); game.addChild(flash); tween(flash, { alpha: 0.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(flash, { alpha: 0 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { if (flash.parent) { flash.destroy(); } } }); } }); // Add rising green healing particles across the screen for (var i = 0; i < 30; i++) { (function () { var particle = game.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, tint: 0x32CD32, // LimeGreen width: 15 + Math.random() * 20, height: 15 + Math.random() * 20, alpha: 0.8, blendMode: 1, x: Math.random() * 2048, y: 2732 + Math.random() * 200 }); tween(particle, { y: particle.y - 800 - Math.random() * 500, alpha: 0 }, { duration: 1500 + Math.random() * 1000, easing: tween.easeOut, onFinish: function onFinish() { if (particle.parent) { particle.destroy(); } } }); })(); } } }, 'ability_1': { name: 'Weakness Curse', description: 'Enemies take 50% more damage for 10 seconds', icon: 'Ability2', color: 0xFF4500, effect: function effect() { for (var i = 0; i < enemies.length; i++) { if (enemies[i] && !enemies[i].weakened) { enemies[i].weakened = true; enemies[i].weakenDuration = 600; // 10 seconds at 60 FPS enemies[i].originalDamageResistance = enemies[i].damageResistance || 1; enemies[i].damageResistance = enemies[i].originalDamageResistance * 1.5; // Take 50% more damage // Add weakness animation on each enemy (function (currentEnemy) { var originalScaleX = currentEnemy.scaleX || 1; var originalScaleY = currentEnemy.scaleY || 1; tween(currentEnemy, { tint: 0xFF0000, scaleX: originalScaleX * 1.3, scaleY: originalScaleY * 1.3 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { if (currentEnemy && currentEnemy.parent) { tween(currentEnemy, { tint: 0xFF4500, scaleX: originalScaleX, scaleY: originalScaleY }, { duration: 500, easing: tween.easeOut }); } } }); // Add dripping curse particles var curseParticles = new Container(); currentEnemy.addChild(curseParticles); for (var p = 0; p < 10; p++) { (function () { var particle = curseParticles.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, tint: 0x8A2BE2, // BlueViolet for curse width: 8 + Math.random() * 8, height: 8 + Math.random() * 8, alpha: 0.9, x: (Math.random() - 0.5) * currentEnemy.children[0].width, y: -currentEnemy.children[0].height / 2 + (Math.random() - 0.5) * 20 }); var duration = 800 + Math.random() * 500; var delay = Math.random() * 9500; // Drip throughout the 10s duration tween(particle, { y: particle.y + 60, alpha: 0 }, { duration: duration, delay: delay, onFinish: function onFinish() { if (particle.parent) { particle.destroy(); } } }); })(); } LK.setTimeout(function () { if (curseParticles.parent) { curseParticles.destroy(); } }, 10000); })(enemies[i]); } } var notification = game.addChild(new Notification("Weakness Curse activated! Enemies weakened!")); } }, 'ability_2': { name: 'Lightning Storm', description: 'Deals 15% damage to all enemies based on their difficulty', icon: 'Ability3', color: 0xFFFF00, effect: function effect() { for (var i = enemies.length - 1; i >= 0; i--) { if (!enemies[i]) { continue; } // Safety check var damage = enemies[i].maxHealth * 0.15; if (enemies[i].type === 'easy') { damage *= 1.5; } else if (enemies[i].type === 'medium') { damage *= 1.0; } else if (enemies[i].type === 'hard') { damage *= 0.7; } enemies[i].health -= damage; if (enemies[i].health <= 0) { enemies[i].health = 0; } else { var healthPercentage = enemies[i].health / enemies[i].maxHealth; enemies[i].healthBar.width = enemies[i].healthBarOriginalWidth * healthPercentage; } // Use an IIFE to create a new scope for each enemy, fixing closure bugs (function (currentEnemy) { if (currentEnemy && currentEnemy.parent && currentEnemy.children[0]) { // Add electricity particle effects var lightningEffectContainer = new Container(); currentEnemy.addChild(lightningEffectContainer); for (var k = 0; k < 7; k++) { // Create 7 lightning bolts (function () { // IIFE for each bolt's closure var bolt = lightningEffectContainer.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5, tint: Math.random() < 0.5 ? 0xADD8E6 : 0xFFFFFF, // Light Blue or White width: 2 + Math.random() * 4, height: 30 + Math.random() * 50, alpha: 0 }); bolt.x = (Math.random() - 0.5) * 70; bolt.y = (Math.random() - 0.5) * 70; bolt.rotation = (Math.random() - 0.5) * Math.PI; tween(bolt, { alpha: 0.9 }, { duration: 100, delay: Math.random() * 100, onFinish: function onFinish() { tween(bolt, { alpha: 0 }, { duration: 150, onFinish: function onFinish() { if (bolt.parent) { bolt.destroy(); } } }); } }); })(); } LK.setTimeout(function () { if (lightningEffectContainer.parent) { lightningEffectContainer.destroy(); } }, 600); // Add lightning strike tint animation on each enemy var enemyGraphics = currentEnemy.children[0]; var originalTint = enemyGraphics.tint; tween(enemyGraphics, { tint: 0x87CEEB // Sky Blue for lightning strike }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { if (enemyGraphics && enemyGraphics.parent) { tween(enemyGraphics, { tint: 0xFFFFFF // White flash }, { duration: 50, easing: tween.easeIn, onFinish: function onFinish() { if (enemyGraphics && enemyGraphics.parent) { tween(enemyGraphics, { tint: 0x87CEEB // Sky Blue for lightning strike }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { if (enemyGraphics && enemyGraphics.parent) { tween(enemyGraphics, { tint: originalTint }, { duration: 100, easing: tween.easeIn }); } } }); } } }); } } }); } })(enemies[i]); } var notification = game.addChild(new Notification("Lightning Storm! All enemies shocked!")); } }, 'ability_3': { name: 'Dust Storm', description: 'Slows all enemies by 70% for 3 seconds', icon: 'Ability4', color: 0x8B4513, effect: function effect() { for (var i = 0; i < enemies.length; i++) { if (enemies[i] && !enemies[i].dustSlowed) { enemies[i].dustSlowed = true; enemies[i].dustSlowDuration = 180; // 3 seconds at 60 FPS if (!enemies[i].originalSpeed) { enemies[i].originalSpeed = enemies[i].speed; } enemies[i].speed = enemies[i].originalSpeed * 0.3; // 70% slower // Add dust swirl animation on each enemy var currentEnemy = enemies[i]; // Store reference to avoid closure issues tween(currentEnemy, { tint: 0x8B4513 }, { duration: 300 }); // Add dust particles swirling around the enemy var dustContainer = new Container(); currentEnemy.addChild(dustContainer); for (var p = 0; p < 8; p++) { (function () { var particle = dustContainer.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, tint: 0xCD853F, // Peru color for dust width: 10 + Math.random() * 15, height: 10 + Math.random() * 15, alpha: 0.8 }); var angle = Math.random() * Math.PI * 2; var startRadius = 20; particle.x = Math.cos(angle) * startRadius; particle.y = Math.sin(angle) * startRadius; var endRadius = 40 + Math.random() * 20; var duration = 1500 + Math.random() * 1000; tween(particle, { x: Math.cos(angle) * endRadius, y: Math.sin(angle) * endRadius, alpha: 0, rotation: particle.rotation + Math.PI }, { duration: duration, onFinish: function onFinish() { if (particle.parent) { particle.destroy(); } } }); })(); } LK.setTimeout(function () { if (dustContainer.parent) { dustContainer.destroy(); } }, 3000); // Effect duration is 3 seconds } } var notification = game.addChild(new Notification("Dust Storm! Enemies slowed by dust!")); } }, 'ability_4': { name: 'Water Flood', description: 'Enemies drown slowly, losing 8% health per second for 5 seconds', icon: 'Ability5', color: 0x0080FF, effect: function effect() { for (var i = 0; i < enemies.length; i++) { if (enemies[i] && !enemies[i].drowning) { enemies[i].drowning = true; enemies[i].drownDuration = 300; // 5 seconds at 60 FPS enemies[i].drownDamage = enemies[i].maxHealth * 0.08 / 60; // 8% per second // Add water splash and drowning animation var currentEnemy = enemies[i]; // Store reference to avoid closure issues tween(currentEnemy, { tint: 0x0080FF }, { duration: 300 }); // Add bubbles rising from the enemy var bubbleContainer = new Container(); currentEnemy.addChild(bubbleContainer); for (var b = 0; b < 5; b++) { (function () { var bubble = bubbleContainer.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, tint: 0xADD8E6, // Light blue for bubbles width: 5 + Math.random() * 10, height: 5 + Math.random() * 10, y: 0, x: (Math.random() - 0.5) * 20, alpha: 0.7 }); var duration = 1000 + Math.random() * 1000; var delay = Math.random() * 4000; // Bubbles appear throughout the 5s duration tween(bubble, { y: -40, alpha: 0 }, { duration: duration, delay: delay, onFinish: function onFinish() { if (bubble.parent) { bubble.destroy(); } } }); })(); } LK.setTimeout(function () { if (bubbleContainer.parent) { bubbleContainer.destroy(); } }, 5000); // Effect duration is 5 seconds } } var notification = game.addChild(new Notification("Water Flood! Enemies are drowning!")); } }, 'ability_5': { name: 'Fire Ring', description: 'Creates fire around enemies, dealing 10% damage for 4 seconds', icon: 'Ability6', color: 0xFF0000, effect: function effect() { for (var i = 0; i < enemies.length; i++) { if (enemies[i] && !enemies[i].burning) { enemies[i].burning = true; enemies[i].burnDuration = 240; // 4 seconds at 60 FPS enemies[i].burnDamage = enemies[i].maxHealth * 0.10 / 60; // 10% per second // Add fire ring animation around each enemy (function (currentEnemy) { var fireIntensity = 0; var _burnAnimation = function burnAnimation() { if (!currentEnemy || !currentEnemy.parent || !currentEnemy.burning) { if (currentEnemy && currentEnemy.parent) { tween(currentEnemy, { tint: 0xFFFFFF, scaleX: 1, scaleY: 1 }, { duration: 200 }); } return; } fireIntensity++; var flameColor = fireIntensity % 2 === 0 ? 0xFF0000 : 0xFF4500; var scale = 1 + (fireIntensity % 2 === 0 ? 0.1 : 0.05); tween(currentEnemy, { tint: flameColor, scaleX: scale, scaleY: scale }, { duration: 250, easing: tween.easeInOut, onFinish: _burnAnimation }); }; _burnAnimation(); // Add rising embers/sparks var fireParticles = new Container(); currentEnemy.addChild(fireParticles); for (var p = 0; p < 8; p++) { (function () { var particle = fireParticles.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, tint: Math.random() < 0.5 ? 0xFF4500 : 0xFFD700, // Orange-red to gold width: 5 + Math.random() * 10, height: 5 + Math.random() * 10, alpha: 0.9, blendMode: 1, x: (Math.random() - 0.5) * currentEnemy.children[0].width, y: (Math.random() - 0.5) * 20 }); var duration = 600 + Math.random() * 400; var delay = Math.random() * 3800; tween(particle, { y: particle.y - 40 - Math.random() * 20, alpha: 0 }, { duration: duration, delay: delay, onFinish: function onFinish() { if (particle.parent) { particle.destroy(); } } }); })(); } LK.setTimeout(function () { if (fireParticles.parent) { fireParticles.destroy(); } }, 4000); })(enemies[i]); } } var notification = game.addChild(new Notification("Fire Ring! Enemies are burning!")); } }, 'ability_6': { name: 'Ice Freeze', description: 'Freezes all enemies for 3 seconds', icon: 'Ability7', color: 0x00FFFF, effect: function effect() { for (var i = 0; i < enemies.length; i++) { if (enemies[i] && !enemies[i].frozen) { enemies[i].frozen = true; enemies[i].freezeDuration = 180; // 3 seconds at 60 FPS if (!enemies[i].originalSpeed) { enemies[i].originalSpeed = enemies[i].speed; } enemies[i].speed = 0; // Completely frozen // Add ice freeze animation var currentEnemy = enemies[i]; // Store reference to avoid closure issues tween(currentEnemy, { tint: 0x00FFFF, scaleX: 0.9, scaleY: 0.9 }, { duration: 400, easing: tween.easeOut }); // Add ice shards that form around the enemy var iceContainer = new Container(); currentEnemy.addChild(iceContainer); for (var s = 0; s < 8; s++) { (function () { var shard = iceContainer.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); shard.tint = 0xB0E0E6; // Powder blue shard.width = 15; shard.height = 5; shard.rotation = Math.random() * Math.PI * 2; var angle = Math.random() * Math.PI * 2; var radius = 20 + Math.random() * 10; shard.x = Math.cos(angle) * radius; shard.y = Math.sin(angle) * radius; shard.alpha = 0; tween(shard, { alpha: 1.0 }, { duration: 400 }); })(); } LK.setTimeout(function () { if (iceContainer.parent) { tween(iceContainer, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { if (iceContainer.parent) { iceContainer.destroy(); } } }); } }, 2700); // Start fading out before freeze ends } } var notification = game.addChild(new Notification("Ice Freeze! All enemies frozen!")); } }, 'ability_7': { name: 'Shield Boost', description: 'Gain 15 health and towers deal 25% more damage for 8 seconds', icon: 'Ability8', color: 0xFFD700, effect: function effect() { lives = Math.min(100, lives + 15); updateUI(); // Boost all towers for (var i = 0; i < towers.length; i++) { if (towers[i] && !towers[i].boosted) { towers[i].boosted = true; towers[i].boostDuration = 480; // 8 seconds at 60 FPS towers[i].originalDamage = towers[i].damage; towers[i].damage = Math.floor(towers[i].damage * 1.25); // Add golden glow animation to boosted towers (function (currentTower) { tween(currentTower, { tint: 0xFFD700, scaleX: 1.15, scaleY: 1.15 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { if (currentTower && currentTower.parent) { tween(currentTower, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeIn }); } } }); // Add shimmering shield particles var shieldParticles = new Container(); currentTower.addChild(shieldParticles); for (var p = 0; p < 12; p++) { (function () { var particle = shieldParticles.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, tint: 0xFFD700, // Gold width: 8 + Math.random() * 12, height: 8 + Math.random() * 12, alpha: 0, blendMode: 1 }); var angle = Math.random() * Math.PI * 2; var startRadius = 30 + Math.random() * 20; var endRadius = 60 + Math.random() * 30; particle.x = Math.cos(angle) * startRadius; particle.y = Math.sin(angle) * startRadius; tween(particle, { alpha: 0.8 }, { duration: 400, delay: Math.random() * 1000, onFinish: function onFinish() { tween(particle, { x: Math.cos(angle) * endRadius, y: Math.sin(angle) * endRadius, alpha: 0 }, { duration: 800 + Math.random() * 500, delay: Math.random() * 6000, onFinish: function onFinish() { if (particle.parent) { particle.destroy(); } } }); } }); })(); } LK.setTimeout(function () { if (shieldParticles.parent) { shieldParticles.destroy(); } }, 8000); // 8 seconds })(towers[i]); } } var notification = game.addChild(new Notification("Shield Boost! Health gained and towers boosted!")); } }, 'ability_8': { name: 'Ultimate Storm', description: 'Combines multiple effects: heal, damage enemies, slow them', icon: 'Ability9', color: 0xFF00FF, effect: function effect() { // Heal player lives = Math.min(100, lives + 20); updateUI(); // Damage and slow all enemies for (var i = 0; i < enemies.length; i++) { if (!enemies[i]) { continue; } // Safety check // Deal damage var damage = enemies[i].maxHealth * 0.20; enemies[i].health -= damage; if (enemies[i].health <= 0) { enemies[i].health = 0; } else { var healthPercentage = enemies[i].health / enemies[i].maxHealth; enemies[i].healthBar.width = enemies[i].healthBarOriginalWidth * healthPercentage; } // Apply slow if (!enemies[i].ultimateSlowed) { enemies[i].ultimateSlowed = true; enemies[i].ultimateSlowDuration = 300; // 5 seconds if (!enemies[i].originalSpeed) { enemies[i].originalSpeed = enemies[i].speed; } enemies[i].speed = enemies[i].originalSpeed * 0.4; // 60% slower } // Add ultimate storm animation - combination of all effects (function (currentEnemy) { var stormPhase = 0; var _ultimateStormAnimation = function ultimateStormAnimation() { stormPhase++; if (!currentEnemy || !currentEnemy.parent) { return; } if (stormPhase === 1) { // Lightning phase (Blue/White) tween(currentEnemy, { tint: 0x87CEEB, scaleX: 1.2, scaleY: 1.2 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { if (currentEnemy && currentEnemy.parent) { tween(currentEnemy, { tint: 0xFFFFFF }, { duration: 150, easing: tween.easeIn, onFinish: _ultimateStormAnimation }); } else { _ultimateStormAnimation(); } } }); } else if (stormPhase === 2) { // Fire phase (Red/Orange) tween(currentEnemy, { tint: 0xFF0000, scaleX: 1.1, scaleY: 1.1 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { if (currentEnemy && currentEnemy.parent) { tween(currentEnemy, { tint: 0xFF4500 }, { duration: 150, easing: tween.easeIn, onFinish: _ultimateStormAnimation }); } else { _ultimateStormAnimation(); } } }); } else if (stormPhase === 3) { // Ice phase (Cyan) tween(currentEnemy, { tint: 0x00FFFF, scaleX: 0.9, scaleY: 0.9 }, { duration: 200, easing: tween.easeOut, onFinish: _ultimateStormAnimation }); } else { // Final phase - return to normal with magical glow tween(currentEnemy, { tint: 0xFF00FF, scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { if (currentEnemy && currentEnemy.parent) { tween(currentEnemy, { tint: 0xFFFFFF }, { duration: 500, easing: tween.easeIn }); } } }); } }; // Start ultimate storm animation _ultimateStormAnimation(); // Add multi-colored particle explosion var stormParticles = new Container(); currentEnemy.addChild(stormParticles); var colors = [0x87CEEB, 0xFFFFFF, 0xFF0000, 0xFF4500, 0x00FFFF, 0xFF00FF]; for (var p = 0; p < 25; p++) { (function () { var particle = stormParticles.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, tint: colors[Math.floor(Math.random() * colors.length)], width: 10 + Math.random() * 15, height: 10 + Math.random() * 15, alpha: 1.0, blendMode: 1 }); var angle = Math.random() * Math.PI * 2; var startRadius = 10; var endRadius = 60 + Math.random() * 40; particle.x = Math.cos(angle) * startRadius; particle.y = Math.sin(angle) * startRadius; var duration = 500 + Math.random() * 400; tween(particle, { x: Math.cos(angle) * endRadius, y: Math.sin(angle) * endRadius, alpha: 0, rotation: particle.rotation + Math.PI * 2 }, { duration: duration, easing: tween.easeOut, onFinish: function onFinish() { if (particle.parent) { particle.destroy(); } } }); })(); } LK.setTimeout(function () { if (stormParticles.parent) { stormParticles.destroy(); } }, 1200); })(enemies[i]); } var notification = game.addChild(new Notification("ULTIMATE STORM! All effects activated!")); } } }; var canStartWaves = false; var canPlaceTowers = false; var gameStarted = false; var showingIntro = true; var showingScores = false; var scoreBackground = null; var scoreMenu = null; var showingAbilities = false; var abilitiesMenu = null; var showingSettings = false; var settingBackground = null; var settingMenu = null; var brightnessOverlay = null; var isDraggingLightButton = false; var isDraggingMusicButton = false; var isDraggingSoundButton = false; var girlCharacter = null; var showingTutorial = false; var tutorialScreen = 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 showAbilities() { if (showingAbilities) { return; } showingAbilities = true; abilitiesMenu = new AbilitiesMenu(); abilitiesMenu.x = 2048 / 2 - 60; abilitiesMenu.y = 2732 / 2; game.addChild(abilitiesMenu); abilitiesMenu.scale.set(0.5); abilitiesMenu.alpha = 0; tween(abilitiesMenu, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 300, easing: tween.easeOut }); } function hideAbilities() { if (!showingAbilities || !abilitiesMenu) { return; } showingAbilities = false; tween(abilitiesMenu, { scaleX: 0.5, scaleY: 0.5, alpha: 0 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { if (abilitiesMenu) { abilitiesMenu.destroy(); abilitiesMenu = null; } } }); } function showSettings() { if (showingSettings) { return; } showingSettings = true; settingBackground = game.attachAsset('SettingBackground', { anchorX: 0.5, anchorY: 0.5 }); var startX = 2048 + settingBackground.width / 2; var targetX = 2048 / 2; settingBackground.x = startX; settingBackground.y = 2732 / 2; game.addChild(settingBackground); settingMenu = new Container(); settingMenu.x = startX; settingMenu.y = 2732 / 2; game.addChild(settingMenu); var closeButton = new Container(); closeButton.attachAsset('CloseButton', { anchorX: 0.5, anchorY: 0.5 }); closeButton.y = settingBackground.height / 2 - 150; settingMenu.addChild(closeButton); closeButton.down = function () { hideSettings(); }; // Calculate vertical positions to center all three volume settings in the settingBackground var totalHeight = 400; // Total height span for all three settings (200px spacing between each) var musicY = -totalHeight / 2; // -200 var soundY = 0; // Center var lightY = totalHeight / 2; // 200 // Calculate horizontal positions to center all elements in the settingBackground var totalWidth = 700; // Approximate total width of icon + bar + percentage text var iconX = -totalWidth / 2 - 200; // Center the icons horizontally, moved 200px left var barX = iconX + 150; // Music Setting var musicIcon = settingMenu.attachAsset('MusicIcon', { anchorX: 0.5, anchorY: 0.5 }); musicIcon.x = iconX; musicIcon.y = musicY; var musicBar = settingMenu.attachAsset('MusicBar', { anchorX: 0, anchorY: 0.5 }); musicBar.name = "musicBar"; musicBar.x = barX; musicBar.y = musicY; var musicButton = settingMenu.attachAsset('MusicButton', { anchorX: 0.5, anchorY: 0.5 }); musicButton.name = "musicButton"; musicButton.y = musicY; // Position music button based on current music volume var currentMusicVolume = game.musicVolume || 1.0; var musicTrackWidth = musicBar.width - musicButton.width; musicButton.x = musicBar.x + musicButton.width / 2 + musicTrackWidth * currentMusicVolume; // Add music volume percentage text var musicPercentage = new Text2(Math.round((game.musicVolume || 1.0) * 100) + "%", { size: 50, fill: 0x00ffb3, weight: 800 }); musicPercentage.anchor.set(0.5, 0.5); musicPercentage.x = musicBar.x + musicBar.width + 80; musicPercentage.y = musicY; musicPercentage.name = "musicPercentage"; settingMenu.addChild(musicPercentage); musicButton.down = function () { isDraggingMusicButton = true; }; // Sound Setting var soundIcon = settingMenu.attachAsset('SoundIcon', { anchorX: 0.5, anchorY: 0.5 }); soundIcon.x = iconX; soundIcon.y = soundY; var soundBar = settingMenu.attachAsset('SoundBar', { anchorX: 0, anchorY: 0.5 }); soundBar.name = "soundBar"; soundBar.x = barX; soundBar.y = soundY; var soundButton = settingMenu.attachAsset('SoundButton', { anchorX: 0.5, anchorY: 0.5 }); soundButton.name = "soundButton"; soundButton.y = soundY; // Add sound volume percentage text var soundPercentage = new Text2(Math.round(game.soundVolume * 100) + "%", { size: 50, fill: 0x00ffb3, weight: 800 }); soundPercentage.anchor.set(0.5, 0.5); soundPercentage.x = soundBar.x + soundBar.width + 80; soundPercentage.y = soundY; soundPercentage.name = "soundPercentage"; settingMenu.addChild(soundPercentage); // Position sound button based on current sound volume var currentSoundVolume = game.soundVolume || 1.0; var soundTrackWidth = soundBar.width - soundButton.width; soundButton.x = soundBar.x + soundButton.width / 2 + soundTrackWidth * currentSoundVolume; soundButton.down = function () { isDraggingSoundButton = true; }; // Light Setting var lightIcon = settingMenu.attachAsset('LightIcon', { anchorX: 0.5, anchorY: 0.5 }); lightIcon.x = iconX; lightIcon.y = lightY; var lightBar = settingMenu.attachAsset('LightBar', { anchorX: 0, anchorY: 0.5 }); lightBar.name = "lightBar"; lightBar.x = barX; lightBar.y = lightY; var lightButton = settingMenu.attachAsset('LightButton', { anchorX: 0.5, anchorY: 0.5 }); lightButton.name = "lightButton"; lightButton.y = lightY; lightButton.x = lightBar.x + lightButton.width / 2; // Add light percentage text var lightPercentage = new Text2("0%", { size: 50, fill: 0x00ffb3, weight: 800 }); lightPercentage.anchor.set(0.5, 0.5); lightPercentage.x = lightBar.x + lightBar.width + 80; lightPercentage.y = lightY; lightPercentage.name = "lightPercentage"; settingMenu.addChild(lightPercentage); lightButton.down = function () { isDraggingLightButton = true; }; tween(settingBackground, { x: targetX }, { duration: 500, easing: tween.easeOut }); tween(settingMenu, { x: targetX }, { duration: 500, easing: tween.easeOut }); } function hideSettings() { if (!showingSettings) { return; } showingSettings = false; var targetX = 2048 + 1600 / 2; if (settingBackground) { tween(settingBackground, { x: targetX }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (settingBackground) { game.removeChild(settingBackground); settingBackground = null; } } }); } if (settingMenu) { tween(settingMenu, { x: targetX }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (settingMenu) { game.removeChild(settingMenu); settingMenu = null; } } }); } } 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: 0x00ffb3, 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 : 0x00ffb3; // 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('ScoreCloseButton', { anchorX: 0.5, anchorY: 0.5 }); 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; } } function showTutorial() { if (showingTutorial) { return; } showingTutorial = true; // Create tutorial screen container tutorialScreen = new Container(); tutorialScreen.x = 2048 / 2; tutorialScreen.y = 2732 / 2; game.addChild(tutorialScreen); // Add background using proper Tutorialbackground asset var tutorialBg = tutorialScreen.attachAsset('Tutorialbackground', { anchorX: 0.5, anchorY: 0.5 }); tutorialBg.width = 2048; tutorialBg.height = 2732; // Add title var title = new Text2("HOW TO PLAY", { size: 80, fill: 0xff0080, weight: 800 }); title.anchor.set(0.5, 0.5); title.y = -tutorialBg.height / 2 + 100; tutorialScreen.addChild(title); // Tutorial content var tutorialContent = [{ title: "1. PLACING TOWERS", desc: "Drag towers from bottom bar\nto the grid. Each tower costs\ncrystals shown below it." }, { title: "2. UPGRADE & SELL", desc: "Tap on a placed tower to\nupgrade (costs more crystals)\nor sell (get 80% back)." }, { title: "3. TOWER TYPES", desc: "Cannon: Basic damage\nRifle: Fast shooting\nFlame: Continuous damage\nRocket: Splash damage\nGumbomb: Slows enemies\nToxinbomb: Poison damage" }, { title: "4. START WAVES", desc: "Click 'Start Game' to begin.\nEnemies spawn from top and\nmove to bottom. Stop them!" }, { title: "5. ABILITIES", desc: "Click ability button (bottom left)\nto use special powers. Each\ncosts crystals but helps a lot!" }, { title: "6. ENEMY TYPES", desc: "Normal: Standard enemies\nFast: Move quickly\nImmune: Can't be slowed\nFlying: Go over towers\nSwarm: Many weak enemies\nBoss: Strong, every 10 waves" }, { title: "7. WIN CONDITION", desc: "Survive all 100 waves to win!\nLose all health and game over.\nEarn crystals by defeating enemies." }]; // Per-section manual positions for title and description var tutorialSectionPositions = [ // 1. PLACING TOWERS { titleX: 0, titleY: -1250, descX: 0, descY: -1170 }, // 2. UPGRADE & SELL { titleX: 0, titleY: -880, descX: 0, descY: -800 }, // 3. TOWER TYPES { titleX: 0, titleY: -660, descX: 0, descY: -580 }, // 4. START WAVES { titleX: 0, titleY: -240, descX: 0, descY: -160 }, // 5. ABILITIES { titleX: 0, titleY: 30, descX: 0, descY: 110 }, // 6. ENEMY TYPES { titleX: 0, titleY: 300, descX: 0, descY: 380 }, // 7. WIN CONDITION { titleX: 0, titleY: 720, descX: 0, descY: 800 }]; // Display tutorial content with manual positions for (var i = 0; i < tutorialContent.length; i++) { var section = tutorialContent[i]; var pos = tutorialSectionPositions[i] || { titleX: 0, titleY: 0, descX: 0, descY: 60 }; // Create a container for each section to ensure proper layering var sectionContainer = new Container(); tutorialScreen.addChild(sectionContainer); // Section title - ensure it's added first and positioned correctly var sectionTitle = new Text2(section.title, { size: 60, fill: 0x00ffb3, weight: 800 }); sectionTitle.anchor.set(0.5, 0); // Anchor at top center sectionTitle.x = pos.titleX; sectionTitle.y = pos.titleY; sectionContainer.addChild(sectionTitle); // Section description - add after title to ensure proper layering var sectionDesc = new Text2(section.desc, { size: 45, fill: 0xff0080, weight: 600, align: 'center' }); sectionDesc.anchor.set(0.5, 0); // Anchor at top center sectionDesc.x = pos.descX; sectionDesc.y = pos.descY; sectionContainer.addChild(sectionDesc); } // Add close button using proper Tutorialclosebutton asset var closeButton = tutorialScreen.attachAsset('Tutorialclosebutton', { anchorX: 0.5, anchorY: 0.5 }); closeButton.x = tutorialBg.width / 2 - 200; closeButton.y = -tutorialBg.height / 2 + 100; closeButton.down = function () { hideTutorial(); }; // Entrance animation tutorialScreen.scaleX = 0.5; tutorialScreen.scaleY = 0.5; tutorialScreen.alpha = 0; tween(tutorialScreen, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 300, easing: tween.easeOut }); } function hideTutorial() { if (!showingTutorial) { return; } showingTutorial = false; tween(tutorialScreen, { scaleX: 0.5, scaleY: 0.5, alpha: 0 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { if (tutorialScreen) { tutorialScreen.destroy(); tutorialScreen = 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 immediately when intro starts LK.playMusic('Intromusic'); // Position buttons vertically below the center of the screen var buttonCenterX = 2048 / 2; var buttonSpacing = 280; // Increased vertical spacing between buttons var startY = 2732 / 2 + 400; // Start position for the first button, to place the group below center // Add StartButton at the top var startButton = game.attachAsset('StartButton', { anchorX: 0.5, anchorY: 0.5 }); startButton.x = buttonCenterX; startButton.y = startY; game.addChild(startButton); // Add Tutorial Button below start button var tutorialButton = game.attachAsset('Tutorialbutton', { anchorX: 0.5, anchorY: 0.5 }); tutorialButton.x = buttonCenterX; tutorialButton.y = startY + buttonSpacing; game.addChild(tutorialButton); // Add ScoreButton below tutorial button var scoreButton = game.attachAsset('ScoreButton', { anchorX: 0.5, anchorY: 0.5 }); scoreButton.x = buttonCenterX; scoreButton.y = startY + buttonSpacing * 2; game.addChild(scoreButton); // Add SettingButton at the bottom var settingButton = game.attachAsset('SettingButton', { anchorX: 0.5, anchorY: 0.5 }); settingButton.x = buttonCenterX; settingButton.y = startY + buttonSpacing * 3; game.addChild(settingButton); // 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; settingButton.visible = false; tutorialButton.visible = false; // Show game elements gameBackground.visible = true; debugLayer.visible = true; towerLayer.visible = true; enemyLayer.visible = true; crystalIcon.visible = true; crystalLabelText.visible = true; crystalNumberText.visible = true; healthBarContainer.visible = true; scoreLabelText.visible = true; scoreNumberText.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(); // Add a small delay to ensure intro music is fully stopped before starting new music LK.setTimeout(function () { if (musicOn) { LK.playMusic('Gamemusic1'); } }, 100); } // Add click handlers for buttons startButton.down = function () { startGame(); }; scoreButton.down = function () { showHighScores(); }; settingButton.down = function () { if (showingIntro && !showingScores) { showSettings(); } }; tutorialButton.down = function () { if (showingIntro && !showingScores && !showingSettings) { showTutorial(); } }; var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; // Get menu background height from the asset var menuHeight = 500; // Default fallback if (menu.children[0]) { menuHeight = menu.children[0].height; } tween(menu, { y: 2732 + menuHeight / 2 }, { 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 crystals = 4000; var lives = 100; var score = 0; var currentWave = 0; var lastWave = 0; var totalWaves = 100; 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 ships = []; var lastShip3Spawn = 0; var ship3SpawnInterval = 2400; // 40 seconds at 60 FPS var crystalLabelText = new Text2('Crystal: ', { size: 60, fill: 0xff0080, weight: 800 }); crystalLabelText.anchor.set(1, 0.9); var crystalNumberText = new Text2(crystals.toString(), { size: 60, fill: 0x00ffb3, weight: 800 }); crystalNumberText.anchor.set(0, 0.9); var healthBarBG = LK.getAsset('healthBarOutline', { anchorX: 0.5, anchorY: 1 }); var healthBarFill = LK.getAsset('gameHealthBar', { anchorX: 0, anchorY: 1.1 }); healthBarFill.x = -healthBarBG.width / 2 + (healthBarBG.width - healthBarFill.width) / 2; healthBarFill.originalWidth = healthBarFill.width; var healthText = new Text2('100/100', { size: 50, fill: 0x00ffb3, weight: 800 }); healthText.anchor.set(0.5, 0.9); var healthBarContainer = new Container(); healthBarContainer.addChild(healthBarBG); healthBarContainer.addChild(healthBarFill); healthBarContainer.addChild(healthText); var scoreLabelText = new Text2('Score: ', { size: 60, fill: 0xff0080, weight: 800 }); scoreLabelText.anchor.set(1, 0.9); var scoreNumberText = new Text2(score.toString(), { size: 60, fill: 0x00ffb3, weight: 800 }); scoreNumberText.anchor.set(0, 0.9); var topMargin = 50; var centerX = 2048 / 2; var spacing = 350; var crystalIcon = LK.getAsset('Crystal', { anchorX: 1.0, anchorY: 0.9, scaleX: 0.4, scaleY: 0.6 }); LK.gui.top.addChild(crystalIcon); LK.gui.top.addChild(crystalLabelText); LK.gui.top.addChild(crystalNumberText); LK.gui.top.addChild(healthBarContainer); LK.gui.top.addChild(scoreLabelText); LK.gui.top.addChild(scoreNumberText); // Hide UI elements during intro crystalIcon.visible = false; crystalLabelText.visible = false; crystalNumberText.visible = false; healthBarContainer.visible = false; scoreLabelText.visible = false; scoreNumberText.visible = false; healthBarContainer.x = 0; healthBarContainer.y = topMargin; // Position Crystal UI elements var crystalTextX = -380; crystalLabelText.x = crystalTextX; crystalLabelText.y = topMargin; crystalNumberText.x = crystalTextX; crystalNumberText.y = topMargin; crystalIcon.x = crystalTextX - 250; crystalIcon.y = topMargin; // Position Score UI elements var scoreTextX = 450; scoreLabelText.x = scoreTextX; scoreLabelText.y = topMargin; scoreNumberText.x = scoreTextX; scoreNumberText.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.1, width: 100, height: 100 }); muteButton.x = 2048 - 50; // 50px from right edge muteButton.y = 0; // 0px 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.1, width: 100, height: 100 }); muteButton.x = 2048 - 50; muteButton.y = 0; 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 var musicToPlay; if (currentWave > 0 && currentWave % 10 === 0) { // isBossWave var bossMusics = ['Bossmusic1', 'Bossmusic2', 'Bossmusic3', 'Bossmusic4', 'Bossmusic5']; var musicIndex = (currentWave / 10 - 1) % bossMusics.length; musicToPlay = bossMusics[musicIndex]; } else { // is normal wave, or wave 0 var gameMusics = ['Gamemusic1', 'Gamemusic2', 'gamemusic3', 'Gamemusic4', 'Gamemusic5']; var musicIndex = Math.floor(currentWave / 10) % gameMusics.length; musicToPlay = gameMusics[musicIndex]; } LK.playMusic(musicToPlay); } } else { // Music is now off - show Mutebutton2 and stop music game.removeChild(muteButton); muteButton = LK.getAsset('Mutebutton2', { anchorX: 1.0, anchorY: -0.1, width: 100, height: 100 }); muteButton.x = 2048 - 50; muteButton.y = 0; 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() { crystalNumberText.setText(crystals.toString()); healthText.setText(lives + '/100'); // Update health bar fill width based on current lives (assuming max 100 lives) var healthPercentage = lives / 100; healthBarFill.width = healthBarFill.originalWidth * healthPercentage; scoreNumberText.setText(score.toString()); } function setCrystals(value) { crystals = 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); // Initialize weather system var weatherSystem = new WeatherSystem(); game.addChild(weatherSystem); brightnessOverlay = LK.getAsset('cell', { width: 2048, height: 2732 }); brightnessOverlay.tint = 0x000000; brightnessOverlay.alpha = 0; brightnessOverlay.x = 0; brightnessOverlay.y = 0; game.addChild(brightnessOverlay); // 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 'ToxinBomb': 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 (crystals >= towerCost) { var tower = new Tower(towerType || 'cannon'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setCrystals(crystals - towerCost); grid.pathFind(); grid.renderDebug(); return true; } else { var notification = game.addChild(new Notification("Not enough crystals!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { // Handle tutorial menu during intro if (showingIntro && showingTutorial) { // Check if click is outside tutorial area var menuLeft = 2048 / 2 - 900; var menuRight = 2048 / 2 + 900; var menuTop = 2732 / 2 - 1200; var menuBottom = 2732 / 2 + 1200; if (x < menuLeft || x > menuRight || y < menuTop || y > menuBottom) { hideTutorial(); } return; } // 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) { if (!canPlaceTowers) { return; } towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); // Apply the same offset as in move handler to ensure consistency when starting drag towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); break; } } }; game.move = function (x, y, obj) { if (isDraggingMusicButton) { if (settingMenu) { var musicBar = null; var musicButton = null; var musicPercentage = null; for (var i = 0; i < settingMenu.children.length; i++) { if (settingMenu.children[i].name === "musicBar") { musicBar = settingMenu.children[i]; } if (settingMenu.children[i].name === "musicButton") { musicButton = settingMenu.children[i]; } if (settingMenu.children[i].name === "musicPercentage") { musicPercentage = settingMenu.children[i]; } } if (musicBar && musicButton) { var localPos = settingMenu.toLocal({ x: x, y: y }); var minX = musicBar.x + musicButton.width / 2; var maxX = musicBar.x + musicBar.width - musicButton.width / 2; var newX = Math.max(minX, Math.min(maxX, localPos.x)); musicButton.x = newX; var trackWidth = maxX - minX; var percentage = trackWidth > 0 ? (newX - minX) / trackWidth : 0; // Store current music volume and apply to currently playing music if (typeof game.musicVolume === 'undefined') { game.musicVolume = 1.0; } game.musicVolume = percentage; // Update percentage display if (musicPercentage) { musicPercentage.setText(Math.round(percentage * 100) + "%"); } // Stop current music and restart with new volume LK.stopMusic(); if (musicOn && percentage > 0) { // Determine which music should be playing and restart with new volume if (showingIntro) { LK.playMusic('Intromusic', { fade: { start: 0, end: percentage, duration: 100 } }); } else if (gameStarted) { // Resume game music based on current wave with new volume var musicToPlay; if (currentWave > 0 && currentWave % 10 === 0) { // isBossWave var bossMusics = ['Bossmusic1', 'Bossmusic2', 'Bossmusic3', 'Bossmusic4', 'Bossmusic5']; var musicIndex = (currentWave / 10 - 1) % bossMusics.length; musicToPlay = bossMusics[musicIndex]; } else { // is normal wave, or wave 0 var gameMusics = ['Gamemusic1', 'Gamemusic2', 'gamemusic3', 'Gamemusic4', 'Gamemusic5']; var musicIndex = Math.floor(currentWave / 10) % gameMusics.length; musicToPlay = gameMusics[musicIndex]; } LK.playMusic(musicToPlay, { fade: { start: 0, end: percentage, duration: 100 } }); } } } } return; } if (isDraggingSoundButton) { if (settingMenu) { var soundBar = null; var soundButton = null; var soundPercentage = null; for (var i = 0; i < settingMenu.children.length; i++) { if (settingMenu.children[i].name === "soundBar") { soundBar = settingMenu.children[i]; } if (settingMenu.children[i].name === "soundButton") { soundButton = settingMenu.children[i]; } if (settingMenu.children[i].name === "soundPercentage") { soundPercentage = settingMenu.children[i]; } } if (soundBar && soundButton) { var localPos = settingMenu.toLocal({ x: x, y: y }); var minX = soundBar.x + soundButton.width / 2; var maxX = soundBar.x + soundBar.width - soundButton.width / 2; var newX = Math.max(minX, Math.min(maxX, localPos.x)); soundButton.x = newX; var trackWidth = maxX - minX; var percentage = trackWidth > 0 ? (newX - minX) / trackWidth : 0; // Update the global sound volume game.soundVolume = percentage; // Save sound volume to storage storage.soundVolume = percentage; // Update percentage display if (soundPercentage) { soundPercentage.setText(Math.round(percentage * 100) + "%"); } // Play a test sound when dragging to give immediate feedback if (percentage > 0) { game.playSoundWithVolume('Bulletsound'); } } } return; } if (isDraggingLightButton) { if (settingMenu) { var lightBar = null; var lightButton = null; var lightPercentage = null; for (var i = 0; i < settingMenu.children.length; i++) { if (settingMenu.children[i].name === "lightBar") { lightBar = settingMenu.children[i]; } if (settingMenu.children[i].name === "lightButton") { lightButton = settingMenu.children[i]; } if (settingMenu.children[i].name === "lightPercentage") { lightPercentage = settingMenu.children[i]; } } if (lightBar && lightButton) { var localPos = settingMenu.toLocal({ x: x, y: y }); var minX = lightBar.x + lightButton.width / 2; var maxX = lightBar.x + lightBar.width - lightButton.width / 2; var newX = Math.max(minX, Math.min(maxX, localPos.x)); lightButton.x = newX; var trackWidth = maxX - minX; var percentage = trackWidth > 0 ? (newX - minX) / trackWidth : 0; if (brightnessOverlay) { brightnessOverlay.alpha = percentage * 0.7; // Max dim alpha 0.7 } // Update percentage display if (lightPercentage) { lightPercentage.setText(Math.round(percentage * 100) + "%"); } } } return; } // 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) { if (isDraggingLightButton) { isDraggingLightButton = false; return; } if (isDraggingMusicButton) { isDraggingMusicButton = false; return; } if (isDraggingSoundButton) { isDraggingSoundButton = false; return; } // 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.y + grid.cells[0].length * CELL_SIZE - CELL_SIZE * 6; } 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); var inGameSettingButton = nextWaveButtonContainer.attachAsset('SettingButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.6, scaleY: 0.6 }); var abilityButton = nextWaveButtonContainer.attachAsset('AbilityButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.6, scaleY: 0.6 }); abilityButton.x = 80; // Move 30px to the right abilityButton.y = 2550 - 150 + 30; // Move 230px down abilityButton.down = function () { if (!gameStarted || showingIntro || !canStartWaves) { var notification = game.addChild(new Notification("Cannot use abilities\nuntil game starts!")); return; } showAbilities(); }; inGameSettingButton.x = 2048 - 70; // Move 50px further to the right inGameSettingButton.y = 2420; inGameSettingButton.down = function () { showSettings(); }; nextWaveButtonContainer.visible = false; game.addChild(nextWaveButtonContainer); var towerTypes = ['cannon', 'rifle', 'flame', 'Rocket', 'gumbomb', 'ToxinBomb']; 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; } // Add static flag to track if intro sequence has been shown if (typeof game.introSequenceShown === 'undefined') { game.introSequenceShown = false; } if (!game.introSequenceShown) { game.introSequenceShown = true; girlCharacter = new SpeakingGirl(); game.addChild(girlCharacter); // Spawn tablet after girl with slight delay LK.setTimeout(function () { var tablet = new Tablet(); game.addChild(tablet); }, 500); // 500ms delay after girl spawns } // Weather system update if (weatherSystem) { weatherSystem.update(); } if (currentWave !== lastWave) { if (musicOn && (typeof game.musicVolume === 'undefined' || game.musicVolume > 0)) { // Only play music if music is enabled and volume is greater than 0 var musicVolume = typeof game.musicVolume !== 'undefined' ? game.musicVolume : 1.0; var musicToPlay = null; if (currentWave > 0 && currentWave % 10 === 0) { // isBossWave var bossMusics = ['Bossmusic1', 'Bossmusic2', 'Bossmusic3', 'Bossmusic4', 'Bossmusic5']; var musicIndex = (currentWave / 10 - 1) % bossMusics.length; musicToPlay = bossMusics[musicIndex]; } else if (currentWave > 1 && (currentWave - 1) % 10 === 0) { // isPostBossWave var gameMusics = ['Gamemusic1', 'Gamemusic2', 'gamemusic3', 'Gamemusic4', 'Gamemusic5']; var musicIndex = Math.floor((currentWave - 1) / 10) % gameMusics.length; musicToPlay = gameMusics[musicIndex]; } if (musicToPlay) { LK.playMusic(musicToPlay, { fade: { start: 0, end: musicVolume, duration: 100 } }); } } 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 crystals and score - increased rewards var crystalsEarned = enemy.isBoss ? Math.floor(25 + (enemy.waveNumber - 1) * 1.5) : Math.floor(3 + (enemy.waveNumber - 1) * 0.4); var crystalIndicator = new CrystalIndicator(crystalsEarned, enemy.x, enemy.y); game.addChild(crystalIndicator); setCrystals(crystals + crystalsEarned); // 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! +" + crystalsEarned + " 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(); } // Ship spawning logic // Spawn ship1 or ship2 randomly, on average every 20 seconds if (Math.random() < 1 / 1200) { // 1200 ticks = 20 seconds at 60 FPS // Randomly select ship type (1 or 2) var shipType = Math.floor(Math.random() * 2) + 1; var ship = new Ship(shipType); // Add ship to game at a lower layer so it appears behind game elements game.addChildAt(ship, 1); // Just above background ships.push(ship); } // Spawn ship3 every 40 seconds if (LK.ticks - lastShip3Spawn >= ship3SpawnInterval) { lastShip3Spawn = LK.ticks; var ship = new Ship(3); // Add ship to game at a lower layer so it appears behind game elements game.addChildAt(ship, 1); // Just above background ships.push(ship); } // Update and cleanup ships for (var i = ships.length - 1; i >= 0; i--) { if (!ships[i].parent) { ships.splice(i, 1); } } };
===================================================================
--- original.js
+++ change.js
@@ -5337,9 +5337,9 @@
LK.playMusic('Intromusic');
// Position buttons vertically below the center of the screen
var buttonCenterX = 2048 / 2;
var buttonSpacing = 280; // Increased vertical spacing between buttons
-var startY = 2732 / 2 + 300; // Start position for the first button, to place the group below center
+var startY = 2732 / 2 + 400; // Start position for the first button, to place the group below center
// Add StartButton at the top
var startButton = game.attachAsset('StartButton', {
anchorX: 0.5,
anchorY: 0.5
Pack top square of towers facing the screen to use it as a grid. future. seen from above. HD colors.
Pack top square of tower facing the screen to use it as a grid. future. seen from above. HD colors.
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"!
Police cyber flying Ship assets without 'police' text, Cyberpunk, Abstract, Futuer, HD colors, Different colors. Horizontal. seen from a side. In-Game asset. 2d. High contrast. No shadows
Ability icon 'healing' . cyberpunk. abstract. Futuer. different colors. HD colors
Ability icon 'weakness curse' . cyberpunk. abstract. Futuer. different colors. HD colors
Ability icon 'Lighting storm' . cyberpunk. abstract. Futuer. different colors. HD colors
Ability icon 'Dust storm' . cyberpunk. abstract. Futuer. different colors. HD colors
Ability icon 'Water flood' . cyberpunk. abstract. Futuer. different colors. HD colors
Ability icon 'Fire ring' . cyberpunk. abstract. Futuer. different colors. HD colors
Ability icon 'Ice freeze' . cyberpunk. abstract. Futuer. different colors. HD colors
Ability icon 'Shield Boost' . cyberpunk. abstract. Futuer. different colors. HD colors
Ability icon 'Ultimate Storm' . cyberpunk. abstract. Futuer. different colors. HD colors
placmentsound1
Sound effect
placmentsound2
Sound effect
placmentsound3
Sound effect
placmentsound4
Sound effect
placmentsound5
Sound effect
placmentsound6
Sound effect
Intromusic
Music
Bulletsound
Sound effect
Riflebulletsound
Sound effect
Flamesound
Sound effect
Rocketsound
Sound effect
Gumbombsound
Sound effect
Gamemusic2
Music
gamemusic3
Music
Gamemusic4
Music
Gamemusic5
Music
Bossmusic1
Music
Bossmusic2
Music
Bossmusic3
Music
Bossmusic4
Music
Bossmusic5
Music
Ship1sound
Sound effect
Ship2sound
Sound effect
Ship3sound
Sound effect
Gamemusic1
Music
Toxinbombsound
Sound effect
Closebuttonsound1
Sound effect
Useabilitysound1
Sound effect
Abilitybuttonsound1
Sound effect
Startbuttonsound1
Sound effect
Bigrobots_damage_sound1
Sound effect
Cybersnake_damage_sound1
Sound effect
Cyborg_Damage_sound1
Sound effect
Electro_Damage_sound1
Sound effect
Eye_Damage_sound1
Sound effect
Firo_Damage_sound1
Sound effect
Robot_Damage_sound1
Sound effect