User prompt
Fix all errors ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Each level add + 1 colony
User prompt
Use colonistdeath everytime player doesn't save colonist,
User prompt
Add bugdeath everytime bugs are killed.
User prompt
Show the coonist and malecoloist fleeing the colony to the bunker. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Delete asset path and sidepath.
User prompt
Place the colony in the sand.
User prompt
Use background for game background.
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'x')' in or related to this line: 'colonyRoad1.x = (colony1.x + colonistPath[0].x) / 2;' Line Number: 652
User prompt
Create roads between colony and bunker,
User prompt
/**** * Assets ****/ LK.init.shape('cell', {width:76, height:76, color:0xffffff, shape:'box'}) LK.init.shape('healthBar', {width:70, height:10, color:0xffffff, shape:'box'}) LK.init.shape('healthBarOutline', {width:72, height:12, color:0x000000, shape:'box'}) LK.init.shape('notification', {width:100, height:100, color:0xffffff, shape:'box'}) LK.init.shape('rangeCircle', {width:1000, height:1000, color:0xffffff, shape:'ellipse'}) LK.init.shape('tower', {width:152, height:152, color:0xffffff, shape:'box'}) LK.init.shape('towerLevelIndicator', {width:10, height:10, color:0xffffff, shape:'box'}) LK.init.shape('towerpreview', {width:100, height:100, color:0xffffff, shape:'box'}) LK.init.image('arrow', {width:25, height:25.27, id:'671be23a3de176b9ade21311'}) LK.init.image('bullet', {width:30, height:30, id:'682dc1755a2fa73fef156d33'}) LK.init.image('defense', {width:100, height:100, id:'671be23a3de176b9ade21311'}) LK.init.image('enemy', {width:70, height:70, id:'682d8ef5c4e193748a079144', orientation:3}) LK.init.image('enemy_fast', {width:50, height:74, id:'671be1713de176b9ade21306'}) LK.init.image('enemy_flying', {width:130, height:80.6, id:'682d8fb8c4e193748a07915a', orientation:1}) LK.init.image('enemy_immune', {width:75, height:111, id:'682d95a4c4e193748a0791fc', orientation:3}) LK.init.image('enemy_swarm', {width:40, height:39.84, id:'682d8ef5c4e193748a079145', orientation:1}) /**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Splash damage to nearby enemies 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 -= 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 === 'poison') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual poison effect var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Apply poison effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } } 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); } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); cellGraphics.tint = 0x880000; return; } numberLabel.visible = true; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; } else { cellGraphics.tint = 0x88 - tint << 8 | tint; } while (debugArrows.length > data.targets.length) { self.removeChild(debugArrows.pop()); } for (var a = 0; a < data.targets.length; a++) { var destination = data.targets[a]; var ox = destination.x - data.x; var oy = destination.y - data.y; var angle = Math.atan2(oy, ox); if (!debugArrows[a]) { debugArrows[a] = LK.getAsset('arrow', { anchorX: -.5, anchorY: 0.5 }); debugArrows[a].alpha = .5; self.addChildAt(debugArrows[a], 1); } debugArrows[a].rotation = angle; } break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0x008800; numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type 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 '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 slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = 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 } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Combine poison (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; if (i > 11 - 3 && i <= 11 + 3) { if (j === 0) { cellType = 2; self.spawns.push(cell); } else if (j <= 4) { cellType = 0; } else if (j === gridHeight - 1) { cellType = 3; self.goals.push(cell); } else if (j >= gridHeight - 4) { cellType = 0; } } cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 1 && node.left.type != 1) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.right.type != 1) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.right.type != 1) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.left.type != 1) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed; // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) var angle = Math.PI / 2; if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update enemy's position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // If enemy has now reached the entry area, update cell coordinates if (enemy.currentCellY >= 4) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { if (cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = 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'; // Increase size of base for easier touch var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); switch (self.towerType) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'sniper': baseGraphics.tint = 0xFF5500; break; case 'splash': baseGraphics.tint = 0x33CC00; break; case 'slow': baseGraphics.tint = 0x9900FF; break; case 'poison': baseGraphics.tint = 0x00FFAA; break; default: baseGraphics.tint = 0xAAAAAA; } var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { 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; // 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, but final upgrade gets a huge boost if (self.level === self.maxLevel) { return 12 * CELL_SIZE; // Significantly increased range for max level } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case '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 'poison': // Poison: base 3.2, +0.5 per level return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; default: // Default: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 30; self.damage = 5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'sniper': self.fireRate = 90; self.damage = 25; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'splash': self.fireRate = 75; self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; case 'slow': self.fireRate = 50; self.damage = 8; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'poison': self.fireRate = 70; self.damage = 12; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 5; break; } var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); switch (self.id) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'sniper': baseGraphics.tint = 0xFF5500; break; case 'splash': baseGraphics.tint = 0x33CC00; break; case 'slow': baseGraphics.tint = 0x9900FF; break; case 'poison': baseGraphics.tint = 0x00FFAA; break; default: baseGraphics.tint = 0xAAAAAA; } 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); var gunGraphics = gunContainer.attachAsset('defense', { 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 'poison': towerLevelIndicator.tint = 0x00FFAA; break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === '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.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range if (distance <= self.getRange()) { // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { self.targetEnemy = self.findTarget(); if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); gunContainer.rotation = angle; if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; } // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0x00AAFF; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'sniper': bullet.children[0].tint = 0xFF5500; bullet.children[0].width = 15; bullet.children[0].height = 15; break; case 'splash': bullet.children[0].tint = 0x33CC00; bullet.children[0].width = 40; bullet.children[0].height = 40; break; case 'slow': bullet.children[0].tint = 0x9900FF; bullet.children[0].width = 35; bullet.children[0].height = 35; break; case 'poison': bullet.children[0].tint = 0x00FFAA; bullet.children[0].width = 35; bullet.children[0].height = 35; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // --- 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 'poison': previewGraphics.tint = 0x00FFAA; 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 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (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) % 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 { 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 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; var gold = 80; var lives = 20; var score = 0; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); } function setGold(value) { gold = value; updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY) { var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 5; switch (towerType) { case 'rapid': cost = 15; break; case 'sniper': cost = 25; break; case 'splash': cost = 35; break; case 'slow': cost = 45; break; case 'poison': cost = 55; break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); grid.pathFind(); grid.renderDebug(); return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } 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); var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; var sourceTowers = []; var towerSpacing = 300; // Increase spacing for larger towers var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; game.update = function () { if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; if (isBossWave && waveType !== 'swarm') { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy = 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; } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Boss enemies give more gold and score var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { LK.showGameOver(); } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } }; ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (1 edits merged)
Please save this source code
User prompt
Klingon Defense: Outer Rim Survival
Initial prompt
A tower defense game where you are the prime minister of a planet called Klingon in the Vulcan Primus system, 3 million light years from Earth. The year is 3541. Humans have expanded their reach across the Outer Rim of the Milky Way Galaxy. Recently, you have been given orders to occupy a new solar system on the border of the Empire. However, it soon becomes clear that there is an unknown enemy. You and your colony are being attacked by alien bugs bent on taking over the planet and eating everyone on site. Build robot tower defenses to defeat the bugs, save the colony structures, and allow the colonists enough time to reach the safety of the bunkers (where you are located). Every bug you kill earns you five coins if it's small, seven coins if it's medium, and 10 coins if it's large. Every level involves you defending different cities on the planet from the alien bugs.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var Bug = Container.expand(function (type, wave) { var self = Container.call(this); self.type = type || 'small'; self.wave = wave || 1; self.isBoss = false; self.isFlying = false; self.isArmored = false; self.slowResistance = 0; self.pathIndex = 0; // Set base stats based on type switch (self.type) { case 'small': self.health = 15; self.speed = 2.5; self.reward = 5; break; case 'medium': self.health = 35; self.speed = 2; self.reward = 7; break; case 'large': self.health = 80; self.speed = 1.5; self.reward = 10; break; case 'fast': self.health = 20; self.speed = 4; self.reward = 8; break; case 'armored': self.health = 120; self.speed = 1; self.reward = 15; self.isArmored = true; break; case 'flying': self.health = 40; self.speed = 3; self.reward = 12; self.isFlying = true; break; case 'boss': self.health = 500; self.speed = 0.8; self.reward = 50; self.isBoss = true; self.slowResistance = 0.7; break; } // Scale with wave var waveMultiplier = 1 + (self.wave - 1) * 0.15; self.health = Math.floor(self.health * waveMultiplier); self.maxHealth = self.health; self.slowEffect = 1; self.poisonTimer = 0; var bugGraphics = self.attachAsset(self.type + 'Bug', { anchorX: 0.5, anchorY: 0.5 }); // Boss scaling if (self.isBoss) { bugGraphics.scaleX = 1.5; bugGraphics.scaleY = 1.5; bugGraphics.tint = 0xFF4444; } // Flying effect if (self.isFlying) { bugGraphics.alpha = 0.8; var originalY = bugGraphics.y; tween(bugGraphics, { y: originalY - 10 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(bugGraphics, { y: originalY + 10 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { // Create infinite loop tween(bugGraphics, { y: originalY - 10 }, { duration: 1000, easing: tween.easeInOut, onFinish: arguments.callee.caller }); } }); } }); } self.takeDamage = function (damage, damageType) { var actualDamage = damage; if (self.isArmored && damageType !== 'piercing') { actualDamage = Math.max(1, damage * 0.5); } if (damageType === 'poison') { self.poisonTimer = 180; // 3 seconds at 60fps actualDamage = damage * 0.3; } self.health -= actualDamage; var healthPercent = self.health / self.maxHealth; if (damageType === 'poison') { bugGraphics.tint = 0x00FF00; } else { bugGraphics.tint = healthPercent < 0.5 ? 0xFF6666 : 0xFFFFFF; } return self.health <= 0; }; self.applySlow = function (slowAmount, duration) { if (self.slowResistance < 1) { var effectiveSlowAmount = slowAmount * (1 - self.slowResistance); self.slowEffect = Math.min(self.slowEffect, 1 - effectiveSlowAmount); LK.setTimeout(function () { self.slowEffect = 1; }, duration); } }; self.update = function () { // Handle poison damage if (self.poisonTimer > 0) { self.poisonTimer--; if (self.poisonTimer % 30 === 0) { // Damage every 0.5 seconds self.takeDamage(5, 'poison'); } if (self.poisonTimer <= 0) { bugGraphics.tint = 0xFFFFFF; } } // Check if bug is visible on screen and play appropriate sound var isOnScreen = self.x >= -50 && self.x <= 2098 && self.y >= -50 && self.y <= 2782; if (isOnScreen) { // Play bug sound based on type every 2 seconds (120 frames) if (LK.ticks % 120 === 0) { if (self.isFlying) { var flyingSound = LK.getSound('bugflying'); if (flyingSound) flyingSound.play(); } else { var crawlingSound = LK.getSound('bugcrawling'); if (crawlingSound) crawlingSound.play(); } } } // Move along path if (self.pathIndex < gamePath.length) { var target = gamePath[self.pathIndex]; var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 30) { self.pathIndex++; } else { var actualSpeed = self.speed * self.slowEffect; self.x += dx / distance * actualSpeed; self.y += dy / distance * actualSpeed; } } }; return self; }); var Bullet = Container.expand(function (bulletType, tower) { var self = Container.call(this); self.speed = 8; self.target = null; self.damage = 1; self.type = bulletType || 'basic'; self.tower = tower; self.splashRadius = 0; self.piercing = false; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); // Customize bullet based on type switch (self.type) { case 'rapid': bulletGraphics.tint = 0x00AAFF; bulletGraphics.scaleX = 0.7; bulletGraphics.scaleY = 0.7; self.speed = 12; break; case 'sniper': bulletGraphics.tint = 0xFF4444; bulletGraphics.scaleX = 1.2; bulletGraphics.scaleY = 1.2; self.speed = 15; self.piercing = true; break; case 'cannon': bulletGraphics.tint = 0xFF8800; bulletGraphics.scaleX = 1.5; bulletGraphics.scaleY = 1.5; self.speed = 6; self.splashRadius = 100; break; case 'freeze': bulletGraphics.tint = 0x88CCFF; self.speed = 10; break; case 'poison': bulletGraphics.tint = 0x00FF00; self.speed = 9; break; } self.update = function () { // Check if target is still valid if (!self.target || !self.target.parent || self.target.health <= 0) { self.destroy(); return; } // Check if bullet is off-screen if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) { self.destroy(); return; } var dx = self.target.x - self.x; var dy = self.target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 25) { self.hitTarget(); self.destroy(); } else { self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; } }; self.hitTarget = function () { var damageType = self.type === 'sniper' ? 'piercing' : self.type === 'poison' ? 'poison' : 'normal'; // Apply main damage if (self.target.takeDamage(self.damage, damageType)) { // Play bugdeath sound immediately when bug dies var bugDeathSound = LK.getSound('bugDeath'); if (bugDeathSound) bugDeathSound.play(); self.killBug(self.target); } // Special effects if (self.type === 'freeze') { self.target.applySlow(0.6, 2000); // 60% slow for 2 seconds } // Splash damage if (self.splashRadius > 0) { for (var i = 0; i < bugs.length; i++) { var bug = bugs[i]; if (bug !== self.target) { var dx = bug.x - self.target.x; var dy = bug.y - self.target.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist <= self.splashRadius) { if (bug.takeDamage(self.damage * 0.6, damageType)) { // Play bugdeath sound for splash damage kills too var bugDeathSound = LK.getSound('bugDeath'); if (bugDeathSound) bugDeathSound.play(); self.killBug(bug); } } } } } }; self.killBug = function (bug) { coins += bug.reward; bugsKilled++; updateUI(); for (var i = bugs.length - 1; i >= 0; i--) { if (bugs[i] === bug) { bug.destroy(); bugs.splice(i, 1); break; } } }; return self; }); var Colonist = Container.expand(function () { var self = Container.call(this); self.speed = 1; self.pathIndex = 0; self.isSafe = false; var colonistGraphics = self.attachAsset('colonist', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (self.isSafe) return; if (self.pathIndex < colonistPath.length) { var target = colonistPath[self.pathIndex]; var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 20) { self.pathIndex++; if (self.pathIndex >= colonistPath.length) { self.isSafe = true; // Move colonist to bunker position and make them visible self.x = bunker.x + (Math.random() - 0.5) * 100; self.y = bunker.y + (Math.random() - 0.5) * 100; self.alpha = 0.8; // Make them slightly transparent to show they're safe // Add a green tint to show they're saved tween(self, { tint: 0x00FF00 }, { duration: 500, onFinish: function onFinish() { tween(self, { tint: 0xFFFFFF, alpha: 0 }, { duration: 1000, onFinish: function onFinish() { self.destroy(); } }); } }); colonistsSaved++; updateUI(); } } else { self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; } } }; return self; }); var MainMenu = Container.expand(function () { var self = Container.call(this); // Main menu background var menuBg = self.attachAsset('menu', { anchorX: 0.5, anchorY: 0.5 }); menuBg.x = 1024; menuBg.y = 1366; menuBg.width = 2048; menuBg.height = 2732; menuBg.alpha = 0.7; // Title text var titleText = new Text2('COLONY DEFENSE', { size: 120, fill: 0xFFFF00 }); titleText.anchor.set(0.5, 0.5); titleText.x = 1024; titleText.y = 800; self.addChild(titleText); // Subtitle text var subtitleText = new Text2('Defend the colonies from alien bugs!', { size: 60, fill: 0x00FFFF }); subtitleText.anchor.set(0.5, 0.5); subtitleText.x = 1024; subtitleText.y = 950; self.addChild(subtitleText); // Start button var startButton = self.attachAsset('playbutton', { anchorX: 0.5, anchorY: 0.5 }); startButton.x = 1024; startButton.y = 1400; startButton.width = 300; startButton.height = 150; startButton.tint = 0x00AA00; var startText = new Text2('START GAME', { size: 50, fill: 0xFFFF00 }); startText.anchor.set(0.5, 0.5); startText.x = 1024; startText.y = 1400; self.addChild(startText); // Instructions text var instructText = new Text2('Use robot towers to stop the alien invasion!\nSave the colonists and defend your colonies!', { size: 40, fill: 0xFFFFFF }); instructText.anchor.set(0.5, 0.5); instructText.x = 1024; instructText.y = 1800; self.addChild(instructText); // High scores display for main menu var menuHighScores = storage.highScores || []; var scoreText = 'TOP 5 SCORES:\n'; for (var i = 0; i < Math.min(5, menuHighScores.length); i++) { scoreText += i + 1 + '. ' + menuHighScores[i] + '\n'; } // Fill empty spots for (var i = menuHighScores.length; i < 5; i++) { scoreText += i + 1 + '. ---\n'; } var menuScoreDisplay = new Text2(scoreText, { size: 45, fill: 0xFFFF00 }); menuScoreDisplay.anchor.set(0.5, 0.5); menuScoreDisplay.x = 1024; menuScoreDisplay.y = 2200; self.addChild(menuScoreDisplay); // Add pulsing animation to start button tween(startButton, { scaleX: 1.1, scaleY: 1.1 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(startButton, { scaleX: 1, scaleY: 1 }, { duration: 1000, easing: tween.easeInOut, onFinish: arguments.callee.caller }); } }); self.down = function (x, y, obj) { // Check if clicked on start button area var dx = x - startButton.x; var dy = y - startButton.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= 150) { startGame(); } }; return self; }); var Tower = Container.expand(function (towerType) { var self = Container.call(this); self.type = towerType || 'basic'; self.range = 150; self.damage = 1; self.fireRate = 30; self.lastShot = 0; self.level = 1; self.maxLevel = 5; self.upgradeCost = 20; self.sellValue = 15; // Set base stats based on tower type switch (self.type) { case 'basic': self.range = 150; self.damage = 8; self.fireRate = 40; self.upgradeCost = 20; break; case 'rapid': self.range = 120; self.damage = 4; self.fireRate = 15; self.upgradeCost = 30; break; case 'sniper': self.range = 250; self.damage = 25; self.fireRate = 80; self.upgradeCost = 50; break; case 'cannon': self.range = 140; self.damage = 20; self.fireRate = 60; self.upgradeCost = 40; break; case 'freeze': self.range = 130; self.damage = 6; self.fireRate = 35; self.upgradeCost = 35; break; case 'poison': self.range = 140; self.damage = 12; self.fireRate = 45; self.upgradeCost = 45; break; } var towerGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); // Ensure proper scaling for visibility towerGraphics.width = 80; towerGraphics.height = 80; towerGraphics.alpha = 1.0; // Move tower up 10 spaces towerGraphics.y = -10; // Set tower color based on type switch (self.type) { case 'rapid': towerGraphics.tint = 0x00AAFF; break; case 'sniper': towerGraphics.tint = 0xFF4444; break; case 'cannon': towerGraphics.tint = 0xFF8800; break; case 'freeze': towerGraphics.tint = 0x88CCFF; break; case 'poison': towerGraphics.tint = 0x00FF00; break; default: towerGraphics.tint = 0x4169E1; } self.canShoot = function () { return LK.ticks - self.lastShot >= self.fireRate; }; self.findTarget = function () { var bestTarget = null; var bestScore = -1; for (var i = 0; i < bugs.length; i++) { var bug = bugs[i]; // Skip if bug is not valid or has no health if (!bug || !bug.parent || bug.health <= 0) continue; var dx = bug.x - self.x; var dy = bug.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.range) { // Enhanced targeting priority system var score = 0; // Base score: progress along path (higher = closer to end) score += bug.pathIndex * 1000; // Boss priority bonus if (bug.isBoss) score += 2000; // Flying bug priority (harder to hit) if (bug.isFlying) score += 800; // Armored bug priority for certain tower types if (bug.isArmored && self.type === 'sniper') score += 1500; // Low health targets for quick kills if (bug.health <= self.damage) score += 1200; // Distance penalty (closer = higher priority) score -= distance; // Type-specific targeting if (self.type === 'poison' && bug.poisonTimer <= 0) score += 600; if (self.type === 'freeze' && bug.slowEffect >= 0.8) score += 400; if (score > bestScore) { bestScore = score; bestTarget = bug; } } } return bestTarget; }; self.shoot = function (target) { if (!self.canShoot()) return; var bullet = new Bullet(self.type, self); bullet.x = self.x; bullet.y = self.y; bullet.target = target; bullet.damage = self.damage; bullets.push(bullet); game.addChild(bullet); self.lastShot = LK.ticks; var shootSound = LK.getSound('shoot'); if (shootSound) shootSound.play(); // Visual muzzle flash towerGraphics.alpha = 1.5; tween(towerGraphics, { alpha: 1 }, { duration: 100 }); }; self.upgrade = function () { if (self.level >= self.maxLevel) return false; if (coins >= self.upgradeCost) { coins -= self.upgradeCost; self.level++; // Upgrade stats self.damage = Math.floor(self.damage * 1.3); self.range += 15; if (self.type === 'rapid') { self.fireRate = Math.max(8, self.fireRate - 3); } else { self.fireRate = Math.max(15, self.fireRate - 5); } self.sellValue += Math.floor(self.upgradeCost * 0.7); self.upgradeCost = Math.floor(self.upgradeCost * 1.5); towerGraphics.tint = 0x00FF00; tween(towerGraphics, { tint: self.type === 'rapid' ? 0x00AAFF : self.type === 'sniper' ? 0xFF4444 : self.type === 'cannon' ? 0xFF8800 : self.type === 'freeze' ? 0x88CCFF : self.type === 'poison' ? 0x00FF00 : 0x4169E1 }, { duration: 500 }); updateUI(); return true; } return false; }; self.sell = function () { coins += self.sellValue; selectedTower = null; for (var i = towers.length - 1; i >= 0; i--) { if (towers[i] === self) { towers.splice(i, 1); break; } } self.destroy(); updateUI(); }; self.update = function () { var target = self.findTarget(); if (target && self.canShoot()) { self.shoot(target); } }; self.down = function (x, y, obj) { var currentTime = Date.now(); // Check for double-click if (lastClickedTower === self && currentTime - lastClickTime < doubleClickDelay) { // Double-click detected - attempt to upgrade if (self.upgrade()) { // Upgrade successful - show visual feedback tween(self, { scaleX: 1.2, scaleY: 1.2 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 150, easing: tween.easeIn }); } }); } // Reset double-click tracking lastClickTime = 0; lastClickedTower = null; return; } // Single click - update tracking and show tower info lastClickTime = currentTime; lastClickedTower = self; // Clear any existing range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.children[i].destroy(); } } selectedTower = self; showTowerInfo(); // Create range indicator for selected tower var towerRangeIndicator = new Container(); towerRangeIndicator.isTowerRange = true; game.addChild(towerRangeIndicator); var rangeCircle = towerRangeIndicator.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); towerRangeIndicator.x = self.x; towerRangeIndicator.y = self.y; rangeCircle.width = self.range * 2; rangeCircle.height = self.range * 2; rangeCircle.alpha = 0.3; rangeCircle.tint = 0x00FF00; }; return self; }); var TowerButton = Container.expand(function (towerType, cost) { var self = Container.call(this); self.towerType = towerType; self.cost = cost; var buttonBg = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); buttonBg.width = 120; buttonBg.height = 120; // Set button color based on tower type switch (towerType) { case 'rapid': buttonBg.tint = 0x00AAFF; break; case 'sniper': buttonBg.tint = 0xFF4444; break; case 'cannon': buttonBg.tint = 0xFF8800; break; case 'freeze': buttonBg.tint = 0x88CCFF; break; case 'poison': buttonBg.tint = 0x00FF00; break; default: buttonBg.tint = 0x4169E1; } var costText = new Text2('$' + cost, { size: 30, fill: 0xFFFF00 }); costText.anchor.set(0.5, 0.5); costText.y = 50; self.addChild(costText); var typeText = new Text2(towerType.toUpperCase(), { size: 25, fill: 0xFFFFFF }); typeText.anchor.set(0.5, 0.5); typeText.y = -50; self.addChild(typeText); self.update = function () { var canAfford = coins >= self.cost; self.alpha = canAfford ? 1 : 0.5; }; self.down = function (x, y, obj) { if (coins >= self.cost) { placingTowerType = self.towerType; selectedTower = null; updateUI(); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x2F4F2F }); /**** * Game Code ****/ var bugs = []; var towers = []; var bullets = []; var colonists = []; var coins = 25; var wave = 1; var maxWaves = 500; // 250 levels × 2 waves per level = 500 total waves var bugsKilled = 0; var colonistsSaved = 0; var selectedTower = null; var placingTowerType = null; var waveTimer = 0; var waveDelay = 300; var bugsToSpawn = 5; var bugsSpawned = 0; var gameWon = false; var towerButtons = []; var gameState = 'menu'; // 'menu' or 'playing' var mainMenu = null; var gameElements = []; // Double-click detection variables var lastClickTime = 0; var lastClickedTower = null; var doubleClickDelay = 300; // milliseconds // Tower costs var towerCosts = { basic: 15, rapid: 25, sniper: 40, cannon: 35, freeze: 30, poison: 45 }; // High score management var highScores = storage.highScores || []; var scoreDisplay = null; function calculateScore() { return bugsKilled * 10 + colonistsSaved * 50 + wave * 100; } function saveHighScore(score) { // Add current score highScores.push(score); // Sort in descending order highScores.sort(function (a, b) { return b - a; }); // Keep only top 5 if (highScores.length > 5) { highScores = highScores.slice(0, 5); } // Save to storage storage.highScores = highScores; updateScoreDisplay(); } function updateScoreDisplay() { if (!scoreDisplay) return; var scoreText = 'TOP 5 SCORES:\n'; for (var i = 0; i < Math.min(5, highScores.length); i++) { scoreText += i + 1 + '. ' + highScores[i] + '\n'; } // Fill empty spots for (var i = highScores.length; i < 5; i++) { scoreText += i + 1 + '. ---\n'; } scoreDisplay.setText(scoreText); } function createScoreDisplay() { scoreDisplay = new Text2('TOP 5 SCORES:\n1. ---\n2. ---\n3. ---\n4. ---\n5. ---', { size: 40, fill: 0xFFFF00 }); scoreDisplay.anchor.set(0.5, 1); scoreDisplay.x = 0; scoreDisplay.y = -20; scoreDisplay.visible = false; LK.gui.bottom.addChild(scoreDisplay); updateScoreDisplay(); } // Game path for bugs var gamePath = [{ x: 0, y: 1366 }, { x: 400, y: 1366 }, { x: 400, y: 800 }, { x: 800, y: 800 }, { x: 800, y: 1366 }, { x: 1200, y: 1366 }, { x: 1200, y: 600 }, { x: 1600, y: 600 }, { x: 1600, y: 1000 }, { x: 2048, y: 1000 }]; // Colonist path to bunker - updated to connect colonies better var colonistPath = [{ x: 500, // Start near colony1 y: 700 }, { x: 900, y: 900 }, { x: 1200, y: 900 }, { x: 1400, y: 800 }, { x: 1600, y: 1000 }, { x: 1800, y: 1400 }]; // Create colonies array and bunker variable var colonies = []; var bunker = null; // Load saved level from storage var savedLevel = storage.level || 1; wave = (savedLevel - 1) * 2; // Convert level back to wave number // Initialize main menu mainMenu = new MainMenu(); game.addChild(mainMenu); // UI Elements var coinText = new Text2('Coins: 50', { size: 60, fill: 0xFFFF00 }); coinText.anchor.set(0, 0); coinText.x = -265; coinText.visible = false; LK.gui.topRight.addChild(coinText); var waveText = new Text2('Wave: 1', { size: 60, fill: 0xFFFFFF }); waveText.anchor.set(0.5, 0); waveText.visible = false; LK.gui.top.addChild(waveText); var infoText = new Text2('Tap empty space to place tower ($15)', { size: 40, fill: 0x00FFFF }); infoText.anchor.set(0.5, 1); infoText.visible = false; LK.gui.bottom.addChild(infoText); var killsText = new Text2('Bugs Killed: 0', { size: 50, fill: 0xFF0000 }); killsText.anchor.set(0, 0); killsText.visible = false; LK.gui.left.addChild(killsText); var savedText = new Text2('Colonists Saved: 0', { size: 50, fill: 0x00FF00 }); savedText.anchor.set(0, 0); savedText.y = 80; savedText.visible = false; LK.gui.left.addChild(savedText); // Create score display createScoreDisplay(); // Create restart button var restartButton = null; // Create trash can in right bottom corner var trashCan = null; // Create tower selection buttons var towerTypes = ['basic', 'rapid', 'sniper', 'cannon', 'freeze', 'poison']; var buttonSpacing = 140; var startX = 100; for (var i = 0; i < towerTypes.length; i++) { var towerButton = new TowerButton(towerTypes[i], towerCosts[towerTypes[i]]); towerButton.x = startX + i * buttonSpacing; towerButton.y = 2600; towerButton.visible = false; game.addChild(towerButton); towerButtons.push(towerButton); } function startGame() { if (gameState === 'playing') return; gameState = 'playing'; // Hide main menu if (mainMenu) { mainMenu.destroy(); mainMenu = null; } // Reset game variables bugs = []; towers = []; bullets = []; colonists = []; colonies = []; // Initialize coins from storage, ensuring we have at least 25 coins var storedCoins = storage.coins; if (typeof storedCoins === 'number' && storedCoins >= 0) { coins = Math.max(25, storedCoins); } else { coins = 25; } var savedLevel = storage.level; if (typeof savedLevel !== 'number' || savedLevel < 1) { savedLevel = 1; } wave = (savedLevel - 1) * 2 + 1; // Start from saved level's first wave bugsKilled = 0; colonistsSaved = 0; selectedTower = null; placingTowerType = null; waveTimer = 0; bugsToSpawn = 5; bugsSpawned = 0; gameWon = false; // Destroy existing tower buttons for (var i = 0; i < towerButtons.length; i++) { if (towerButtons[i] && towerButtons[i].parent) { towerButtons[i].destroy(); } } towerButtons = []; // Recreate tower selection buttons var towerTypes = ['basic', 'rapid', 'sniper', 'cannon', 'freeze', 'poison']; var buttonSpacing = 140; var startX = 100; for (var i = 0; i < towerTypes.length; i++) { var towerButton = new TowerButton(towerTypes[i], towerCosts[towerTypes[i]]); towerButton.x = startX + i * buttonSpacing; towerButton.y = 2600; towerButton.visible = true; towerButton.alpha = 1.0; game.addChild(towerButton); towerButtons.push(towerButton); } // Initialize game elements initializeGameElements(); // Show UI elements coinText.visible = true; waveText.visible = true; infoText.visible = true; killsText.visible = true; savedText.visible = true; // Keep score display hidden during gameplay if (scoreDisplay) scoreDisplay.visible = false; // Create restart button if (restartButton) { restartButton.destroy(); } restartButton = game.addChild(LK.getAsset('playbutton', { anchorX: 0.5, anchorY: 0.5 })); restartButton.x = 200; restartButton.y = 150; restartButton.width = 150; restartButton.height = 80; restartButton.tint = 0xFF4444; restartButton.alpha = 0.8; var restartText = new Text2('RESTART', { size: 30, fill: 0xFFFFFF }); restartText.anchor.set(0.5, 0.5); restartText.x = 200; restartText.y = 150; game.addChild(restartText); // Create trash can in right bottom corner if (trashCan) { trashCan.destroy(); } trashCan = game.addChild(LK.getAsset('trashCan', { anchorX: 0.5, anchorY: 0.5 })); trashCan.x = 1950; // Right side of screen trashCan.y = 2600; // Bottom area trashCan.width = 80; trashCan.height = 100; trashCan.alpha = 0.8; // Make tower buttons visible and ensure proper positioning for (var i = 0; i < towerButtons.length; i++) { if (towerButtons[i]) { towerButtons[i].visible = true; // Ensure buttons are positioned at bottom of screen for visibility towerButtons[i].y = 2600; towerButtons[i].alpha = 1.0; // Make sure they're on top of other game elements game.setChildIndex(towerButtons[i], game.children.length - 1); } } // Spawn initial fleeing colonists before any enemies appear spawnInitialFleeingColonists(); // Ensure we start at level 1 and begin first wave wave = 0; // Start at 0 so first call to startNewWave makes it 1 waveText.setText('Level: 1 | Wave: 1 | Colonies: ' + colonies.length); LK.setTimeout(function () { startNewWave(); }, 3000); } function initializeGameElements() { // Add background image var backgroundImage = game.addChild(LK.getAsset('background', { anchorX: 0.5, anchorY: 0.5 })); backgroundImage.x = 1024; backgroundImage.y = 1366; backgroundImage.width = 2048; backgroundImage.height = 2732; gameElements.push(backgroundImage); // Create initial colony var colony1 = game.addChild(LK.getAsset('colony', { anchorX: 0.5, anchorY: 0.5 })); colony1.x = 400; colony1.y = 600; colonies.push(colony1); gameElements.push(colony1); // Create bunker bunker = game.addChild(LK.getAsset('bunker', { anchorX: 0.5, anchorY: 0.5 })); bunker.x = 1800; bunker.y = 1400; gameElements.push(bunker); // Show initial instructions var instructionText = new Text2('Defend the colonies from alien bugs!\nUse robot towers to stop them!', { size: 60, fill: 0x00FFFF }); instructionText.anchor.set(0.5, 0.5); instructionText.x = 1024; instructionText.y = 600; game.addChild(instructionText); gameElements.push(instructionText); tween(instructionText, { alpha: 0 }, { duration: 4000, onFinish: function onFinish() { instructionText.destroy(); } }); // Play background music var bgMusic = LK.getMusic ? LK.getMusic('gameMusic') : null; if (bgMusic || LK.playMusic) { LK.playMusic('gameMusic'); } } function updateUI() { if (gameState !== 'playing') return; coinText.setText('Coins: ' + coins); killsText.setText('Bugs Killed: ' + bugsKilled); savedText.setText('Colonists Saved: ' + colonistsSaved); if (selectedTower) { var upgradeText = selectedTower.level >= selectedTower.maxLevel ? 'MAX LEVEL' : 'Double-tap to Upgrade ($' + selectedTower.upgradeCost + ')'; infoText.setText('Tower Lv.' + selectedTower.level + ' - ' + upgradeText + ' | Tap RIGHT to Sell ($' + selectedTower.sellValue + ') | Drag to TRASH to delete'); } else if (placingTowerType) { infoText.setText('Placing ' + placingTowerType.toUpperCase() + ' tower ($' + towerCosts[placingTowerType] + ') - Tap open area'); } else { infoText.setText('Select a tower type below to place'); } } function showTowerInfo() { updateUI(); } function spawnBug() { if (bugsSpawned >= bugsToSpawn) return; var bugType; // Random chance for queenbug (boss) to appear in any wave - no set limit if (Math.random() < 0.15) { // 15% chance for queenbug to spawn bugType = 'boss'; } else if (wave >= 15) { var advancedTypes = ['medium', 'large', 'fast', 'armored', 'flying']; bugType = advancedTypes[Math.floor(Math.random() * advancedTypes.length)]; } else if (wave >= 10) { var midTypes = ['small', 'medium', 'fast', 'flying']; bugType = midTypes[Math.floor(Math.random() * midTypes.length)]; } else if (wave >= 5) { var earlyTypes = ['small', 'medium', 'fast']; bugType = earlyTypes[Math.floor(Math.random() * earlyTypes.length)]; } else { var basicTypes = ['small', 'small', 'medium']; bugType = basicTypes[Math.floor(Math.random() * basicTypes.length)]; } var bug = new Bug(bugType, wave); bug.x = gamePath[0].x; bug.y = gamePath[0].y; bugs.push(bug); game.addChild(bug); bugsSpawned++; // Play bug crawling sound when bug spawns var crawlingSound = LK.getSound('bugcrawling'); if (crawlingSound) crawlingSound.play(); } function spawnNewColony() { // Only spawn if we have fewer than 8 colonies total if (colonies.length >= 8) return; // Generate random position for new colony in safe areas var attempts = 0; var newColony = null; while (attempts < 50 && !newColony) { var x = 300 + Math.random() * 1400; // Random X between 300-1700 var y = 500 + Math.random() * 800; // Random Y between 500-1300 var canPlace = true; attempts++; // Ensure we're not too close to screen edges if (x < 200 || x > 1848 || y < 200 || y > 2532) { canPlace = false; continue; } // Check distance from path for (var i = 0; i < gamePath.length; i++) { var dx = x - gamePath[i].x; var dy = y - gamePath[i].y; if (Math.sqrt(dx * dx + dy * dy) < 150) { canPlace = false; break; } } if (!canPlace) continue; // Check distance from existing colonies and bunker var structures = colonies.slice(); structures.push(bunker); for (var i = 0; i < structures.length; i++) { var dx = x - structures[i].x; var dy = y - structures[i].y; if (Math.sqrt(dx * dx + dy * dy) < 200) { canPlace = false; break; } } if (!canPlace) continue; // Check distance from existing towers for (var i = 0; i < towers.length; i++) { var dx = x - towers[i].x; var dy = y - towers[i].y; if (Math.sqrt(dx * dx + dy * dy) < 150) { canPlace = false; break; } } if (canPlace) { newColony = game.addChild(LK.getAsset('colony', { anchorX: 0.5, anchorY: 0.5 })); newColony.x = x; newColony.y = y; colonies.push(newColony); gameElements.push(newColony); // Flash effect for new colony LK.effects.flashObject(newColony, 0x00FF00, 1000); break; } } } function spawnInitialFleeingColonists() { // Spawn colonists fleeing from the initial colony before enemies arrive if (colonies.length > 0) { var initialColony = colonies[0]; // Spawn 2-3 regular colonists fleeing from the main colony var colonistsToSpawn = 2 + Math.floor(Math.random() * 2); for (var i = 0; i < colonistsToSpawn; i++) { var colonist = new Colonist(); colonist.x = initialColony.x + (Math.random() - 0.5) * 100; colonist.y = initialColony.y + (Math.random() - 0.5) * 100; colonists.push(colonist); game.addChild(colonist); // Add dramatic fleeing animation with tween var targetX = colonistPath[0].x + (Math.random() - 0.5) * 80; var targetY = colonistPath[0].y + (Math.random() - 0.5) * 80; tween(colonist, { x: targetX, y: targetY }, { duration: 1500 + Math.random() * 1000, easing: tween.easeOut }); } // Spawn 1 male colonist fleeing with panic animation var maleColonist = game.addChild(LK.getAsset('colonistmale', { anchorX: 0.5, anchorY: 0.5 })); maleColonist.x = initialColony.x + (Math.random() - 0.5) * 120; maleColonist.y = initialColony.y + (Math.random() - 0.5) * 120; maleColonist.speed = 1.2; maleColonist.pathIndex = 0; maleColonist.isSafe = false; maleColonist.isMale = true; colonists.push(maleColonist); // Add panic shake animation then flee tween(maleColonist, { x: maleColonist.x + 25 }, { duration: 120, easing: tween.easeInOut, onFinish: function onFinish() { tween(maleColonist, { x: maleColonist.x - 50 }, { duration: 120, easing: tween.easeInOut, onFinish: function onFinish() { tween(maleColonist, { x: colonistPath[0].x + (Math.random() - 0.5) * 60, y: colonistPath[0].y + (Math.random() - 0.5) * 60 }, { duration: 2000 + Math.random() * 1000, easing: tween.easeOut }); } }); } }); // Add update method for male colonist maleColonist.update = function () { if (maleColonist.isSafe) return; if (maleColonist.pathIndex < colonistPath.length) { var target = colonistPath[maleColonist.pathIndex]; var dx = target.x - maleColonist.x; var dy = target.y - maleColonist.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 20) { maleColonist.pathIndex++; if (maleColonist.pathIndex >= colonistPath.length) { maleColonist.isSafe = true; // Move male colonist to bunker position and make them visible maleColonist.x = bunker.x + (Math.random() - 0.5) * 120; maleColonist.y = bunker.y + (Math.random() - 0.5) * 120; maleColonist.alpha = 0.8; // Make them slightly transparent to show they're safe // Add a green tint to show they're saved tween(maleColonist, { tint: 0x00FF00 }, { duration: 500, onFinish: function onFinish() { tween(maleColonist, { tint: 0xFFFFFF, alpha: 0 }, { duration: 1000, onFinish: function onFinish() { maleColonist.destroy(); } }); } }); colonistsSaved++; updateUI(); } } else { maleColonist.x += dx / distance * maleColonist.speed; maleColonist.y += dy / distance * maleColonist.speed; } } }; } } function startNewWave() { // Increment wave for new wave (skip increment for very first wave call) if (bugsSpawned > 0 || wave > 1) { wave++; } // Calculate current level (1-250) based on wave progression // Level 1: waves 1-2, Level 2: waves 3-4, Level 3: waves 5-6, etc. var currentLevel = Math.ceil(wave / 2); var expectedColonies = currentLevel; // Add colonies to match the current level (1 colony per level) while (colonies.length < expectedColonies && colonies.length < 8) { spawnNewColony(); } bugsToSpawn = Math.min(15, 3 + wave * 2); bugsSpawned = 0; waveTimer = 0; waveText.setText('Level: ' + currentLevel + ' | Wave: ' + wave + ' | Colonies: ' + colonies.length); // Spawn fleeing colonists and male colonists with dramatic animations if (wave <= 15 && Math.random() < 0.7) { // Spawn regular colonists fleeing from random colonies var colonistsToSpawn = Math.min(colonies.length, Math.ceil(wave / 2)); for (var i = 0; i < colonistsToSpawn; i++) { var randomColony = colonies[Math.floor(Math.random() * colonies.length)]; var colonist = new Colonist(); colonist.x = randomColony.x + (Math.random() - 0.5) * 150; colonist.y = randomColony.y + (Math.random() - 0.5) * 150; colonists.push(colonist); game.addChild(colonist); // Add fleeing animation - quick movement toward first waypoint tween(colonist, { x: colonistPath[0].x + (Math.random() - 0.5) * 50, y: colonistPath[0].y + (Math.random() - 0.5) * 50 }, { duration: 1000 + Math.random() * 1000, easing: tween.easeOut }); } // Spawn male colonists fleeing from random colonies if (Math.random() < 0.5 && colonies.length > 1) { var randomColony = colonies[Math.floor(Math.random() * colonies.length)]; var maleColonist = game.addChild(LK.getAsset('colonistmale', { anchorX: 0.5, anchorY: 0.5 })); maleColonist.x = randomColony.x + (Math.random() - 0.5) * 150; maleColonist.y = randomColony.y + (Math.random() - 0.5) * 150; maleColonist.speed = 1.2; maleColonist.pathIndex = 0; maleColonist.isSafe = false; maleColonist.isMale = true; // Add to colonists array for tracking colonists.push(maleColonist); // Add panic animation - shake then flee tween(maleColonist, { x: maleColonist.x + 20 }, { duration: 100, easing: tween.easeInOut, onFinish: function onFinish() { tween(maleColonist, { x: maleColonist.x - 40 }, { duration: 100, easing: tween.easeInOut, onFinish: function onFinish() { tween(maleColonist, { x: colonistPath[Math.floor(colonistPath.length / 2)].x, y: colonistPath[Math.floor(colonistPath.length / 2)].y }, { duration: 2000 + Math.random() * 1500, easing: tween.easeOut }); } }); } }); // Add custom update method for male colonist maleColonist.update = function () { if (maleColonist.isSafe) return; if (maleColonist.pathIndex < colonistPath.length) { var target = colonistPath[maleColonist.pathIndex]; var dx = target.x - maleColonist.x; var dy = target.y - maleColonist.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 20) { maleColonist.pathIndex++; if (maleColonist.pathIndex >= colonistPath.length) { maleColonist.isSafe = true; // Move male colonist to bunker position and make them visible maleColonist.x = bunker.x + (Math.random() - 0.5) * 120; maleColonist.y = bunker.y + (Math.random() - 0.5) * 120; maleColonist.alpha = 0.8; // Make them slightly transparent to show they're safe // Add a green tint to show they're saved tween(maleColonist, { tint: 0x00FF00 }, { duration: 500, onFinish: function onFinish() { tween(maleColonist, { tint: 0xFFFFFF, alpha: 0 }, { duration: 1000, onFinish: function onFinish() { maleColonist.destroy(); } }); } }); colonistsSaved++; updateUI(); } } else { maleColonist.x += dx / distance * maleColonist.speed; maleColonist.y += dy / distance * maleColonist.speed; } } }; } } } var towerPreview = null; var rangeIndicator = null; game.down = function (x, y, obj) { if (gameState !== 'playing') return; // Check if clicked on restart button if (restartButton) { var dx = x - restartButton.x; var dy = y - restartButton.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= 75) { // Restart the game gameState = 'menu'; // Clean up current game state for (var i = bugs.length - 1; i >= 0; i--) { if (bugs[i] && bugs[i].destroy) bugs[i].destroy(); } for (var i = towers.length - 1; i >= 0; i--) { if (towers[i] && towers[i].destroy) towers[i].destroy(); } for (var i = bullets.length - 1; i >= 0; i--) { if (bullets[i] && bullets[i].destroy) bullets[i].destroy(); } for (var i = colonists.length - 1; i >= 0; i--) { if (colonists[i] && colonists[i].destroy) colonists[i].destroy(); } for (var i = gameElements.length - 1; i >= 0; i--) { if (gameElements[i] && gameElements[i].destroy) gameElements[i].destroy(); } for (var i = towerButtons.length - 1; i >= 0; i--) { if (towerButtons[i] && towerButtons[i].destroy) towerButtons[i].destroy(); } // Clean up preview elements if (towerPreview && towerPreview.destroy) { towerPreview.destroy(); towerPreview = null; } if (rangeIndicator && rangeIndicator.destroy) { rangeIndicator.destroy(); rangeIndicator = null; } // Clear any tower range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i] && game.children[i].isTowerRange) { game.children[i].destroy(); } } // Hide UI elements coinText.visible = false; waveText.visible = false; infoText.visible = false; killsText.visible = false; savedText.visible = false; if (restartButton) restartButton.destroy(); if (trashCan) trashCan.destroy(); // Clear arrays bugs = []; towers = []; bullets = []; colonists = []; colonies = []; gameElements = []; towerButtons = []; // Show main menu mainMenu = new MainMenu(); game.addChild(mainMenu); return; } } // First check if we clicked on a tower button var clickedTowerButton = false; for (var i = 0; i < towerButtons.length; i++) { var button = towerButtons[i]; if (button && button.visible) { var dx = x - button.x; var dy = y - button.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= 60) { // Button click radius clickedTowerButton = true; if (coins >= button.cost) { placingTowerType = button.towerType; selectedTower = null; // Clean up any existing preview elements when starting new placement if (towerPreview) { towerPreview.destroy(); towerPreview = null; } if (rangeIndicator) { rangeIndicator.destroy(); rangeIndicator = null; } updateUI(); } break; } } } if (clickedTowerButton) return; // Don't process other clicks if we clicked a button // Check if clicked on trash can when tower is selected if (selectedTower && trashCan) { var dx = x - trashCan.x; var dy = y - trashCan.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= 60) { // Delete tower without refund for (var i = towers.length - 1; i >= 0; i--) { if (towers[i] === selectedTower) { towers.splice(i, 1); break; } } selectedTower.destroy(); selectedTower = null; // Clear tower range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.children[i].destroy(); } } updateUI(); return; } } // Handle tower sell if tower is selected and clicked in UI area if (selectedTower && y > 2400 && x >= 1024) { // Right side - sell button area selectedTower.sell(); updateUI(); return; } // Handle tower placement - allow placement anywhere except near UI elements if (placingTowerType && y < 2500) { // Don't place towers in bottom UI area var canPlace = true; var cost = towerCosts[placingTowerType]; // Check if we have enough coins if (coins < cost) { canPlace = false; } // Check distance from path for (var i = 0; i < gamePath.length; i++) { var dx = x - gamePath[i].x; var dy = y - gamePath[i].y; if (Math.sqrt(dx * dx + dy * dy) < 80) { canPlace = false; break; } } // Check distance from existing towers for (var i = 0; i < towers.length; i++) { var dx = x - towers[i].x; var dy = y - towers[i].y; if (Math.sqrt(dx * dx + dy * dy) < 100) { canPlace = false; break; } } // Check distance from colonies and bunker var structures = colonies.slice(); // Copy colonies array structures.push(bunker); for (var i = 0; i < structures.length; i++) { var dx = x - structures[i].x; var dy = y - structures[i].y; if (Math.sqrt(dx * dx + dy * dy) < 120) { canPlace = false; break; } } if (canPlace) { var tower = new Tower(placingTowerType); tower.x = x; tower.y = y; towers.push(tower); game.addChild(tower); coins -= cost; placingTowerType = null; // Clean up preview elements if (towerPreview) { towerPreview.destroy(); towerPreview = null; } if (rangeIndicator) { rangeIndicator.destroy(); rangeIndicator = null; } updateUI(); } } else if (!placingTowerType) { // Clear selection only if we're not placing a tower selectedTower = null; // Clean up any remaining preview elements if (towerPreview) { towerPreview.destroy(); towerPreview = null; } if (rangeIndicator) { rangeIndicator.destroy(); rangeIndicator = null; } // Clear tower range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.children[i].destroy(); } } } updateUI(); }; game.update = function () { if (gameState !== 'playing') return; if (gameWon) return; // Spawn bugs waveTimer++; if (waveTimer % 45 === 0 && bugsSpawned < bugsToSpawn) { spawnBug(); } // Check if wave is complete if (bugsSpawned >= bugsToSpawn && bugs.length === 0) { var currentLevel = Math.ceil(wave / 2); if (currentLevel >= 250) { gameWon = true; // Save game progress safely if (storage) { storage.coins = coins; storage.level = Math.ceil(wave / 2); // Save current level } var finalScore = calculateScore(); saveHighScore(finalScore); LK.setTimeout(function () { LK.showYouWin(); }, 1000); } else { // Properly advance to next wave/level LK.setTimeout(function () { startNewWave(); }, 2000); } } // Clean up bullets that are no longer in the game for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; if (!bullet || !bullet.parent) { bullets.splice(i, 1); } } // Clean up destroyed bugs for (var i = bugs.length - 1; i >= 0; i--) { var bug = bugs[i]; if (!bug || !bug.parent || bug.health <= 0) { bugs.splice(i, 1); } } // Update towers to shoot at bugs for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (tower.update && typeof tower.update === 'function') { tower.update(); } } // Update colonists that have update methods for (var i = 0; i < colonists.length; i++) { var colonist = colonists[i]; if (colonist.update && typeof colonist.update === 'function') { colonist.update(); } } // Check for bug-colonist intersections for (var i = bugs.length - 1; i >= 0; i--) { var bug = bugs[i]; for (var j = colonists.length - 1; j >= 0; j--) { var colonist = colonists[j]; if (!colonist.isSafe && colonist.intersects && colonist.intersects(bug)) { // Play colonist scream sound when killed by bug var screamSound = LK.getSound('colonistscream'); if (screamSound) screamSound.play(); // Remove the colonist colonist.destroy(); colonists.splice(j, 1); break; // One bug can only kill one colonist per frame } } } // Check for bugs reaching the end var bugsReachedEnd = 0; for (var i = bugs.length - 1; i >= 0; i--) { var bug = bugs[i]; if (bug.pathIndex >= gamePath.length) { bug.destroy(); bugs.splice(i, 1); bugsReachedEnd++; if (bug.isBoss) { bugsReachedEnd += 4; // Boss counts as 5 bugs reaching end } } } // Check lose condition if (bugsReachedEnd >= 3) { // Save game progress safely if (storage) { storage.coins = coins; storage.level = Math.ceil(wave / 2); // Save current level } var finalScore = calculateScore(); saveHighScore(finalScore); LK.showGameOver(); } // Update UI periodically if (LK.ticks % 60 === 0) { updateUI(); } // Save coins to storage periodically if (LK.ticks % 300 === 0 && storage) { storage.coins = coins; } }; game.move = function (x, y, obj) { if (gameState !== 'playing') return; if (placingTowerType && y < 2500) { // Remove existing preview and range indicator if (towerPreview) { towerPreview.destroy(); towerPreview = null; } if (rangeIndicator) { rangeIndicator.destroy(); rangeIndicator = null; } // Create tower preview towerPreview = game.addChild(LK.getAsset('tower', { anchorX: 0.5, anchorY: 0.5 })); towerPreview.x = x; towerPreview.y = y; towerPreview.width = 80; towerPreview.height = 80; towerPreview.alpha = 0.7; // Set preview color based on tower type switch (placingTowerType) { case 'rapid': towerPreview.tint = 0x00AAFF; break; case 'sniper': towerPreview.tint = 0xFF4444; break; case 'cannon': towerPreview.tint = 0xFF8800; break; case 'freeze': towerPreview.tint = 0x88CCFF; break; case 'poison': towerPreview.tint = 0x00FF00; break; default: towerPreview.tint = 0x4169E1; } // Create range indicator rangeIndicator = new Container(); game.addChild(rangeIndicator); var rangeCircle = rangeIndicator.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); // Set range based on tower type var range = 150; switch (placingTowerType) { case 'basic': range = 150; break; case 'rapid': range = 120; break; case 'sniper': range = 250; break; case 'cannon': range = 140; break; case 'freeze': range = 130; break; case 'poison': range = 140; break; } rangeIndicator.x = x; rangeIndicator.y = y; rangeCircle.width = range * 2; rangeCircle.height = range * 2; rangeCircle.alpha = 0.2; rangeCircle.tint = 0xFFFFFF; // Check if placement is valid var canPlace = true; var cost = towerCosts[placingTowerType]; if (coins < cost) { canPlace = false; } // Check distance from path for (var i = 0; i < gamePath.length; i++) { var dx = x - gamePath[i].x; var dy = y - gamePath[i].y; if (Math.sqrt(dx * dx + dy * dy) < 80) { canPlace = false; break; } } // Check distance from existing towers for (var i = 0; i < towers.length; i++) { var dx = x - towers[i].x; var dy = y - towers[i].y; if (Math.sqrt(dx * dx + dy * dy) < 100) { canPlace = false; break; } } // Check distance from colonies and bunker var structures = colonies.slice(); structures.push(bunker); for (var i = 0; i < structures.length; i++) { var dx = x - structures[i].x; var dy = y - structures[i].y; if (Math.sqrt(dx * dx + dy * dy) < 120) { canPlace = false; break; } } // Update preview colors based on validity if (!canPlace) { towerPreview.tint = 0xFF0000; rangeCircle.tint = 0xFF0000; } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Bug = Container.expand(function (type, wave) {
var self = Container.call(this);
self.type = type || 'small';
self.wave = wave || 1;
self.isBoss = false;
self.isFlying = false;
self.isArmored = false;
self.slowResistance = 0;
self.pathIndex = 0;
// Set base stats based on type
switch (self.type) {
case 'small':
self.health = 15;
self.speed = 2.5;
self.reward = 5;
break;
case 'medium':
self.health = 35;
self.speed = 2;
self.reward = 7;
break;
case 'large':
self.health = 80;
self.speed = 1.5;
self.reward = 10;
break;
case 'fast':
self.health = 20;
self.speed = 4;
self.reward = 8;
break;
case 'armored':
self.health = 120;
self.speed = 1;
self.reward = 15;
self.isArmored = true;
break;
case 'flying':
self.health = 40;
self.speed = 3;
self.reward = 12;
self.isFlying = true;
break;
case 'boss':
self.health = 500;
self.speed = 0.8;
self.reward = 50;
self.isBoss = true;
self.slowResistance = 0.7;
break;
}
// Scale with wave
var waveMultiplier = 1 + (self.wave - 1) * 0.15;
self.health = Math.floor(self.health * waveMultiplier);
self.maxHealth = self.health;
self.slowEffect = 1;
self.poisonTimer = 0;
var bugGraphics = self.attachAsset(self.type + 'Bug', {
anchorX: 0.5,
anchorY: 0.5
});
// Boss scaling
if (self.isBoss) {
bugGraphics.scaleX = 1.5;
bugGraphics.scaleY = 1.5;
bugGraphics.tint = 0xFF4444;
}
// Flying effect
if (self.isFlying) {
bugGraphics.alpha = 0.8;
var originalY = bugGraphics.y;
tween(bugGraphics, {
y: originalY - 10
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(bugGraphics, {
y: originalY + 10
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Create infinite loop
tween(bugGraphics, {
y: originalY - 10
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: arguments.callee.caller
});
}
});
}
});
}
self.takeDamage = function (damage, damageType) {
var actualDamage = damage;
if (self.isArmored && damageType !== 'piercing') {
actualDamage = Math.max(1, damage * 0.5);
}
if (damageType === 'poison') {
self.poisonTimer = 180; // 3 seconds at 60fps
actualDamage = damage * 0.3;
}
self.health -= actualDamage;
var healthPercent = self.health / self.maxHealth;
if (damageType === 'poison') {
bugGraphics.tint = 0x00FF00;
} else {
bugGraphics.tint = healthPercent < 0.5 ? 0xFF6666 : 0xFFFFFF;
}
return self.health <= 0;
};
self.applySlow = function (slowAmount, duration) {
if (self.slowResistance < 1) {
var effectiveSlowAmount = slowAmount * (1 - self.slowResistance);
self.slowEffect = Math.min(self.slowEffect, 1 - effectiveSlowAmount);
LK.setTimeout(function () {
self.slowEffect = 1;
}, duration);
}
};
self.update = function () {
// Handle poison damage
if (self.poisonTimer > 0) {
self.poisonTimer--;
if (self.poisonTimer % 30 === 0) {
// Damage every 0.5 seconds
self.takeDamage(5, 'poison');
}
if (self.poisonTimer <= 0) {
bugGraphics.tint = 0xFFFFFF;
}
}
// Check if bug is visible on screen and play appropriate sound
var isOnScreen = self.x >= -50 && self.x <= 2098 && self.y >= -50 && self.y <= 2782;
if (isOnScreen) {
// Play bug sound based on type every 2 seconds (120 frames)
if (LK.ticks % 120 === 0) {
if (self.isFlying) {
var flyingSound = LK.getSound('bugflying');
if (flyingSound) flyingSound.play();
} else {
var crawlingSound = LK.getSound('bugcrawling');
if (crawlingSound) crawlingSound.play();
}
}
}
// Move along path
if (self.pathIndex < gamePath.length) {
var target = gamePath[self.pathIndex];
var dx = target.x - self.x;
var dy = target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 30) {
self.pathIndex++;
} else {
var actualSpeed = self.speed * self.slowEffect;
self.x += dx / distance * actualSpeed;
self.y += dy / distance * actualSpeed;
}
}
};
return self;
});
var Bullet = Container.expand(function (bulletType, tower) {
var self = Container.call(this);
self.speed = 8;
self.target = null;
self.damage = 1;
self.type = bulletType || 'basic';
self.tower = tower;
self.splashRadius = 0;
self.piercing = false;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
// Customize bullet based on type
switch (self.type) {
case 'rapid':
bulletGraphics.tint = 0x00AAFF;
bulletGraphics.scaleX = 0.7;
bulletGraphics.scaleY = 0.7;
self.speed = 12;
break;
case 'sniper':
bulletGraphics.tint = 0xFF4444;
bulletGraphics.scaleX = 1.2;
bulletGraphics.scaleY = 1.2;
self.speed = 15;
self.piercing = true;
break;
case 'cannon':
bulletGraphics.tint = 0xFF8800;
bulletGraphics.scaleX = 1.5;
bulletGraphics.scaleY = 1.5;
self.speed = 6;
self.splashRadius = 100;
break;
case 'freeze':
bulletGraphics.tint = 0x88CCFF;
self.speed = 10;
break;
case 'poison':
bulletGraphics.tint = 0x00FF00;
self.speed = 9;
break;
}
self.update = function () {
// Check if target is still valid
if (!self.target || !self.target.parent || self.target.health <= 0) {
self.destroy();
return;
}
// Check if bullet is off-screen
if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) {
self.destroy();
return;
}
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 25) {
self.hitTarget();
self.destroy();
} else {
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
}
};
self.hitTarget = function () {
var damageType = self.type === 'sniper' ? 'piercing' : self.type === 'poison' ? 'poison' : 'normal';
// Apply main damage
if (self.target.takeDamage(self.damage, damageType)) {
// Play bugdeath sound immediately when bug dies
var bugDeathSound = LK.getSound('bugDeath');
if (bugDeathSound) bugDeathSound.play();
self.killBug(self.target);
}
// Special effects
if (self.type === 'freeze') {
self.target.applySlow(0.6, 2000); // 60% slow for 2 seconds
}
// Splash damage
if (self.splashRadius > 0) {
for (var i = 0; i < bugs.length; i++) {
var bug = bugs[i];
if (bug !== self.target) {
var dx = bug.x - self.target.x;
var dy = bug.y - self.target.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= self.splashRadius) {
if (bug.takeDamage(self.damage * 0.6, damageType)) {
// Play bugdeath sound for splash damage kills too
var bugDeathSound = LK.getSound('bugDeath');
if (bugDeathSound) bugDeathSound.play();
self.killBug(bug);
}
}
}
}
}
};
self.killBug = function (bug) {
coins += bug.reward;
bugsKilled++;
updateUI();
for (var i = bugs.length - 1; i >= 0; i--) {
if (bugs[i] === bug) {
bug.destroy();
bugs.splice(i, 1);
break;
}
}
};
return self;
});
var Colonist = Container.expand(function () {
var self = Container.call(this);
self.speed = 1;
self.pathIndex = 0;
self.isSafe = false;
var colonistGraphics = self.attachAsset('colonist', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
if (self.isSafe) return;
if (self.pathIndex < colonistPath.length) {
var target = colonistPath[self.pathIndex];
var dx = target.x - self.x;
var dy = target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 20) {
self.pathIndex++;
if (self.pathIndex >= colonistPath.length) {
self.isSafe = true;
// Move colonist to bunker position and make them visible
self.x = bunker.x + (Math.random() - 0.5) * 100;
self.y = bunker.y + (Math.random() - 0.5) * 100;
self.alpha = 0.8; // Make them slightly transparent to show they're safe
// Add a green tint to show they're saved
tween(self, {
tint: 0x00FF00
}, {
duration: 500,
onFinish: function onFinish() {
tween(self, {
tint: 0xFFFFFF,
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
colonistsSaved++;
updateUI();
}
} else {
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
}
}
};
return self;
});
var MainMenu = Container.expand(function () {
var self = Container.call(this);
// Main menu background
var menuBg = self.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
menuBg.x = 1024;
menuBg.y = 1366;
menuBg.width = 2048;
menuBg.height = 2732;
menuBg.alpha = 0.7;
// Title text
var titleText = new Text2('COLONY DEFENSE', {
size: 120,
fill: 0xFFFF00
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 1024;
titleText.y = 800;
self.addChild(titleText);
// Subtitle text
var subtitleText = new Text2('Defend the colonies from alien bugs!', {
size: 60,
fill: 0x00FFFF
});
subtitleText.anchor.set(0.5, 0.5);
subtitleText.x = 1024;
subtitleText.y = 950;
self.addChild(subtitleText);
// Start button
var startButton = self.attachAsset('playbutton', {
anchorX: 0.5,
anchorY: 0.5
});
startButton.x = 1024;
startButton.y = 1400;
startButton.width = 300;
startButton.height = 150;
startButton.tint = 0x00AA00;
var startText = new Text2('START GAME', {
size: 50,
fill: 0xFFFF00
});
startText.anchor.set(0.5, 0.5);
startText.x = 1024;
startText.y = 1400;
self.addChild(startText);
// Instructions text
var instructText = new Text2('Use robot towers to stop the alien invasion!\nSave the colonists and defend your colonies!', {
size: 40,
fill: 0xFFFFFF
});
instructText.anchor.set(0.5, 0.5);
instructText.x = 1024;
instructText.y = 1800;
self.addChild(instructText);
// High scores display for main menu
var menuHighScores = storage.highScores || [];
var scoreText = 'TOP 5 SCORES:\n';
for (var i = 0; i < Math.min(5, menuHighScores.length); i++) {
scoreText += i + 1 + '. ' + menuHighScores[i] + '\n';
}
// Fill empty spots
for (var i = menuHighScores.length; i < 5; i++) {
scoreText += i + 1 + '. ---\n';
}
var menuScoreDisplay = new Text2(scoreText, {
size: 45,
fill: 0xFFFF00
});
menuScoreDisplay.anchor.set(0.5, 0.5);
menuScoreDisplay.x = 1024;
menuScoreDisplay.y = 2200;
self.addChild(menuScoreDisplay);
// Add pulsing animation to start button
tween(startButton, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(startButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: arguments.callee.caller
});
}
});
self.down = function (x, y, obj) {
// Check if clicked on start button area
var dx = x - startButton.x;
var dy = y - startButton.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 150) {
startGame();
}
};
return self;
});
var Tower = Container.expand(function (towerType) {
var self = Container.call(this);
self.type = towerType || 'basic';
self.range = 150;
self.damage = 1;
self.fireRate = 30;
self.lastShot = 0;
self.level = 1;
self.maxLevel = 5;
self.upgradeCost = 20;
self.sellValue = 15;
// Set base stats based on tower type
switch (self.type) {
case 'basic':
self.range = 150;
self.damage = 8;
self.fireRate = 40;
self.upgradeCost = 20;
break;
case 'rapid':
self.range = 120;
self.damage = 4;
self.fireRate = 15;
self.upgradeCost = 30;
break;
case 'sniper':
self.range = 250;
self.damage = 25;
self.fireRate = 80;
self.upgradeCost = 50;
break;
case 'cannon':
self.range = 140;
self.damage = 20;
self.fireRate = 60;
self.upgradeCost = 40;
break;
case 'freeze':
self.range = 130;
self.damage = 6;
self.fireRate = 35;
self.upgradeCost = 35;
break;
case 'poison':
self.range = 140;
self.damage = 12;
self.fireRate = 45;
self.upgradeCost = 45;
break;
}
var towerGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper scaling for visibility
towerGraphics.width = 80;
towerGraphics.height = 80;
towerGraphics.alpha = 1.0;
// Move tower up 10 spaces
towerGraphics.y = -10;
// Set tower color based on type
switch (self.type) {
case 'rapid':
towerGraphics.tint = 0x00AAFF;
break;
case 'sniper':
towerGraphics.tint = 0xFF4444;
break;
case 'cannon':
towerGraphics.tint = 0xFF8800;
break;
case 'freeze':
towerGraphics.tint = 0x88CCFF;
break;
case 'poison':
towerGraphics.tint = 0x00FF00;
break;
default:
towerGraphics.tint = 0x4169E1;
}
self.canShoot = function () {
return LK.ticks - self.lastShot >= self.fireRate;
};
self.findTarget = function () {
var bestTarget = null;
var bestScore = -1;
for (var i = 0; i < bugs.length; i++) {
var bug = bugs[i];
// Skip if bug is not valid or has no health
if (!bug || !bug.parent || bug.health <= 0) continue;
var dx = bug.x - self.x;
var dy = bug.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.range) {
// Enhanced targeting priority system
var score = 0;
// Base score: progress along path (higher = closer to end)
score += bug.pathIndex * 1000;
// Boss priority bonus
if (bug.isBoss) score += 2000;
// Flying bug priority (harder to hit)
if (bug.isFlying) score += 800;
// Armored bug priority for certain tower types
if (bug.isArmored && self.type === 'sniper') score += 1500;
// Low health targets for quick kills
if (bug.health <= self.damage) score += 1200;
// Distance penalty (closer = higher priority)
score -= distance;
// Type-specific targeting
if (self.type === 'poison' && bug.poisonTimer <= 0) score += 600;
if (self.type === 'freeze' && bug.slowEffect >= 0.8) score += 400;
if (score > bestScore) {
bestScore = score;
bestTarget = bug;
}
}
}
return bestTarget;
};
self.shoot = function (target) {
if (!self.canShoot()) return;
var bullet = new Bullet(self.type, self);
bullet.x = self.x;
bullet.y = self.y;
bullet.target = target;
bullet.damage = self.damage;
bullets.push(bullet);
game.addChild(bullet);
self.lastShot = LK.ticks;
var shootSound = LK.getSound('shoot');
if (shootSound) shootSound.play();
// Visual muzzle flash
towerGraphics.alpha = 1.5;
tween(towerGraphics, {
alpha: 1
}, {
duration: 100
});
};
self.upgrade = function () {
if (self.level >= self.maxLevel) return false;
if (coins >= self.upgradeCost) {
coins -= self.upgradeCost;
self.level++;
// Upgrade stats
self.damage = Math.floor(self.damage * 1.3);
self.range += 15;
if (self.type === 'rapid') {
self.fireRate = Math.max(8, self.fireRate - 3);
} else {
self.fireRate = Math.max(15, self.fireRate - 5);
}
self.sellValue += Math.floor(self.upgradeCost * 0.7);
self.upgradeCost = Math.floor(self.upgradeCost * 1.5);
towerGraphics.tint = 0x00FF00;
tween(towerGraphics, {
tint: self.type === 'rapid' ? 0x00AAFF : self.type === 'sniper' ? 0xFF4444 : self.type === 'cannon' ? 0xFF8800 : self.type === 'freeze' ? 0x88CCFF : self.type === 'poison' ? 0x00FF00 : 0x4169E1
}, {
duration: 500
});
updateUI();
return true;
}
return false;
};
self.sell = function () {
coins += self.sellValue;
selectedTower = null;
for (var i = towers.length - 1; i >= 0; i--) {
if (towers[i] === self) {
towers.splice(i, 1);
break;
}
}
self.destroy();
updateUI();
};
self.update = function () {
var target = self.findTarget();
if (target && self.canShoot()) {
self.shoot(target);
}
};
self.down = function (x, y, obj) {
var currentTime = Date.now();
// Check for double-click
if (lastClickedTower === self && currentTime - lastClickTime < doubleClickDelay) {
// Double-click detected - attempt to upgrade
if (self.upgrade()) {
// Upgrade successful - show visual feedback
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 150,
easing: tween.easeIn
});
}
});
}
// Reset double-click tracking
lastClickTime = 0;
lastClickedTower = null;
return;
}
// Single click - update tracking and show tower info
lastClickTime = currentTime;
lastClickedTower = self;
// Clear any existing range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.children[i].destroy();
}
}
selectedTower = self;
showTowerInfo();
// Create range indicator for selected tower
var towerRangeIndicator = new Container();
towerRangeIndicator.isTowerRange = true;
game.addChild(towerRangeIndicator);
var rangeCircle = towerRangeIndicator.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
towerRangeIndicator.x = self.x;
towerRangeIndicator.y = self.y;
rangeCircle.width = self.range * 2;
rangeCircle.height = self.range * 2;
rangeCircle.alpha = 0.3;
rangeCircle.tint = 0x00FF00;
};
return self;
});
var TowerButton = Container.expand(function (towerType, cost) {
var self = Container.call(this);
self.towerType = towerType;
self.cost = cost;
var buttonBg = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 120;
buttonBg.height = 120;
// Set button color based on tower type
switch (towerType) {
case 'rapid':
buttonBg.tint = 0x00AAFF;
break;
case 'sniper':
buttonBg.tint = 0xFF4444;
break;
case 'cannon':
buttonBg.tint = 0xFF8800;
break;
case 'freeze':
buttonBg.tint = 0x88CCFF;
break;
case 'poison':
buttonBg.tint = 0x00FF00;
break;
default:
buttonBg.tint = 0x4169E1;
}
var costText = new Text2('$' + cost, {
size: 30,
fill: 0xFFFF00
});
costText.anchor.set(0.5, 0.5);
costText.y = 50;
self.addChild(costText);
var typeText = new Text2(towerType.toUpperCase(), {
size: 25,
fill: 0xFFFFFF
});
typeText.anchor.set(0.5, 0.5);
typeText.y = -50;
self.addChild(typeText);
self.update = function () {
var canAfford = coins >= self.cost;
self.alpha = canAfford ? 1 : 0.5;
};
self.down = function (x, y, obj) {
if (coins >= self.cost) {
placingTowerType = self.towerType;
selectedTower = null;
updateUI();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2F4F2F
});
/****
* Game Code
****/
var bugs = [];
var towers = [];
var bullets = [];
var colonists = [];
var coins = 25;
var wave = 1;
var maxWaves = 500; // 250 levels × 2 waves per level = 500 total waves
var bugsKilled = 0;
var colonistsSaved = 0;
var selectedTower = null;
var placingTowerType = null;
var waveTimer = 0;
var waveDelay = 300;
var bugsToSpawn = 5;
var bugsSpawned = 0;
var gameWon = false;
var towerButtons = [];
var gameState = 'menu'; // 'menu' or 'playing'
var mainMenu = null;
var gameElements = [];
// Double-click detection variables
var lastClickTime = 0;
var lastClickedTower = null;
var doubleClickDelay = 300; // milliseconds
// Tower costs
var towerCosts = {
basic: 15,
rapid: 25,
sniper: 40,
cannon: 35,
freeze: 30,
poison: 45
};
// High score management
var highScores = storage.highScores || [];
var scoreDisplay = null;
function calculateScore() {
return bugsKilled * 10 + colonistsSaved * 50 + wave * 100;
}
function saveHighScore(score) {
// Add current score
highScores.push(score);
// Sort in descending order
highScores.sort(function (a, b) {
return b - a;
});
// Keep only top 5
if (highScores.length > 5) {
highScores = highScores.slice(0, 5);
}
// Save to storage
storage.highScores = highScores;
updateScoreDisplay();
}
function updateScoreDisplay() {
if (!scoreDisplay) return;
var scoreText = 'TOP 5 SCORES:\n';
for (var i = 0; i < Math.min(5, highScores.length); i++) {
scoreText += i + 1 + '. ' + highScores[i] + '\n';
}
// Fill empty spots
for (var i = highScores.length; i < 5; i++) {
scoreText += i + 1 + '. ---\n';
}
scoreDisplay.setText(scoreText);
}
function createScoreDisplay() {
scoreDisplay = new Text2('TOP 5 SCORES:\n1. ---\n2. ---\n3. ---\n4. ---\n5. ---', {
size: 40,
fill: 0xFFFF00
});
scoreDisplay.anchor.set(0.5, 1);
scoreDisplay.x = 0;
scoreDisplay.y = -20;
scoreDisplay.visible = false;
LK.gui.bottom.addChild(scoreDisplay);
updateScoreDisplay();
}
// Game path for bugs
var gamePath = [{
x: 0,
y: 1366
}, {
x: 400,
y: 1366
}, {
x: 400,
y: 800
}, {
x: 800,
y: 800
}, {
x: 800,
y: 1366
}, {
x: 1200,
y: 1366
}, {
x: 1200,
y: 600
}, {
x: 1600,
y: 600
}, {
x: 1600,
y: 1000
}, {
x: 2048,
y: 1000
}];
// Colonist path to bunker - updated to connect colonies better
var colonistPath = [{
x: 500,
// Start near colony1
y: 700
}, {
x: 900,
y: 900
}, {
x: 1200,
y: 900
}, {
x: 1400,
y: 800
}, {
x: 1600,
y: 1000
}, {
x: 1800,
y: 1400
}];
// Create colonies array and bunker variable
var colonies = [];
var bunker = null;
// Load saved level from storage
var savedLevel = storage.level || 1;
wave = (savedLevel - 1) * 2; // Convert level back to wave number
// Initialize main menu
mainMenu = new MainMenu();
game.addChild(mainMenu);
// UI Elements
var coinText = new Text2('Coins: 50', {
size: 60,
fill: 0xFFFF00
});
coinText.anchor.set(0, 0);
coinText.x = -265;
coinText.visible = false;
LK.gui.topRight.addChild(coinText);
var waveText = new Text2('Wave: 1', {
size: 60,
fill: 0xFFFFFF
});
waveText.anchor.set(0.5, 0);
waveText.visible = false;
LK.gui.top.addChild(waveText);
var infoText = new Text2('Tap empty space to place tower ($15)', {
size: 40,
fill: 0x00FFFF
});
infoText.anchor.set(0.5, 1);
infoText.visible = false;
LK.gui.bottom.addChild(infoText);
var killsText = new Text2('Bugs Killed: 0', {
size: 50,
fill: 0xFF0000
});
killsText.anchor.set(0, 0);
killsText.visible = false;
LK.gui.left.addChild(killsText);
var savedText = new Text2('Colonists Saved: 0', {
size: 50,
fill: 0x00FF00
});
savedText.anchor.set(0, 0);
savedText.y = 80;
savedText.visible = false;
LK.gui.left.addChild(savedText);
// Create score display
createScoreDisplay();
// Create restart button
var restartButton = null;
// Create trash can in right bottom corner
var trashCan = null;
// Create tower selection buttons
var towerTypes = ['basic', 'rapid', 'sniper', 'cannon', 'freeze', 'poison'];
var buttonSpacing = 140;
var startX = 100;
for (var i = 0; i < towerTypes.length; i++) {
var towerButton = new TowerButton(towerTypes[i], towerCosts[towerTypes[i]]);
towerButton.x = startX + i * buttonSpacing;
towerButton.y = 2600;
towerButton.visible = false;
game.addChild(towerButton);
towerButtons.push(towerButton);
}
function startGame() {
if (gameState === 'playing') return;
gameState = 'playing';
// Hide main menu
if (mainMenu) {
mainMenu.destroy();
mainMenu = null;
}
// Reset game variables
bugs = [];
towers = [];
bullets = [];
colonists = [];
colonies = [];
// Initialize coins from storage, ensuring we have at least 25 coins
var storedCoins = storage.coins;
if (typeof storedCoins === 'number' && storedCoins >= 0) {
coins = Math.max(25, storedCoins);
} else {
coins = 25;
}
var savedLevel = storage.level;
if (typeof savedLevel !== 'number' || savedLevel < 1) {
savedLevel = 1;
}
wave = (savedLevel - 1) * 2 + 1; // Start from saved level's first wave
bugsKilled = 0;
colonistsSaved = 0;
selectedTower = null;
placingTowerType = null;
waveTimer = 0;
bugsToSpawn = 5;
bugsSpawned = 0;
gameWon = false;
// Destroy existing tower buttons
for (var i = 0; i < towerButtons.length; i++) {
if (towerButtons[i] && towerButtons[i].parent) {
towerButtons[i].destroy();
}
}
towerButtons = [];
// Recreate tower selection buttons
var towerTypes = ['basic', 'rapid', 'sniper', 'cannon', 'freeze', 'poison'];
var buttonSpacing = 140;
var startX = 100;
for (var i = 0; i < towerTypes.length; i++) {
var towerButton = new TowerButton(towerTypes[i], towerCosts[towerTypes[i]]);
towerButton.x = startX + i * buttonSpacing;
towerButton.y = 2600;
towerButton.visible = true;
towerButton.alpha = 1.0;
game.addChild(towerButton);
towerButtons.push(towerButton);
}
// Initialize game elements
initializeGameElements();
// Show UI elements
coinText.visible = true;
waveText.visible = true;
infoText.visible = true;
killsText.visible = true;
savedText.visible = true;
// Keep score display hidden during gameplay
if (scoreDisplay) scoreDisplay.visible = false;
// Create restart button
if (restartButton) {
restartButton.destroy();
}
restartButton = game.addChild(LK.getAsset('playbutton', {
anchorX: 0.5,
anchorY: 0.5
}));
restartButton.x = 200;
restartButton.y = 150;
restartButton.width = 150;
restartButton.height = 80;
restartButton.tint = 0xFF4444;
restartButton.alpha = 0.8;
var restartText = new Text2('RESTART', {
size: 30,
fill: 0xFFFFFF
});
restartText.anchor.set(0.5, 0.5);
restartText.x = 200;
restartText.y = 150;
game.addChild(restartText);
// Create trash can in right bottom corner
if (trashCan) {
trashCan.destroy();
}
trashCan = game.addChild(LK.getAsset('trashCan', {
anchorX: 0.5,
anchorY: 0.5
}));
trashCan.x = 1950; // Right side of screen
trashCan.y = 2600; // Bottom area
trashCan.width = 80;
trashCan.height = 100;
trashCan.alpha = 0.8;
// Make tower buttons visible and ensure proper positioning
for (var i = 0; i < towerButtons.length; i++) {
if (towerButtons[i]) {
towerButtons[i].visible = true;
// Ensure buttons are positioned at bottom of screen for visibility
towerButtons[i].y = 2600;
towerButtons[i].alpha = 1.0;
// Make sure they're on top of other game elements
game.setChildIndex(towerButtons[i], game.children.length - 1);
}
}
// Spawn initial fleeing colonists before any enemies appear
spawnInitialFleeingColonists();
// Ensure we start at level 1 and begin first wave
wave = 0; // Start at 0 so first call to startNewWave makes it 1
waveText.setText('Level: 1 | Wave: 1 | Colonies: ' + colonies.length);
LK.setTimeout(function () {
startNewWave();
}, 3000);
}
function initializeGameElements() {
// Add background image
var backgroundImage = game.addChild(LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5
}));
backgroundImage.x = 1024;
backgroundImage.y = 1366;
backgroundImage.width = 2048;
backgroundImage.height = 2732;
gameElements.push(backgroundImage);
// Create initial colony
var colony1 = game.addChild(LK.getAsset('colony', {
anchorX: 0.5,
anchorY: 0.5
}));
colony1.x = 400;
colony1.y = 600;
colonies.push(colony1);
gameElements.push(colony1);
// Create bunker
bunker = game.addChild(LK.getAsset('bunker', {
anchorX: 0.5,
anchorY: 0.5
}));
bunker.x = 1800;
bunker.y = 1400;
gameElements.push(bunker);
// Show initial instructions
var instructionText = new Text2('Defend the colonies from alien bugs!\nUse robot towers to stop them!', {
size: 60,
fill: 0x00FFFF
});
instructionText.anchor.set(0.5, 0.5);
instructionText.x = 1024;
instructionText.y = 600;
game.addChild(instructionText);
gameElements.push(instructionText);
tween(instructionText, {
alpha: 0
}, {
duration: 4000,
onFinish: function onFinish() {
instructionText.destroy();
}
});
// Play background music
var bgMusic = LK.getMusic ? LK.getMusic('gameMusic') : null;
if (bgMusic || LK.playMusic) {
LK.playMusic('gameMusic');
}
}
function updateUI() {
if (gameState !== 'playing') return;
coinText.setText('Coins: ' + coins);
killsText.setText('Bugs Killed: ' + bugsKilled);
savedText.setText('Colonists Saved: ' + colonistsSaved);
if (selectedTower) {
var upgradeText = selectedTower.level >= selectedTower.maxLevel ? 'MAX LEVEL' : 'Double-tap to Upgrade ($' + selectedTower.upgradeCost + ')';
infoText.setText('Tower Lv.' + selectedTower.level + ' - ' + upgradeText + ' | Tap RIGHT to Sell ($' + selectedTower.sellValue + ') | Drag to TRASH to delete');
} else if (placingTowerType) {
infoText.setText('Placing ' + placingTowerType.toUpperCase() + ' tower ($' + towerCosts[placingTowerType] + ') - Tap open area');
} else {
infoText.setText('Select a tower type below to place');
}
}
function showTowerInfo() {
updateUI();
}
function spawnBug() {
if (bugsSpawned >= bugsToSpawn) return;
var bugType;
// Random chance for queenbug (boss) to appear in any wave - no set limit
if (Math.random() < 0.15) {
// 15% chance for queenbug to spawn
bugType = 'boss';
} else if (wave >= 15) {
var advancedTypes = ['medium', 'large', 'fast', 'armored', 'flying'];
bugType = advancedTypes[Math.floor(Math.random() * advancedTypes.length)];
} else if (wave >= 10) {
var midTypes = ['small', 'medium', 'fast', 'flying'];
bugType = midTypes[Math.floor(Math.random() * midTypes.length)];
} else if (wave >= 5) {
var earlyTypes = ['small', 'medium', 'fast'];
bugType = earlyTypes[Math.floor(Math.random() * earlyTypes.length)];
} else {
var basicTypes = ['small', 'small', 'medium'];
bugType = basicTypes[Math.floor(Math.random() * basicTypes.length)];
}
var bug = new Bug(bugType, wave);
bug.x = gamePath[0].x;
bug.y = gamePath[0].y;
bugs.push(bug);
game.addChild(bug);
bugsSpawned++;
// Play bug crawling sound when bug spawns
var crawlingSound = LK.getSound('bugcrawling');
if (crawlingSound) crawlingSound.play();
}
function spawnNewColony() {
// Only spawn if we have fewer than 8 colonies total
if (colonies.length >= 8) return;
// Generate random position for new colony in safe areas
var attempts = 0;
var newColony = null;
while (attempts < 50 && !newColony) {
var x = 300 + Math.random() * 1400; // Random X between 300-1700
var y = 500 + Math.random() * 800; // Random Y between 500-1300
var canPlace = true;
attempts++;
// Ensure we're not too close to screen edges
if (x < 200 || x > 1848 || y < 200 || y > 2532) {
canPlace = false;
continue;
}
// Check distance from path
for (var i = 0; i < gamePath.length; i++) {
var dx = x - gamePath[i].x;
var dy = y - gamePath[i].y;
if (Math.sqrt(dx * dx + dy * dy) < 150) {
canPlace = false;
break;
}
}
if (!canPlace) continue;
// Check distance from existing colonies and bunker
var structures = colonies.slice();
structures.push(bunker);
for (var i = 0; i < structures.length; i++) {
var dx = x - structures[i].x;
var dy = y - structures[i].y;
if (Math.sqrt(dx * dx + dy * dy) < 200) {
canPlace = false;
break;
}
}
if (!canPlace) continue;
// Check distance from existing towers
for (var i = 0; i < towers.length; i++) {
var dx = x - towers[i].x;
var dy = y - towers[i].y;
if (Math.sqrt(dx * dx + dy * dy) < 150) {
canPlace = false;
break;
}
}
if (canPlace) {
newColony = game.addChild(LK.getAsset('colony', {
anchorX: 0.5,
anchorY: 0.5
}));
newColony.x = x;
newColony.y = y;
colonies.push(newColony);
gameElements.push(newColony);
// Flash effect for new colony
LK.effects.flashObject(newColony, 0x00FF00, 1000);
break;
}
}
}
function spawnInitialFleeingColonists() {
// Spawn colonists fleeing from the initial colony before enemies arrive
if (colonies.length > 0) {
var initialColony = colonies[0];
// Spawn 2-3 regular colonists fleeing from the main colony
var colonistsToSpawn = 2 + Math.floor(Math.random() * 2);
for (var i = 0; i < colonistsToSpawn; i++) {
var colonist = new Colonist();
colonist.x = initialColony.x + (Math.random() - 0.5) * 100;
colonist.y = initialColony.y + (Math.random() - 0.5) * 100;
colonists.push(colonist);
game.addChild(colonist);
// Add dramatic fleeing animation with tween
var targetX = colonistPath[0].x + (Math.random() - 0.5) * 80;
var targetY = colonistPath[0].y + (Math.random() - 0.5) * 80;
tween(colonist, {
x: targetX,
y: targetY
}, {
duration: 1500 + Math.random() * 1000,
easing: tween.easeOut
});
}
// Spawn 1 male colonist fleeing with panic animation
var maleColonist = game.addChild(LK.getAsset('colonistmale', {
anchorX: 0.5,
anchorY: 0.5
}));
maleColonist.x = initialColony.x + (Math.random() - 0.5) * 120;
maleColonist.y = initialColony.y + (Math.random() - 0.5) * 120;
maleColonist.speed = 1.2;
maleColonist.pathIndex = 0;
maleColonist.isSafe = false;
maleColonist.isMale = true;
colonists.push(maleColonist);
// Add panic shake animation then flee
tween(maleColonist, {
x: maleColonist.x + 25
}, {
duration: 120,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(maleColonist, {
x: maleColonist.x - 50
}, {
duration: 120,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(maleColonist, {
x: colonistPath[0].x + (Math.random() - 0.5) * 60,
y: colonistPath[0].y + (Math.random() - 0.5) * 60
}, {
duration: 2000 + Math.random() * 1000,
easing: tween.easeOut
});
}
});
}
});
// Add update method for male colonist
maleColonist.update = function () {
if (maleColonist.isSafe) return;
if (maleColonist.pathIndex < colonistPath.length) {
var target = colonistPath[maleColonist.pathIndex];
var dx = target.x - maleColonist.x;
var dy = target.y - maleColonist.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 20) {
maleColonist.pathIndex++;
if (maleColonist.pathIndex >= colonistPath.length) {
maleColonist.isSafe = true;
// Move male colonist to bunker position and make them visible
maleColonist.x = bunker.x + (Math.random() - 0.5) * 120;
maleColonist.y = bunker.y + (Math.random() - 0.5) * 120;
maleColonist.alpha = 0.8; // Make them slightly transparent to show they're safe
// Add a green tint to show they're saved
tween(maleColonist, {
tint: 0x00FF00
}, {
duration: 500,
onFinish: function onFinish() {
tween(maleColonist, {
tint: 0xFFFFFF,
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
maleColonist.destroy();
}
});
}
});
colonistsSaved++;
updateUI();
}
} else {
maleColonist.x += dx / distance * maleColonist.speed;
maleColonist.y += dy / distance * maleColonist.speed;
}
}
};
}
}
function startNewWave() {
// Increment wave for new wave (skip increment for very first wave call)
if (bugsSpawned > 0 || wave > 1) {
wave++;
}
// Calculate current level (1-250) based on wave progression
// Level 1: waves 1-2, Level 2: waves 3-4, Level 3: waves 5-6, etc.
var currentLevel = Math.ceil(wave / 2);
var expectedColonies = currentLevel;
// Add colonies to match the current level (1 colony per level)
while (colonies.length < expectedColonies && colonies.length < 8) {
spawnNewColony();
}
bugsToSpawn = Math.min(15, 3 + wave * 2);
bugsSpawned = 0;
waveTimer = 0;
waveText.setText('Level: ' + currentLevel + ' | Wave: ' + wave + ' | Colonies: ' + colonies.length);
// Spawn fleeing colonists and male colonists with dramatic animations
if (wave <= 15 && Math.random() < 0.7) {
// Spawn regular colonists fleeing from random colonies
var colonistsToSpawn = Math.min(colonies.length, Math.ceil(wave / 2));
for (var i = 0; i < colonistsToSpawn; i++) {
var randomColony = colonies[Math.floor(Math.random() * colonies.length)];
var colonist = new Colonist();
colonist.x = randomColony.x + (Math.random() - 0.5) * 150;
colonist.y = randomColony.y + (Math.random() - 0.5) * 150;
colonists.push(colonist);
game.addChild(colonist);
// Add fleeing animation - quick movement toward first waypoint
tween(colonist, {
x: colonistPath[0].x + (Math.random() - 0.5) * 50,
y: colonistPath[0].y + (Math.random() - 0.5) * 50
}, {
duration: 1000 + Math.random() * 1000,
easing: tween.easeOut
});
}
// Spawn male colonists fleeing from random colonies
if (Math.random() < 0.5 && colonies.length > 1) {
var randomColony = colonies[Math.floor(Math.random() * colonies.length)];
var maleColonist = game.addChild(LK.getAsset('colonistmale', {
anchorX: 0.5,
anchorY: 0.5
}));
maleColonist.x = randomColony.x + (Math.random() - 0.5) * 150;
maleColonist.y = randomColony.y + (Math.random() - 0.5) * 150;
maleColonist.speed = 1.2;
maleColonist.pathIndex = 0;
maleColonist.isSafe = false;
maleColonist.isMale = true;
// Add to colonists array for tracking
colonists.push(maleColonist);
// Add panic animation - shake then flee
tween(maleColonist, {
x: maleColonist.x + 20
}, {
duration: 100,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(maleColonist, {
x: maleColonist.x - 40
}, {
duration: 100,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(maleColonist, {
x: colonistPath[Math.floor(colonistPath.length / 2)].x,
y: colonistPath[Math.floor(colonistPath.length / 2)].y
}, {
duration: 2000 + Math.random() * 1500,
easing: tween.easeOut
});
}
});
}
});
// Add custom update method for male colonist
maleColonist.update = function () {
if (maleColonist.isSafe) return;
if (maleColonist.pathIndex < colonistPath.length) {
var target = colonistPath[maleColonist.pathIndex];
var dx = target.x - maleColonist.x;
var dy = target.y - maleColonist.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 20) {
maleColonist.pathIndex++;
if (maleColonist.pathIndex >= colonistPath.length) {
maleColonist.isSafe = true;
// Move male colonist to bunker position and make them visible
maleColonist.x = bunker.x + (Math.random() - 0.5) * 120;
maleColonist.y = bunker.y + (Math.random() - 0.5) * 120;
maleColonist.alpha = 0.8; // Make them slightly transparent to show they're safe
// Add a green tint to show they're saved
tween(maleColonist, {
tint: 0x00FF00
}, {
duration: 500,
onFinish: function onFinish() {
tween(maleColonist, {
tint: 0xFFFFFF,
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
maleColonist.destroy();
}
});
}
});
colonistsSaved++;
updateUI();
}
} else {
maleColonist.x += dx / distance * maleColonist.speed;
maleColonist.y += dy / distance * maleColonist.speed;
}
}
};
}
}
}
var towerPreview = null;
var rangeIndicator = null;
game.down = function (x, y, obj) {
if (gameState !== 'playing') return;
// Check if clicked on restart button
if (restartButton) {
var dx = x - restartButton.x;
var dy = y - restartButton.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 75) {
// Restart the game
gameState = 'menu';
// Clean up current game state
for (var i = bugs.length - 1; i >= 0; i--) {
if (bugs[i] && bugs[i].destroy) bugs[i].destroy();
}
for (var i = towers.length - 1; i >= 0; i--) {
if (towers[i] && towers[i].destroy) towers[i].destroy();
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (bullets[i] && bullets[i].destroy) bullets[i].destroy();
}
for (var i = colonists.length - 1; i >= 0; i--) {
if (colonists[i] && colonists[i].destroy) colonists[i].destroy();
}
for (var i = gameElements.length - 1; i >= 0; i--) {
if (gameElements[i] && gameElements[i].destroy) gameElements[i].destroy();
}
for (var i = towerButtons.length - 1; i >= 0; i--) {
if (towerButtons[i] && towerButtons[i].destroy) towerButtons[i].destroy();
}
// Clean up preview elements
if (towerPreview && towerPreview.destroy) {
towerPreview.destroy();
towerPreview = null;
}
if (rangeIndicator && rangeIndicator.destroy) {
rangeIndicator.destroy();
rangeIndicator = null;
}
// Clear any tower range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i] && game.children[i].isTowerRange) {
game.children[i].destroy();
}
}
// Hide UI elements
coinText.visible = false;
waveText.visible = false;
infoText.visible = false;
killsText.visible = false;
savedText.visible = false;
if (restartButton) restartButton.destroy();
if (trashCan) trashCan.destroy();
// Clear arrays
bugs = [];
towers = [];
bullets = [];
colonists = [];
colonies = [];
gameElements = [];
towerButtons = [];
// Show main menu
mainMenu = new MainMenu();
game.addChild(mainMenu);
return;
}
}
// First check if we clicked on a tower button
var clickedTowerButton = false;
for (var i = 0; i < towerButtons.length; i++) {
var button = towerButtons[i];
if (button && button.visible) {
var dx = x - button.x;
var dy = y - button.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 60) {
// Button click radius
clickedTowerButton = true;
if (coins >= button.cost) {
placingTowerType = button.towerType;
selectedTower = null;
// Clean up any existing preview elements when starting new placement
if (towerPreview) {
towerPreview.destroy();
towerPreview = null;
}
if (rangeIndicator) {
rangeIndicator.destroy();
rangeIndicator = null;
}
updateUI();
}
break;
}
}
}
if (clickedTowerButton) return; // Don't process other clicks if we clicked a button
// Check if clicked on trash can when tower is selected
if (selectedTower && trashCan) {
var dx = x - trashCan.x;
var dy = y - trashCan.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 60) {
// Delete tower without refund
for (var i = towers.length - 1; i >= 0; i--) {
if (towers[i] === selectedTower) {
towers.splice(i, 1);
break;
}
}
selectedTower.destroy();
selectedTower = null;
// Clear tower range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.children[i].destroy();
}
}
updateUI();
return;
}
}
// Handle tower sell if tower is selected and clicked in UI area
if (selectedTower && y > 2400 && x >= 1024) {
// Right side - sell button area
selectedTower.sell();
updateUI();
return;
}
// Handle tower placement - allow placement anywhere except near UI elements
if (placingTowerType && y < 2500) {
// Don't place towers in bottom UI area
var canPlace = true;
var cost = towerCosts[placingTowerType];
// Check if we have enough coins
if (coins < cost) {
canPlace = false;
}
// Check distance from path
for (var i = 0; i < gamePath.length; i++) {
var dx = x - gamePath[i].x;
var dy = y - gamePath[i].y;
if (Math.sqrt(dx * dx + dy * dy) < 80) {
canPlace = false;
break;
}
}
// Check distance from existing towers
for (var i = 0; i < towers.length; i++) {
var dx = x - towers[i].x;
var dy = y - towers[i].y;
if (Math.sqrt(dx * dx + dy * dy) < 100) {
canPlace = false;
break;
}
}
// Check distance from colonies and bunker
var structures = colonies.slice(); // Copy colonies array
structures.push(bunker);
for (var i = 0; i < structures.length; i++) {
var dx = x - structures[i].x;
var dy = y - structures[i].y;
if (Math.sqrt(dx * dx + dy * dy) < 120) {
canPlace = false;
break;
}
}
if (canPlace) {
var tower = new Tower(placingTowerType);
tower.x = x;
tower.y = y;
towers.push(tower);
game.addChild(tower);
coins -= cost;
placingTowerType = null;
// Clean up preview elements
if (towerPreview) {
towerPreview.destroy();
towerPreview = null;
}
if (rangeIndicator) {
rangeIndicator.destroy();
rangeIndicator = null;
}
updateUI();
}
} else if (!placingTowerType) {
// Clear selection only if we're not placing a tower
selectedTower = null;
// Clean up any remaining preview elements
if (towerPreview) {
towerPreview.destroy();
towerPreview = null;
}
if (rangeIndicator) {
rangeIndicator.destroy();
rangeIndicator = null;
}
// Clear tower range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.children[i].destroy();
}
}
}
updateUI();
};
game.update = function () {
if (gameState !== 'playing') return;
if (gameWon) return;
// Spawn bugs
waveTimer++;
if (waveTimer % 45 === 0 && bugsSpawned < bugsToSpawn) {
spawnBug();
}
// Check if wave is complete
if (bugsSpawned >= bugsToSpawn && bugs.length === 0) {
var currentLevel = Math.ceil(wave / 2);
if (currentLevel >= 250) {
gameWon = true;
// Save game progress safely
if (storage) {
storage.coins = coins;
storage.level = Math.ceil(wave / 2); // Save current level
}
var finalScore = calculateScore();
saveHighScore(finalScore);
LK.setTimeout(function () {
LK.showYouWin();
}, 1000);
} else {
// Properly advance to next wave/level
LK.setTimeout(function () {
startNewWave();
}, 2000);
}
}
// Clean up bullets that are no longer in the game
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
if (!bullet || !bullet.parent) {
bullets.splice(i, 1);
}
}
// Clean up destroyed bugs
for (var i = bugs.length - 1; i >= 0; i--) {
var bug = bugs[i];
if (!bug || !bug.parent || bug.health <= 0) {
bugs.splice(i, 1);
}
}
// Update towers to shoot at bugs
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.update && typeof tower.update === 'function') {
tower.update();
}
}
// Update colonists that have update methods
for (var i = 0; i < colonists.length; i++) {
var colonist = colonists[i];
if (colonist.update && typeof colonist.update === 'function') {
colonist.update();
}
}
// Check for bug-colonist intersections
for (var i = bugs.length - 1; i >= 0; i--) {
var bug = bugs[i];
for (var j = colonists.length - 1; j >= 0; j--) {
var colonist = colonists[j];
if (!colonist.isSafe && colonist.intersects && colonist.intersects(bug)) {
// Play colonist scream sound when killed by bug
var screamSound = LK.getSound('colonistscream');
if (screamSound) screamSound.play();
// Remove the colonist
colonist.destroy();
colonists.splice(j, 1);
break; // One bug can only kill one colonist per frame
}
}
}
// Check for bugs reaching the end
var bugsReachedEnd = 0;
for (var i = bugs.length - 1; i >= 0; i--) {
var bug = bugs[i];
if (bug.pathIndex >= gamePath.length) {
bug.destroy();
bugs.splice(i, 1);
bugsReachedEnd++;
if (bug.isBoss) {
bugsReachedEnd += 4; // Boss counts as 5 bugs reaching end
}
}
}
// Check lose condition
if (bugsReachedEnd >= 3) {
// Save game progress safely
if (storage) {
storage.coins = coins;
storage.level = Math.ceil(wave / 2); // Save current level
}
var finalScore = calculateScore();
saveHighScore(finalScore);
LK.showGameOver();
}
// Update UI periodically
if (LK.ticks % 60 === 0) {
updateUI();
}
// Save coins to storage periodically
if (LK.ticks % 300 === 0 && storage) {
storage.coins = coins;
}
};
game.move = function (x, y, obj) {
if (gameState !== 'playing') return;
if (placingTowerType && y < 2500) {
// Remove existing preview and range indicator
if (towerPreview) {
towerPreview.destroy();
towerPreview = null;
}
if (rangeIndicator) {
rangeIndicator.destroy();
rangeIndicator = null;
}
// Create tower preview
towerPreview = game.addChild(LK.getAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
}));
towerPreview.x = x;
towerPreview.y = y;
towerPreview.width = 80;
towerPreview.height = 80;
towerPreview.alpha = 0.7;
// Set preview color based on tower type
switch (placingTowerType) {
case 'rapid':
towerPreview.tint = 0x00AAFF;
break;
case 'sniper':
towerPreview.tint = 0xFF4444;
break;
case 'cannon':
towerPreview.tint = 0xFF8800;
break;
case 'freeze':
towerPreview.tint = 0x88CCFF;
break;
case 'poison':
towerPreview.tint = 0x00FF00;
break;
default:
towerPreview.tint = 0x4169E1;
}
// Create range indicator
rangeIndicator = new Container();
game.addChild(rangeIndicator);
var rangeCircle = rangeIndicator.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
// Set range based on tower type
var range = 150;
switch (placingTowerType) {
case 'basic':
range = 150;
break;
case 'rapid':
range = 120;
break;
case 'sniper':
range = 250;
break;
case 'cannon':
range = 140;
break;
case 'freeze':
range = 130;
break;
case 'poison':
range = 140;
break;
}
rangeIndicator.x = x;
rangeIndicator.y = y;
rangeCircle.width = range * 2;
rangeCircle.height = range * 2;
rangeCircle.alpha = 0.2;
rangeCircle.tint = 0xFFFFFF;
// Check if placement is valid
var canPlace = true;
var cost = towerCosts[placingTowerType];
if (coins < cost) {
canPlace = false;
}
// Check distance from path
for (var i = 0; i < gamePath.length; i++) {
var dx = x - gamePath[i].x;
var dy = y - gamePath[i].y;
if (Math.sqrt(dx * dx + dy * dy) < 80) {
canPlace = false;
break;
}
}
// Check distance from existing towers
for (var i = 0; i < towers.length; i++) {
var dx = x - towers[i].x;
var dy = y - towers[i].y;
if (Math.sqrt(dx * dx + dy * dy) < 100) {
canPlace = false;
break;
}
}
// Check distance from colonies and bunker
var structures = colonies.slice();
structures.push(bunker);
for (var i = 0; i < structures.length; i++) {
var dx = x - structures[i].x;
var dy = y - structures[i].y;
if (Math.sqrt(dx * dx + dy * dy) < 120) {
canPlace = false;
break;
}
}
// Update preview colors based on validity
if (!canPlace) {
towerPreview.tint = 0xFF0000;
rangeCircle.tint = 0xFF0000;
}
}
};
Modern App Store icon, high definition, square with rounded corners, for a game titled "Klingon Defense: Outer Rim Survival" and with the description "Defend human colonies on planet Klingon from alien bug invasions using strategic robot tower placement. Earn coins by eliminating bugs and upgrade defenses to protect settlements across multiple cities.". No text on icon!
Fullscreen modern App Store art style with futuristic laser bullet, 16:9, high definition. No text captions!
Front facing icon Fullscreen modern App Store art style with futuristic bunker, 16:9, high definition. No text captions!
Front-facing icon, Fullscreen modern App Store art style with futuristic male colonist, 16:9, high definition. No text captions!
Front-facing icon, Fullscreen modern App Store art style with futuristic colony, 16:9, high definition. No text captions!
Front-facing icon, Fullscreen modern App Store art style with futuristic tower defense, 16:9, high definition. No text captions!
Front-facing icon, Fullscreen modern App Store art style with futuristic alien bug , 16:9, high definition. No text captions!
Front-facing icon, Fullscreen modern App Store art style with futuristic medium alien bug , 16:9, high definition. No text captions!
Front-facing icon, Fullscreen modern App Store art style with futuristic small flying alien bug , 16:9, high definition. No text captions!
Fullscreen modern App Store landscape banner, 16:9, high definition, red sand with blue rocks jutting out No text on banner!
Front-facing icon, Fullscreen modern App Store art style with futuristic alien queenbug , 16:9, high definition. No text captions!