User prompt
Add new enemies: King Hippo (will eat your towers to pass!), Dragon (flies the skies and can stun your towers), Pink Kangaroo (jumps over your towers)
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'storage.achievementStats = achievementStats;' Line Number: 5032 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'storage.achievements = achievements;' Line Number: 5010 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Add new towers: Gatling Gun (fires immensly fast! Faster even than the crossbow. Type: single damage), Swordsman (has a sword spinning around him, it will deal decent damage. Type: area damage)
User prompt
Add new thingy: Achievements! Complete Achievements to get progress to fully complete the game (achievements: Builder: build 10 towers; Slayer: kill 200 enemies; power engineer: Make a tower be at max level + have the special ability; all of Em': make all types of towers be at max level+ have their special ability; Winner: defeat all waves; Loser: get defeated 3 times in a row while having spent at least 1000 gold in defenses; Farmer: collect 1000 gold worth of Farm Income; Miss: make a Mortar Tower miss; Cowardly: make a magician tower hit 2 enemies in a row with a projectile with confusion) ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Make it so that there's an offline production cap of 4 hours ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'Error: Invalid value. Only literals or 1-level deep objects/arrays containing literals are allowed.' in or related to this line: 'storage.villageLastCollection = villageLastCollection;' Line Number: 4419 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'Error: Invalid value. Only literals or 1-level deep objects/arrays containing literals are allowed.' in or related to this line: 'storage.villageLastCollection = villageLastCollection;' Line Number: 4419 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Add new game mode: Village builder! Build your own village! Making a village can give you crystals, but you need to invest some... Fortunately, your first building [that will always be a town hall] is free! (Buildings: Town hall you can only have one town hall. Creates 5 crystals every 30 minutes, INCLUDING when you're offline; House: makes 1 crystal every 30 minutes including when you're offline; Crystal Mine creates 2 crystals every 15 minutes including when you're offline; Defense tower [can be any tower from the standard game that's isn't a farm or a church, for now, it does nothing], Barracks (also does nothing for now)) ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'ymirsHammer')' in or related to this line: 'var lives = artifacts.ymirsHammer ? 35 : 20;' Line Number: 4068
User prompt
Add new thingy: Artifacts! Gain artifacts to get permanent buffs! (Artifacts: Thunderbolt of Zeus: all projectiles stun enemies for 0.25 seconds; Ares' helmet: all projectiles deal double damage; Ymir's hammer: start with 15 extra lives). They cost Crystals that are a resouce that isnt reset after games and is gained by defeating bosses (Without using any cheats) or by upgrading a tower to MAX level including buying the special ability ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Add a title screen to the game
User prompt
Make it so that there is a path system for towers. For now it's exclusive to the archer tower (Upgrade path 1 upgrades ordered: Faster shooting, Even Faster shooting, Rapid fire (AKA special ability), Super Rapid Fire [Firing multiple bullets concecutively increases]. Upgrade path 2 upgrades ordered: Sharp arrows, Sharpest arrows [both make the arrows deal extra damage], Giant arrow [Activated ability: Make the bow tower shoot a big arrow that deals high damage and pierced thru all enemies], Even bigger arrow [Activated ability damage increased]). You can upgrade both paths but you can only have one Third upgrade (AKA only one upgrade that is ordered third or higher in a path can be bought)
User prompt
Make the targeting button under all upgrades when choosing path
User prompt
Make the targeting button under all upgrades when choosing path
User prompt
Move the targeting button to be under the selling button
User prompt
Make the buttons more spread out when choosing path
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'text')' in or related to this line: 'if (buttonText.text !== newText) {' Line Number: 3757
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'tint')' in or related to this line: 'buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;' Line Number: 3753
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'tint')' in or related to this line: 'buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;' Line Number: 3751
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'tint')' in or related to this line: 'buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;' Line Number: 3751
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'tint')' in or related to this line: 'buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;' Line Number: 3749
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot set properties of undefined (setting 'down')' in or related to this line: 'upgradeButton.down = function (x, y, obj) {' Line Number: 3552
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot set properties of undefined (setting 'down')' in or related to this line: 'upgradeButton.down = function (x, y, obj) {' Line Number: 3552
User prompt
Make it so that there is a path system for towers. For now it's exclusive to the archer tower (Upgrade path 1 upgrades ordered: Faster shooting, Even Faster shooting, Rapid fire (AKA special ability), Super Rapid Fire [Firing multiple bullets concecutively increases]. Upgrade path 2 upgrades ordered: Sharp arrows, Sharpest arrows [both make the arrows deal extra damage], Giant arrow [Activated ability: Make the bow tower shoot a big arrow that deals high damage and pierced thru all enemies], Even bigger arrow [Activated ability damage increased])
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var AchievementsMenu = Container.expand(function () { var self = Container.call(this); // Background var background = self.attachAsset('manualBackground', { anchorX: 0.5, anchorY: 0.5 }); background.alpha = 0.95; // Title var titleText = new Text2('ACHIEVEMENTS', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0); titleText.x = 0; titleText.y = -1150; self.addChild(titleText); // Create scrollable content container var contentContainer = new Container(); self.addChild(contentContainer); var yPos = -1000; var achievementSpacing = 120; // Calculate completion percentage var completedCount = 0; var totalCount = 0; for (var key in achievements) { totalCount++; if (achievements[key].completed) { completedCount++; } } // Progress display var progressText = new Text2('Progress: ' + completedCount + '/' + totalCount + ' (' + Math.round(completedCount / totalCount * 100) + '%)', { size: 60, fill: 0x00FFFF, weight: 800 }); progressText.anchor.set(0.5, 0); progressText.x = 0; progressText.y = yPos; contentContainer.addChild(progressText); yPos += 100; // Achievement list for (var key in achievements) { var achievement = achievements[key]; var achievementContainer = new Container(); achievementContainer.y = yPos; var achievementBg = achievementContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); achievementBg.width = 1400; achievementBg.height = 100; achievementBg.tint = achievement.completed ? 0x00AA00 : 0x444444; var nameText = new Text2(achievement.name, { size: 50, fill: 0xFFFFFF, weight: 800 }); nameText.anchor.set(0, 0.5); nameText.x = -650; nameText.y = -20; achievementContainer.addChild(nameText); var descText = new Text2(achievement.description, { size: 40, fill: 0xCCCCCC, weight: 400 }); descText.anchor.set(0, 0.5); descText.x = -650; descText.y = 20; achievementContainer.addChild(descText); var progressText = new Text2(achievement.completed ? 'COMPLETED' : achievement.progress + '/' + achievement.target, { size: 45, fill: achievement.completed ? 0x00FF00 : 0xFFFFFF, weight: 800 }); progressText.anchor.set(1, 0.5); progressText.x = 650; progressText.y = 0; achievementContainer.addChild(progressText); contentContainer.addChild(achievementContainer); yPos += achievementSpacing; } // Scrolling variables var isDragging = false; var startY = 0; var startContentY = 0; var velocity = 0; var minY = -1000; var maxY = Math.min(0, -yPos + 1800); // Close button var closeButton = new Container(); var closeBg = closeButton.attachAsset('manualButton', { anchorX: 0.5, anchorY: 0.5 }); closeBg.width = 100; closeBg.height = 100; closeBg.tint = 0xFF4444; var closeText = new Text2('X', { size: 60, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = 850; closeButton.y = -1150; self.addChild(closeButton); closeButton.down = function () { self.destroy(); }; // Touch event handlers for scrolling self.down = function (x, y, obj) { var dx = x - closeButton.x; var dy = y - closeButton.y; if (Math.abs(dx) < 50 && Math.abs(dy) < 50) { return; } isDragging = true; startY = y; startContentY = contentContainer.y; velocity = 0; }; self.move = function (x, y, obj) { if (isDragging) { var deltaY = y - startY; var newY = startContentY + deltaY; newY = Math.max(maxY, Math.min(minY, newY)); contentContainer.y = newY; velocity = deltaY * 0.5; } }; self.up = function (x, y, obj) { isDragging = false; }; // Update function for momentum scrolling self.update = function () { if (!isDragging && Math.abs(velocity) > 0.1) { contentContainer.y += velocity; contentContainer.y = Math.max(maxY, Math.min(minY, contentContainer.y)); velocity *= 0.92; if (Math.abs(velocity) < 0.1) { velocity = 0; } } }; // Initial position self.x = 2048 / 2; self.y = 2732 / 2; return self; }); var ArtifactMenu = Container.expand(function () { var self = Container.call(this); // Background var background = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); background.width = 1600; background.height = 1200; background.tint = 0x222222; background.alpha = 0.95; // Title var titleText = new Text2('ARTIFACTS', { size: 80, fill: 0x00FFFF, weight: 800 }); titleText.anchor.set(0.5, 0); titleText.x = 0; titleText.y = -500; self.addChild(titleText); // Crystal display var crystalDisplay = new Text2('Crystals: ' + crystals, { size: 60, fill: 0x00FFFF, weight: 800 }); crystalDisplay.anchor.set(0.5, 0); crystalDisplay.x = 0; crystalDisplay.y = -400; self.addChild(crystalDisplay); // Artifacts var artifactData = [{ id: 'thunderbolt', name: 'Thunderbolt of Zeus', description: 'All projectiles stun enemies for 0.25s', owned: artifacts.thunderbolt, price: artifactPrices.thunderbolt }, { id: 'aresHelmet', name: "Ares' Helmet", description: 'All projectiles deal double damage', owned: artifacts.aresHelmet, price: artifactPrices.aresHelmet }, { id: 'ymirsHammer', name: "Ymir's Hammer", description: 'Start with 15 extra lives', owned: artifacts.ymirsHammer, price: artifactPrices.ymirsHammer }]; var artifactButtons = []; var yPos = -250; for (var i = 0; i < artifactData.length; i++) { var artifact = artifactData[i]; var artifactContainer = new Container(); artifactContainer.y = yPos; var artifactBg = artifactContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); artifactBg.width = 1400; artifactBg.height = 200; artifactBg.tint = artifact.owned ? 0x00AA00 : 0x444444; var nameText = new Text2(artifact.name, { size: 50, fill: 0xFFFFFF, weight: 800 }); nameText.anchor.set(0, 0.5); nameText.x = -650; nameText.y = -40; artifactContainer.addChild(nameText); var descText = new Text2(artifact.description, { size: 40, fill: 0xCCCCCC, weight: 400 }); descText.anchor.set(0, 0.5); descText.x = -650; descText.y = 20; artifactContainer.addChild(descText); var statusText = new Text2(artifact.owned ? 'OWNED' : artifact.price + ' Crystals', { size: 50, fill: artifact.owned ? 0x00FF00 : 0x00FFFF, weight: 800 }); statusText.anchor.set(1, 0.5); statusText.x = 650; statusText.y = 0; artifactContainer.addChild(statusText); if (!artifact.owned) { artifactContainer.artifactId = artifact.id; artifactContainer.price = artifact.price; artifactContainer.down = function () { if (crystals >= this.price) { crystals -= this.price; artifacts[this.artifactId] = true; storage.crystals = crystals; storage.artifacts = artifacts; // Refresh menu self.destroy(); var newMenu = new ArtifactMenu(); game.addChild(newMenu); } else { var notification = game.addChild(new Notification("Not enough crystals!")); notification.x = 2048 / 2; notification.y = grid.height - 50; LK.getSound('error').play(); } }; } self.addChild(artifactContainer); artifactButtons.push(artifactContainer); yPos += 250; } // Close button var closeButton = new Container(); var closeBg = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBg.width = 90; closeBg.height = 90; closeBg.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = background.width / 2 - 57; closeButton.y = -background.height / 2 + 57; self.addChild(closeButton); closeButton.down = function () { self.destroy(); }; self.x = 2048 / 2; self.y = 2732 / 2; 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; // Use default bullet asset initially, will be updated based on type var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { // Mortar projectile logic if (self.type === 'mortar') { var dx = self.targetX - self.x; var dy = self.targetY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Create explosion effect var mortarEffect = new EffectIndicator(self.targetX, self.targetY, 'mortar'); game.addChild(mortarEffect); // Area damage var explosionRadius = CELL_SIZE * 1.5; var hitAnyEnemy = false; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var enemyDx = enemy.x - self.targetX; var enemyDy = enemy.y - self.targetY; var enemyDist = Math.sqrt(enemyDx * enemyDx + enemyDy * enemyDy); if (enemyDist <= explosionRadius) { hitAnyEnemy = true; // Damage falls off with distance var damageFactor = 1 - enemyDist / explosionRadius * 0.5; enemy.health -= oneHitKillCheat ? 999999 : self.damage * damageFactor; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } } } // Achievement tracking: Miss if (!hitAnyEnemy) { achievementStats.mortarMisses++; if (!achievements.miss.completed) { achievements.miss.completed = true; achievements.miss.progress = 1; var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.miss.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; saveAchievements(); saveAchievementStats(); } } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } return; } if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; LK.getSound('enemy_hit').play(); // Apply Thunderbolt of Zeus stun effect if (artifacts.thunderbolt && !self.targetEnemy.isImmune && !self.targetEnemy.isStunned) { self.targetEnemy.isStunned = true; self.targetEnemy.stunDuration = 15; // 0.25 seconds at 60 FPS self.targetEnemy.originalSpeed = self.targetEnemy.originalSpeed || self.targetEnemy.speed; self.targetEnemy.speed = 0; // Visual effect LK.effects.flashObject(self.targetEnemy, 0xFFFF00, 250); } if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= oneHitKillCheat ? 999999 : self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Apply slow effect // Make slow percentage scale with tower level (default 50%, up to 80% at max level) var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6 var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; // Slow by X% self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { self.targetEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } else if (self.type === 'wizard') { // Check if tower has charm ability if (self.hasCharmAbility) { // Charm effect instead of confusion if (!self.targetEnemy.isCharmed && !self.targetEnemy.isImmune) { self.targetEnemy.isCharmed = true; self.targetEnemy.charmDuration = 300; // 5 seconds at 60 FPS self.targetEnemy.originalTeam = 'enemy'; // Visual effect for charm var charmEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'charm'); game.addChild(charmEffect); } } else { // Regular confusion effect if (Math.random() < 0.5 && !self.targetEnemy.isConfused && !self.targetEnemy.isImmune) { self.targetEnemy.isConfused = true; self.targetEnemy.confusionDuration = 180; // 3 seconds at 60 FPS // Visual effect for confusion var confuseEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'confuse'); game.addChild(confuseEffect); // Achievement tracking: Cowardly (wizard confusion streak) achievementStats.wizardConfusionStreak++; if (!achievements.cowardly.completed) { achievements.cowardly.progress = achievementStats.wizardConfusionStreak; if (achievements.cowardly.progress >= achievements.cowardly.target) { achievements.cowardly.completed = true; var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.cowardly.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; saveAchievements(); saveAchievementStats(); } } } else { // Reset streak if confusion didn't trigger achievementStats.wizardConfusionStreak = 0; } } } else if (self.type === 'rapid' && self.hasBleedAbility) { // 10% chance to apply bleed if (Math.random() < 0.1 && !self.targetEnemy.isBleeding && !self.targetEnemy.isImmune) { self.targetEnemy.isBleeding = true; self.targetEnemy.bleedDuration = 300; // 5 seconds at 60 FPS self.targetEnemy.bleedDamage = 5; // 5 damage per second // Visual effect for bleed var bleedEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'bleed'); game.addChild(bleedEffect); } } else if (self.type === 'slow' && self.hasPermafrostAbility) { // Permanent slow effect if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Apply permanent slow if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 0.2; // 80% slow self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 999999; // Permanent self.targetEnemy.permanentSlow = true; } } } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var CheatMenu = Container.expand(function () { var self = Container.call(this); // Background var background = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); background.width = 800; background.height = 600; background.tint = 0x222222; background.alpha = 0.95; // Title var titleText = new Text2('CHEATS', { size: 80, fill: 0xFF0000, weight: 800 }); titleText.anchor.set(0.5, 0); titleText.x = 0; titleText.y = -250; self.addChild(titleText); // Unlimited Money Toggle var moneyToggle = new Container(); var moneyBg = moneyToggle.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); moneyBg.width = 700; moneyBg.height = 120; moneyBg.tint = unlimitedMoneyCheat ? 0x00FF00 : 0x888888; var moneyText = new Text2('Unlimited Money: ' + (unlimitedMoneyCheat ? 'ON' : 'OFF'), { size: 50, fill: 0xFFFFFF, weight: 800 }); moneyText.anchor.set(0.5, 0.5); moneyToggle.addChild(moneyText); moneyToggle.y = -80; self.addChild(moneyToggle); moneyToggle.down = function () { unlimitedMoneyCheat = !unlimitedMoneyCheat; moneyBg.tint = unlimitedMoneyCheat ? 0x00FF00 : 0x888888; moneyText.setText('Unlimited Money: ' + (unlimitedMoneyCheat ? 'ON' : 'OFF')); if (unlimitedMoneyCheat) { setGold(999999); } }; // One Hit Kill Toggle var oneHitToggle = new Container(); var oneHitBg = oneHitToggle.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); oneHitBg.width = 700; oneHitBg.height = 120; oneHitBg.tint = oneHitKillCheat ? 0x00FF00 : 0x888888; var oneHitText = new Text2('One Hit Kills: ' + (oneHitKillCheat ? 'ON' : 'OFF'), { size: 50, fill: 0xFFFFFF, weight: 800 }); oneHitText.anchor.set(0.5, 0.5); oneHitToggle.addChild(oneHitText); oneHitToggle.y = 80; self.addChild(oneHitToggle); oneHitToggle.down = function () { oneHitKillCheat = !oneHitKillCheat; oneHitBg.tint = oneHitKillCheat ? 0x00FF00 : 0x888888; oneHitText.setText('One Hit Kills: ' + (oneHitKillCheat ? 'ON' : 'OFF')); }; // Close button var closeButton = new Container(); var closeBg = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBg.width = 90; closeBg.height = 90; closeBg.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = background.width / 2 - 57; closeButton.y = -background.height / 2 + 57; self.addChild(closeButton); closeButton.down = function () { self.destroy(); }; // Initial position (centered) self.x = 2048 / 2; self.y = 2732 / 2; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); numberLabel.visible = false; self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.visible = false; cellGraphics.tint = 0x880000; return; } numberLabel.visible = false; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; } else { cellGraphics.tint = 0x88 - tint << 8 | tint; } self.removeArrows(); break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0x008800; numberLabel.visible = false; break; } } }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'confuse': effectGraphics.tint = 0xFF1493; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.2; break; case 'mortar': effectGraphics.tint = 0x8B4513; effectGraphics.width = effectGraphics.height = CELL_SIZE * 3; break; case 'charm': effectGraphics.tint = 0xFFB6C1; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'bleed': effectGraphics.tint = 0x8B0000; effectGraphics.width = effectGraphics.height = CELL_SIZE * 0.8; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; self.isHidden = false; self.hideDuration = 0; self.hideTimer = 0; self.hasSpawned = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'flying_horde': self.isFlying = true; self.maxHealth = 25; // Very weak self.speed *= 1.5; // Fast break; case 'mole': self.maxHealth = 120; // Slightly tankier self.speed *= 0.8; // Slower when visible self.hideTimer = 300; // 5 seconds until first hide break; case 'mixed': // Mixed will be handled as a special spawner type self.maxHealth = 1; // Placeholder, should not be attacked self.isSpawner = true; break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle mole digging mechanics if (self.type === 'mole') { self.hideTimer--; if (self.hideTimer <= 0) { if (!self.isHidden) { // Start hiding (go underground) self.isHidden = true; self.hideDuration = 90; // 1.5 seconds at 60 FPS self.hideTimer = 300; // 5 seconds until next hide // Double speed while hidden if (self.originalSpeed === undefined) { self.originalSpeed = self.speed; } self.speed = self.originalSpeed * 2; // Visual effect - make semi-transparent and darker enemyGraphics.alpha = 0.3; enemyGraphics.tint = 0x444444; // Hide health bar while underground healthBarOutline.visible = false; healthBarBG.visible = false; healthBar.visible = false; } else { // Come back up (surface) self.isHidden = false; self.hideTimer = 300; // 5 seconds until next hide // Reset speed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } // Restore visibility enemyGraphics.alpha = 1; enemyGraphics.tint = 0xFFFFFF; // Show health bar again healthBarOutline.visible = true; healthBarBG.visible = true; healthBar.visible = true; } } else if (self.isHidden) { self.hideDuration--; if (self.hideDuration <= 0) { // Force surface if duration is over self.isHidden = false; self.hideTimer = 300; // Reset timer // Reset speed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } // Restore visibility enemyGraphics.alpha = 1; enemyGraphics.tint = 0xFFFFFF; // Show health bar again healthBarOutline.visible = true; healthBarBG.visible = true; healthBar.visible = true; } } } // Handle flying horde spawning if (self.type === 'flying_horde' && !self.hasSpawned && self.currentCellY >= 2) { self.hasSpawned = true; // Spawn 4-6 additional weak flying enemies var spawnCount = 4 + Math.floor(Math.random() * 3); for (var i = 0; i < spawnCount; i++) { var newEnemy = new Enemy('flying'); newEnemy.maxHealth = 15; // Very weak newEnemy.health = newEnemy.maxHealth; newEnemy.speed = self.speed * (0.8 + Math.random() * 0.4); // Varied speed // Position around the spawner var angle = Math.PI * 2 / spawnCount * i + Math.random() * 0.5; var distance = 50 + Math.random() * 30; newEnemy.cellX = self.cellX; newEnemy.cellY = self.cellY; newEnemy.currentCellX = self.currentCellX + Math.cos(angle) * distance / CELL_SIZE; newEnemy.currentCellY = self.currentCellY + Math.sin(angle) * distance / CELL_SIZE; newEnemy.x = grid.x + newEnemy.currentCellX * CELL_SIZE; newEnemy.y = grid.y + newEnemy.currentCellY * CELL_SIZE; newEnemy.waveNumber = self.waveNumber; // Add to flying layer enemyLayerTop.addChild(newEnemy); if (newEnemy.shadow) { enemyLayerMiddle.addChild(newEnemy.shadow); } enemies.push(newEnemy); } } // Handle confusion effect if (self.isConfused && !self.isImmune) { self.confusionDuration--; if (self.confusionDuration <= 0) { self.isConfused = false; // Reset pathfinding when confusion ends self.currentTarget = undefined; } } // Handle charm effect if (self.isCharmed && !self.isImmune) { self.charmDuration--; if (self.charmDuration <= 0) { self.isCharmed = false; self.originalTeam = 'enemy'; // Reset pathfinding when charm ends self.currentTarget = undefined; } } // Handle bleed effect if (self.isBleeding && !self.isImmune) { self.bleedDuration--; // Take 5 damage every second (60 ticks) if (self.bleedDuration % 60 === 0) { self.health -= oneHitKillCheat ? 999999 : self.bleedDamage; if (self.health <= 0) { self.health = 0; } else { self.healthBar.width = self.health / self.maxHealth * 70; } // Visual feedback LK.effects.flashObject(self, 0x8B0000, 100); } if (self.bleedDuration <= 0) { self.isBleeding = false; } } // Handle stun effect if (self.isStunned && !self.isImmune) { self.stunDuration--; if (self.stunDuration <= 0) { self.isStunned = false; if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed, clear any such effects self.slowed = false; self.slowEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.isCharmed) { enemyGraphics.tint = 0xFFB6C1; // Light pink for charmed } else if (self.isConfused) { enemyGraphics.tint = 0xFF1493; // Pink for confused } else if (self.isBleeding) { enemyGraphics.tint = 0x8B0000; // Dark red for bleeding } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; if (i > 11 - 3 && i <= 11 + 3) { if (j === 0) { cellType = 2; self.spawns.push(cell); } else if (j <= 4) { cellType = 0; } else if (j === gridHeight - 1) { cellType = 3; self.goals.push(cell); } else if (j >= gridHeight - 4) { cellType = 0; } } cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 1 && node.left.type != 1) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.right.type != 1) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.right.type != 1) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.left.type != 1) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed; // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) var angle = Math.PI / 2; if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update enemy's position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // If enemy has now reached the entry area, update cell coordinates if (enemy.currentCellY >= 4) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (enemy.isCharmed) { // Charmed enemies attack other enemies var nearestEnemy = null; var nearestDist = Infinity; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== enemy && !otherEnemy.isCharmed) { var dx = otherEnemy.cellX - enemy.cellX; var dy = otherEnemy.cellY - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < nearestDist) { nearestDist = dist; nearestEnemy = otherEnemy; } } } if (nearestEnemy) { // Move towards the nearest enemy enemy.currentTarget = { x: nearestEnemy.cellX, y: nearestEnemy.cellY }; // Deal damage if close enough if (nearestDist < 2) { if (LK.ticks % 60 === 0) { // Attack once per second nearestEnemy.health -= oneHitKillCheat ? 999999 : 10; if (nearestEnemy.health <= 0) { nearestEnemy.health = 0; } else { nearestEnemy.healthBar.width = nearestEnemy.health / nearestEnemy.maxHealth * 70; } LK.effects.flashObject(nearestEnemy, 0xFFB6C1, 100); } } } } else if (enemy.isConfused) { // Confused enemies move towards the nearest tower var nearestTower = null; var nearestDist = Infinity; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.gridX - enemy.cellX; var dy = tower.gridY - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < nearestDist) { nearestDist = dist; nearestTower = tower; } } if (nearestTower) { // Set target to move towards the tower enemy.currentTarget = { x: nearestTower.gridX, y: nearestTower.gridY }; } } else if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { if (!enemy.isConfused && cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var LaserWall = Container.expand(function (tower1, tower2) { var self = Container.call(this); self.tower1 = tower1; self.tower2 = tower2; self.damage = 18; // Base damage per tick (increased by 2) self.level = 1; self.maxLevel = 6; // Create laser visual var laserGraphics = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); laserGraphics.tint = 0xFF0088; laserGraphics.alpha = 0.8; // Position between the two towers self.updatePosition = function () { self.x = Math.min(self.tower1.x, self.tower2.x); self.y = self.tower1.y; var distance = Math.abs(self.tower2.x - self.tower1.x); laserGraphics.width = distance; laserGraphics.height = CELL_SIZE * 0.5; }; self.updatePosition(); // Level indicators var levelIndicators = []; var maxDots = self.maxLevel; var dotSize = CELL_SIZE / 8; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 2; outlineCircle.height = dotSize + 2; outlineCircle.tint = 0x000000; var levelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); levelIndicator.width = dotSize; levelIndicator.height = dotSize; levelIndicator.tint = 0xCCCCCC; dot.y = -CELL_SIZE * 0.4; self.addChild(dot); levelIndicators.push(dot); } self.updateLevelIndicators = function () { var centerX = laserGraphics.width / 2; var spacing = laserGraphics.width / (maxDots + 1); for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; dot.x = spacing * (i + 1); var levelIndicator = dot.children[1]; if (i < self.level) { levelIndicator.tint = 0xFFFFFF; } else { levelIndicator.tint = 0xFF0088; } } }; self.updateLevelIndicators(); self.upgrade = function () { if (self.level < self.maxLevel) { var upgradeCost = Math.floor(40 * Math.pow(2, self.level - 1)); if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(upgradeCost * 3.5 / 2); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; self.damage = 18 + self.level * 24; // Increased base by 2 and maintain scaling // Unlock special ability at level 3 if (self.level === 3) { self.hasSpecialAbility = true; var notification = game.addChild(new Notification("Laser Wall: Sky Piercer unlocked!")); notification.x = 2048 / 2; notification.y = grid.height - 100; } self.updateLevelIndicators(); // Visual feedback tween(laserGraphics, { scaleY: 1.5, alpha: 1 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(laserGraphics, { scaleY: 1, alpha: 0.8 }, { duration: 200, easing: tween.easeIn }); } }); return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade laser wall!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.getTotalValue = function () { var totalInvestment = 0; for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(40 * Math.pow(2, i - 1)); } return totalInvestment; }; self.update = function () { // Check for enemies passing through the laser for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Skip flying enemies unless laser wall has special ability if (enemy.isFlying && !self.hasSpecialAbility) { continue; } // Skip hidden moles unless laser wall has special ability if (enemy.isHidden && !self.hasSpecialAbility) { continue; } // Check if enemy is in the same row as the laser if (Math.abs(enemy.y - self.y) < CELL_SIZE / 2) { // Check if enemy is within the laser's horizontal range if (enemy.x >= self.x && enemy.x <= self.x + laserGraphics.width) { // Deal damage every 30 ticks (0.5 seconds) if (LK.ticks % 30 === 0) { enemy.health -= oneHitKillCheat ? 999999 : self.damage; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } // Visual effect LK.effects.flashObject(enemy, 0xFF0088, 100); // Play laser sound occasionally if (LK.ticks % 120 === 0) { LK.getSound('laser_beam').play(); } } } } } // Pulse effect laserGraphics.alpha = 0.8 + Math.sin(LK.ticks * 0.1) * 0.2; }; self.down = function (x, y, obj) { // Show upgrade menu for laser wall var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } selectedTower = self; var upgradeMenu = new LaserWallUpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 275 }, { duration: 200, easing: tween.backOut }); }; return self; }); var LaserWallUpgradeMenu = Container.expand(function (laserWall) { var self = Container.call(this); self.laserWall = laserWall; self.y = 2732 + 275; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var titleText = new Text2('Laser Wall', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0, 0); titleText.x = -840; titleText.y = -160; self.addChild(titleText); var statsText = new Text2('Level: ' + self.laserWall.level + '/' + self.laserWall.maxLevel + '\nDamage: ' + self.laserWall.damage + '/0.5s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); // Upgrade button var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.laserWall.level >= self.laserWall.maxLevel; var upgradeCost = 0; if (!isMaxLevel) { upgradeCost = Math.floor(40 * Math.pow(2, self.laserWall.level - 1)); if (self.laserWall.level === self.laserWall.maxLevel - 1) { upgradeCost = Math.floor(upgradeCost * 3.5 / 2); } } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); // Sell button var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var sellValue = getTowerSellValue(self.laserWall.getTotalValue()); var sellButtonText = new Text2('Remove Wall: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; // Close button var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function () { if (self.laserWall.level >= self.laserWall.maxLevel) { var notification = game.addChild(new Notification("Laser wall is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.laserWall.upgrade()) { statsText.setText('Level: ' + self.laserWall.level + '/' + self.laserWall.maxLevel + '\nDamage: ' + self.laserWall.damage + '/0.5s'); if (self.laserWall.level >= self.laserWall.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } else { var newCost = Math.floor(40 * Math.pow(2, self.laserWall.level - 1)); if (self.laserWall.level === self.laserWall.maxLevel - 1) { newCost = Math.floor(newCost * 3.5 / 2); } buttonText.setText('Upgrade: ' + newCost + ' gold'); } var newSellValue = getTowerSellValue(self.laserWall.getTotalValue()); sellButtonText.setText('Remove Wall: +' + newSellValue + ' gold'); } }; sellButton.down = function () { var sellValue = getTowerSellValue(self.laserWall.getTotalValue()); setGold(gold + sellValue); var notification = game.addChild(new Notification("Laser wall removed for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Remove laser wall from game var wallIndex = laserWalls.indexOf(self.laserWall); if (wallIndex !== -1) { laserWalls.splice(wallIndex, 1); } game.removeChild(self.laserWall); self.destroy(); selectedTower = null; }; closeButton.down = function () { hideUpgradeMenu(self); selectedTower = null; }; self.update = function () { if (self.laserWall.level >= self.laserWall.maxLevel) { return; } var currentUpgradeCost = Math.floor(40 * Math.pow(2, self.laserWall.level - 1)); if (self.laserWall.level === self.laserWall.maxLevel - 1) { currentUpgradeCost = Math.floor(currentUpgradeCost * 3.5 / 2); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; }; return self; }); var Manual = Container.expand(function () { var self = Container.call(this); // Manual background var background = self.attachAsset('manualBackground', { anchorX: 0.5, anchorY: 0.5 }); background.alpha = 0.95; // Title var titleText = new Text2('TOWER DEFENSE MANUAL', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0); titleText.x = 0; titleText.y = -1150; self.addChild(titleText); // Create scrollable content container var contentContainer = new Container(); self.addChild(contentContainer); var yPos = -1000; var sectionSpacing = 350; // Scrolling variables var isDragging = false; var startY = 0; var startContentY = 0; var velocity = 0; var minY = -1000; // Top limit var maxY = 0; // Will be calculated based on content height var contentHeight = 0; // Will be calculated after adding all content // Tower Classes Section self.createSection = function (title, items, startY) { var sectionY = startY; // Section title var sectionTitle = new Text2(title, { size: 60, fill: 0xFFD700, weight: 800 }); sectionTitle.anchor.set(0.5, 0); sectionTitle.x = 0; sectionTitle.y = sectionY; contentContainer.addChild(sectionTitle); sectionY += 80; // Items for (var i = 0; i < items.length; i++) { var item = items[i]; // Item background var itemBg = contentContainer.attachAsset('manualSection', { anchorX: 0.5, anchorY: 0 }); itemBg.x = 0; itemBg.y = sectionY; itemBg.height = 200; itemBg.alpha = 0.3; // Item name var itemName = new Text2(item.name, { size: 50, fill: 0xFFFFFF, weight: 800 }); itemName.anchor.set(0, 0); itemName.x = -800; itemName.y = sectionY + 20; contentContainer.addChild(itemName); // Item stats var itemStats = new Text2(item.stats, { size: 40, fill: 0xCCCCCC, weight: 400 }); itemStats.anchor.set(0, 0); itemStats.x = -800; itemStats.y = sectionY + 80; contentContainer.addChild(itemStats); sectionY += 220; } return sectionY + 50; }; // Tower data var towerData = [{ name: 'BOW (Default)', stats: 'Cost: 5 gold • Damage: 10 • Range: 3 tiles • Rate: 1/s\nBalanced tower good for early game' }, { name: 'CROSSBOW (Rapid)', stats: 'Cost: 15 gold • Damage: 5 • Range: 2.5 tiles • Rate: 2/s\nFast firing tower, good against swarms' }, { name: 'SNIPER', stats: 'Cost: 25 gold • Damage: 25 • Range: 5 tiles • Rate: 0.67/s\nLong range, high damage, slow firing' }, { name: 'CANNON (Splash)', stats: 'Cost: 35 gold • Damage: 15 • Range: 2 tiles • Rate: 0.8/s\nArea damage affects nearby enemies' }, { name: 'ICE WIZARD (Slow)', stats: 'Cost: 45 gold • Damage: 8 • Range: 3.5 tiles • Rate: 1.2/s\nSlows enemies, ineffective vs immune' }, { name: 'FARM', stats: 'Cost: 30 gold • Income: 10 gold/wave (+15 per level)\nGenerates passive income, no combat' }, { name: 'LASER BASE ALPHA', stats: 'Cost: 40 gold • Requires 2+ in same row\nCreates laser wall, blocks ground enemies' }, { name: 'INFERNO (Single)', stats: 'Cost: 50 gold • Damage: 5 DPS • Range: 4 tiles\nContinuous beam that drains health' }, { name: 'TRICKY WIZARD (Weakening)', stats: 'Cost: 40 gold • Damage: 10 • Range: 3 tiles\n50% chance to confuse enemies' }, { name: 'CHURCH (Income)', stats: 'Cost: 60 gold • Gives 1 life per wave\nNo combat ability, provides lives' }, { name: 'MORTAR (Area)', stats: 'Cost: 55 gold • Damage: 30 • Range: 7 tiles\nHigh range area damage, min range 2 tiles' }]; // Enemy data var enemyData = [{ name: 'NORMAL', stats: 'Health: 100 • Speed: Normal • Ground unit\nBasic enemy type' }, { name: 'FAST', stats: 'Health: 100 • Speed: 2x Normal • Ground unit\nQuick moving enemy' }, { name: 'IMMUNE', stats: 'Health: 80 • Speed: Normal • Ground unit\nImmune to slow effects' }, { name: 'FLYING', stats: 'Health: 80 • Speed: Normal • Flying unit\nFlies over obstacles, immune to laser walls' }, { name: 'SWARM', stats: 'Health: 50 • Speed: Normal • Ground unit\nWeaker but comes in large numbers' }, { name: 'FLYING HORDE', stats: 'Health: 25 • Speed: 1.5x Normal • Flying unit\nSpawner creates multiple weak flying enemies' }, { name: 'MOLE DIGGER', stats: 'Health: 120 • Speed: 0.8x/2x Normal • Ground unit\nHides underground periodically, untargetable while hidden' }, { name: 'BOSS', stats: 'Health: 20x Normal • Speed: 0.7x Normal\nAppears every 10th wave, massive health' }, { name: 'MIXED', stats: 'Spawns various random enemy types\nUnpredictable wave composition' }]; // Create sections yPos = self.createSection('TOWER TYPES', towerData, yPos); yPos = self.createSection('ENEMY TYPES', enemyData, yPos); // Game mechanics section var mechanicsData = [{ name: 'TOWER TARGETING', stats: 'First: Closest to exit • Last: Farthest from exit\nStrong: Highest HP • Crowd: Most nearby enemies' }, { name: 'WAVE PROGRESSION', stats: 'Waves 1-10: Fixed types to introduce mechanics\nAfter wave 10: Random types except boss waves' }, { name: 'DIFFICULTY SCALING', stats: 'Enemy health increases 12% per wave\nBoss waves every 10th wave' }, { name: 'TOWER UPGRADES', stats: 'Level 1-5: Exponential cost increase\nLevel 6: Extra powerful, costs 3.5x previous' }, { name: 'SELLING TOWERS', stats: 'Returns 60% of total investment\nStrategic for repositioning' }]; yPos = self.createSection('GAME MECHANICS', mechanicsData, yPos); // Calculate content height and set scroll limits contentHeight = yPos + 1000; // yPos is negative, so we add back the initial offset maxY = Math.min(0, -contentHeight + 1800); // 1800 is approximate visible height // Close button var closeButton = new Container(); self.addChild(closeButton); var closeBg = closeButton.attachAsset('manualButton', { anchorX: 0.5, anchorY: 0.5 }); closeBg.width = 100; closeBg.height = 100; closeBg.tint = 0xFF4444; var closeText = new Text2('X', { size: 60, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = 850; closeButton.y = -1150; closeButton.down = function () { self.destroy(); }; // Touch event handlers for scrolling self.down = function (x, y, obj) { // Don't start scrolling if clicking on the close button // Check if click is on close button using passed x,y coordinates var dx = x - closeButton.x; var dy = y - closeButton.y; if (Math.abs(dx) < 50 && Math.abs(dy) < 50) { return; } isDragging = true; startY = y; startContentY = contentContainer.y; velocity = 0; }; self.move = function (x, y, obj) { if (isDragging) { var deltaY = y - startY; var newY = startContentY + deltaY; // Apply bounds newY = Math.max(maxY, Math.min(minY, newY)); contentContainer.y = newY; // Calculate velocity for momentum velocity = deltaY * 0.5; } }; self.up = function (x, y, obj) { isDragging = false; }; // Update function for momentum scrolling self.update = function () { if (!isDragging && Math.abs(velocity) > 0.1) { // Apply momentum contentContainer.y += velocity; // Apply bounds contentContainer.y = Math.max(maxY, Math.min(minY, contentContainer.y)); // Apply friction velocity *= 0.92; // Stop if velocity is too small if (Math.abs(velocity) < 0.1) { velocity = 0; } } }; // Initial position (hidden) self.x = 2048 / 2; // Center horizontally self.y = 2732 + 1200; // Animate in tween(self, { y: 2732 / 2 }, { duration: 300, easing: tween.backOut }); return self; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Use specific tower asset based on type var towerAssetId = 'tower_' + self.towerType; if (self.towerType === 'default') { towerAssetId = 'tower_default'; } // Increase size of base for easier touch var baseGraphics = self.attachAsset(towerAssetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); // No need to tint anymore as we have specific assets var towerCost = getTowerCost(self.towerType); // Get display name for tower type var displayName = self.towerType; switch (self.towerType) { case 'default': displayName = 'Bow'; break; case 'rapid': displayName = 'Crossbow'; break; case 'splash': displayName = 'Cannon'; break; case 'slow': displayName = 'Ice Wizard'; break; case 'farm': displayName = 'Farm'; break; case 'laser': displayName = 'Laser Base'; break; case 'inferno': displayName = 'Inferno'; break; case 'wizard': displayName = 'Tricky Wizard'; break; case 'church': displayName = 'Church'; break; case 'mortar': displayName = 'Mortar'; break; case 'gatling': displayName = 'Gatling Gun'; break; case 'swordsman': displayName = 'Swordsman'; break; } // Add shadow for tower type label var typeLabelShadow = new Text2(displayName, { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(displayName, { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -20; // Position above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; self.targetingMode = 'first'; // Options: 'first', 'last', 'strong', 'crowd' self.hasSpecialAbility = false; // Unlocked at max level self.specialAbilityActive = false; self.specialAbilityTimer = 0; self.rapidBurstCount = 0; // For archer rapid fire self.infernoTargets = []; // For inferno multi-target // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case 'sniper': // Sniper: base 5, +0.8 per level, infinite range with special ability if (self.hasSpecialAbility) { return 999 * CELL_SIZE; // Effectively infinite range } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case 'splash': // Splash: base 2, +0.2 per level (max ~4 blocks at max level) return (2 + (self.level - 1) * 0.2) * CELL_SIZE; case 'rapid': // Rapid: base 2.5, +0.5 per level return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'slow': // Slow: base 3.5, +0.5 per level return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'farm': // Farm has no range return 0; case 'laser': // Laser has no shooting range return 0; case 'inferno': // Inferno: base 4, +0.3 per level return (4 + (self.level - 1) * 0.3) * CELL_SIZE; case 'wizard': // Wizard: base 3, +0.4 per level return (3 + (self.level - 1) * 0.4) * CELL_SIZE; case 'church': // Church has no range return 0; case 'mortar': // Mortar: base 7, +0.5 per level (high range) return (7 + (self.level - 1) * 0.5) * CELL_SIZE; case 'gatling': // Gatling: base 2.5, +0.3 per level return (2.5 + (self.level - 1) * 0.3) * CELL_SIZE; case 'swordsman': // Swordsman: base 1.5, +0.2 per level (very close range) return (1.5 + (self.level - 1) * 0.2) * CELL_SIZE; default: // Default: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 30; self.damage = 5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'sniper': self.fireRate = 90; self.damage = 25; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'splash': self.fireRate = 75; self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; case 'slow': self.fireRate = 50; self.damage = 8; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'farm': self.fireRate = 999999; // Farm doesn't shoot self.damage = 0; self.range = 0; self.bulletSpeed = 0; break; case 'laser': self.fireRate = 999999; // Laser doesn't shoot normally self.damage = 0; self.range = 0; self.bulletSpeed = 0; break; case 'inferno': self.fireRate = 999999; // Inferno uses continuous beam self.damage = 5; // Base DPS self.range = 4 * CELL_SIZE; self.bulletSpeed = 0; break; case 'wizard': self.fireRate = 120; // Slower fire rate self.damage = 10; self.range = 3 * CELL_SIZE; self.bulletSpeed = 6; break; case 'church': self.fireRate = 999999; // Church doesn't shoot self.damage = 0; self.range = 0; self.bulletSpeed = 0; break; case 'mortar': self.fireRate = 180; // Very slow fire rate self.damage = 30; self.range = 7 * CELL_SIZE; self.minRange = 2 * CELL_SIZE; // Can't hit close enemies self.bulletSpeed = 3; break; case 'gatling': self.fireRate = 8; // Extremely fast fire rate self.damage = 3; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 12; break; case 'swordsman': self.fireRate = 999999; // Doesn't shoot bullets self.damage = 8; // Base damage per tick self.range = 1.5 * CELL_SIZE; // Very close range self.bulletSpeed = 0; break; } // Use specific tower asset based on type var towerAssetId = 'tower_' + self.id; if (self.id === 'default') { towerAssetId = 'tower_default'; } var baseGraphics = self.attachAsset(towerAssetId, { anchorX: 0.5, anchorY: 0.5 }); // No need to tint anymore as we have specific assets var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 6; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); // Use specific defense asset based on type var defenseAssetId = 'defense_' + self.id; if (self.id === 'default') { defenseAssetId = 'defense_default'; } var gunGraphics = gunContainer.attachAsset(defenseAssetId, { anchorX: 0.5, anchorY: 0.5 }); self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { switch (self.id) { case 'rapid': towerLevelIndicator.tint = 0x00AAFF; break; case 'sniper': towerLevelIndicator.tint = 0xFF5500; break; case 'splash': towerLevelIndicator.tint = 0x33CC00; break; case 'slow': towerLevelIndicator.tint = 0x9900FF; break; case 'farm': towerLevelIndicator.tint = 0xFFD700; // Gold color break; case 'laser': towerLevelIndicator.tint = 0xFF0088; // Pink color break; case 'inferno': towerLevelIndicator.tint = 0xFF4500; // Orange-red break; case 'wizard': towerLevelIndicator.tint = 0xFF1493; // Deep pink break; case 'church': towerLevelIndicator.tint = 0xFFD700; // Gold break; case 'mortar': towerLevelIndicator.tint = 0x8B4513; // Saddle brown break; case 'gatling': towerLevelIndicator.tint = 0x708090; // Slate gray break; case 'swordsman': towerLevelIndicator.tint = 0x8B4513; // Saddle brown break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); // Create inferno beam visual if this is an inferno tower if (self.id === 'inferno') { self.infernoBeam = self.attachAsset('infernoBeam', { anchorX: 0, anchorY: 0.5 }); self.infernoBeam.visible = false; self.infernoBeam.alpha = 0.8; self.infernoDamageTimer = 0; } // Create spinning sword visual if this is a swordsman tower if (self.id === 'swordsman') { self.sword = self.attachAsset('defense_swordsman', { anchorX: 0.5, anchorY: 0.5 }); self.sword.alpha = 0.8; self.sword.tint = 0xC0C0C0; // Silver color self.swordsmanDamageTimer = 0; } self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.getSpecialAbilityName = function () { switch (self.id) { case 'default': return 'Rapid Fire'; case 'rapid': return 'Bleeding Shots'; case 'sniper': return 'Infinite Range'; case 'inferno': return 'Triple Beam'; case 'splash': return 'Mega Blast'; case 'laser': return 'Sky Piercer'; case 'mortar': return 'Quad Shot'; case 'slow': return 'Permafrost'; case 'wizard': return 'Charm'; case 'farm': return 'Gold Boost'; case 'church': return 'Divine Intervention'; case 'gatling': return 'Bullet Storm'; case 'swordsman': return 'Whirlwind Strike'; default: return null; } }; self.getSpecialAbilityCost = function () { // Special ability costs 3x the base tower cost return getTowerCost(self.id) * 3; }; self.unlockSpecialAbility = function () { if (self.level >= 3 && !self.hasSpecialAbility) { var cost = self.getSpecialAbilityCost(); if (gold >= cost) { setGold(gold - cost); self.hasSpecialAbility = true; // Show special ability unlocked notification var abilityName = self.getSpecialAbilityName(); if (abilityName) { var notification = game.addChild(new Notification("Special Ability Unlocked: " + abilityName)); notification.x = 2048 / 2; notification.y = grid.height - 100; } // Check if tower is now at max level with special ability (award crystal) if (self.level === self.maxLevel && self.hasSpecialAbility && !unlimitedMoneyCheat && !oneHitKillCheat) { crystals += 1; storage.crystals = crystals; updateUI(); var crystalNotification = game.addChild(new Notification("+1 Crystal for MAX tower!")); crystalNotification.x = 2048 / 2; crystalNotification.y = grid.height - 150; } return true; } else { var notification = game.addChild(new Notification("Not enough gold for special ability!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; LK.getSound('tower_upgrade').play(); // Special ability is unlocked separately at level 3 (no longer automatic) // Special ability unlocking is now handled by separate purchase button // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rapid') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect self.damage = 5 + self.level * 10; // double the effect self.bulletSpeed = 7 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades self.damage = 5 + self.level * 3; self.bulletSpeed = 7 + self.level * 0.7; } } else if (self.id === 'inferno') { // Inferno tower damage scales differently (DPS) self.damage = 5 + self.level * 3; } else if (self.id === 'wizard') { if (self.level === self.maxLevel) { self.fireRate = Math.max(30, 120 - self.level * 30); self.damage = 10 + self.level * 15; self.bulletSpeed = 6 + self.level * 1.5; } else { self.fireRate = Math.max(60, 120 - self.level * 10); self.damage = 10 + self.level * 5; self.bulletSpeed = 6 + self.level * 0.5; } } else if (self.id === 'mortar') { if (self.level === self.maxLevel) { self.fireRate = Math.max(60, 180 - self.level * 40); self.damage = 30 + self.level * 25; self.bulletSpeed = 3 + self.level * 1.0; } else { self.fireRate = Math.max(120, 180 - self.level * 10); self.damage = 30 + self.level * 8; self.bulletSpeed = 3 + self.level * 0.3; } } else if (self.id === 'gatling') { if (self.level === self.maxLevel) { self.fireRate = Math.max(2, 8 - self.level * 2); // Extremely fast at max level self.damage = 3 + self.level * 8; self.bulletSpeed = 12 + self.level * 3; } else { self.fireRate = Math.max(5, 8 - self.level * 1); self.damage = 3 + self.level * 2; self.bulletSpeed = 12 + self.level * 1; } } else if (self.id === 'swordsman') { // Swordsman damage scales with level (no fire rate since it's area damage) self.damage = 8 + self.level * 4; } 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(); // Check if tower reached max level with special ability (award crystal) if (self.level === self.maxLevel && self.hasSpecialAbility && !unlimitedMoneyCheat && !oneHitKillCheat) { crystals += 1; storage.crystals = crystals; updateUI(); var crystalNotification = game.addChild(new Notification("+1 Crystal for MAX tower!")); crystalNotification.x = 2048 / 2; crystalNotification.y = grid.height - 150; // Achievement tracking: Power Engineer if (!achievements.power_engineer.completed) { achievements.power_engineer.completed = true; achievements.power_engineer.progress = 1; var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.power_engineer.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Achievement tracking: All of Em' var towerTypeName = self.id === 'default' ? 'default' : self.id; if (achievementStats.maxLevelTowerTypes.indexOf(towerTypeName) === -1) { achievementStats.maxLevelTowerTypes.push(towerTypeName); } if (!achievements.all_of_em.completed) { achievements.all_of_em.progress = achievementStats.maxLevelTowerTypes.length; if (achievements.all_of_em.progress >= achievements.all_of_em.target) { achievements.all_of_em.completed = true; var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.all_of_em.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 250; } } saveAchievements(); saveAchievementStats(); } if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var enemiesInRange = []; // First, collect all enemies in range for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range and not hidden underground if (distance <= self.getRange() && !enemy.isHidden) { // Mortar can't hit enemies that are too close if (self.id === 'mortar' && distance < self.minRange) { continue; } enemiesInRange.push({ enemy: enemy, distance: distance, dx: dx, dy: dy }); } } if (enemiesInRange.length === 0) { self.targetEnemy = null; return null; } var targetEnemy = null; switch (self.targetingMode) { case 'first': // Target enemy closest to the base (lowest path score or closest to goal for flying) var lowestScore = Infinity; for (var i = 0; i < enemiesInRange.length; i++) { var enemyData = enemiesInRange[i]; var enemy = enemyData.enemy; if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < lowestScore) { lowestScore = distToGoal; targetEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (enemyData.distance < lowestScore) { lowestScore = enemyData.distance; targetEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < lowestScore) { lowestScore = cell.score; targetEnemy = enemy; } } } } break; case 'last': // Target enemy farthest from the base (highest path score) var highestScore = -Infinity; for (var i = 0; i < enemiesInRange.length; i++) { var enemyData = enemiesInRange[i]; var enemy = enemyData.enemy; if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal (inverse) if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use inverse distance to goal as score if (distToGoal > highestScore) { highestScore = distToGoal; targetEnemy = enemy; } } } else { // For ground enemies var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Higher score means farther from exit if (cell.score > highestScore) { highestScore = cell.score; targetEnemy = enemy; } } } } break; case 'strong': // Target enemy with the most HP var mostHP = -1; for (var i = 0; i < enemiesInRange.length; i++) { var enemy = enemiesInRange[i].enemy; if (enemy.health > mostHP) { mostHP = enemy.health; targetEnemy = enemy; } } break; case 'crowd': // Target enemy with the most other enemies nearby var maxCrowdScore = -1; for (var i = 0; i < enemiesInRange.length; i++) { var enemyData = enemiesInRange[i]; var enemy = enemyData.enemy; var crowdScore = 0; // Count enemies within 2 cells of this enemy for (var j = 0; j < enemies.length; j++) { if (enemies[j] !== enemy) { var edx = enemies[j].x - enemy.x; var edy = enemies[j].y - enemy.y; var edist = Math.sqrt(edx * edx + edy * edy); if (edist <= CELL_SIZE * 2) { crowdScore++; } } } if (crowdScore > maxCrowdScore) { maxCrowdScore = crowdScore; targetEnemy = enemy; } } break; } // For inferno tower with special ability, find up to 3 targets if (self.id === 'inferno' && self.hasSpecialAbility && enemiesInRange.length > 0) { self.infernoTargets = []; // Sort enemies by distance enemiesInRange.sort(function (a, b) { return a.distance - b.distance; }); // Get up to 3 targets for (var i = 0; i < Math.min(3, enemiesInRange.length); i++) { self.infernoTargets.push(enemiesInRange[i].enemy); } return self.infernoTargets[0]; // Return primary target } return targetEnemy; }; self.update = function () { // Farm towers don't target or shoot if (self.id === 'farm') { return; } // Laser towers are handled separately if (self.id === 'laser') { return; } // Church towers don't shoot if (self.id === 'church') { return; } // Inferno tower uses continuous beam if (self.id === 'inferno') { self.targetEnemy = self.findTarget(); if (self.targetEnemy) { // Show and position beam self.infernoBeam.visible = true; var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); var distance = Math.sqrt(dx * dx + dy * dy); self.infernoBeam.rotation = angle; self.infernoBeam.width = distance; gunContainer.rotation = angle; // Deal damage every 10 ticks self.infernoDamageTimer++; if (self.infernoDamageTimer >= 10) { self.infernoDamageTimer = 0; // Calculate damage based on level var damagePerTick = oneHitKillCheat ? 999999 : self.damage + self.level * 3; self.targetEnemy.health -= damagePerTick; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Visual effect LK.effects.flashObject(self.targetEnemy, 0xFF4500, 100); // Handle multi-target ability if (self.hasSpecialAbility && self.infernoTargets.length > 1) { // Damage additional targets (50% damage) for (var i = 1; i < self.infernoTargets.length; i++) { var additionalTarget = self.infernoTargets[i]; if (additionalTarget && additionalTarget.parent && !additionalTarget.isHidden) { additionalTarget.health -= oneHitKillCheat ? 999999 : damagePerTick * 0.5; if (additionalTarget.health <= 0) { additionalTarget.health = 0; } else { additionalTarget.healthBar.width = additionalTarget.health / additionalTarget.maxHealth * 70; } LK.effects.flashObject(additionalTarget, 0xFF4500, 100); } } } } } else { self.infernoBeam.visible = false; self.infernoDamageTimer = 0; self.infernoTargets = []; } return; } // Swordsman tower uses spinning sword area damage if (self.id === 'swordsman') { // Continuously spin the sword self.sword.rotation += 0.3; // Fast spinning // Deal damage every 15 ticks (0.25 seconds) self.swordsmanDamageTimer++; if (self.swordsmanDamageTimer >= 15) { self.swordsmanDamageTimer = 0; // Find all enemies in range var enemiesInRange = []; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.getRange() && !enemy.isHidden) { enemiesInRange.push(enemy); } } // Damage all enemies in range for (var i = 0; i < enemiesInRange.length; i++) { var enemy = enemiesInRange[i]; var damagePerTick = oneHitKillCheat ? 999999 : self.damage + self.level * 2; enemy.health -= damagePerTick; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } // Visual effect LK.effects.flashObject(enemy, 0xC0C0C0, 100); } // Play sound if enemies were hit if (enemiesInRange.length > 0) { LK.getSound('tower_shoot').play(); } } return; } self.targetEnemy = self.findTarget(); if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); gunContainer.rotation = angle; // Handle rapid burst fire for archer if (self.rapidBurstCount > 0) { if (LK.ticks - self.lastFired >= 6) { // Very fast fire rate during burst self.fire(); self.lastFired = LK.ticks; self.rapidBurstCount--; if (self.rapidBurstCount === 0) { self.specialAbilityActive = false; } } } else if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 325 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { // Handle archer rapid fire ability if (self.id === 'default' && self.hasSpecialAbility && Math.random() < 0.25) { self.rapidBurstCount = 5; self.specialAbilityActive = true; } // Handle mortar quad shot ability if (self.id === 'mortar' && self.hasSpecialAbility) { // Fire 4 projectiles at once if (self.targetEnemy) { for (var i = 0; i < 4; i++) { var offsetAngle = (i - 1.5) * 0.3; // Spread the shots var targetX = self.targetEnemy.x + Math.cos(offsetAngle) * 50; var targetY = self.targetEnemy.y + Math.sin(offsetAngle) * 50; var bulletX = self.x + Math.cos(gunContainer.rotation + offsetAngle) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation + offsetAngle) * 40; var bullet = new Bullet(bulletX, bulletY, null, self.damage, self.bulletSpeed); bullet.type = 'mortar'; bullet.targetX = targetX; bullet.targetY = targetY; // Replace bullet graphics bullet.removeChild(bullet.children[0]); var mortarGraphics = bullet.attachAsset('bullet_mortar', { anchorX: 0.5, anchorY: 0.5 }); game.addChild(bullet); bullets.push(bullet); } } return; } // Handle cannon mega blast ability if (self.id === 'splash' && self.hasSpecialAbility && self.specialAbilityTimer <= 0) { self.fireRate = 25; // Triple fire rate self.specialAbilityTimer = 180; // 3 second duration } if (self.id === 'splash' && self.specialAbilityTimer > 0) { self.specialAbilityTimer--; if (self.specialAbilityTimer === 0) { self.fireRate = 75; // Reset to normal } } if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bulletDamage = oneHitKillCheat ? 999999 : artifacts.aresHelmet ? self.damage * 2 : self.damage; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, bulletDamage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; bullet.hasPermafrostAbility = self.hasSpecialAbility; } else if (self.id === 'rapid') { bullet.hasBleedAbility = self.hasSpecialAbility; } else if (self.id === 'wizard') { bullet.hasCharmAbility = self.hasSpecialAbility; } // For mortar, store target position instead of enemy if (self.id === 'mortar') { bullet.targetX = self.targetEnemy.x; bullet.targetY = self.targetEnemy.y; bullet.targetEnemy = null; // Mortar targets location, not enemy } // Replace bullet graphics with tower-specific asset var bulletAssetId = 'bullet'; if (self.id !== 'default') { bulletAssetId = 'bullet_' + self.id; } // Remove the old generic bullet graphic bullet.removeChild(bullet.children[0]); // Attach the specific bullet asset var specificBulletGraphics = bullet.attachAsset(bulletAssetId, { anchorX: 0.5, anchorY: 0.5 }); game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // Play shooting sound based on tower type if (self.id === 'sniper') { LK.getSound('tower_shoot').play(); } else if (self.id === 'splash' || self.id === 'mortar') { LK.getSound('explosion').play(); } else if (self.id === 'slow') { LK.getSound('freeze').play(); } else if (self.id === 'wizard') { LK.getSound('charm').play(); } else { LK.getSound('tower_shoot').play(); } // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; } } } self.refreshCellsInRange(); }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; self.hasEnoughGold = true; var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; break; case 'sniper': previewGraphics.tint = 0xFF5500; break; case 'splash': previewGraphics.tint = 0x33CC00; break; case 'slow': previewGraphics.tint = 0x9900FF; break; case 'farm': previewGraphics.tint = 0xFFD700; // Gold color break; case 'laser': previewGraphics.tint = 0xFF0088; // Pink color break; case 'inferno': previewGraphics.tint = 0xFF4500; // Orange-red break; case 'wizard': previewGraphics.tint = 0xFF1493; // Deep pink break; case 'church': previewGraphics.tint = 0xFFD700; // Gold break; case 'mortar': previewGraphics.tint = 0x8B4513; // Saddle brown break; case 'gatling': previewGraphics.tint = 0x708090; // Slate gray break; case 'swordsman': previewGraphics.tint = 0x8B4513; // Saddle brown break; default: previewGraphics.tint = 0xAAAAAA; } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) { validGridPlacement = false; } else { for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (!cell || cell.type !== 0) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) { continue; } // Only check non-flying enemies, flying enemies can pass over towers if (!enemy.isFlying) { if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) { self.blockedByEnemy = true; break; } } } } } self.canPlace = validGridPlacement && !self.blockedByEnemy; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; self.checkPlacement(); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 325; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 700; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; // Get display name for tower type var displayName = self.tower.id; switch (self.tower.id) { case 'default': displayName = 'Bow'; break; case 'rapid': displayName = 'Crossbow'; break; case 'splash': displayName = 'Cannon'; break; case 'slow': displayName = 'Ice Wizard'; break; case 'farm': displayName = 'Farm'; break; case 'laser': displayName = 'Laser Base Alpha'; break; case 'inferno': displayName = 'Inferno'; break; case 'wizard': displayName = 'Tricky Wizard'; break; case 'church': displayName = 'Church'; break; case 'mortar': displayName = 'Mortar'; break; case 'gatling': displayName = 'Gatling Gun'; break; case 'swordsman': displayName = 'Swordsman'; break; } var towerTypeText = new Text2(displayName + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2(self.tower.id === 'inferno' ? 'Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + (self.tower.damage + self.tower.level * 3) + ' DPS\nRange: ' + (self.tower.getRange() / CELL_SIZE).toFixed(1) + ' tiles' : self.tower.id === 'church' ? 'Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nLives per wave: ' + self.tower.level + '\nNo combat ability' : 'Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); // Special ability button var specialButton = new Container(); buttonsContainer.addChild(specialButton); var specialBackground = specialButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); specialBackground.width = 500; specialBackground.height = 150; var hasAbility = self.tower.hasSpecialAbility; var abilityName = self.tower.getSpecialAbilityName(); var canUnlock = self.tower.level >= 3 && !hasAbility; var abilityCost = canUnlock ? self.tower.getSpecialAbilityCost() : 0; var lockText = self.tower.level < 3 ? 'Locked (Lv 3)' : canUnlock ? 'Buy: ' + abilityCost + ' gold' : 'Locked'; specialBackground.tint = hasAbility ? 0x9400D3 : canUnlock ? gold >= abilityCost ? 0x00AA00 : 0x888888 : 0x444444; var specialText = new Text2(hasAbility ? abilityName : lockText, { size: 60, fill: 0xFFFFFF, weight: 800 }); specialText.anchor.set(0.5, 0.5); specialButton.addChild(specialText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); // Targeting mode button var targetingButton = new Container(); buttonsContainer.addChild(targetingButton); var targetingBackground = targetingButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); targetingBackground.width = 500; targetingBackground.height = 150; targetingBackground.tint = 0x0088FF; var targetingModeNames = { 'first': 'First', 'last': 'Last', 'strong': 'Strong', 'crowd': 'Crowd' }; var targetingText = new Text2('Target: ' + targetingModeNames[self.tower.targetingMode], { size: 60, fill: 0xFFFFFF, weight: 800 }); targetingText.anchor.set(0.5, 0.5); targetingButton.addChild(targetingText); upgradeButton.y = -210; specialButton.y = -70; targetingButton.y = 70; sellButton.y = 210; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { LK.getSound('button_click').play(); if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } if (self.tower.id === 'inferno') { statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + (self.tower.damage + self.tower.level * 3) + ' DPS\nRange: ' + (self.tower.getRange() / CELL_SIZE).toFixed(1) + ' tiles'); } else if (self.tower.id === 'church') { statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nLives per wave: ' + self.tower.level + '\nNo combat ability'); } else { statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); } // Update special ability button when upgrading if (self.tower.hasSpecialAbility && !hasAbility) { specialBackground.tint = 0x9400D3; specialText.setText(self.tower.getSpecialAbilityName()); } buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; specialButton.down = function () { if (self.tower.hasSpecialAbility) { var notification = game.addChild(new Notification("Special Ability: " + self.tower.getSpecialAbilityName())); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (self.tower.level >= 3) { // Try to purchase special ability if (self.tower.unlockSpecialAbility()) { // Update button appearance after successful purchase specialBackground.tint = 0x9400D3; specialText.setText(self.tower.getSpecialAbilityName()); } } else { var notification = game.addChild(new Notification("Reach level 3 to unlock special ability!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; targetingButton.down = function () { // Cycle through targeting modes var modes = ['first', 'last', 'strong', 'crowd']; var currentIndex = modes.indexOf(self.tower.targetingMode); var nextIndex = (currentIndex + 1) % modes.length; self.tower.targetingMode = modes[nextIndex]; var targetingModeNames = { 'first': 'First', 'last': 'Last', 'strong': 'Strong', 'crowd': 'Crowd' }; targetingText.setText('Target: ' + targetingModeNames[self.tower.targetingMode]); // Visual feedback tween(targetingButton, { scaleX: 1.1, scaleY: 1.1 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(targetingButton, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); // Update laser walls if we removed a laser tower if (self.tower.id === 'laser') { updateLaserWalls(); } self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } } else { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } } // Update special ability button if (!self.tower.hasSpecialAbility && self.tower.level >= 3) { var abilityCost = self.tower.getSpecialAbilityCost(); var canAffordAbility = gold >= abilityCost; specialBackground.tint = canAffordAbility ? 0x00AA00 : 0x888888; var newSpecialText = 'Buy: ' + abilityCost + ' gold'; if (specialText.text !== newSpecialText) { specialText.setText(newSpecialText); } } }; return self; }); var VillageBuilderMenu = Container.expand(function () { var self = Container.call(this); // Background var background = self.attachAsset('manualBackground', { anchorX: 0.5, anchorY: 0.5 }); background.alpha = 0.95; // Title var titleText = new Text2('VILLAGE BUILDER', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0); titleText.x = 0; titleText.y = -1150; self.addChild(titleText); // Crystal display var crystalDisplay = new Text2('Crystals: ' + crystals, { size: 60, fill: 0x00FFFF, weight: 800 }); crystalDisplay.anchor.set(0.5, 0); crystalDisplay.x = 0; crystalDisplay.y = -1000; self.addChild(crystalDisplay); // Building data var buildingData = [{ id: 'town_hall', name: 'Town Hall', description: 'Creates 5 crystals every 30 minutes', cost: 0, owned: villageBuildings.town_hall ? 1 : 0, maxOwned: 1 }, { id: 'house', name: 'House', description: 'Makes 1 crystal every 30 minutes', cost: 10, owned: villageBuildings.house || 0, maxOwned: 10 }, { id: 'crystal_mine', name: 'Crystal Mine', description: 'Creates 2 crystals every 15 minutes', cost: 25, owned: villageBuildings.crystal_mine || 0, maxOwned: 5 }, { id: 'defense_tower', name: 'Defense Tower', description: 'Defensive structure (cosmetic for now)', cost: 15, owned: villageBuildings.defense_tower || 0, maxOwned: 8 }, { id: 'barracks', name: 'Barracks', description: 'Military building (cosmetic for now)', cost: 20, owned: villageBuildings.barracks || 0, maxOwned: 3 }]; var buildingButtons = []; var yPos = -850; for (var i = 0; i < buildingData.length; i++) { var building = buildingData[i]; var buildingContainer = new Container(); buildingContainer.y = yPos; var buildingBg = buildingContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buildingBg.width = 1400; buildingBg.height = 180; buildingBg.tint = building.owned >= building.maxOwned ? 0x444444 : crystals >= building.cost ? 0x00AA00 : 0x888888; var nameText = new Text2(building.name + ' (' + building.owned + '/' + building.maxOwned + ')', { size: 50, fill: 0xFFFFFF, weight: 800 }); nameText.anchor.set(0, 0.5); nameText.x = -650; nameText.y = -30; buildingContainer.addChild(nameText); var descText = new Text2(building.description, { size: 40, fill: 0xCCCCCC, weight: 400 }); descText.anchor.set(0, 0.5); descText.x = -650; descText.y = 20; buildingContainer.addChild(descText); var costText = new Text2(building.owned >= building.maxOwned ? 'MAX BUILT' : building.cost === 0 ? 'FREE' : building.cost + ' Crystals', { size: 50, fill: building.owned >= building.maxOwned ? 0x888888 : building.cost === 0 ? 0x00FF00 : 0x00FFFF, weight: 800 }); costText.anchor.set(1, 0.5); costText.x = 650; costText.y = 0; buildingContainer.addChild(costText); if (building.owned < building.maxOwned) { buildingContainer.buildingId = building.id; buildingContainer.cost = building.cost; buildingContainer.down = function () { if (crystals >= this.cost || this.cost === 0) { crystals -= this.cost; villageBuildings[this.buildingId] = (villageBuildings[this.buildingId] || 0) + 1; // Update last collection time for new buildings var currentTime = Date.now(); if (this.buildingId === 'town_hall') { villageLastCollection.town_hall = currentTime; } else if (this.buildingId === 'house') { var houseKey = 'house_' + (villageBuildings.house - 1); villageLastCollection[houseKey] = currentTime; } else if (this.buildingId === 'crystal_mine') { var mineKey = 'crystal_mine_' + (villageBuildings.crystal_mine - 1); villageLastCollection[mineKey] = currentTime; } storage.crystals = crystals; storage.villageBuildings = villageBuildings; // Save collection times using flattened structure storage.villageLastCollection_town_hall = villageLastCollection.town_hall; if (this.buildingId === 'house') { var houseKey = 'house_' + (villageBuildings.house - 1); storage['villageLastCollection_' + houseKey] = villageLastCollection[houseKey]; } else if (this.buildingId === 'crystal_mine') { var mineKey = 'crystal_mine_' + (villageBuildings.crystal_mine - 1); storage['villageLastCollection_' + mineKey] = villageLastCollection[mineKey]; } // Refresh menu self.destroy(); var newMenu = new VillageBuilderMenu(); game.addChild(newMenu); } else { var notification = game.addChild(new Notification("Not enough crystals!")); notification.x = 2048 / 2; notification.y = grid.height - 50; LK.getSound('error').play(); } }; } self.addChild(buildingContainer); buildingButtons.push(buildingContainer); yPos += 200; } // Collect all button var collectButton = new Container(); collectButton.y = yPos + 50; var collectBg = collectButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); collectBg.width = 600; collectBg.height = 120; collectBg.tint = 0x00FF00; var collectText = new Text2('COLLECT ALL CRYSTALS', { size: 50, fill: 0xFFFFFF, weight: 800 }); collectText.anchor.set(0.5, 0.5); collectButton.addChild(collectText); collectButton.down = function () { var totalCollected = collectVillageCrystals(); if (totalCollected > 0) { crystals += totalCollected; storage.crystals = crystals; var notification = game.addChild(new Notification("Collected " + totalCollected + " crystals!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Refresh menu self.destroy(); var newMenu = new VillageBuilderMenu(); game.addChild(newMenu); } else { var notification = game.addChild(new Notification("No crystals ready to collect!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; self.addChild(collectButton); // Close button var closeButton = new Container(); var closeBg = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBg.width = 90; closeBg.height = 90; closeBg.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = background.width / 2 - 57; closeButton.y = -background.height / 2 + 57; self.addChild(closeButton); closeButton.down = function () { self.destroy(); }; self.x = 2048 / 2; self.y = 2732 / 2; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; LK.getSound('wave_start').play(); } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (i === 5) { block.tint = 0x00FFFF; waveType = "Flying Horde"; enemyType = "flying_horde"; enemyCount = 3; // Few spawners that create many } else if (i === 6) { block.tint = 0x8B4513; waveType = "Mole Digger"; enemyType = "mole"; enemyCount = 8; } else if (i === 7) { block.tint = 0xFFFFFF; waveType = "Mixed"; enemyType = "mixed"; enemyCount = 12; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0xFFFF00; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Boss Normal"; break; case 'fast': block.tint = 0x00AAFF; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xAA0000; waveType = "Boss Immune"; break; case 'flying': block.tint = 0xFFFF00; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the color of the wave type switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; break; case 'fast': block.tint = 0x00AAFF; break; case 'immune': block.tint = 0xAA0000; break; case 'flying': block.tint = 0xFFFF00; break; default: block.tint = 0xFF0000; break; } } else if (i + 1 > 10) { // After wave 10, all non-boss waves are completely random var randomWaveTypes = [{ type: 'normal', name: 'Normal', tint: 0xAAAAAA, count: 10 }, { type: 'fast', name: 'Fast', tint: 0x00AAFF, count: 10 }, { type: 'immune', name: 'Immune', tint: 0xAA0000, count: 10 }, { type: 'flying', name: 'Flying', tint: 0xFFFF00, count: 10 }, { type: 'swarm', name: 'Swarm', tint: 0xFF00FF, count: 30 }, { type: 'flying_horde', name: 'Flying Horde', tint: 0x00FFFF, count: 3 }, { type: 'mole', name: 'Mole Digger', tint: 0x8B4513, count: 8 }, { type: 'mixed', name: 'Mixed', tint: 0xFFFFFF, count: 12 }]; var randomChoice = randomWaveTypes[Math.floor(Math.random() * randomWaveTypes.length)]; enemyType = randomChoice.type; waveType = randomChoice.name; block.tint = randomChoice.tint; enemyCount = randomChoice.count; } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if ((i + 1) % 8 === 0) { // Every 8th non-boss wave is flying horde block.tint = 0x00FFFF; waveType = "Flying Horde"; enemyType = "flying_horde"; enemyCount = 3; } else if ((i + 1) % 9 === 0) { // Every 9th non-boss wave is mole digger block.tint = 0x8B4513; waveType = "Mole Digger"; enemyType = "mole"; enemyCount = 8; } else if ((i + 1) % 6 === 0) { // Every 6th non-boss wave is mixed block.tint = 0xFFFFFF; waveType = "Mixed"; enemyType = "mixed"; enemyCount = 12; } else { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a crown or some indicator to the wave marker for boss waves var bossIndicator = marker.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); bossIndicator.width = 30; bossIndicator.height = 30; bossIndicator.tint = 0xFFD700; // Gold color bossIndicator.y = -block.height / 2 - 15; // Change the wave type text to indicate boss waveType = "BOSS"; } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2(waveType, { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (every 10th wave) if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; for (var i = 0; i < totalWaves + 1; i++) { var marker = self.waveMarkers[i]; if (i === 0) { continue; } var block = marker.children[0]; if (i - 1 < currentWave) { block.alpha = .5; } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { waveTimer++; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; if (currentWave != 1) { var waveType = self.getWaveTypeName(currentWave); var enemyCount = self.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } } }; self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 325 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var laserWalls = []; var selectedTower = null; var gold = 100; // Crystal and artifact system var crystals = storage.crystals || 0; var artifacts = storage.artifacts || { thunderbolt: false, aresHelmet: false, ymirsHammer: false }; // Village builder system var villageBuildings = storage.villageBuildings || { town_hall: 0, house: 0, crystal_mine: 0, defense_tower: 0, barracks: 0 }; // Load village collection times from flattened storage var villageLastCollection = { town_hall: storage.villageLastCollection_town_hall || 0 }; // Load house collection times for (var i = 0; i < 10; i++) { var houseKey = 'house_' + i; var storageKey = 'villageLastCollection_' + houseKey; if (storage[storageKey]) { villageLastCollection[houseKey] = storage[storageKey]; } } // Load crystal mine collection times for (var i = 0; i < 5; i++) { var mineKey = 'crystal_mine_' + i; var storageKey = 'villageLastCollection_' + mineKey; if (storage[storageKey]) { villageLastCollection[mineKey] = storage[storageKey]; } } // Ensure first town hall is free if (villageBuildings.town_hall === 0) { villageBuildings.town_hall = 1; villageLastCollection.town_hall = Date.now(); storage.villageBuildings = villageBuildings; storage.villageLastCollection_town_hall = villageLastCollection.town_hall; } var lives = artifacts.ymirsHammer ? 35 : 20; var score = 0; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave // Cheat variables var unlimitedMoneyCheat = false; var oneHitKillCheat = false; var artifactPrices = { thunderbolt: 50, aresHelmet: 100, ymirsHammer: 75 }; // Achievement system - flattened structure for storage compatibility var achievements = { builder: { completed: storage.achievement_builder_completed || false, progress: storage.achievement_builder_progress || 0, target: 10, name: "Builder", description: "Build 10 towers" }, slayer: { completed: storage.achievement_slayer_completed || false, progress: storage.achievement_slayer_progress || 0, target: 200, name: "Slayer", description: "Kill 200 enemies" }, power_engineer: { completed: storage.achievement_power_engineer_completed || false, progress: storage.achievement_power_engineer_progress || 0, target: 1, name: "Power Engineer", description: "Make a tower be at max level + have the special ability" }, all_of_em: { completed: storage.achievement_all_of_em_completed || false, progress: storage.achievement_all_of_em_progress || 0, target: 11, name: "All of Em'", description: "Make all types of towers be at max level+ have their special ability" }, winner: { completed: storage.achievement_winner_completed || false, progress: storage.achievement_winner_progress || 0, target: 1, name: "Winner", description: "Defeat all waves" }, loser: { completed: storage.achievement_loser_completed || false, progress: storage.achievement_loser_progress || 0, target: 3, name: "Loser", description: "Get defeated 3 times in a row while having spent at least 1000 gold in defenses" }, farmer: { completed: storage.achievement_farmer_completed || false, progress: storage.achievement_farmer_progress || 0, target: 1000, name: "Farmer", description: "Collect 1000 gold worth of Farm Income" }, miss: { completed: storage.achievement_miss_completed || false, progress: storage.achievement_miss_progress || 0, target: 1, name: "Miss", description: "Make a Mortar Tower miss" }, cowardly: { completed: storage.achievement_cowardly_completed || false, progress: storage.achievement_cowardly_progress || 0, target: 2, name: "Cowardly", description: "Make a magician tower hit 2 enemies in a row with a projectile with confusion" } }; // Function to save achievements to flattened storage function saveAchievements() { storage.achievement_builder_completed = achievements.builder.completed; storage.achievement_builder_progress = achievements.builder.progress; storage.achievement_slayer_completed = achievements.slayer.completed; storage.achievement_slayer_progress = achievements.slayer.progress; storage.achievement_power_engineer_completed = achievements.power_engineer.completed; storage.achievement_power_engineer_progress = achievements.power_engineer.progress; storage.achievement_all_of_em_completed = achievements.all_of_em.completed; storage.achievement_all_of_em_progress = achievements.all_of_em.progress; storage.achievement_winner_completed = achievements.winner.completed; storage.achievement_winner_progress = achievements.winner.progress; storage.achievement_loser_completed = achievements.loser.completed; storage.achievement_loser_progress = achievements.loser.progress; storage.achievement_farmer_completed = achievements.farmer.completed; storage.achievement_farmer_progress = achievements.farmer.progress; storage.achievement_miss_completed = achievements.miss.completed; storage.achievement_miss_progress = achievements.miss.progress; storage.achievement_cowardly_completed = achievements.cowardly.completed; storage.achievement_cowardly_progress = achievements.cowardly.progress; } // Function to save achievement stats to flattened storage function saveAchievementStats() { storage.achievementStats_totalTowersBuilt = achievementStats.totalTowersBuilt; storage.achievementStats_totalEnemiesKilled = achievementStats.totalEnemiesKilled; storage.achievementStats_totalFarmIncome = achievementStats.totalFarmIncome; storage.achievementStats_consecutiveDefeats = achievementStats.consecutiveDefeats; storage.achievementStats_lastGameResult = achievementStats.lastGameResult; storage.achievementStats_totalGoldSpentOnDefenses = achievementStats.totalGoldSpentOnDefenses; storage.achievementStats_lastGameGoldSpent = achievementStats.lastGameGoldSpent; storage.achievementStats_mortarMisses = achievementStats.mortarMisses; storage.achievementStats_wizardConfusionStreak = achievementStats.wizardConfusionStreak; storage.achievementStats_maxLevelTowerTypes = achievementStats.maxLevelTowerTypes; } var achievementStats = { totalTowersBuilt: storage.achievementStats_totalTowersBuilt || 0, totalEnemiesKilled: storage.achievementStats_totalEnemiesKilled || 0, totalFarmIncome: storage.achievementStats_totalFarmIncome || 0, consecutiveDefeats: storage.achievementStats_consecutiveDefeats || 0, lastGameResult: storage.achievementStats_lastGameResult || 'none', totalGoldSpentOnDefenses: storage.achievementStats_totalGoldSpentOnDefenses || 0, lastGameGoldSpent: storage.achievementStats_lastGameGoldSpent || 0, mortarMisses: storage.achievementStats_mortarMisses || 0, wizardConfusionStreak: storage.achievementStats_wizardConfusionStreak || 0, maxLevelTowerTypes: storage.achievementStats_maxLevelTowerTypes || [] }; var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; var crystalText = new Text2('Crystals: ' + crystals, { size: 60, fill: 0x00FFFF, weight: 800 }); crystalText.anchor.set(0.5, 0.5); crystalText.x = spacing; crystalText.y = topMargin + 70; LK.gui.top.addChild(crystalText); function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); crystalText.setText('Crystals: ' + crystals); } function collectVillageCrystals() { var currentTime = Date.now(); var totalCrystals = 0; var minutesToMs = 60 * 1000; var maxOfflineMs = 4 * 60 * minutesToMs; // 4 hours cap // Town hall: 5 crystals every 30 minutes if (villageBuildings.town_hall > 0) { var townHallInterval = 30 * minutesToMs; var townHallLastTime = villageLastCollection.town_hall || 0; var elapsedTime = Math.min(currentTime - townHallLastTime, maxOfflineMs); var townHallCycles = Math.floor(elapsedTime / townHallInterval); if (townHallCycles > 0) { totalCrystals += townHallCycles * 5; villageLastCollection.town_hall = townHallLastTime + townHallCycles * townHallInterval; } } // Houses: 1 crystal every 30 minutes each if (villageBuildings.house > 0) { var houseInterval = 30 * minutesToMs; for (var i = 0; i < villageBuildings.house; i++) { var houseKey = 'house_' + i; var houseLastTime = villageLastCollection[houseKey] || currentTime; var elapsedTime = Math.min(currentTime - houseLastTime, maxOfflineMs); var houseCycles = Math.floor(elapsedTime / houseInterval); if (houseCycles > 0) { totalCrystals += houseCycles * 1; villageLastCollection[houseKey] = houseLastTime + houseCycles * houseInterval; } } } // Crystal mines: 2 crystals every 15 minutes each if (villageBuildings.crystal_mine > 0) { var mineInterval = 15 * minutesToMs; for (var i = 0; i < villageBuildings.crystal_mine; i++) { var mineKey = 'crystal_mine_' + i; var mineLastTime = villageLastCollection[mineKey] || currentTime; var elapsedTime = Math.min(currentTime - mineLastTime, maxOfflineMs); var mineCycles = Math.floor(elapsedTime / mineInterval); if (mineCycles > 0) { totalCrystals += mineCycles * 2; villageLastCollection[mineKey] = mineLastTime + mineCycles * mineInterval; } } } // Save updated collection times using flattened structure storage.villageLastCollection_town_hall = villageLastCollection.town_hall; // Save house collection times for (var i = 0; i < villageBuildings.house; i++) { var houseKey = 'house_' + i; if (villageLastCollection[houseKey]) { storage['villageLastCollection_' + houseKey] = villageLastCollection[houseKey]; } } // Save crystal mine collection times for (var i = 0; i < villageBuildings.crystal_mine; i++) { var mineKey = 'crystal_mine_' + i; if (villageLastCollection[mineKey]) { storage['villageLastCollection_' + mineKey] = villageLastCollection[mineKey]; } } return totalCrystals; } function setGold(value) { if (unlimitedMoneyCheat) { gold = 999999; } else { gold = value; } updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY) { var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 5; switch (towerType) { case 'rapid': cost = 15; break; case 'sniper': cost = 25; break; case 'splash': cost = 35; break; case 'slow': cost = 45; break; case 'farm': cost = 30; break; case 'laser': cost = 40; break; case 'inferno': cost = 50; break; case 'wizard': cost = 40; break; case 'church': cost = 60; break; case 'mortar': cost = 55; break; case 'gatling': cost = 20; break; case 'swordsman': cost = 25; break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function updateLaserWalls() { // Remove all existing laser walls for (var i = laserWalls.length - 1; i >= 0; i--) { game.removeChild(laserWalls[i]); } laserWalls = []; // Group laser towers by row var laserTowersByRow = {}; for (var i = 0; i < towers.length; i++) { if (towers[i].id === 'laser') { var row = towers[i].gridY; if (!laserTowersByRow[row]) { laserTowersByRow[row] = []; } laserTowersByRow[row].push(towers[i]); } } // Create laser walls for rows with 2 or more laser towers for (var row in laserTowersByRow) { var rowTowers = laserTowersByRow[row]; if (rowTowers.length >= 2) { // Sort towers by x position rowTowers.sort(function (a, b) { return a.x - b.x; }); // Create walls between adjacent towers for (var i = 0; i < rowTowers.length - 1; i++) { var wall = new LaserWall(rowTowers[i], rowTowers[i + 1]); game.addChild(wall); laserWalls.push(wall); } } } } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); LK.getSound('place_tower').play(); grid.pathFind(); grid.renderDebug(); // Update laser walls if we placed a laser tower if (towerType === 'laser') { updateLaserWalls(); } // Achievement tracking: Builder achievementStats.totalTowersBuilt++; achievementStats.totalGoldSpentOnDefenses += towerCost; achievementStats.lastGameGoldSpent += towerCost; if (!achievements.builder.completed) { achievements.builder.progress = achievementStats.totalTowersBuilt; if (achievements.builder.progress >= achievements.builder.target) { achievements.builder.completed = true; var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.builder.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 100; } } saveAchievements(); saveAchievementStats(); return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; LK.getSound('error').play(); return false; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } // Check if clicking on a class button for (var i = 0; i < classButtons.length; i++) { var button = classButtons[i]; if (x >= button.x - button.width / 2 && x <= button.x + button.width / 2 && y >= button.y - button.height / 2 && y <= button.y + button.height / 2) { // Class button was clicked, let it handle the event return; } } for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); // Apply the same offset as in move handler to ensure consistency when starting drag towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); break; } } }; game.move = function (x, y, obj) { if (isDragging) { // Shift the y position upward by 1.5 tiles to show preview above finger towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } }; game.up = function (x, y, obj) { var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; if (towerPreview.canPlace) { if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; if (isDragging) { var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } } }; var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2; waveIndicator.y = 2732 - 80; game.addChild(waveIndicator); var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); // Tower classes configuration var towerClasses = { 'Single Damage': { towers: ['default', 'rapid', 'sniper', 'inferno', 'gatling'], color: 0x4488FF }, 'Area Damage': { towers: ['splash', 'laser', 'mortar', 'swordsman'], color: 0xFF8844 }, 'Weakening': { towers: ['slow', 'wizard'], color: 0x8844FF }, 'Income/Lives': { towers: ['farm', 'church'], color: 0x44FF88 } }; var sourceTowers = []; var classButtons = []; var currentClass = null; var towerSelectionContainer = new Container(); game.addChild(towerSelectionContainer); // Create class buttons var classButtonWidth = 400; var classButtonHeight = 80; var classButtonSpacing = 20; var classNames = Object.keys(towerClasses); var totalClassWidth = classNames.length * classButtonWidth + (classNames.length - 1) * classButtonSpacing; var classStartX = 2048 / 2 - totalClassWidth / 2 + classButtonWidth / 2; var classButtonY = 2732 - CELL_SIZE * 3 - 180; for (var i = 0; i < classNames.length; i++) { var className = classNames[i]; var classData = towerClasses[className]; var classButton = new Container(); classButton.className = className; classButton.classData = classData; var buttonBg = classButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBg.width = classButtonWidth; buttonBg.height = classButtonHeight; buttonBg.tint = classData.color; buttonBg.alpha = 0.7; var buttonText = new Text2(className, { size: 40, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); classButton.addChild(buttonText); classButton.x = classStartX + i * (classButtonWidth + classButtonSpacing); classButton.y = classButtonY; classButton.down = function () { selectTowerClass(this.className); }; towerSelectionContainer.addChild(classButton); classButtons.push(classButton); } // Function to select a tower class function selectTowerClass(className) { // Update class button appearances for (var i = 0; i < classButtons.length; i++) { var button = classButtons[i]; var bg = button.children[0]; if (button.className === className) { bg.alpha = 1.0; bg.tint = button.classData.color; } else { bg.alpha = 0.3; bg.tint = 0x666666; } } // Clear existing source towers for (var i = 0; i < sourceTowers.length; i++) { towerLayer.removeChild(sourceTowers[i]); } sourceTowers = []; // Create towers for selected class currentClass = className; var classData = towerClasses[className]; var towerTypes = classData.towers; var towerSpacing = 300; var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } } // Select first class by default selectTowerClass(classNames[0]); // Play theme music LK.playMusic('game_theme'); // Create manual button var manualButton = new Container(); var manualBg = manualButton.attachAsset('manualButton', { anchorX: 0.5, anchorY: 0.5 }); manualBg.tint = 0x4CAF50; var manualText = new Text2('?', { size: 70, fill: 0xFFFFFF, weight: 800 }); manualText.anchor.set(0.5, 0.5); manualButton.addChild(manualText); manualButton.x = 2048 - 80; manualButton.y = 150; manualButton.down = function () { var manual = new Manual(); game.addChild(manual); }; game.addChild(manualButton); // Create artifact button var artifactButton = new Container(); var artifactBg = artifactButton.attachAsset('manualButton', { anchorX: 0.5, anchorY: 0.5 }); artifactBg.tint = 0x00FFFF; var artifactText = new Text2('A', { size: 70, fill: 0xFFFFFF, weight: 800 }); artifactText.anchor.set(0.5, 0.5); artifactButton.addChild(artifactText); artifactButton.x = 2048 - 320; artifactButton.y = 150; artifactButton.down = function () { var artifactMenu = new ArtifactMenu(); game.addChild(artifactMenu); }; game.addChild(artifactButton); // Create cheat button var cheatButton = new Container(); var cheatBg = cheatButton.attachAsset('manualButton', { anchorX: 0.5, anchorY: 0.5 }); cheatBg.tint = 0xFF0000; var cheatText = new Text2('C', { size: 70, fill: 0xFFFFFF, weight: 800 }); cheatText.anchor.set(0.5, 0.5); cheatButton.addChild(cheatText); cheatButton.x = 2048 - 200; cheatButton.y = 150; cheatButton.down = function () { var cheatMenu = new CheatMenu(); game.addChild(cheatMenu); }; game.addChild(cheatButton); // Create village builder button var villageButton = new Container(); var villageBg = villageButton.attachAsset('manualButton', { anchorX: 0.5, anchorY: 0.5 }); villageBg.tint = 0x8B4513; var villageText = new Text2('V', { size: 70, fill: 0xFFFFFF, weight: 800 }); villageText.anchor.set(0.5, 0.5); villageButton.addChild(villageText); villageButton.x = 2048 - 440; villageButton.y = 150; villageButton.down = function () { // Collect crystals automatically when opening village var collected = collectVillageCrystals(); if (collected > 0) { crystals += collected; storage.crystals = crystals; updateUI(); var notification = game.addChild(new Notification("Auto-collected " + collected + " crystals!")); notification.x = 2048 / 2; notification.y = grid.height - 100; } var villageMenu = new VillageBuilderMenu(); game.addChild(villageMenu); }; game.addChild(villageButton); // Create achievements button var achievementsButton = new Container(); var achievementsBg = achievementsButton.attachAsset('manualButton', { anchorX: 0.5, anchorY: 0.5 }); achievementsBg.tint = 0xFFD700; var achievementsText = new Text2('⚡', { size: 70, fill: 0xFFFFFF, weight: 800 }); achievementsText.anchor.set(0.5, 0.5); achievementsButton.addChild(achievementsText); achievementsButton.x = 2048 - 560; achievementsButton.y = 150; achievementsButton.down = function () { var achievementsMenu = new AchievementsMenu(); game.addChild(achievementsMenu); }; game.addChild(achievementsButton); sourceTower = null; enemiesToSpawn = 10; game.update = function () { if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; if (isBossWave && waveType !== 'swarm') { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; LK.getSound('boss_spawn').play(); } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy; if (waveType === 'mixed') { // Mixed wave spawns random enemy types var mixedTypes = ['normal', 'fast', 'immune', 'flying', 'swarm', 'mole']; var randomType = mixedTypes[Math.floor(Math.random() * mixedTypes.length)]; enemy = new Enemy(randomType); } else { enemy = new Enemy(waveType); } // Add enemy to the appropriate layer based on type if (enemy.isFlying) { // Add flying enemy to the top layer enemyLayerTop.addChild(enemy); // If it's a flying enemy, add its shadow to the middle layer if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else { // Add normal/ground enemies to the bottom layer enemyLayerBottom.addChild(enemy); } // Scale difficulty with wave number but don't apply to boss // as bosses already have their health multiplier // Use exponential scaling for health var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // All enemy types now spawn in the middle 6 tiles at the top spacing var gridWidth = 24; var midPoint = Math.floor(gridWidth / 2); // 12 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading enemy.cellX = spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; // Give income from all farm towers var totalFarmIncome = 0; for (var i = 0; i < towers.length; i++) { if (towers[i].id === 'farm') { // Base income 10, +15 per level var farmIncome = 10 + (towers[i].level - 1) * 15; totalFarmIncome += farmIncome; } } if (totalFarmIncome > 0) { setGold(gold + totalFarmIncome); var notification = game.addChild(new Notification("Farm income: +" + totalFarmIncome + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 250; // Achievement tracking: Farmer achievementStats.totalFarmIncome += totalFarmIncome; if (!achievements.farmer.completed) { achievements.farmer.progress = achievementStats.totalFarmIncome; if (achievements.farmer.progress >= achievements.farmer.target) { achievements.farmer.completed = true; var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.farmer.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 300; } } } // Give lives from all church towers var totalChurchLives = 0; var hasDivineIntervention = false; for (var i = 0; i < towers.length; i++) { if (towers[i].id === 'church') { // Church gives 1 life per wave at level 1, +1 per level upgrade totalChurchLives += towers[i].level; // Level 1 = 1 life, Level 2 = 2 lives, etc. // Check for divine intervention ability if (towers[i].hasSpecialAbility && Math.random() < 0.1) { // 10% chance hasDivineIntervention = true; } } } if (totalChurchLives > 0) { lives += totalChurchLives; updateUI(); var notification = game.addChild(new Notification("Church blessing: +" + totalChurchLives + " lives!")); notification.x = 2048 / 2; notification.y = grid.height - 300; } // Handle divine intervention if (hasDivineIntervention && currentWave < totalWaves) { // Skip the next wave and give 350 gold currentWave++; waveTimer = 0; setGold(gold + 350); var notification = game.addChild(new Notification("Divine Intervention! Wave skipped +350 gold!")); notification.x = 2048 / 2; notification.y = grid.height - 350; LK.getSound('divine_intervention').play(); } // Check for laser tower pairs and create/update laser walls updateLaserWalls(); } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Achievement tracking: Slayer achievementStats.totalEnemiesKilled++; if (!achievements.slayer.completed) { achievements.slayer.progress = achievementStats.totalEnemiesKilled; if (achievements.slayer.progress >= achievements.slayer.target) { achievements.slayer.completed = true; var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.slayer.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } // Boss enemies give more gold and score var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); // Check if killed by a tower in range of a farm with special ability var farmBonus = 0; for (var i = 0; i < towers.length; i++) { if (towers[i].id === 'farm' && towers[i].hasSpecialAbility) { // Check if any tower that could have killed this enemy is in range of the farm var farmX = towers[i].x; var farmY = towers[i].y; var farmRange = 3 * CELL_SIZE; // Farm boost range for (var j = 0; j < towers.length; j++) { if (towers[j].id !== 'farm') { var dx = towers[j].x - farmX; var dy = towers[j].y - farmY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= farmRange) { // Check if this tower could have killed the enemy var towerToEnemyDx = enemy.x - towers[j].x; var towerToEnemyDy = enemy.y - towers[j].y; var towerToEnemyDist = Math.sqrt(towerToEnemyDx * towerToEnemyDx + towerToEnemyDy * towerToEnemyDy); if (towerToEnemyDist <= towers[j].getRange()) { farmBonus = Math.ceil(goldEarned * 0.25); // 25% bonus break; } } } } if (farmBonus > 0) break; } } var totalGold = goldEarned + farmBonus; var goldIndicator = new GoldIndicator(totalGold, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + totalGold); LK.getSound('enemy_death').play(); LK.getSound('gold_collect').play(); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; // Award crystals for boss kill (only if no cheats active) if (!unlimitedMoneyCheat && !oneHitKillCheat) { crystals += 5; storage.crystals = crystals; updateUI(); var crystalNotification = game.addChild(new Notification("+5 Crystals!")); crystalNotification.x = 2048 / 2; crystalNotification.y = grid.height - 200; } } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { LK.getSound('game_over').play(); LK.stopMusic(); // Achievement tracking: Loser achievementStats.lastGameResult = 'lose'; if (achievementStats.lastGameGoldSpent >= 1000) { achievementStats.consecutiveDefeats++; if (!achievements.loser.completed) { achievements.loser.progress = achievementStats.consecutiveDefeats; if (achievements.loser.progress >= achievements.loser.target) { achievements.loser.completed = true; var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.loser.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } } else { achievementStats.consecutiveDefeats = 0; } // Reset game gold spent for next game achievementStats.lastGameGoldSpent = 0; saveAchievements(); saveAchievementStats(); LK.showGameOver(); } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } // Update all laser walls for (var i = 0; i < laserWalls.length; i++) { laserWalls[i].update(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.getSound('victory').play(); LK.stopMusic(); // Achievement tracking: Winner achievementStats.lastGameResult = 'win'; achievementStats.consecutiveDefeats = 0; if (!achievements.winner.completed) { achievements.winner.completed = true; achievements.winner.progress = 1; var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.winner.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 100; } saveAchievements(); saveAchievementStats(); LK.showYouWin(); } // Auto-collect village crystals every 5 minutes if (LK.ticks % (60 * 60 * 5) === 0) { var collected = collectVillageCrystals(); if (collected > 0) { crystals += collected; storage.crystals = crystals; updateUI(); var notification = game.addChild(new Notification("Village produced " + collected + " crystals!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } };
===================================================================
--- original.js
+++ change.js
@@ -354,9 +354,9 @@
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.miss.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
saveAchievements();
- storage.achievementStats = achievementStats;
+ saveAchievementStats();
}
}
self.destroy();
} else {
@@ -472,9 +472,9 @@
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.cowardly.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
saveAchievements();
- storage.achievementStats = achievementStats;
+ saveAchievementStats();
}
}
} else {
// Reset streak if confusion didn't trigger
@@ -2758,9 +2758,9 @@
notification.y = grid.height - 250;
}
}
saveAchievements();
- storage.achievementStats = achievementStats;
+ saveAchievementStats();
}
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
@@ -4662,19 +4662,32 @@
storage.achievement_miss_progress = achievements.miss.progress;
storage.achievement_cowardly_completed = achievements.cowardly.completed;
storage.achievement_cowardly_progress = achievements.cowardly.progress;
}
-var achievementStats = storage.achievementStats || {
- totalTowersBuilt: 0,
- totalEnemiesKilled: 0,
- totalFarmIncome: 0,
- consecutiveDefeats: 0,
- lastGameResult: 'none',
- totalGoldSpentOnDefenses: 0,
- lastGameGoldSpent: 0,
- mortarMisses: 0,
- wizardConfusionStreak: 0,
- maxLevelTowerTypes: []
+// Function to save achievement stats to flattened storage
+function saveAchievementStats() {
+ storage.achievementStats_totalTowersBuilt = achievementStats.totalTowersBuilt;
+ storage.achievementStats_totalEnemiesKilled = achievementStats.totalEnemiesKilled;
+ storage.achievementStats_totalFarmIncome = achievementStats.totalFarmIncome;
+ storage.achievementStats_consecutiveDefeats = achievementStats.consecutiveDefeats;
+ storage.achievementStats_lastGameResult = achievementStats.lastGameResult;
+ storage.achievementStats_totalGoldSpentOnDefenses = achievementStats.totalGoldSpentOnDefenses;
+ storage.achievementStats_lastGameGoldSpent = achievementStats.lastGameGoldSpent;
+ storage.achievementStats_mortarMisses = achievementStats.mortarMisses;
+ storage.achievementStats_wizardConfusionStreak = achievementStats.wizardConfusionStreak;
+ storage.achievementStats_maxLevelTowerTypes = achievementStats.maxLevelTowerTypes;
+}
+var achievementStats = {
+ totalTowersBuilt: storage.achievementStats_totalTowersBuilt || 0,
+ totalEnemiesKilled: storage.achievementStats_totalEnemiesKilled || 0,
+ totalFarmIncome: storage.achievementStats_totalFarmIncome || 0,
+ consecutiveDefeats: storage.achievementStats_consecutiveDefeats || 0,
+ lastGameResult: storage.achievementStats_lastGameResult || 'none',
+ totalGoldSpentOnDefenses: storage.achievementStats_totalGoldSpentOnDefenses || 0,
+ lastGameGoldSpent: storage.achievementStats_lastGameGoldSpent || 0,
+ mortarMisses: storage.achievementStats_mortarMisses || 0,
+ wizardConfusionStreak: storage.achievementStats_wizardConfusionStreak || 0,
+ maxLevelTowerTypes: storage.achievementStats_maxLevelTowerTypes || []
};
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
@@ -4944,9 +4957,9 @@
notification.y = grid.height - 100;
}
}
saveAchievements();
- storage.achievementStats = achievementStats;
+ saveAchievementStats();
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
@@ -5570,9 +5583,9 @@
}
// Reset game gold spent for next game
achievementStats.lastGameGoldSpent = 0;
saveAchievements();
- storage.achievementStats = achievementStats;
+ saveAchievementStats();
LK.showGameOver();
}
}
}
@@ -5608,9 +5621,9 @@
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
saveAchievements();
- storage.achievementStats = achievementStats;
+ saveAchievementStats();
LK.showYouWin();
}
// Auto-collect village crystals every 5 minutes
if (LK.ticks % (60 * 60 * 5) === 0) {
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
Wooden Guard Tower. In-Game asset. 2d. High contrast. No shadows
Bow: make it face 90 degrees In-Game asset. 2d. High contrast. No shadows
Wooden Tower base like the ones the Cannons in Clash of Clans have. In-Game asset. 2d. High contrast. No shadows. Topdown
Cannon without wheels or base, just the cannon. In-Game asset. 2d. High contrast. No shadows. Topdown
Crossbow rotated 90 degrees. In-Game asset. 2d. High contrast. No shadows
Sniper rifle rotated 90 degrees. In-Game asset. 2d. High contrast. No shadows
Ice tower. In-Game asset. 2d. High contrast. No shadows
Ice staff. In-Game asset. 2d. High contrast. No shadows
Windmill. In-Game asset. 2d. High contrast. No shadows
Laser pointer without pointing a laser. In-Game asset. 2d. High contrast. No shadows. Topdown
Laser projectile In-Game asset. 2d. High contrast. No shadows
Orc holding a small axe. In-Game asset. 2d. High contrast. No shadows
Orc with a big wooden shield full of spikes. In-Game asset. 2d. High contrast. No shadows
Orc in a wooden helicopter. In-Game asset. 2d. High contrast. No shadows
Spider. In-Game asset. 2d. High contrast. No shadows
One minion from Clash Royale. In-Game asset. 2d. High contrast. No shadows
Mole with a minerer's hat and a pickaxe. In-Game asset. 2d. High contrast. No shadows
Wolf. In-Game asset. 2d. High contrast. No shadows
Dark magma tower. In-Game asset. 2d. High contrast. No shadows. Topdown
Add outlines
Magician's staff. In-Game asset. 2d. High contrast. No shadows
Mortar from Clash Royale without base, just the Mortar. In-Game asset. 2d. High contrast. No shadows. Topdown
Tophat house. In-Game asset. 2d. High contrast. No shadows
Church. In-Game asset. 2d. High contrast. No shadows
tower_upgrade
Sound effect
enemy_hit
Sound effect
enemy_death
Sound effect
wave_start
Sound effect
place_tower
Sound effect
boss_spawn
Sound effect
game_over
Sound effect
victory
Sound effect
laser_beam
Sound effect
game_theme
Music
explosion
Sound effect
freeze
Sound effect
divine_intervention
Sound effect
gold_collect
Sound effect