/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var BackgroundFlame = Container.expand(function (x, y) { var self = Container.call(this); self.x = x; self.y = y; var flameGraphics = self.attachAsset('flame', { anchorX: 0.5, anchorY: 1.0 }); // Random flame colors (orange to red) var flameColors = [0xff6b35, 0xff4500, 0xff6347, 0xdc143c, 0xb22222]; flameGraphics.tint = flameColors[Math.floor(Math.random() * flameColors.length)]; // Random size variation var sizeVariation = 0.5 + Math.random() * 0.8; // 0.5 to 1.3 scale flameGraphics.scaleX = sizeVariation; flameGraphics.scaleY = sizeVariation; // Start invisible self.alpha = 0; self.scaleX = 0.1; self.scaleY = 0.1; // Animate appearance var appearDuration = 800 + Math.random() * 600; // 800-1400ms tween(self, { alpha: 0.3 + Math.random() * 0.4, // 0.3 to 0.7 alpha scaleX: sizeVariation, scaleY: sizeVariation }, { duration: appearDuration, easing: tween.easeOut, onFinish: function onFinish() { // Flicker for a while self.startFlickering(); } }); self.startFlickering = function () { if (!self.parent) return; // Stop if destroyed // Random flicker duration var flickerTime = 2000 + Math.random() * 3000; // 2-5 seconds var flickerInterval = 80 + Math.random() * 120; // Flicker every 80-200ms var flickerTimer = LK.setInterval(function () { if (!self.parent) { LK.clearInterval(flickerTimer); return; } // Quick flicker animation var currentAlpha = self.alpha; var flickerAlpha = Math.max(0.1, currentAlpha - 0.15 + Math.random() * 0.3); tween(self, { alpha: flickerAlpha }, { duration: 60, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.parent) return; tween(self, { alpha: currentAlpha }, { duration: 60, easing: tween.easeInOut }); } }); }, flickerInterval); // Start disappearing after flicker time LK.setTimeout(function () { if (!self.parent) return; LK.clearInterval(flickerTimer); self.startDisappearing(); }, flickerTime); }; self.startDisappearing = function () { if (!self.parent) return; // Stop if destroyed var disappearDuration = 600 + Math.random() * 400; // 600-1000ms tween(self, { alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: disappearDuration, easing: tween.easeIn, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); }; return self; }); var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); // Store reference for customization self.bulletGraphics = bulletGraphics; 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; } // If this is a sniper (Harry) bullet and enemy survives, split it if (self.type === 'sniper' && self.targetEnemy.health > 0 && !self.targetEnemy.hasBeenSplit) { // Mark this enemy as split to prevent infinite splitting self.targetEnemy.hasBeenSplit = true; // Create two new enemies with half the remaining health var splitHealth = self.targetEnemy.health / 2; for (var splitIndex = 0; splitIndex < 2; splitIndex++) { var newEnemy = new Enemy(self.targetEnemy.type); // Copy properties from original enemy newEnemy.maxHealth = splitHealth; newEnemy.health = splitHealth; newEnemy.speed = self.targetEnemy.speed; newEnemy.hasBeenSplit = true; // Prevent further splitting newEnemy.waveNumber = self.targetEnemy.waveNumber; // Copy status effects if (self.targetEnemy.slowed) { newEnemy.slowed = true; newEnemy.slowDuration = self.targetEnemy.slowDuration; newEnemy.originalSpeed = self.targetEnemy.originalSpeed; newEnemy.speed = self.targetEnemy.speed; } if (self.targetEnemy.poisoned) { newEnemy.poisoned = true; newEnemy.poisonDuration = self.targetEnemy.poisonDuration; newEnemy.poisonDamage = self.targetEnemy.poisonDamage; } // Position the new enemies slightly offset from original var offsetX = splitIndex === 0 ? -0.5 : 0.5; var offsetY = (Math.random() - 0.5) * 0.5; newEnemy.cellX = self.targetEnemy.cellX; newEnemy.cellY = self.targetEnemy.cellY; newEnemy.currentCellX = self.targetEnemy.currentCellX + offsetX; newEnemy.currentCellY = self.targetEnemy.currentCellY + offsetY; newEnemy.x = grid.x + newEnemy.currentCellX * CELL_SIZE; newEnemy.y = grid.y + newEnemy.currentCellY * CELL_SIZE; // Copy flying target if applicable if (self.targetEnemy.isFlying && self.targetEnemy.flyingTarget) { newEnemy.flyingTarget = self.targetEnemy.flyingTarget; } // Add to appropriate layer if (newEnemy.isFlying) { enemyLayerTop.addChild(newEnemy); if (newEnemy.shadow) { enemyLayerMiddle.addChild(newEnemy.shadow); } } else { enemyLayerBottom.addChild(newEnemy); } // Add to enemies array enemies.push(newEnemy); // Update health bar newEnemy.healthBar.width = newEnemy.health / newEnemy.maxHealth * 70; } // Remove the original enemy self.targetEnemy.health = 0; } // 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 }); // Remove the random tint to show the stone texture properly cellGraphics.tint = 0xFFFFFF; cellGraphics.alpha = 0.8; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); numberLabel.visible = false; self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); // Dark reddish stone for non-path cells cellGraphics.tint = 0x8B4513; cellGraphics.alpha = 0.5; return; } numberLabel.visible = false; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; cellGraphics.alpha = 0.6; } else { // Apply subtle tinting to the stone texture based on path score var pathTint = 0x88 - tint << 8 | tint; // Blend the tint with the stone texture color cellGraphics.tint = 0xD2B48C; // Light tan/stone color cellGraphics.alpha = 0.7 + tint / 0x88 * 0.3; // Vary alpha based on path score } // Arrows removed - no visual indicators break; } case 1: { self.removeArrows(); // Darker stone for walls cellGraphics.tint = 0x654321; cellGraphics.alpha = 0.9; numberLabel.visible = false; break; } case 3: { self.removeArrows(); // Goal cells with greenish stone tint cellGraphics.tint = 0x8FBC8F; cellGraphics.alpha = 0.8; numberLabel.visible = false; break; } } // Number labels removed }; }); // 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 *= 6; // Three times as fast (was 2x, now 6x total) self.maxHealth = 50; // Half health (was 100, now 50) 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; } // Make all enemies 25% faster self.speed = self.speed * 1.25; 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 }); self.enemyGraphics = enemyGraphics; // Store reference for walking animation // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Dark wizard enemies with magical effects // Different wizard types with distinct magical auras /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x3742fa; // Blue aura for fast dark wizards break; case 'immune': enemyGraphics.tint = 0xff3838; // Red aura for immune dark wizards break; case 'flying': enemyGraphics.tint = 0xf1c40f; // Golden aura for broom-riding wizards break; case 'swarm': enemyGraphics.tint = 0x8c7ae6; // Purple aura for swarm dark wizards 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; } // Add walking animation effect if (!self.walkingAnimation && self.speed > 0) { // Create a subtle bobbing effect for walking var _createWalkingAnimation = function createWalkingAnimation() { if (!self.parent) return; // Stop if enemy is destroyed // Adjust animation speed based on enemy speed var animDuration = self.slowed ? 400 : 200; if (self.type === 'fast') animDuration = 100; // Vertical bobbing motion tween(enemyGraphics, { y: -5 }, { duration: animDuration, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.parent) return; tween(enemyGraphics, { y: 0 }, { duration: animDuration, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.parent) return; _createWalkingAnimation(); // Continue the animation loop } }); } }); }; self.walkingAnimation = true; _createWalkingAnimation(); } 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; self.fadeOutTime = 120; // Default fade out time self.update = function () { if (self.fadeOutTime > 0) { self.fadeOutTime--; self.alpha = Math.min(self.fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var SmokeParticle = Container.expand(function (startX, startY, targetX, targetY, color) { var self = Container.call(this); self.x = startX; self.y = startY; // Create multiple smoke tendrils for realistic wispy smoke var smokeTendrils = []; var numTendrils = 3 + Math.floor(Math.random() * 3); // 3-5 tendrils per smoke for (var i = 0; i < numTendrils; i++) { var tendrilContainer = new Container(); self.addChild(tendrilContainer); // Create elongated smoke shapes using multiple stretched circles var numSegments = 4 + Math.floor(Math.random() * 3); // 4-6 segments per tendril var segmentGraphics = []; for (var j = 0; j < numSegments; j++) { var segment = tendrilContainer.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); // Create elongated, thin smoke shapes var baseWidth = 80 + Math.random() * 120 - j * 15; // Tapers towards end var baseHeight = 250 + Math.random() * 350 + j * 50; // Gets longer segment.width = baseWidth; segment.height = baseHeight; // Position segments to create continuous tendril segment.y = -j * (baseHeight * 0.3); // Overlap segments segment.x = (Math.random() - 0.5) * 30 * j; // Slight sideways drift // Dark, horror-themed smoke colors var smokeType = Math.random(); if (smokeType < 0.4) { // Very dark grey-black smoke (40% chance) var darkness = 0x08 + Math.floor(Math.random() * 0x18); segment.tint = darkness << 16 | darkness << 8 | darkness; } else if (smokeType < 0.6) { // Dark purple-black smoke for horror effect (20% chance) var purple = 0x20 + Math.floor(Math.random() * 0x20); var darkness = 0x10 + Math.floor(Math.random() * 0x10); segment.tint = purple << 16 | darkness << 8 | purple; } else if (smokeType < 0.8) { // Dark red-black smoke (20% chance) var red = 0x30 + Math.floor(Math.random() * 0x20); var darkness = 0x08 + Math.floor(Math.random() * 0x10); segment.tint = red << 16 | darkness << 8 | darkness; } else { // Ashy grey smoke (20% chance) var grayValue = 0x30 + Math.floor(Math.random() * 0x30); segment.tint = grayValue << 16 | grayValue << 8 | grayValue; } // Vary alpha for depth - more transparent at the edges segment.alpha = 0.4 - j * 0.08 - Math.abs(i - numTendrils / 2) * 0.1; segment.blendMode = 1; // Additive blend for ethereal effect segmentGraphics.push(segment); } // Random rotation for each tendril tendrilContainer.rotation = (Math.random() - 0.5) * Math.PI * 0.4; // Offset tendrils for natural spread tendrilContainer.x = (i - numTendrils / 2) * 60 + (Math.random() - 0.5) * 40; tendrilContainer.y = (Math.random() - 0.5) * 100; smokeTendrils.push({ container: tendrilContainer, segments: segmentGraphics, baseRotation: tendrilContainer.rotation, swaySpeed: 0.02 + Math.random() * 0.03, swayAmount: 0.1 + Math.random() * 0.2 }); } // Add dark wisps occasionally for extra horror effect if (Math.random() < 0.4) { var wispLayer = new Container(); self.addChild(wispLayer); var wisp = wispLayer.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); wisp.width = 40 + Math.random() * 60; wisp.height = 200 + Math.random() * 300; // Very dark wisp wisp.tint = 0x000000; wisp.alpha = 0.3; wisp.blendMode = 1; wispLayer.x = (Math.random() - 0.5) * 150; wispLayer.y = -100 - Math.random() * 200; wispLayer.rotation = (Math.random() - 0.5) * Math.PI * 0.6; } // Start with 0 alpha and small scale self.alpha = 0; self.scaleX = 0.3; self.scaleY = 0.2; // Calculate movement direction with creeping motion var dx = targetX - startX; var dy = targetY - startY; var distance = Math.sqrt(dx * dx + dy * dy); // Add strong upward bias and sideways drift for smoke var windDrift = (Math.random() - 0.5) * 400; var upwardForce = -150 - Math.random() * 100; // Strong upward movement // Animate appearance with slow, creeping emergence tween(self, { alpha: 0.85, scaleX: 0.8, scaleY: 1.2, x: startX + dx * 0.1 + windDrift * 0.2, y: startY + dy * 0.1 + upwardForce * 0.3 }, { duration: 2000, easing: tween.easeOut, onFinish: function onFinish() { // Continue creeping and growing tween(self, { alpha: 0.6, scaleX: 1.2, scaleY: 1.6, x: startX + dx * 0.3 + windDrift * 0.6, y: startY + dy * 0.3 + upwardForce * 0.7 }, { duration: 2500, easing: tween.linear, onFinish: function onFinish() { // Final dissipation tween(self, { alpha: 0, scaleX: 1.5, scaleY: 2.0, x: startX + dx * 0.5 + windDrift, y: startY + dy * 0.5 + upwardForce * 1.2 }, { duration: 1500, easing: tween.easeIn, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); } }); } }); // Animate individual tendrils for undulating, creepy movement self.animationTime = 0; self.update = function () { if (!self.parent) return; self.animationTime += 0.016; // Assuming 60 FPS // Animate each tendril with swaying motion for (var i = 0; i < smokeTendrils.length; i++) { var tendril = smokeTendrils[i]; // Sinusoidal swaying motion var swayAngle = Math.sin(self.animationTime * tendril.swaySpeed + i) * tendril.swayAmount; tendril.container.rotation = tendril.baseRotation + swayAngle; // Animate individual segments for more fluid motion for (var j = 0; j < tendril.segments.length; j++) { var segment = tendril.segments[j]; // Add slight pulsing to segments var pulseFactor = 1 + Math.sin(self.animationTime * 0.5 + j * 0.3) * 0.05; segment.scaleX = pulseFactor; // Slight vertical bobbing segment.y = -j * (segment.height * 0.3) + Math.sin(self.animationTime + j * 0.5) * 5; } } }; return self; }); var Snake = Container.expand(function (startX, startY, tower) { var self = Container.call(this); self.x = startX; self.y = startY; self.tower = tower; self.speed = 3; self.damage = 15; self.fireRate = 30; // Fire every 0.5 seconds self.lastFired = 0; self.lifeTime = 300; // 5 seconds at 60 FPS self.currentLifeTime = 0; // Create snake body segments using yilan image self.segments = []; var numSegments = 8; // More segments for smoother snake for (var i = 0; i < numSegments; i++) { var segment = new Container(); // Create snake segment using yilan image asset var segmentGraphics = segment.attachAsset('yilan', { anchorX: 0.5, anchorY: 0.5 }); // Make segments progressively smaller - increased base size var segmentSize = 90 - i * 8; // Larger, more gradual taper segmentGraphics.width = segmentSize; segmentGraphics.height = segmentSize; // Keep the natural color of the yilan image segmentGraphics.tint = 0xFFFFFF; // No tint to preserve original colors segmentGraphics.alpha = 0.9; segment.x = -i * 20; // Tighter spacing for realistic movement segment.y = 0; self.addChild(segment); self.segments.push(segment); } // Create snake head with larger yilan image var head = self.segments[0]; // Remove the old head graphics first head.removeChildAt(0); // Main head shape (larger) using yilan image var headGraphics = head.attachAsset('yilan', { anchorX: 0.5, anchorY: 0.5 }); headGraphics.width = 110; // Larger head headGraphics.height = 110; // Keep square aspect ratio for image headGraphics.tint = 0xFFFFFF; // No tint to preserve original image // The yilan image already has eyes and details, so we don't need to add them // Movement pattern variables self.moveAngle = Math.random() * Math.PI * 2; self.waveAmplitude = 30; self.waveFrequency = 0.1; self.time = 0; self.findTarget = function () { var closestEnemy = null; var closestDistance = 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); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } return closestEnemy; }; self.shootFire = function (targetEnemy) { if (!targetEnemy || !targetEnemy.parent) return; // Create fire projectile from snake's mouth var fireX = self.x + self.segments[0].x; var fireY = self.y + self.segments[0].y; // Create flame bullet var flame = new Bullet(fireX, fireY, targetEnemy, self.damage, 8); flame.type = 'snake_fire'; // Customize flame appearance flame.bulletGraphics.tint = 0xFF4500; // Orange-red flame flame.bulletGraphics.width = 40; // Increased from 25 flame.bulletGraphics.height = 40; // Increased from 25 // Add flame effect tween(flame.bulletGraphics, { scaleX: 1.2, scaleY: 1.2, alpha: 0.7 }, { duration: 200, easing: tween.easeInOut, loop: true, yoyo: true }); game.addChild(flame); bullets.push(flame); targetEnemy.bulletsTargetingThis.push(flame); }; self.update = function () { self.currentLifeTime++; self.time++; // Destroy snake after lifetime expires if (self.currentLifeTime >= self.lifeTime) { self.destroy(); return; } // Find nearest enemy var target = self.findTarget(); if (target) { // Move towards target with snake-like motion var dx = target.x - self.x; var dy = target.y - self.y; var targetAngle = Math.atan2(dy, dx); // Smooth angle transition var angleDiff = targetAngle - self.moveAngle; while (angleDiff > Math.PI) angleDiff -= Math.PI * 2; while (angleDiff < -Math.PI) angleDiff += Math.PI * 2; self.moveAngle += angleDiff * 0.1; // Move with sinusoidal pattern var sineOffset = Math.sin(self.time * self.waveFrequency) * self.waveAmplitude; self.x += Math.cos(self.moveAngle) * self.speed; self.y += Math.sin(self.moveAngle) * self.speed; // Apply perpendicular offset for snake-like movement var perpAngle = self.moveAngle + Math.PI / 2; self.x += Math.cos(perpAngle) * sineOffset * 0.02; self.y += Math.sin(perpAngle) * sineOffset * 0.02; // Fire at enemies if (LK.ticks - self.lastFired >= self.fireRate) { self.shootFire(target); self.lastFired = LK.ticks; } } else { // Wander if no target self.x += Math.cos(self.moveAngle) * self.speed * 0.5; self.y += Math.sin(self.moveAngle) * self.speed * 0.5; // Random direction changes if (Math.random() < 0.05) { self.moveAngle += (Math.random() - 0.5) * Math.PI * 0.5; } } // Update segment positions to follow head with snake-like motion for (var i = 1; i < self.segments.length; i++) { var prevSegment = self.segments[i - 1]; var segment = self.segments[i]; // Smooth following with delay - more fluid for realistic motion var followSpeed = 0.25 - i * 0.02; segment.x += (prevSegment.x - segment.x - 20) * followSpeed; segment.y += (prevSegment.y - segment.y) * followSpeed; // Add more complex oscillation for realistic slithering var oscillation = Math.sin(self.time * 0.15 + i * 0.6) * (4 - i * 0.3); var perpAngle = self.moveAngle + Math.PI / 2; segment.x += Math.cos(perpAngle) * oscillation; segment.y += Math.sin(perpAngle) * oscillation; // Rotate segments to face movement direction if (i === 0 && self.segments[0].children[0]) { // Head should face movement direction self.segments[0].children[0].rotation = self.moveAngle; } else if (segment.children[0] && prevSegment) { // Body segments should face towards the previous segment var dx = prevSegment.x - segment.x; var dy = prevSegment.y - segment.y; segment.children[0].rotation = Math.atan2(dy, dx); } } // No eye animation needed since yilan image has built-in eyes // Fade out near end of life if (self.currentLifeTime > self.lifeTime - 60) { self.alpha = (self.lifeTime - self.currentLifeTime) / 60; } }; 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; if (self.towerType === 'default') { baseGraphics = self.attachAsset('1', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } else if (self.towerType === 'rapid') { baseGraphics = self.attachAsset('2', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } else if (self.towerType === 'sniper') { baseGraphics = self.attachAsset('3', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } else if (self.towerType === 'splash') { baseGraphics = self.attachAsset('4', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } else if (self.towerType === 'slow') { baseGraphics = self.attachAsset('5', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } else if (self.towerType === 'poison') { baseGraphics = self.attachAsset('6', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } else { baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } switch (self.towerType) { case 'rapid': // Don't tint if using image asset for rapid tower break; case 'sniper': // Don't tint if using image asset for sniper tower break; case 'splash': // Don't tint if using image asset for splash tower break; case 'slow': // Don't tint if using image asset for slow tower break; case 'poison': // Don't tint if using image asset for poison tower break; default: // Don't tint if using image asset for default tower break; } var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var towerDisplayNameShadow = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1); if (self.towerType === 'rapid') { towerDisplayNameShadow = 'Twins'; } if (self.towerType === 'default') { towerDisplayNameShadow = 'Student'; } if (self.towerType === 'sniper') { towerDisplayNameShadow = 'Harry'; } if (self.towerType === 'splash') { towerDisplayNameShadow = 'Harmony'; } if (self.towerType === 'slow') { towerDisplayNameShadow = 'Ron'; } if (self.towerType === 'poison') { towerDisplayNameShadow = 'Snape'; } var typeLabelShadow = new Text2(towerDisplayNameShadow, { 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 towerDisplayName = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1); if (self.towerType === 'rapid') { towerDisplayName = 'Twins'; } if (self.towerType === 'default') { towerDisplayName = 'Student'; } if (self.towerType === 'sniper') { towerDisplayName = 'Harry'; } if (self.towerType === 'splash') { towerDisplayName = 'Harmony'; } if (self.towerType === 'slow') { towerDisplayName = 'Ron'; } if (self.towerType === 'poison') { towerDisplayName = 'Snape'; } var typeLabel = new Text2(towerDisplayName, { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -20; // Position above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; self.specialAbilityCooldown = 0; // Cooldown for special abilities // 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; if (self.id === 'default') { baseGraphics = self.attachAsset('1', { anchorX: 0.5, anchorY: 0.5 }); // Ensure proper sizing for image asset baseGraphics.width = CELL_SIZE * 2.5; baseGraphics.height = CELL_SIZE * 2.5; } else if (self.id === 'rapid') { baseGraphics = self.attachAsset('2', { anchorX: 0.5, anchorY: 0.5 }); // Ensure proper sizing for image asset baseGraphics.width = CELL_SIZE * 2.5; baseGraphics.height = CELL_SIZE * 2.5; } else if (self.id === 'sniper') { baseGraphics = self.attachAsset('3', { anchorX: 0.5, anchorY: 0.5 }); // Ensure proper sizing for image asset baseGraphics.width = CELL_SIZE * 2.5; baseGraphics.height = CELL_SIZE * 2.5; } else if (self.id === 'splash') { baseGraphics = self.attachAsset('4', { anchorX: 0.5, anchorY: 0.5 }); // Ensure proper sizing for image asset baseGraphics.width = CELL_SIZE * 2.5; baseGraphics.height = CELL_SIZE * 2.5; } else if (self.id === 'slow') { baseGraphics = self.attachAsset('5', { anchorX: 0.5, anchorY: 0.5 }); // Ensure proper sizing for image asset baseGraphics.width = CELL_SIZE * 2.5; baseGraphics.height = CELL_SIZE * 2.5; } else if (self.id === 'poison') { baseGraphics = self.attachAsset('6', { anchorX: 0.5, anchorY: 0.5 }); // Ensure proper sizing for image asset baseGraphics.width = CELL_SIZE * 2.5; baseGraphics.height = CELL_SIZE * 2.5; } else { baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); } // Store reference to base graphics self.baseGraphics = baseGraphics; switch (self.id) { case 'rapid': // Don't tint if using image asset for rapid tower break; case 'sniper': // Don't tint if using image asset for sniper tower break; case 'splash': // Don't tint if using image asset for splash tower break; case 'slow': // Don't tint if using image asset for slow tower break; case 'poison': // Don't tint if using image asset for poison tower break; default: // Don't tint if using image asset for default tower // Ensure default tower image is not tinted if (self.id === 'default') { baseGraphics.tint = 0xFFFFFF; // Pure white (no tint) } break; } 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 }); // Make gun slightly smaller for default towers to not obscure the base if (self.id === 'default') { gunGraphics.scaleX = 0.8; gunGraphics.scaleY = 0.8; } 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(); // Add cooldown indicator for harmony tower if (self.id === 'splash') { var cooldownIndicator = new Container(); self.cooldownIndicator = cooldownIndicator; self.addChild(cooldownIndicator); var cooldownBg = cooldownIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); cooldownBg.width = CELL_SIZE * 0.8; cooldownBg.height = CELL_SIZE * 0.8; cooldownBg.tint = 0x000000; cooldownBg.alpha = 0.5; var cooldownFill = cooldownIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); cooldownFill.width = CELL_SIZE * 0.7; cooldownFill.height = CELL_SIZE * 0.7; cooldownFill.tint = 0x00FF00; self.cooldownFill = cooldownFill; cooldownIndicator.y = -CELL_SIZE * 1.2; } 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 () { // Update special ability cooldown if (self.specialAbilityCooldown > 0) { self.specialAbilityCooldown--; } // Update cooldown visual indicator for harmony tower if (self.id === 'splash' && self.cooldownIndicator) { if (self.specialAbilityCooldown > 0) { // Show cooldown progress self.cooldownIndicator.visible = true; var cooldownProgress = 1 - self.specialAbilityCooldown / 600; self.cooldownFill.scaleX = cooldownProgress; self.cooldownFill.scaleY = cooldownProgress; self.cooldownFill.tint = 0xFF0000; // Red when on cooldown // Hide the ability ready text when on cooldown if (self.abilityReadyText) { self.abilityReadyText.visible = false; } } else { // Ready to use self.cooldownIndicator.visible = true; self.cooldownFill.scaleX = 1; self.cooldownFill.scaleY = 1; self.cooldownFill.tint = 0x00FF00; // Green when ready // Show the ability ready text if (!self.abilityReadyText) { // Create the text if it doesn't exist self.abilityReadyText = new Container(); // Add shadow text var shadowText = new Text2('Press to slow down', { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.abilityReadyText.addChild(shadowText); // Add main text var mainText = new Text2('Press to slow down', { size: 45, fill: 0xFFFFFF, weight: 800 }); mainText.anchor.set(0.5, 0.5); self.abilityReadyText.addChild(mainText); // Position above the tower self.abilityReadyText.y = -CELL_SIZE * 2; self.addChild(self.abilityReadyText); } self.abilityReadyText.visible = true; } } 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; } } // Flying behavior for students (default) towers if (self.id === 'default') { // Initialize flying properties if not already set if (!self.isFlying) { self.isFlying = true; self.flightRadius = 200 + Math.random() * 300; // Random radius between 200-500 self.flightSpeed = 0.02 + Math.random() * 0.03; // Random speed self.flightAngle = Math.random() * Math.PI * 2; // Random starting angle self.flightCenterX = 300 + Math.random() * (2048 - 600); // Random center X across screen self.flightCenterY = 400 + Math.random() * (2200 - 400); // Random center Y across screen self.flightTimer = 0; self.flightDuration = 1800 + Math.random() * 1200; // Flight duration 30-50 seconds } // Update flight position self.flightTimer++; self.flightAngle += self.flightSpeed; // Calculate circular flight path var targetX = self.flightCenterX + Math.cos(self.flightAngle) * self.flightRadius; var targetY = self.flightCenterY + Math.sin(self.flightAngle) * self.flightRadius; // Smoothly move to target position var lerpFactor = 0.1; self.x += (targetX - self.x) * lerpFactor; self.y += (targetY - self.y) * lerpFactor; // Change flight pattern periodically if (self.flightTimer >= self.flightDuration) { self.flightTimer = 0; self.flightRadius = 200 + Math.random() * 300; self.flightSpeed = 0.02 + Math.random() * 0.03; self.flightCenterX = 300 + Math.random() * (2048 - 600); self.flightCenterY = 400 + Math.random() * (2200 - 400); self.flightDuration = 1800 + Math.random() * 1200; } // Add floating animation to the tower graphic if (!self.floatingAnimation) { self.floatingAnimation = true; var _createFloatingAnimation2 = function _createFloatingAnimation() { if (!self.parent) return; tween(self.baseGraphics, { y: -8 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.parent) return; tween(self.baseGraphics, { y: 8 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.parent) return; _createFloatingAnimation2(); } }); } }); }; _createFloatingAnimation2(); } } else { // Original tower following behavior for non-students towers if (!self.isMoving && !self.moveTimer) { self.moveTimer = LK.ticks + Math.random() * 300 + 180; // Random interval 3-8 seconds } if (!self.isMoving && LK.ticks >= self.moveTimer) { // Find closest enemy in range to follow var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.originalX; var dy = enemy.y - self.originalY; var distance = Math.sqrt(dx * dx + dy * dy); // Increase following range to 2.5x the attack range if (distance <= self.getRange() * 2.5 && distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } if (closestEnemy) { self.isMoving = true; // Calculate movement towards enemy (but not too close) var enemyDx = closestEnemy.x - self.originalX; var enemyDy = closestEnemy.y - self.originalY; var enemyDistance = Math.sqrt(enemyDx * enemyDx + enemyDy * enemyDy); // Move 40% of the way towards the enemy (increased from 30%) var moveDistance = Math.min(CELL_SIZE * 3, enemyDistance * 0.4); var moveAngle = Math.atan2(enemyDy, enemyDx); var newX = self.originalX + Math.cos(moveAngle) * moveDistance; var newY = self.originalY + Math.sin(moveAngle) * moveDistance; // Animate towards enemy tween(self, { x: newX, y: newY }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { // Stay at new position for a moment LK.setTimeout(function () { // Return to original position tween(self, { x: self.originalX, y: self.originalY }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { self.isMoving = false; self.moveTimer = LK.ticks + Math.random() * 300 + 180; // Reset timer } }); }, 1000); } }); } else { // No enemy found, reset timer self.moveTimer = LK.ticks + Math.random() * 180 + 120; // Shorter wait if no target } } } }; self.down = function (x, y, obj) { // Handle harmony tower special ability if (self.id === 'splash' && self.specialAbilityCooldown === 0) { // Activate special ability: slow all enemies to 50% for 5 seconds var slowDuration = 300; // 5 seconds at 60 FPS var slowPercentage = 0.5; // 50% speed // Apply slow to all enemies for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Skip immune enemies if (!enemy.isImmune) { // Store original speed if not already slowed if (!enemy.slowed) { enemy.originalSpeed = enemy.speed; } enemy.speed = enemy.originalSpeed * slowPercentage; enemy.slowed = true; enemy.slowDuration = slowDuration; // Create visual effect var slowEffect = new EffectIndicator(enemy.x, enemy.y, 'slow'); game.addChild(slowEffect); } } // Set cooldown to 10 seconds self.specialAbilityCooldown = 600; // 10 seconds at 60 FPS // Play harmony sound effect LK.getSound('harmony').play(); // Create notification var notification = game.addChild(new Notification("Harmony activated! All enemies slowed!")); notification.x = 2048 / 2; notification.y = grid.height - 150; // Visual feedback on the tower tween(self, { scaleX: 1.3, scaleY: 1.3 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeIn }); } }); return; // Don't open upgrade menu when using ability } 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 'default': bullet.bulletGraphics.tint = 0xd2b48c; // Default tower bullet color bullet.bulletGraphics.width = 30; bullet.bulletGraphics.height = 30; // Randomly play either 111 or abrakadabra sound when default tower fires if (Math.random() < 0.5) { LK.getSound('111').play(); } else { LK.getSound('abrakadabra').play(); } // Change default tower to alternate between image assets 11 and 1 after firing if (self.baseGraphics) { // Check current asset and switch var currentAssetId = self.baseGraphics.texture ? '1' : '11'; if (self.lastUsedAsset === '1' || self.lastUsedAsset === undefined) { // Switch to asset 11 self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('11', { anchorX: 0.5, anchorY: 0.5 }); self.lastUsedAsset = '11'; } else { // Switch to asset 1 self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('1', { anchorX: 0.5, anchorY: 0.5 }); self.lastUsedAsset = '1'; } self.baseGraphics.width = CELL_SIZE * 2.5; self.baseGraphics.height = CELL_SIZE * 2.5; // Move base graphics to correct position in display list (before gun container) self.setChildIndex(self.baseGraphics, 0); } break; case 'rapid': bullet.bulletGraphics.tint = 0x87ceeb; // Bright sky blue bolt bullet.bulletGraphics.width = 20; bullet.bulletGraphics.height = 20; // Play 222 sound when rapid tower fires LK.getSound('222').play(); // Change rapid tower to alternate between image assets 22 and 2 after firing if (self.baseGraphics) { // Check current asset and switch if (self.lastUsedAsset === '2' || self.lastUsedAsset === undefined) { // Switch to asset 22 self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('22', { anchorX: 0.5, anchorY: 0.5 }); self.lastUsedAsset = '22'; } else { // Switch to asset 2 self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('2', { anchorX: 0.5, anchorY: 0.5 }); self.lastUsedAsset = '2'; } self.baseGraphics.width = CELL_SIZE * 2.5; self.baseGraphics.height = CELL_SIZE * 2.5; // Move base graphics to correct position in display list (before gun container) self.setChildIndex(self.baseGraphics, 0); } break; case 'sniper': bullet.bulletGraphics.tint = 0xff6347; // Bright orange-red spell bullet.bulletGraphics.width = 15; bullet.bulletGraphics.height = 15; // Play 33 sound when sniper tower fires LK.getSound('33').play(); // Change sniper tower to alternate between image assets 3 and 32 after firing if (self.baseGraphics) { // Check current asset and switch if (self.lastUsedAsset === '3' || self.lastUsedAsset === undefined) { // Switch to asset 32 self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('32', { anchorX: 0.5, anchorY: 0.5 }); self.lastUsedAsset = '32'; } else { // Switch to asset 3 self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('3', { anchorX: 0.5, anchorY: 0.5 }); self.lastUsedAsset = '3'; } self.baseGraphics.width = CELL_SIZE * 2.5; self.baseGraphics.height = CELL_SIZE * 2.5; // Move base graphics to correct position in display list (before gun container) self.setChildIndex(self.baseGraphics, 0); } break; case 'splash': bullet.bulletGraphics.tint = 0x90ee90; // Bright light green spell bullet.bulletGraphics.width = 40; bullet.bulletGraphics.height = 40; // Play harmony sound when splash tower fires LK.getSound('harmony').play(); // Change splash tower to use image asset 42 after firing if (self.baseGraphics) { self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('42', { anchorX: 0.5, anchorY: 0.5 }); self.baseGraphics.width = CELL_SIZE * 2.5; self.baseGraphics.height = CELL_SIZE * 2.5; // Move base graphics to correct position in display list (before gun container) self.setChildIndex(self.baseGraphics, 0); } break; case 'slow': bullet.bulletGraphics.tint = 0xdda0dd; // Bright plum spell bullet.bulletGraphics.width = 35; bullet.bulletGraphics.height = 35; // Play ron sound when slow tower fires LK.getSound('ron').play(); // Change slow tower to alternate between image assets 5 and 52 after firing if (self.baseGraphics) { // Check current asset and switch if (self.lastUsedAsset === '5' || self.lastUsedAsset === undefined) { // Switch to asset 52 self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('52', { anchorX: 0.5, anchorY: 0.5 }); self.lastUsedAsset = '52'; } else { // Switch to asset 5 self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('5', { anchorX: 0.5, anchorY: 0.5 }); self.lastUsedAsset = '5'; } self.baseGraphics.width = CELL_SIZE * 2.5; self.baseGraphics.height = CELL_SIZE * 2.5; // Move base graphics to correct position in display list (before gun container) self.setChildIndex(self.baseGraphics, 0); } break; case 'poison': bullet.bulletGraphics.tint = 0x98fb98; // Bright pale green spell bullet.bulletGraphics.width = 35; bullet.bulletGraphics.height = 35; // Play snape sound when poison tower fires LK.getSound('snape').play(); // Create snake that shoots fire at enemies var snakeX = self.x + Math.cos(gunContainer.rotation) * 60; var snakeY = self.y + Math.sin(gunContainer.rotation) * 60; var snake = new Snake(snakeX, snakeY, self); game.addChild(snake); snakes.push(snake); // Change poison tower to alternate between image assets 6 and 62 after firing if (self.baseGraphics) { // Check current asset and switch if (self.lastUsedAsset === '6' || self.lastUsedAsset === undefined) { // Switch to asset 62 self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('62', { anchorX: 0.5, anchorY: 0.5 }); self.lastUsedAsset = '62'; } else { // Switch to asset 6 self.removeChild(self.baseGraphics); self.baseGraphics = self.attachAsset('6', { anchorX: 0.5, anchorY: 0.5 }); self.lastUsedAsset = '6'; } self.baseGraphics.width = CELL_SIZE * 2.5; self.baseGraphics.height = CELL_SIZE * 2.5; // Move base graphics to correct position in display list (before gun container) self.setChildIndex(self.baseGraphics, 0); } 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; // Store original position for movement behavior self.originalX = self.x; self.originalY = self.y; self.isMoving = false; self.moveTimer = null; 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; // For default tower type, replace the preview graphics with image asset 1 if (self.towerType === 'default') { // Remove existing preview graphics self.removeChild(previewGraphics); // Add image asset 1 as preview previewGraphics = self.attachAsset('1', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; // Move preview graphics to correct position in display list (before range indicator) self.setChildIndex(previewGraphics, 1); } else if (self.towerType === 'rapid') { // Remove existing preview graphics self.removeChild(previewGraphics); // Add image asset 2 as preview previewGraphics = self.attachAsset('2', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; // Move preview graphics to correct position in display list (before range indicator) self.setChildIndex(previewGraphics, 1); } else if (self.towerType === 'sniper') { // Remove existing preview graphics self.removeChild(previewGraphics); // Add image asset 3 as preview previewGraphics = self.attachAsset('3', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; // Move preview graphics to correct position in display list (before range indicator) self.setChildIndex(previewGraphics, 1); } else if (self.towerType === 'splash') { // Remove existing preview graphics self.removeChild(previewGraphics); // Add image asset 4 as preview previewGraphics = self.attachAsset('4', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; // Move preview graphics to correct position in display list (before range indicator) self.setChildIndex(previewGraphics, 1); } else if (self.towerType === 'slow') { // Remove existing preview graphics self.removeChild(previewGraphics); // Add image asset 5 as preview previewGraphics = self.attachAsset('5', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; // Move preview graphics to correct position in display list (before range indicator) self.setChildIndex(previewGraphics, 1); } else if (self.towerType === 'poison') { // Remove existing preview graphics self.removeChild(previewGraphics); // Add image asset 5 as preview previewGraphics = self.attachAsset('5', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; // Move preview graphics to correct position in display list (before range indicator) self.setChildIndex(previewGraphics, 1); } else { // For non-default towers, ensure we're using the shape asset if (previewGraphics.texture && previewGraphics.texture.baseTexture) { // Already using an image, need to switch back to shape self.removeChild(previewGraphics); previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.setChildIndex(previewGraphics, 1); } } switch (self.towerType) { case 'rapid': // Don't tint if using image asset for rapid tower break; case 'sniper': // Don't tint if using image asset for sniper tower break; case 'splash': // Don't tint if using image asset for splash tower break; case 'slow': // Don't tint if using image asset for slow tower break; case 'poison': // Don't tint if using image asset for poison tower break; default: // Don't tint if using image asset for default tower if (self.towerType !== 'default' && self.towerType !== 'rapid' && self.towerType !== 'sniper' && self.towerType !== 'splash' && self.towerType !== 'slow' && self.towerType !== 'poison') { previewGraphics.tint = 0xd2b48c; // Light tan } } 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 towerTypeName = self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1); if (self.tower.id === 'rapid') { towerTypeName = 'Twins'; } if (self.tower.id === 'default') { towerTypeName = 'Student'; } if (self.tower.id === 'sniper') { towerTypeName = 'Harry'; } if (self.tower.id === 'splash') { towerTypeName = 'Harmony'; } if (self.tower.id === 'slow') { towerTypeName = 'Ron'; } if (self.tower.id === 'poison') { towerTypeName = 'Snape'; } var towerTypeText = new Text2(towerTypeName + ' 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 () { // Start button is now disabled - game starts automatically with countdown }; 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: 0x8B7355 }); /**** * Game Code ****/ var isHidingUpgradeMenu = false; var gameStarted = false; var startScreen = new Container(); 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 snakes = []; var selectedTower = null; var gold = 130; var lives = 5; var score = 0; var xp = 0; var level = 1; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 2000; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave var backgroundFlames = []; var flameSpawnTimer = 0; var flameSpawnInterval = 180 + Math.random() * 240; // Spawn flame every 3-7 seconds var lightningTimer = 0; var lightningInterval = 900; // 15 seconds at 60 FPS var groundShakeTimer = 0; var groundShakeInterval = 1200; // 20 seconds at 60 FPS var flameLayer = new Container(); var smokeLayer = new Container(); var smokeParticles = []; 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 xpText = new Text2('XP: ' + xp, { size: 60, fill: 0x9400D3, weight: 800 }); xpText.anchor.set(0.5, 0.5); var levelText = new Text2('Level: ' + level, { size: 60, fill: 0x00CED1, weight: 800 }); levelText.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); LK.gui.top.addChild(xpText); LK.gui.top.addChild(levelText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; xpText.x = 0; xpText.y = topMargin + 80; levelText.x = 0; levelText.y = topMargin + 160; function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); xpText.setText('XP: ' + xp); levelText.setText('Level: ' + level); } 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(); // Add background image var backgroundImage = LK.getAsset('arkaplan', { anchorX: 0, anchorY: 0, scaleX: 1, scaleY: 1 }); backgroundImage.x = 0; backgroundImage.y = 0; game.addChild(backgroundImage); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(flameLayer); game.addChild(smokeLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; var draggingTower = null; var dragOffset = { x: 0, y: 0 }; 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) { // Ensure towerType is 'default' if not specified var actualTowerType = towerType || 'default'; var tower = new Tower(actualTowerType); // Apply level 2 fire rate bonus if applicable if (level >= 2) { tower.fireRate = 30; // 0.5 second attack interval } 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) { if (!gameStarted) { return; } var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } // Check if clicking on an existing tower first for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (x >= tower.x - tower.baseGraphics.width / 2 && x <= tower.x + tower.baseGraphics.width / 2 && y >= tower.y - tower.baseGraphics.height / 2 && y <= tower.y + tower.baseGraphics.height / 2) { // Start dragging the existing tower draggingTower = tower; isDragging = true; dragOffset.x = x - tower.x; dragOffset.y = y - tower.y; // Clear the tower's grid cells for (var j = 0; j < 2; j++) { for (var k = 0; k < 2; k++) { var cell = grid.getCell(tower.gridX + j, tower.gridY + k); if (cell) { cell.type = 0; } } } // Show visual feedback that tower is being dragged tower.alpha = 0.7; // Remove tower from towers in range for all cells tower.refreshCellsInRange(); grid.pathFind(); grid.renderDebug(); return; } } // Check source towers for creating new towers 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 (!gameStarted) { return; } if (isDragging) { if (draggingTower) { // Move the existing tower draggingTower.x = x - dragOffset.x; draggingTower.y = y - dragOffset.y; } else { // Shift the y position upward by 1.5 tiles to show preview above finger towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } } }; game.up = function (x, y, obj) { if (!gameStarted) { return; } var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; if (draggingTower) { // Handle existing tower repositioning var gridPosX = draggingTower.x - grid.x; var gridPosY = draggingTower.y - grid.y; var newGridX = Math.floor(gridPosX / CELL_SIZE); var newGridY = Math.floor(gridPosY / CELL_SIZE); // Check if the new position is valid var validPlacement = true; var blockedByEnemy = false; // Check bounds if (newGridY <= 4 || newGridY + 1 >= grid.cells[0].length - 4) { validPlacement = false; } else { // Check if cells are available for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(newGridX + i, newGridY + j); if (!cell || cell.type !== 0) { validPlacement = false; break; } } if (!validPlacement) break; } // Check for enemies in the way if (validPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) continue; if (!enemy.isFlying) { if (enemy.cellX >= newGridX && enemy.cellX < newGridX + 2 && enemy.cellY >= newGridY && enemy.cellY < newGridY + 2) { blockedByEnemy = true; break; } } } } } // Check if placement would block path var wouldBlock = false; if (validPlacement && !blockedByEnemy) { wouldBlock = wouldBlockPath(newGridX, newGridY); } if (validPlacement && !blockedByEnemy && !wouldBlock) { // Place tower in new position draggingTower.placeOnGrid(newGridX, newGridY); draggingTower.alpha = 1; var notification = game.addChild(new Notification("Tower moved!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { // Return tower to original position draggingTower.x = grid.x + draggingTower.gridX * CELL_SIZE + CELL_SIZE / 2; draggingTower.y = grid.y + draggingTower.gridY * CELL_SIZE + CELL_SIZE / 2; draggingTower.alpha = 1; // Restore grid cells for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(draggingTower.gridX + i, draggingTower.gridY + j); if (cell) { cell.type = 1; } } } draggingTower.refreshCellsInRange(); if (!validPlacement) { var notification = game.addChild(new Notification("Cannot move tower here!")); } else if (blockedByEnemy) { var notification = game.addChild(new Notification("Cannot move: Enemy in the way!")); } else if (wouldBlock) { var notification = game.addChild(new Notification("Cannot move: Would block path!")); } notification.x = 2048 / 2; notification.y = grid.height - 50; } draggingTower = null; grid.pathFind(); grid.renderDebug(); } else { // Handle new tower placement 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); // Create start screen with Hogwarts under attack image var startScreenBackground = startScreen.attachAsset('hogwartsAttack', { anchorX: 0.5, anchorY: 0.5 }); startScreenBackground.x = 1024; startScreenBackground.y = 1366; // Center vertically // Image is already 2048x2732, perfect fit startScreenBackground.scaleX = 1; startScreenBackground.scaleY = 1; // Title text removed as requested // Subtitle removed as requested // Add instruction text to tap anywhere var tapInstructionShadow = new Text2("Tap anywhere to start", { size: 80, fill: 0x000000, weight: 600 }); tapInstructionShadow.anchor.set(0.5, 0.5); tapInstructionShadow.x = 1024 + 4; tapInstructionShadow.y = 2732 - 600 + 4; startScreen.addChild(tapInstructionShadow); var tapInstruction = new Text2("Tap anywhere to start", { size: 80, fill: 0xFFD700, weight: 600 }); tapInstruction.anchor.set(0.5, 0.5); tapInstruction.x = 1024; tapInstruction.y = 2732 - 600; startScreen.addChild(tapInstruction); // Add pulsing animation to instruction text var _pulseAnimation = function pulseAnimation() { if (tapInstruction.parent) { tween(tapInstruction, { alpha: 0.4, scaleX: 0.95, scaleY: 0.95 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { if (tapInstruction.parent) { tween(tapInstruction, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 1000, easing: tween.easeInOut, onFinish: _pulseAnimation }); } } }); } }; _pulseAnimation(); // Make entire start screen clickable startScreen.down = function () { // Remove start screen game.removeChild(startScreen); // Start countdown directly startCountdown(); }; // Function to start countdown function startCountdown() { // Add 100 image after hogwartsAttack var image100Container = new Container(); game.addChild(image100Container); var image100 = image100Container.attachAsset('100', { anchorX: 0.5, anchorY: 0.5 }); // Scale to fill the entire screen image100.width = 2048; image100.height = 2732; image100Container.x = 2048 / 2; image100Container.y = 2732 / 2; image100Container.alpha = 0; // Ensure it's on top of other elements game.setChildIndex(image100Container, game.children.length - 1); // Fade in the 100 image tween(image100Container, { alpha: 1 }, { duration: 500, easing: tween.easeOut }); // Make 100 image clickable to start the game image100Container.down = function () { // Fade out and start game tween(image100Container, { alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (image100Container.parent) { image100Container.destroy(); } // Start the actual game after clicking 100 gameStarted = true; // Update wave indicator to show game has started waveIndicator.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; // Update start marker visual var startMarker = waveIndicator.waveMarkers[0]; if (startMarker && startMarker.children[0]) { startMarker.children[0].tint = 0x00FF00; } if (startMarker && startMarker.children[1]) { startMarker.children[1].setText("Started!"); } if (startMarker && startMarker.children[2]) { startMarker.children[2].setText("Started!"); startMarker.children[2].x = 4; startMarker.children[2].y = 4; } // Add wizard movement warning var movementWarning = game.addChild(new Notification("Wizards can be moved by dragging them!")); movementWarning.x = 2048 / 2; movementWarning.y = 2732 / 2; // Make the warning last longer than normal notifications movementWarning.fadeOutTime = 300; // 5 seconds at 60 FPS } }); }; } // Add start screen to game game.addChild(startScreen); // Play exciting music on start screen LK.playMusic('g1', { loop: true }); // Wizard movement warning and music are now handled in the start screen button handler // Create restart button var restartButton = new Container(); var restartBackground = restartButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); restartBackground.width = 300; restartBackground.height = 120; restartBackground.tint = 0xFF6B6B; var restartText = new Text2("Restart", { size: 60, fill: 0xFFFFFF, weight: 800 }); restartText.anchor.set(0.5, 0.5); restartButton.addChild(restartText); // Position in top left area but outside the reserved 100x100 zone restartButton.x = 250; // Adjusted position for larger button restartButton.y = 140; // Adjusted position for larger button restartButton.down = function () { // Reset all game variables to initial state gold = 200; lives = 20; score = 0; xp = 0; level = 1; currentWave = 0; waveTimer = 0; waveInProgress = false; waveSpawned = false; selectedTower = null; isDragging = false; draggingTower = null; // Clear all enemies for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); } if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } } enemies = []; // Clear all bullets for (var i = bullets.length - 1; i >= 0; i--) { if (bullets[i].parent) { bullets[i].destroy(); } } bullets = []; // Clear all towers for (var i = towers.length - 1; i >= 0; i--) { var tower = towers[i]; // Reset grid cells for (var j = 0; j < 2; j++) { for (var k = 0; k < 2; k++) { var cell = grid.getCell(tower.gridX + j, tower.gridY + k); if (cell) { cell.type = 0; } } } towerLayer.removeChild(tower); } towers = []; // Clear any upgrade menus var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } // Clear any range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } // Clear background flames for (var i = backgroundFlames.length - 1; i >= 0; i--) { if (backgroundFlames[i].parent) { backgroundFlames[i].destroy(); } } backgroundFlames = []; // Clear smoke particles for (var i = smokeParticles.length - 1; i >= 0; i--) { if (smokeParticles[i].parent) { smokeParticles[i].destroy(); } } smokeParticles = []; flameSpawnTimer = 0; lightningTimer = 0; groundShakeTimer = 0; // Reset wave indicator waveIndicator.gameStarted = false; var startMarker = waveIndicator.waveMarkers[0]; if (startMarker && startMarker.children[0]) { startMarker.children[0].tint = 0x00AA00; } if (startMarker && startMarker.children[1]) { startMarker.children[1].setText("Start Game"); } if (startMarker && startMarker.children[2]) { startMarker.children[2].setText("Start Game"); } // Reset grid grid.pathFind(); grid.renderDebug(); // Update UI updateUI(); // Hide tower preview towerPreview.visible = false; var notification = game.addChild(new Notification("Game restarted!")); notification.x = 2048 / 2; notification.y = grid.height - 150; }; game.addChild(restartButton); 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 (!gameStarted) { return; } 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); // Play monster sound when enemy spawns LK.getSound('ron').play(); // 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) : 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; // Increase XP by 10 when enemy is defeated xp += 10; // Check for level up if (xp >= 100 && level < 2) { level = 2; xp = 0; // Reset XP to 0 when reaching level 2 // Set attack interval to 0.5 seconds (30 frames at 60 FPS) for all towers for (var i = 0; i < towers.length; i++) { towers[i].fireRate = 30; } // Show level up notification var levelUpNotification = game.addChild(new Notification("LEVEL UP! Attack speed increased!")); levelUpNotification.x = 2048 / 2; levelUpNotification.y = 2732 / 2; levelUpNotification.fadeOutTime = 240; // 4 seconds } // 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); } } // Clean up destroyed snakes for (var i = snakes.length - 1; i >= 0; i--) { if (!snakes[i].parent) { snakes.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } // Background flame spawning logic if (gameStarted) { flameSpawnTimer++; if (flameSpawnTimer >= flameSpawnInterval) { flameSpawnTimer = 0; flameSpawnInterval = 180 + Math.random() * 240; // Reset interval // Spawn a new flame at random position var flameX = 100 + Math.random() * (2048 - 200); // Avoid edges var flameY = 300 + Math.random() * (2200 - 300); // Avoid UI areas var flame = new BackgroundFlame(flameX, flameY); flameLayer.addChild(flame); backgroundFlames.push(flame); } // Clean up destroyed flames from array for (var i = backgroundFlames.length - 1; i >= 0; i--) { if (!backgroundFlames[i].parent) { backgroundFlames.splice(i, 1); } } } // Lightning effect every 15 seconds if (gameStarted) { lightningTimer++; if (lightningTimer >= lightningInterval) { lightningTimer = 0; // Play pat sound for explosion LK.getSound('pat').play(); // Play lightning sound LK.getSound('lightning').play(); // Create smoke particles from edges without dark overlay var smokeCount = 80; // Even more particles for realistic creeping fog var centerX = 1024; var centerY = 1366; // Create initial wave of smoke with more focused origin points (like fire sources) for (var i = 0; i < smokeCount; i++) { var side = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left var startX, startY; // Create clusters of smoke from specific "burning" points var clusterOffset = Math.floor(i / 8) * 300; // Larger clusters switch (side) { case 0: // Top edge - create fire source clusters startX = (clusterOffset + Math.random() * 200) % 2048; startY = -200 - Math.random() * 100; break; case 1: // Right edge - create fire source clusters startX = 2248 + Math.random() * 100; startY = (clusterOffset + Math.random() * 200) % 2732; break; case 2: // Bottom edge - create fire source clusters startX = (clusterOffset + Math.random() * 200) % 2048; startY = 2932 + Math.random() * 100; break; case 3: // Left edge - create fire source clusters startX = -200 - Math.random() * 100; startY = (clusterOffset + Math.random() * 200) % 2732; break; } // Create smoke with varied delays for waves of fog LK.setTimeout(function (x, y) { return function () { // Vary the target point for more organic movement // Smoke creeps inward with strong upward bias var inwardAngle = Math.atan2(centerY - y, centerX - x); var spreadAngle = inwardAngle + (Math.random() - 0.5) * Math.PI * 0.5; var spreadDistance = 300 + Math.random() * 600; var targetX = x + Math.cos(spreadAngle) * spreadDistance; var targetY = y + Math.sin(spreadAngle) * spreadDistance - 200; // Strong upward bias var smoke = new SmokeParticle(x, y, targetX, targetY); smokeLayer.addChild(smoke); smokeParticles.push(smoke); }; }(startX, startY), Math.random() * 1200); // Staggered over 1200ms for more gradual effect } // Create second wave of smoke for continuous burning effect LK.setTimeout(function () { for (var i = 0; i < smokeCount * 0.75; i++) { var side = Math.floor(Math.random() * 4); var startX, startY; // More concentrated fire sources for second wave var fireSourceX = Math.floor(Math.random() * 5) * 400 + 200; // 5 main fire sources var fireSourceY = Math.floor(Math.random() * 5) * 546 + 273; switch (side) { case 0: startX = fireSourceX + (Math.random() - 0.5) * 250; startY = -150 - Math.random() * 100; break; case 1: startX = 2198 + Math.random() * 100; startY = fireSourceY + (Math.random() - 0.5) * 250; break; case 2: startX = fireSourceX + (Math.random() - 0.5) * 250; startY = 2882 + Math.random() * 100; break; case 3: startX = -150 - Math.random() * 100; startY = fireSourceY + (Math.random() - 0.5) * 250; break; } // Create more varied smoke movement patterns LK.setTimeout(function (x, y) { return function () { // Smoke rises and creeps in a more realistic pattern var riseAngle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI / 2; // Mostly upward with variation var creepDistance = 400 + Math.random() * 600; var targetX = x + Math.cos(riseAngle) * creepDistance * 0.6; var targetY = y + Math.sin(riseAngle) * creepDistance - 100; var smoke = new SmokeParticle(x, y, targetX, targetY); smokeLayer.addChild(smoke); smokeParticles.push(smoke); }; }(startX, startY), Math.random() * 800); } }, 800); } } // Ground shake effect every 20 seconds if (gameStarted) { groundShakeTimer++; if (groundShakeTimer >= groundShakeInterval) { groundShakeTimer = 0; // Shake the ground (game container) var shakeIntensity = 15; var originalX = game.x; var originalY = game.y; // Create shake sequence var _shakeSequence = function shakeSequence(count) { if (count <= 0) { // Return to original position tween(game, { x: originalX, y: originalY }, { duration: 100, easing: tween.easeOut }); return; } // Random shake offset var shakeX = originalX + (Math.random() - 0.5) * shakeIntensity; var shakeY = originalY + (Math.random() - 0.5) * shakeIntensity; tween(game, { x: shakeX, y: shakeY }, { duration: 50, easing: tween.easeInOut, onFinish: function onFinish() { _shakeSequence(count - 1); } }); }; // Start shake sequence with 8 shakes _shakeSequence(8); } } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var BackgroundFlame = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
var flameGraphics = self.attachAsset('flame', {
anchorX: 0.5,
anchorY: 1.0
});
// Random flame colors (orange to red)
var flameColors = [0xff6b35, 0xff4500, 0xff6347, 0xdc143c, 0xb22222];
flameGraphics.tint = flameColors[Math.floor(Math.random() * flameColors.length)];
// Random size variation
var sizeVariation = 0.5 + Math.random() * 0.8; // 0.5 to 1.3 scale
flameGraphics.scaleX = sizeVariation;
flameGraphics.scaleY = sizeVariation;
// Start invisible
self.alpha = 0;
self.scaleX = 0.1;
self.scaleY = 0.1;
// Animate appearance
var appearDuration = 800 + Math.random() * 600; // 800-1400ms
tween(self, {
alpha: 0.3 + Math.random() * 0.4,
// 0.3 to 0.7 alpha
scaleX: sizeVariation,
scaleY: sizeVariation
}, {
duration: appearDuration,
easing: tween.easeOut,
onFinish: function onFinish() {
// Flicker for a while
self.startFlickering();
}
});
self.startFlickering = function () {
if (!self.parent) return; // Stop if destroyed
// Random flicker duration
var flickerTime = 2000 + Math.random() * 3000; // 2-5 seconds
var flickerInterval = 80 + Math.random() * 120; // Flicker every 80-200ms
var flickerTimer = LK.setInterval(function () {
if (!self.parent) {
LK.clearInterval(flickerTimer);
return;
}
// Quick flicker animation
var currentAlpha = self.alpha;
var flickerAlpha = Math.max(0.1, currentAlpha - 0.15 + Math.random() * 0.3);
tween(self, {
alpha: flickerAlpha
}, {
duration: 60,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.parent) return;
tween(self, {
alpha: currentAlpha
}, {
duration: 60,
easing: tween.easeInOut
});
}
});
}, flickerInterval);
// Start disappearing after flicker time
LK.setTimeout(function () {
if (!self.parent) return;
LK.clearInterval(flickerTimer);
self.startDisappearing();
}, flickerTime);
};
self.startDisappearing = function () {
if (!self.parent) return; // Stop if destroyed
var disappearDuration = 600 + Math.random() * 400; // 600-1000ms
tween(self, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: disappearDuration,
easing: tween.easeIn,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
return self;
});
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
// Store reference for customization
self.bulletGraphics = bulletGraphics;
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;
}
// If this is a sniper (Harry) bullet and enemy survives, split it
if (self.type === 'sniper' && self.targetEnemy.health > 0 && !self.targetEnemy.hasBeenSplit) {
// Mark this enemy as split to prevent infinite splitting
self.targetEnemy.hasBeenSplit = true;
// Create two new enemies with half the remaining health
var splitHealth = self.targetEnemy.health / 2;
for (var splitIndex = 0; splitIndex < 2; splitIndex++) {
var newEnemy = new Enemy(self.targetEnemy.type);
// Copy properties from original enemy
newEnemy.maxHealth = splitHealth;
newEnemy.health = splitHealth;
newEnemy.speed = self.targetEnemy.speed;
newEnemy.hasBeenSplit = true; // Prevent further splitting
newEnemy.waveNumber = self.targetEnemy.waveNumber;
// Copy status effects
if (self.targetEnemy.slowed) {
newEnemy.slowed = true;
newEnemy.slowDuration = self.targetEnemy.slowDuration;
newEnemy.originalSpeed = self.targetEnemy.originalSpeed;
newEnemy.speed = self.targetEnemy.speed;
}
if (self.targetEnemy.poisoned) {
newEnemy.poisoned = true;
newEnemy.poisonDuration = self.targetEnemy.poisonDuration;
newEnemy.poisonDamage = self.targetEnemy.poisonDamage;
}
// Position the new enemies slightly offset from original
var offsetX = splitIndex === 0 ? -0.5 : 0.5;
var offsetY = (Math.random() - 0.5) * 0.5;
newEnemy.cellX = self.targetEnemy.cellX;
newEnemy.cellY = self.targetEnemy.cellY;
newEnemy.currentCellX = self.targetEnemy.currentCellX + offsetX;
newEnemy.currentCellY = self.targetEnemy.currentCellY + offsetY;
newEnemy.x = grid.x + newEnemy.currentCellX * CELL_SIZE;
newEnemy.y = grid.y + newEnemy.currentCellY * CELL_SIZE;
// Copy flying target if applicable
if (self.targetEnemy.isFlying && self.targetEnemy.flyingTarget) {
newEnemy.flyingTarget = self.targetEnemy.flyingTarget;
}
// Add to appropriate layer
if (newEnemy.isFlying) {
enemyLayerTop.addChild(newEnemy);
if (newEnemy.shadow) {
enemyLayerMiddle.addChild(newEnemy.shadow);
}
} else {
enemyLayerBottom.addChild(newEnemy);
}
// Add to enemies array
enemies.push(newEnemy);
// Update health bar
newEnemy.healthBar.width = newEnemy.health / newEnemy.maxHealth * 70;
}
// Remove the original enemy
self.targetEnemy.health = 0;
}
// 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
});
// Remove the random tint to show the stone texture properly
cellGraphics.tint = 0xFFFFFF;
cellGraphics.alpha = 0.8;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
numberLabel.visible = false;
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.setText("-");
// Dark reddish stone for non-path cells
cellGraphics.tint = 0x8B4513;
cellGraphics.alpha = 0.5;
return;
}
numberLabel.visible = false;
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff;
cellGraphics.alpha = 0.6;
} else {
// Apply subtle tinting to the stone texture based on path score
var pathTint = 0x88 - tint << 8 | tint;
// Blend the tint with the stone texture color
cellGraphics.tint = 0xD2B48C; // Light tan/stone color
cellGraphics.alpha = 0.7 + tint / 0x88 * 0.3; // Vary alpha based on path score
}
// Arrows removed - no visual indicators
break;
}
case 1:
{
self.removeArrows();
// Darker stone for walls
cellGraphics.tint = 0x654321;
cellGraphics.alpha = 0.9;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
// Goal cells with greenish stone tint
cellGraphics.tint = 0x8FBC8F;
cellGraphics.alpha = 0.8;
numberLabel.visible = false;
break;
}
}
// Number labels removed
};
});
// 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 *= 6; // Three times as fast (was 2x, now 6x total)
self.maxHealth = 50; // Half health (was 100, now 50)
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;
}
// Make all enemies 25% faster
self.speed = self.speed * 1.25;
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
});
self.enemyGraphics = enemyGraphics; // Store reference for walking animation
// Scale up boss enemies
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Dark wizard enemies with magical effects
// Different wizard types with distinct magical auras
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x3742fa; // Blue aura for fast dark wizards
break;
case 'immune':
enemyGraphics.tint = 0xff3838; // Red aura for immune dark wizards
break;
case 'flying':
enemyGraphics.tint = 0xf1c40f; // Golden aura for broom-riding wizards
break;
case 'swarm':
enemyGraphics.tint = 0x8c7ae6; // Purple aura for swarm dark wizards
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;
}
// Add walking animation effect
if (!self.walkingAnimation && self.speed > 0) {
// Create a subtle bobbing effect for walking
var _createWalkingAnimation = function createWalkingAnimation() {
if (!self.parent) return; // Stop if enemy is destroyed
// Adjust animation speed based on enemy speed
var animDuration = self.slowed ? 400 : 200;
if (self.type === 'fast') animDuration = 100;
// Vertical bobbing motion
tween(enemyGraphics, {
y: -5
}, {
duration: animDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.parent) return;
tween(enemyGraphics, {
y: 0
}, {
duration: animDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.parent) return;
_createWalkingAnimation(); // Continue the animation loop
}
});
}
});
};
self.walkingAnimation = true;
_createWalkingAnimation();
}
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;
self.fadeOutTime = 120; // Default fade out time
self.update = function () {
if (self.fadeOutTime > 0) {
self.fadeOutTime--;
self.alpha = Math.min(self.fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var SmokeParticle = Container.expand(function (startX, startY, targetX, targetY, color) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
// Create multiple smoke tendrils for realistic wispy smoke
var smokeTendrils = [];
var numTendrils = 3 + Math.floor(Math.random() * 3); // 3-5 tendrils per smoke
for (var i = 0; i < numTendrils; i++) {
var tendrilContainer = new Container();
self.addChild(tendrilContainer);
// Create elongated smoke shapes using multiple stretched circles
var numSegments = 4 + Math.floor(Math.random() * 3); // 4-6 segments per tendril
var segmentGraphics = [];
for (var j = 0; j < numSegments; j++) {
var segment = tendrilContainer.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
// Create elongated, thin smoke shapes
var baseWidth = 80 + Math.random() * 120 - j * 15; // Tapers towards end
var baseHeight = 250 + Math.random() * 350 + j * 50; // Gets longer
segment.width = baseWidth;
segment.height = baseHeight;
// Position segments to create continuous tendril
segment.y = -j * (baseHeight * 0.3); // Overlap segments
segment.x = (Math.random() - 0.5) * 30 * j; // Slight sideways drift
// Dark, horror-themed smoke colors
var smokeType = Math.random();
if (smokeType < 0.4) {
// Very dark grey-black smoke (40% chance)
var darkness = 0x08 + Math.floor(Math.random() * 0x18);
segment.tint = darkness << 16 | darkness << 8 | darkness;
} else if (smokeType < 0.6) {
// Dark purple-black smoke for horror effect (20% chance)
var purple = 0x20 + Math.floor(Math.random() * 0x20);
var darkness = 0x10 + Math.floor(Math.random() * 0x10);
segment.tint = purple << 16 | darkness << 8 | purple;
} else if (smokeType < 0.8) {
// Dark red-black smoke (20% chance)
var red = 0x30 + Math.floor(Math.random() * 0x20);
var darkness = 0x08 + Math.floor(Math.random() * 0x10);
segment.tint = red << 16 | darkness << 8 | darkness;
} else {
// Ashy grey smoke (20% chance)
var grayValue = 0x30 + Math.floor(Math.random() * 0x30);
segment.tint = grayValue << 16 | grayValue << 8 | grayValue;
}
// Vary alpha for depth - more transparent at the edges
segment.alpha = 0.4 - j * 0.08 - Math.abs(i - numTendrils / 2) * 0.1;
segment.blendMode = 1; // Additive blend for ethereal effect
segmentGraphics.push(segment);
}
// Random rotation for each tendril
tendrilContainer.rotation = (Math.random() - 0.5) * Math.PI * 0.4;
// Offset tendrils for natural spread
tendrilContainer.x = (i - numTendrils / 2) * 60 + (Math.random() - 0.5) * 40;
tendrilContainer.y = (Math.random() - 0.5) * 100;
smokeTendrils.push({
container: tendrilContainer,
segments: segmentGraphics,
baseRotation: tendrilContainer.rotation,
swaySpeed: 0.02 + Math.random() * 0.03,
swayAmount: 0.1 + Math.random() * 0.2
});
}
// Add dark wisps occasionally for extra horror effect
if (Math.random() < 0.4) {
var wispLayer = new Container();
self.addChild(wispLayer);
var wisp = wispLayer.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
wisp.width = 40 + Math.random() * 60;
wisp.height = 200 + Math.random() * 300;
// Very dark wisp
wisp.tint = 0x000000;
wisp.alpha = 0.3;
wisp.blendMode = 1;
wispLayer.x = (Math.random() - 0.5) * 150;
wispLayer.y = -100 - Math.random() * 200;
wispLayer.rotation = (Math.random() - 0.5) * Math.PI * 0.6;
}
// Start with 0 alpha and small scale
self.alpha = 0;
self.scaleX = 0.3;
self.scaleY = 0.2;
// Calculate movement direction with creeping motion
var dx = targetX - startX;
var dy = targetY - startY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Add strong upward bias and sideways drift for smoke
var windDrift = (Math.random() - 0.5) * 400;
var upwardForce = -150 - Math.random() * 100; // Strong upward movement
// Animate appearance with slow, creeping emergence
tween(self, {
alpha: 0.85,
scaleX: 0.8,
scaleY: 1.2,
x: startX + dx * 0.1 + windDrift * 0.2,
y: startY + dy * 0.1 + upwardForce * 0.3
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
// Continue creeping and growing
tween(self, {
alpha: 0.6,
scaleX: 1.2,
scaleY: 1.6,
x: startX + dx * 0.3 + windDrift * 0.6,
y: startY + dy * 0.3 + upwardForce * 0.7
}, {
duration: 2500,
easing: tween.linear,
onFinish: function onFinish() {
// Final dissipation
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 2.0,
x: startX + dx * 0.5 + windDrift,
y: startY + dy * 0.5 + upwardForce * 1.2
}, {
duration: 1500,
easing: tween.easeIn,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
}
});
}
});
// Animate individual tendrils for undulating, creepy movement
self.animationTime = 0;
self.update = function () {
if (!self.parent) return;
self.animationTime += 0.016; // Assuming 60 FPS
// Animate each tendril with swaying motion
for (var i = 0; i < smokeTendrils.length; i++) {
var tendril = smokeTendrils[i];
// Sinusoidal swaying motion
var swayAngle = Math.sin(self.animationTime * tendril.swaySpeed + i) * tendril.swayAmount;
tendril.container.rotation = tendril.baseRotation + swayAngle;
// Animate individual segments for more fluid motion
for (var j = 0; j < tendril.segments.length; j++) {
var segment = tendril.segments[j];
// Add slight pulsing to segments
var pulseFactor = 1 + Math.sin(self.animationTime * 0.5 + j * 0.3) * 0.05;
segment.scaleX = pulseFactor;
// Slight vertical bobbing
segment.y = -j * (segment.height * 0.3) + Math.sin(self.animationTime + j * 0.5) * 5;
}
}
};
return self;
});
var Snake = Container.expand(function (startX, startY, tower) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.tower = tower;
self.speed = 3;
self.damage = 15;
self.fireRate = 30; // Fire every 0.5 seconds
self.lastFired = 0;
self.lifeTime = 300; // 5 seconds at 60 FPS
self.currentLifeTime = 0;
// Create snake body segments using yilan image
self.segments = [];
var numSegments = 8; // More segments for smoother snake
for (var i = 0; i < numSegments; i++) {
var segment = new Container();
// Create snake segment using yilan image asset
var segmentGraphics = segment.attachAsset('yilan', {
anchorX: 0.5,
anchorY: 0.5
});
// Make segments progressively smaller - increased base size
var segmentSize = 90 - i * 8; // Larger, more gradual taper
segmentGraphics.width = segmentSize;
segmentGraphics.height = segmentSize;
// Keep the natural color of the yilan image
segmentGraphics.tint = 0xFFFFFF; // No tint to preserve original colors
segmentGraphics.alpha = 0.9;
segment.x = -i * 20; // Tighter spacing for realistic movement
segment.y = 0;
self.addChild(segment);
self.segments.push(segment);
}
// Create snake head with larger yilan image
var head = self.segments[0];
// Remove the old head graphics first
head.removeChildAt(0);
// Main head shape (larger) using yilan image
var headGraphics = head.attachAsset('yilan', {
anchorX: 0.5,
anchorY: 0.5
});
headGraphics.width = 110; // Larger head
headGraphics.height = 110; // Keep square aspect ratio for image
headGraphics.tint = 0xFFFFFF; // No tint to preserve original image
// The yilan image already has eyes and details, so we don't need to add them
// Movement pattern variables
self.moveAngle = Math.random() * Math.PI * 2;
self.waveAmplitude = 30;
self.waveFrequency = 0.1;
self.time = 0;
self.findTarget = function () {
var closestEnemy = null;
var closestDistance = 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);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
return closestEnemy;
};
self.shootFire = function (targetEnemy) {
if (!targetEnemy || !targetEnemy.parent) return;
// Create fire projectile from snake's mouth
var fireX = self.x + self.segments[0].x;
var fireY = self.y + self.segments[0].y;
// Create flame bullet
var flame = new Bullet(fireX, fireY, targetEnemy, self.damage, 8);
flame.type = 'snake_fire';
// Customize flame appearance
flame.bulletGraphics.tint = 0xFF4500; // Orange-red flame
flame.bulletGraphics.width = 40; // Increased from 25
flame.bulletGraphics.height = 40; // Increased from 25
// Add flame effect
tween(flame.bulletGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.7
}, {
duration: 200,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
game.addChild(flame);
bullets.push(flame);
targetEnemy.bulletsTargetingThis.push(flame);
};
self.update = function () {
self.currentLifeTime++;
self.time++;
// Destroy snake after lifetime expires
if (self.currentLifeTime >= self.lifeTime) {
self.destroy();
return;
}
// Find nearest enemy
var target = self.findTarget();
if (target) {
// Move towards target with snake-like motion
var dx = target.x - self.x;
var dy = target.y - self.y;
var targetAngle = Math.atan2(dy, dx);
// Smooth angle transition
var angleDiff = targetAngle - self.moveAngle;
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
self.moveAngle += angleDiff * 0.1;
// Move with sinusoidal pattern
var sineOffset = Math.sin(self.time * self.waveFrequency) * self.waveAmplitude;
self.x += Math.cos(self.moveAngle) * self.speed;
self.y += Math.sin(self.moveAngle) * self.speed;
// Apply perpendicular offset for snake-like movement
var perpAngle = self.moveAngle + Math.PI / 2;
self.x += Math.cos(perpAngle) * sineOffset * 0.02;
self.y += Math.sin(perpAngle) * sineOffset * 0.02;
// Fire at enemies
if (LK.ticks - self.lastFired >= self.fireRate) {
self.shootFire(target);
self.lastFired = LK.ticks;
}
} else {
// Wander if no target
self.x += Math.cos(self.moveAngle) * self.speed * 0.5;
self.y += Math.sin(self.moveAngle) * self.speed * 0.5;
// Random direction changes
if (Math.random() < 0.05) {
self.moveAngle += (Math.random() - 0.5) * Math.PI * 0.5;
}
}
// Update segment positions to follow head with snake-like motion
for (var i = 1; i < self.segments.length; i++) {
var prevSegment = self.segments[i - 1];
var segment = self.segments[i];
// Smooth following with delay - more fluid for realistic motion
var followSpeed = 0.25 - i * 0.02;
segment.x += (prevSegment.x - segment.x - 20) * followSpeed;
segment.y += (prevSegment.y - segment.y) * followSpeed;
// Add more complex oscillation for realistic slithering
var oscillation = Math.sin(self.time * 0.15 + i * 0.6) * (4 - i * 0.3);
var perpAngle = self.moveAngle + Math.PI / 2;
segment.x += Math.cos(perpAngle) * oscillation;
segment.y += Math.sin(perpAngle) * oscillation;
// Rotate segments to face movement direction
if (i === 0 && self.segments[0].children[0]) {
// Head should face movement direction
self.segments[0].children[0].rotation = self.moveAngle;
} else if (segment.children[0] && prevSegment) {
// Body segments should face towards the previous segment
var dx = prevSegment.x - segment.x;
var dy = prevSegment.y - segment.y;
segment.children[0].rotation = Math.atan2(dy, dx);
}
}
// No eye animation needed since yilan image has built-in eyes
// Fade out near end of life
if (self.currentLifeTime > self.lifeTime - 60) {
self.alpha = (self.lifeTime - self.currentLifeTime) / 60;
}
};
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;
if (self.towerType === 'default') {
baseGraphics = self.attachAsset('1', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.towerType === 'rapid') {
baseGraphics = self.attachAsset('2', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.towerType === 'sniper') {
baseGraphics = self.attachAsset('3', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.towerType === 'splash') {
baseGraphics = self.attachAsset('4', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.towerType === 'slow') {
baseGraphics = self.attachAsset('5', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.towerType === 'poison') {
baseGraphics = self.attachAsset('6', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else {
baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
}
switch (self.towerType) {
case 'rapid':
// Don't tint if using image asset for rapid tower
break;
case 'sniper':
// Don't tint if using image asset for sniper tower
break;
case 'splash':
// Don't tint if using image asset for splash tower
break;
case 'slow':
// Don't tint if using image asset for slow tower
break;
case 'poison':
// Don't tint if using image asset for poison tower
break;
default:
// Don't tint if using image asset for default tower
break;
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var towerDisplayNameShadow = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
if (self.towerType === 'rapid') {
towerDisplayNameShadow = 'Twins';
}
if (self.towerType === 'default') {
towerDisplayNameShadow = 'Student';
}
if (self.towerType === 'sniper') {
towerDisplayNameShadow = 'Harry';
}
if (self.towerType === 'splash') {
towerDisplayNameShadow = 'Harmony';
}
if (self.towerType === 'slow') {
towerDisplayNameShadow = 'Ron';
}
if (self.towerType === 'poison') {
towerDisplayNameShadow = 'Snape';
}
var typeLabelShadow = new Text2(towerDisplayNameShadow, {
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 towerDisplayName = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
if (self.towerType === 'rapid') {
towerDisplayName = 'Twins';
}
if (self.towerType === 'default') {
towerDisplayName = 'Student';
}
if (self.towerType === 'sniper') {
towerDisplayName = 'Harry';
}
if (self.towerType === 'splash') {
towerDisplayName = 'Harmony';
}
if (self.towerType === 'slow') {
towerDisplayName = 'Ron';
}
if (self.towerType === 'poison') {
towerDisplayName = 'Snape';
}
var typeLabel = new Text2(towerDisplayName, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -20; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var costLabelShadow = new Text2(towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 4;
costLabelShadow.y = 24 + 12;
self.addChild(costLabelShadow);
// Add cost label
var costLabel = new Text2(towerCost, {
size: 50,
fill: 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 20 + 12;
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
self.specialAbilityCooldown = 0; // Cooldown for special abilities
// 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;
if (self.id === 'default') {
baseGraphics = self.attachAsset('1', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else if (self.id === 'rapid') {
baseGraphics = self.attachAsset('2', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else if (self.id === 'sniper') {
baseGraphics = self.attachAsset('3', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else if (self.id === 'splash') {
baseGraphics = self.attachAsset('4', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else if (self.id === 'slow') {
baseGraphics = self.attachAsset('5', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else if (self.id === 'poison') {
baseGraphics = self.attachAsset('6', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else {
baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Store reference to base graphics
self.baseGraphics = baseGraphics;
switch (self.id) {
case 'rapid':
// Don't tint if using image asset for rapid tower
break;
case 'sniper':
// Don't tint if using image asset for sniper tower
break;
case 'splash':
// Don't tint if using image asset for splash tower
break;
case 'slow':
// Don't tint if using image asset for slow tower
break;
case 'poison':
// Don't tint if using image asset for poison tower
break;
default:
// Don't tint if using image asset for default tower
// Ensure default tower image is not tinted
if (self.id === 'default') {
baseGraphics.tint = 0xFFFFFF; // Pure white (no tint)
}
break;
}
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
});
// Make gun slightly smaller for default towers to not obscure the base
if (self.id === 'default') {
gunGraphics.scaleX = 0.8;
gunGraphics.scaleY = 0.8;
}
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();
// Add cooldown indicator for harmony tower
if (self.id === 'splash') {
var cooldownIndicator = new Container();
self.cooldownIndicator = cooldownIndicator;
self.addChild(cooldownIndicator);
var cooldownBg = cooldownIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
cooldownBg.width = CELL_SIZE * 0.8;
cooldownBg.height = CELL_SIZE * 0.8;
cooldownBg.tint = 0x000000;
cooldownBg.alpha = 0.5;
var cooldownFill = cooldownIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
cooldownFill.width = CELL_SIZE * 0.7;
cooldownFill.height = CELL_SIZE * 0.7;
cooldownFill.tint = 0x00FF00;
self.cooldownFill = cooldownFill;
cooldownIndicator.y = -CELL_SIZE * 1.2;
}
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 () {
// Update special ability cooldown
if (self.specialAbilityCooldown > 0) {
self.specialAbilityCooldown--;
}
// Update cooldown visual indicator for harmony tower
if (self.id === 'splash' && self.cooldownIndicator) {
if (self.specialAbilityCooldown > 0) {
// Show cooldown progress
self.cooldownIndicator.visible = true;
var cooldownProgress = 1 - self.specialAbilityCooldown / 600;
self.cooldownFill.scaleX = cooldownProgress;
self.cooldownFill.scaleY = cooldownProgress;
self.cooldownFill.tint = 0xFF0000; // Red when on cooldown
// Hide the ability ready text when on cooldown
if (self.abilityReadyText) {
self.abilityReadyText.visible = false;
}
} else {
// Ready to use
self.cooldownIndicator.visible = true;
self.cooldownFill.scaleX = 1;
self.cooldownFill.scaleY = 1;
self.cooldownFill.tint = 0x00FF00; // Green when ready
// Show the ability ready text
if (!self.abilityReadyText) {
// Create the text if it doesn't exist
self.abilityReadyText = new Container();
// Add shadow text
var shadowText = new Text2('Press to slow down', {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.abilityReadyText.addChild(shadowText);
// Add main text
var mainText = new Text2('Press to slow down', {
size: 45,
fill: 0xFFFFFF,
weight: 800
});
mainText.anchor.set(0.5, 0.5);
self.abilityReadyText.addChild(mainText);
// Position above the tower
self.abilityReadyText.y = -CELL_SIZE * 2;
self.addChild(self.abilityReadyText);
}
self.abilityReadyText.visible = true;
}
}
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;
}
}
// Flying behavior for students (default) towers
if (self.id === 'default') {
// Initialize flying properties if not already set
if (!self.isFlying) {
self.isFlying = true;
self.flightRadius = 200 + Math.random() * 300; // Random radius between 200-500
self.flightSpeed = 0.02 + Math.random() * 0.03; // Random speed
self.flightAngle = Math.random() * Math.PI * 2; // Random starting angle
self.flightCenterX = 300 + Math.random() * (2048 - 600); // Random center X across screen
self.flightCenterY = 400 + Math.random() * (2200 - 400); // Random center Y across screen
self.flightTimer = 0;
self.flightDuration = 1800 + Math.random() * 1200; // Flight duration 30-50 seconds
}
// Update flight position
self.flightTimer++;
self.flightAngle += self.flightSpeed;
// Calculate circular flight path
var targetX = self.flightCenterX + Math.cos(self.flightAngle) * self.flightRadius;
var targetY = self.flightCenterY + Math.sin(self.flightAngle) * self.flightRadius;
// Smoothly move to target position
var lerpFactor = 0.1;
self.x += (targetX - self.x) * lerpFactor;
self.y += (targetY - self.y) * lerpFactor;
// Change flight pattern periodically
if (self.flightTimer >= self.flightDuration) {
self.flightTimer = 0;
self.flightRadius = 200 + Math.random() * 300;
self.flightSpeed = 0.02 + Math.random() * 0.03;
self.flightCenterX = 300 + Math.random() * (2048 - 600);
self.flightCenterY = 400 + Math.random() * (2200 - 400);
self.flightDuration = 1800 + Math.random() * 1200;
}
// Add floating animation to the tower graphic
if (!self.floatingAnimation) {
self.floatingAnimation = true;
var _createFloatingAnimation2 = function _createFloatingAnimation() {
if (!self.parent) return;
tween(self.baseGraphics, {
y: -8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.parent) return;
tween(self.baseGraphics, {
y: 8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.parent) return;
_createFloatingAnimation2();
}
});
}
});
};
_createFloatingAnimation2();
}
} else {
// Original tower following behavior for non-students towers
if (!self.isMoving && !self.moveTimer) {
self.moveTimer = LK.ticks + Math.random() * 300 + 180; // Random interval 3-8 seconds
}
if (!self.isMoving && LK.ticks >= self.moveTimer) {
// Find closest enemy in range to follow
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.originalX;
var dy = enemy.y - self.originalY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Increase following range to 2.5x the attack range
if (distance <= self.getRange() * 2.5 && distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.isMoving = true;
// Calculate movement towards enemy (but not too close)
var enemyDx = closestEnemy.x - self.originalX;
var enemyDy = closestEnemy.y - self.originalY;
var enemyDistance = Math.sqrt(enemyDx * enemyDx + enemyDy * enemyDy);
// Move 40% of the way towards the enemy (increased from 30%)
var moveDistance = Math.min(CELL_SIZE * 3, enemyDistance * 0.4);
var moveAngle = Math.atan2(enemyDy, enemyDx);
var newX = self.originalX + Math.cos(moveAngle) * moveDistance;
var newY = self.originalY + Math.sin(moveAngle) * moveDistance;
// Animate towards enemy
tween(self, {
x: newX,
y: newY
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
// Stay at new position for a moment
LK.setTimeout(function () {
// Return to original position
tween(self, {
x: self.originalX,
y: self.originalY
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
self.isMoving = false;
self.moveTimer = LK.ticks + Math.random() * 300 + 180; // Reset timer
}
});
}, 1000);
}
});
} else {
// No enemy found, reset timer
self.moveTimer = LK.ticks + Math.random() * 180 + 120; // Shorter wait if no target
}
}
}
};
self.down = function (x, y, obj) {
// Handle harmony tower special ability
if (self.id === 'splash' && self.specialAbilityCooldown === 0) {
// Activate special ability: slow all enemies to 50% for 5 seconds
var slowDuration = 300; // 5 seconds at 60 FPS
var slowPercentage = 0.5; // 50% speed
// Apply slow to all enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Skip immune enemies
if (!enemy.isImmune) {
// Store original speed if not already slowed
if (!enemy.slowed) {
enemy.originalSpeed = enemy.speed;
}
enemy.speed = enemy.originalSpeed * slowPercentage;
enemy.slowed = true;
enemy.slowDuration = slowDuration;
// Create visual effect
var slowEffect = new EffectIndicator(enemy.x, enemy.y, 'slow');
game.addChild(slowEffect);
}
}
// Set cooldown to 10 seconds
self.specialAbilityCooldown = 600; // 10 seconds at 60 FPS
// Play harmony sound effect
LK.getSound('harmony').play();
// Create notification
var notification = game.addChild(new Notification("Harmony activated! All enemies slowed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Visual feedback on the tower
tween(self, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn
});
}
});
return; // Don't open upgrade menu when using ability
}
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 'default':
bullet.bulletGraphics.tint = 0xd2b48c; // Default tower bullet color
bullet.bulletGraphics.width = 30;
bullet.bulletGraphics.height = 30;
// Randomly play either 111 or abrakadabra sound when default tower fires
if (Math.random() < 0.5) {
LK.getSound('111').play();
} else {
LK.getSound('abrakadabra').play();
}
// Change default tower to alternate between image assets 11 and 1 after firing
if (self.baseGraphics) {
// Check current asset and switch
var currentAssetId = self.baseGraphics.texture ? '1' : '11';
if (self.lastUsedAsset === '1' || self.lastUsedAsset === undefined) {
// Switch to asset 11
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('11', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '11';
} else {
// Switch to asset 1
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('1', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '1';
}
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
case 'rapid':
bullet.bulletGraphics.tint = 0x87ceeb; // Bright sky blue bolt
bullet.bulletGraphics.width = 20;
bullet.bulletGraphics.height = 20;
// Play 222 sound when rapid tower fires
LK.getSound('222').play();
// Change rapid tower to alternate between image assets 22 and 2 after firing
if (self.baseGraphics) {
// Check current asset and switch
if (self.lastUsedAsset === '2' || self.lastUsedAsset === undefined) {
// Switch to asset 22
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('22', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '22';
} else {
// Switch to asset 2
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('2', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '2';
}
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
case 'sniper':
bullet.bulletGraphics.tint = 0xff6347; // Bright orange-red spell
bullet.bulletGraphics.width = 15;
bullet.bulletGraphics.height = 15;
// Play 33 sound when sniper tower fires
LK.getSound('33').play();
// Change sniper tower to alternate between image assets 3 and 32 after firing
if (self.baseGraphics) {
// Check current asset and switch
if (self.lastUsedAsset === '3' || self.lastUsedAsset === undefined) {
// Switch to asset 32
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('32', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '32';
} else {
// Switch to asset 3
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('3', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '3';
}
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
case 'splash':
bullet.bulletGraphics.tint = 0x90ee90; // Bright light green spell
bullet.bulletGraphics.width = 40;
bullet.bulletGraphics.height = 40;
// Play harmony sound when splash tower fires
LK.getSound('harmony').play();
// Change splash tower to use image asset 42 after firing
if (self.baseGraphics) {
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('42', {
anchorX: 0.5,
anchorY: 0.5
});
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
case 'slow':
bullet.bulletGraphics.tint = 0xdda0dd; // Bright plum spell
bullet.bulletGraphics.width = 35;
bullet.bulletGraphics.height = 35;
// Play ron sound when slow tower fires
LK.getSound('ron').play();
// Change slow tower to alternate between image assets 5 and 52 after firing
if (self.baseGraphics) {
// Check current asset and switch
if (self.lastUsedAsset === '5' || self.lastUsedAsset === undefined) {
// Switch to asset 52
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('52', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '52';
} else {
// Switch to asset 5
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('5', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '5';
}
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
case 'poison':
bullet.bulletGraphics.tint = 0x98fb98; // Bright pale green spell
bullet.bulletGraphics.width = 35;
bullet.bulletGraphics.height = 35;
// Play snape sound when poison tower fires
LK.getSound('snape').play();
// Create snake that shoots fire at enemies
var snakeX = self.x + Math.cos(gunContainer.rotation) * 60;
var snakeY = self.y + Math.sin(gunContainer.rotation) * 60;
var snake = new Snake(snakeX, snakeY, self);
game.addChild(snake);
snakes.push(snake);
// Change poison tower to alternate between image assets 6 and 62 after firing
if (self.baseGraphics) {
// Check current asset and switch
if (self.lastUsedAsset === '6' || self.lastUsedAsset === undefined) {
// Switch to asset 62
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('62', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '62';
} else {
// Switch to asset 6
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('6', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '6';
}
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
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;
// Store original position for movement behavior
self.originalX = self.x;
self.originalY = self.y;
self.isMoving = false;
self.moveTimer = null;
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;
// For default tower type, replace the preview graphics with image asset 1
if (self.towerType === 'default') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 1 as preview
previewGraphics = self.attachAsset('1', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else if (self.towerType === 'rapid') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 2 as preview
previewGraphics = self.attachAsset('2', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else if (self.towerType === 'sniper') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 3 as preview
previewGraphics = self.attachAsset('3', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else if (self.towerType === 'splash') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 4 as preview
previewGraphics = self.attachAsset('4', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else if (self.towerType === 'slow') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 5 as preview
previewGraphics = self.attachAsset('5', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else if (self.towerType === 'poison') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 5 as preview
previewGraphics = self.attachAsset('5', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else {
// For non-default towers, ensure we're using the shape asset
if (previewGraphics.texture && previewGraphics.texture.baseTexture) {
// Already using an image, need to switch back to shape
self.removeChild(previewGraphics);
previewGraphics = self.attachAsset('towerpreview', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.setChildIndex(previewGraphics, 1);
}
}
switch (self.towerType) {
case 'rapid':
// Don't tint if using image asset for rapid tower
break;
case 'sniper':
// Don't tint if using image asset for sniper tower
break;
case 'splash':
// Don't tint if using image asset for splash tower
break;
case 'slow':
// Don't tint if using image asset for slow tower
break;
case 'poison':
// Don't tint if using image asset for poison tower
break;
default:
// Don't tint if using image asset for default tower
if (self.towerType !== 'default' && self.towerType !== 'rapid' && self.towerType !== 'sniper' && self.towerType !== 'splash' && self.towerType !== 'slow' && self.towerType !== 'poison') {
previewGraphics.tint = 0xd2b48c; // Light tan
}
}
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 towerTypeName = self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1);
if (self.tower.id === 'rapid') {
towerTypeName = 'Twins';
}
if (self.tower.id === 'default') {
towerTypeName = 'Student';
}
if (self.tower.id === 'sniper') {
towerTypeName = 'Harry';
}
if (self.tower.id === 'splash') {
towerTypeName = 'Harmony';
}
if (self.tower.id === 'slow') {
towerTypeName = 'Ron';
}
if (self.tower.id === 'poison') {
towerTypeName = 'Snape';
}
var towerTypeText = new Text2(towerTypeName + ' 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 () {
// Start button is now disabled - game starts automatically with countdown
};
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: 0x8B7355
});
/****
* Game Code
****/
var isHidingUpgradeMenu = false;
var gameStarted = false;
var startScreen = new Container();
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 snakes = [];
var selectedTower = null;
var gold = 130;
var lives = 5;
var score = 0;
var xp = 0;
var level = 1;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 2000;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var backgroundFlames = [];
var flameSpawnTimer = 0;
var flameSpawnInterval = 180 + Math.random() * 240; // Spawn flame every 3-7 seconds
var lightningTimer = 0;
var lightningInterval = 900; // 15 seconds at 60 FPS
var groundShakeTimer = 0;
var groundShakeInterval = 1200; // 20 seconds at 60 FPS
var flameLayer = new Container();
var smokeLayer = new Container();
var smokeParticles = [];
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 xpText = new Text2('XP: ' + xp, {
size: 60,
fill: 0x9400D3,
weight: 800
});
xpText.anchor.set(0.5, 0.5);
var levelText = new Text2('Level: ' + level, {
size: 60,
fill: 0x00CED1,
weight: 800
});
levelText.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);
LK.gui.top.addChild(xpText);
LK.gui.top.addChild(levelText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
xpText.x = 0;
xpText.y = topMargin + 80;
levelText.x = 0;
levelText.y = topMargin + 160;
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
xpText.setText('XP: ' + xp);
levelText.setText('Level: ' + level);
}
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();
// Add background image
var backgroundImage = LK.getAsset('arkaplan', {
anchorX: 0,
anchorY: 0,
scaleX: 1,
scaleY: 1
});
backgroundImage.x = 0;
backgroundImage.y = 0;
game.addChild(backgroundImage);
debugLayer.addChild(grid);
game.addChild(debugLayer);
game.addChild(flameLayer);
game.addChild(smokeLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
var draggingTower = null;
var dragOffset = {
x: 0,
y: 0
};
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) {
// Ensure towerType is 'default' if not specified
var actualTowerType = towerType || 'default';
var tower = new Tower(actualTowerType);
// Apply level 2 fire rate bonus if applicable
if (level >= 2) {
tower.fireRate = 30; // 0.5 second attack interval
}
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) {
if (!gameStarted) {
return;
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Check if clicking on an existing tower first
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (x >= tower.x - tower.baseGraphics.width / 2 && x <= tower.x + tower.baseGraphics.width / 2 && y >= tower.y - tower.baseGraphics.height / 2 && y <= tower.y + tower.baseGraphics.height / 2) {
// Start dragging the existing tower
draggingTower = tower;
isDragging = true;
dragOffset.x = x - tower.x;
dragOffset.y = y - tower.y;
// Clear the tower's grid cells
for (var j = 0; j < 2; j++) {
for (var k = 0; k < 2; k++) {
var cell = grid.getCell(tower.gridX + j, tower.gridY + k);
if (cell) {
cell.type = 0;
}
}
}
// Show visual feedback that tower is being dragged
tower.alpha = 0.7;
// Remove tower from towers in range for all cells
tower.refreshCellsInRange();
grid.pathFind();
grid.renderDebug();
return;
}
}
// Check source towers for creating new towers
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 (!gameStarted) {
return;
}
if (isDragging) {
if (draggingTower) {
// Move the existing tower
draggingTower.x = x - dragOffset.x;
draggingTower.y = y - dragOffset.y;
} else {
// Shift the y position upward by 1.5 tiles to show preview above finger
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
}
}
};
game.up = function (x, y, obj) {
if (!gameStarted) {
return;
}
var clickedOnTower = false;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var towerLeft = tower.x - tower.width / 2;
var towerRight = tower.x + tower.width / 2;
var towerTop = tower.y - tower.height / 2;
var towerBottom = tower.y + tower.height / 2;
if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) {
clickedOnTower = true;
break;
}
}
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
var menuWidth = 2048;
var menuHeight = 450;
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
hideUpgradeMenu(menu);
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
grid.renderDebug();
}
}
if (isDragging) {
isDragging = false;
if (draggingTower) {
// Handle existing tower repositioning
var gridPosX = draggingTower.x - grid.x;
var gridPosY = draggingTower.y - grid.y;
var newGridX = Math.floor(gridPosX / CELL_SIZE);
var newGridY = Math.floor(gridPosY / CELL_SIZE);
// Check if the new position is valid
var validPlacement = true;
var blockedByEnemy = false;
// Check bounds
if (newGridY <= 4 || newGridY + 1 >= grid.cells[0].length - 4) {
validPlacement = false;
} else {
// Check if cells are available
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(newGridX + i, newGridY + j);
if (!cell || cell.type !== 0) {
validPlacement = false;
break;
}
}
if (!validPlacement) break;
}
// Check for enemies in the way
if (validPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) continue;
if (!enemy.isFlying) {
if (enemy.cellX >= newGridX && enemy.cellX < newGridX + 2 && enemy.cellY >= newGridY && enemy.cellY < newGridY + 2) {
blockedByEnemy = true;
break;
}
}
}
}
}
// Check if placement would block path
var wouldBlock = false;
if (validPlacement && !blockedByEnemy) {
wouldBlock = wouldBlockPath(newGridX, newGridY);
}
if (validPlacement && !blockedByEnemy && !wouldBlock) {
// Place tower in new position
draggingTower.placeOnGrid(newGridX, newGridY);
draggingTower.alpha = 1;
var notification = game.addChild(new Notification("Tower moved!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
// Return tower to original position
draggingTower.x = grid.x + draggingTower.gridX * CELL_SIZE + CELL_SIZE / 2;
draggingTower.y = grid.y + draggingTower.gridY * CELL_SIZE + CELL_SIZE / 2;
draggingTower.alpha = 1;
// Restore grid cells
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(draggingTower.gridX + i, draggingTower.gridY + j);
if (cell) {
cell.type = 1;
}
}
}
draggingTower.refreshCellsInRange();
if (!validPlacement) {
var notification = game.addChild(new Notification("Cannot move tower here!"));
} else if (blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot move: Enemy in the way!"));
} else if (wouldBlock) {
var notification = game.addChild(new Notification("Cannot move: Would block path!"));
}
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
draggingTower = null;
grid.pathFind();
grid.renderDebug();
} else {
// Handle new tower placement
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);
// Create start screen with Hogwarts under attack image
var startScreenBackground = startScreen.attachAsset('hogwartsAttack', {
anchorX: 0.5,
anchorY: 0.5
});
startScreenBackground.x = 1024;
startScreenBackground.y = 1366; // Center vertically
// Image is already 2048x2732, perfect fit
startScreenBackground.scaleX = 1;
startScreenBackground.scaleY = 1;
// Title text removed as requested
// Subtitle removed as requested
// Add instruction text to tap anywhere
var tapInstructionShadow = new Text2("Tap anywhere to start", {
size: 80,
fill: 0x000000,
weight: 600
});
tapInstructionShadow.anchor.set(0.5, 0.5);
tapInstructionShadow.x = 1024 + 4;
tapInstructionShadow.y = 2732 - 600 + 4;
startScreen.addChild(tapInstructionShadow);
var tapInstruction = new Text2("Tap anywhere to start", {
size: 80,
fill: 0xFFD700,
weight: 600
});
tapInstruction.anchor.set(0.5, 0.5);
tapInstruction.x = 1024;
tapInstruction.y = 2732 - 600;
startScreen.addChild(tapInstruction);
// Add pulsing animation to instruction text
var _pulseAnimation = function pulseAnimation() {
if (tapInstruction.parent) {
tween(tapInstruction, {
alpha: 0.4,
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (tapInstruction.parent) {
tween(tapInstruction, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: _pulseAnimation
});
}
}
});
}
};
_pulseAnimation();
// Make entire start screen clickable
startScreen.down = function () {
// Remove start screen
game.removeChild(startScreen);
// Start countdown directly
startCountdown();
};
// Function to start countdown
function startCountdown() {
// Add 100 image after hogwartsAttack
var image100Container = new Container();
game.addChild(image100Container);
var image100 = image100Container.attachAsset('100', {
anchorX: 0.5,
anchorY: 0.5
});
// Scale to fill the entire screen
image100.width = 2048;
image100.height = 2732;
image100Container.x = 2048 / 2;
image100Container.y = 2732 / 2;
image100Container.alpha = 0;
// Ensure it's on top of other elements
game.setChildIndex(image100Container, game.children.length - 1);
// Fade in the 100 image
tween(image100Container, {
alpha: 1
}, {
duration: 500,
easing: tween.easeOut
});
// Make 100 image clickable to start the game
image100Container.down = function () {
// Fade out and start game
tween(image100Container, {
alpha: 0
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
if (image100Container.parent) {
image100Container.destroy();
}
// Start the actual game after clicking 100
gameStarted = true;
// Update wave indicator to show game has started
waveIndicator.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
// Update start marker visual
var startMarker = waveIndicator.waveMarkers[0];
if (startMarker && startMarker.children[0]) {
startMarker.children[0].tint = 0x00FF00;
}
if (startMarker && startMarker.children[1]) {
startMarker.children[1].setText("Started!");
}
if (startMarker && startMarker.children[2]) {
startMarker.children[2].setText("Started!");
startMarker.children[2].x = 4;
startMarker.children[2].y = 4;
}
// Add wizard movement warning
var movementWarning = game.addChild(new Notification("Wizards can be moved by dragging them!"));
movementWarning.x = 2048 / 2;
movementWarning.y = 2732 / 2;
// Make the warning last longer than normal notifications
movementWarning.fadeOutTime = 300; // 5 seconds at 60 FPS
}
});
};
}
// Add start screen to game
game.addChild(startScreen);
// Play exciting music on start screen
LK.playMusic('g1', {
loop: true
});
// Wizard movement warning and music are now handled in the start screen button handler
// Create restart button
var restartButton = new Container();
var restartBackground = restartButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
restartBackground.width = 300;
restartBackground.height = 120;
restartBackground.tint = 0xFF6B6B;
var restartText = new Text2("Restart", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
restartText.anchor.set(0.5, 0.5);
restartButton.addChild(restartText);
// Position in top left area but outside the reserved 100x100 zone
restartButton.x = 250; // Adjusted position for larger button
restartButton.y = 140; // Adjusted position for larger button
restartButton.down = function () {
// Reset all game variables to initial state
gold = 200;
lives = 20;
score = 0;
xp = 0;
level = 1;
currentWave = 0;
waveTimer = 0;
waveInProgress = false;
waveSpawned = false;
selectedTower = null;
isDragging = false;
draggingTower = null;
// Clear all enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
}
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
}
enemies = [];
// Clear all bullets
for (var i = bullets.length - 1; i >= 0; i--) {
if (bullets[i].parent) {
bullets[i].destroy();
}
}
bullets = [];
// Clear all towers
for (var i = towers.length - 1; i >= 0; i--) {
var tower = towers[i];
// Reset grid cells
for (var j = 0; j < 2; j++) {
for (var k = 0; k < 2; k++) {
var cell = grid.getCell(tower.gridX + j, tower.gridY + k);
if (cell) {
cell.type = 0;
}
}
}
towerLayer.removeChild(tower);
}
towers = [];
// Clear any upgrade menus
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
// Clear any range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
// Clear background flames
for (var i = backgroundFlames.length - 1; i >= 0; i--) {
if (backgroundFlames[i].parent) {
backgroundFlames[i].destroy();
}
}
backgroundFlames = [];
// Clear smoke particles
for (var i = smokeParticles.length - 1; i >= 0; i--) {
if (smokeParticles[i].parent) {
smokeParticles[i].destroy();
}
}
smokeParticles = [];
flameSpawnTimer = 0;
lightningTimer = 0;
groundShakeTimer = 0;
// Reset wave indicator
waveIndicator.gameStarted = false;
var startMarker = waveIndicator.waveMarkers[0];
if (startMarker && startMarker.children[0]) {
startMarker.children[0].tint = 0x00AA00;
}
if (startMarker && startMarker.children[1]) {
startMarker.children[1].setText("Start Game");
}
if (startMarker && startMarker.children[2]) {
startMarker.children[2].setText("Start Game");
}
// Reset grid
grid.pathFind();
grid.renderDebug();
// Update UI
updateUI();
// Hide tower preview
towerPreview.visible = false;
var notification = game.addChild(new Notification("Game restarted!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
};
game.addChild(restartButton);
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 (!gameStarted) {
return;
}
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);
// Play monster sound when enemy spawns
LK.getSound('ron').play();
// 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) : 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;
// Increase XP by 10 when enemy is defeated
xp += 10;
// Check for level up
if (xp >= 100 && level < 2) {
level = 2;
xp = 0; // Reset XP to 0 when reaching level 2
// Set attack interval to 0.5 seconds (30 frames at 60 FPS) for all towers
for (var i = 0; i < towers.length; i++) {
towers[i].fireRate = 30;
}
// Show level up notification
var levelUpNotification = game.addChild(new Notification("LEVEL UP! Attack speed increased!"));
levelUpNotification.x = 2048 / 2;
levelUpNotification.y = 2732 / 2;
levelUpNotification.fadeOutTime = 240; // 4 seconds
}
// 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);
}
}
// Clean up destroyed snakes
for (var i = snakes.length - 1; i >= 0; i--) {
if (!snakes[i].parent) {
snakes.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Background flame spawning logic
if (gameStarted) {
flameSpawnTimer++;
if (flameSpawnTimer >= flameSpawnInterval) {
flameSpawnTimer = 0;
flameSpawnInterval = 180 + Math.random() * 240; // Reset interval
// Spawn a new flame at random position
var flameX = 100 + Math.random() * (2048 - 200); // Avoid edges
var flameY = 300 + Math.random() * (2200 - 300); // Avoid UI areas
var flame = new BackgroundFlame(flameX, flameY);
flameLayer.addChild(flame);
backgroundFlames.push(flame);
}
// Clean up destroyed flames from array
for (var i = backgroundFlames.length - 1; i >= 0; i--) {
if (!backgroundFlames[i].parent) {
backgroundFlames.splice(i, 1);
}
}
}
// Lightning effect every 15 seconds
if (gameStarted) {
lightningTimer++;
if (lightningTimer >= lightningInterval) {
lightningTimer = 0;
// Play pat sound for explosion
LK.getSound('pat').play();
// Play lightning sound
LK.getSound('lightning').play();
// Create smoke particles from edges without dark overlay
var smokeCount = 80; // Even more particles for realistic creeping fog
var centerX = 1024;
var centerY = 1366;
// Create initial wave of smoke with more focused origin points (like fire sources)
for (var i = 0; i < smokeCount; i++) {
var side = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
var startX, startY;
// Create clusters of smoke from specific "burning" points
var clusterOffset = Math.floor(i / 8) * 300; // Larger clusters
switch (side) {
case 0:
// Top edge - create fire source clusters
startX = (clusterOffset + Math.random() * 200) % 2048;
startY = -200 - Math.random() * 100;
break;
case 1:
// Right edge - create fire source clusters
startX = 2248 + Math.random() * 100;
startY = (clusterOffset + Math.random() * 200) % 2732;
break;
case 2:
// Bottom edge - create fire source clusters
startX = (clusterOffset + Math.random() * 200) % 2048;
startY = 2932 + Math.random() * 100;
break;
case 3:
// Left edge - create fire source clusters
startX = -200 - Math.random() * 100;
startY = (clusterOffset + Math.random() * 200) % 2732;
break;
}
// Create smoke with varied delays for waves of fog
LK.setTimeout(function (x, y) {
return function () {
// Vary the target point for more organic movement
// Smoke creeps inward with strong upward bias
var inwardAngle = Math.atan2(centerY - y, centerX - x);
var spreadAngle = inwardAngle + (Math.random() - 0.5) * Math.PI * 0.5;
var spreadDistance = 300 + Math.random() * 600;
var targetX = x + Math.cos(spreadAngle) * spreadDistance;
var targetY = y + Math.sin(spreadAngle) * spreadDistance - 200; // Strong upward bias
var smoke = new SmokeParticle(x, y, targetX, targetY);
smokeLayer.addChild(smoke);
smokeParticles.push(smoke);
};
}(startX, startY), Math.random() * 1200); // Staggered over 1200ms for more gradual effect
}
// Create second wave of smoke for continuous burning effect
LK.setTimeout(function () {
for (var i = 0; i < smokeCount * 0.75; i++) {
var side = Math.floor(Math.random() * 4);
var startX, startY;
// More concentrated fire sources for second wave
var fireSourceX = Math.floor(Math.random() * 5) * 400 + 200; // 5 main fire sources
var fireSourceY = Math.floor(Math.random() * 5) * 546 + 273;
switch (side) {
case 0:
startX = fireSourceX + (Math.random() - 0.5) * 250;
startY = -150 - Math.random() * 100;
break;
case 1:
startX = 2198 + Math.random() * 100;
startY = fireSourceY + (Math.random() - 0.5) * 250;
break;
case 2:
startX = fireSourceX + (Math.random() - 0.5) * 250;
startY = 2882 + Math.random() * 100;
break;
case 3:
startX = -150 - Math.random() * 100;
startY = fireSourceY + (Math.random() - 0.5) * 250;
break;
}
// Create more varied smoke movement patterns
LK.setTimeout(function (x, y) {
return function () {
// Smoke rises and creeps in a more realistic pattern
var riseAngle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI / 2; // Mostly upward with variation
var creepDistance = 400 + Math.random() * 600;
var targetX = x + Math.cos(riseAngle) * creepDistance * 0.6;
var targetY = y + Math.sin(riseAngle) * creepDistance - 100;
var smoke = new SmokeParticle(x, y, targetX, targetY);
smokeLayer.addChild(smoke);
smokeParticles.push(smoke);
};
}(startX, startY), Math.random() * 800);
}
}, 800);
}
}
// Ground shake effect every 20 seconds
if (gameStarted) {
groundShakeTimer++;
if (groundShakeTimer >= groundShakeInterval) {
groundShakeTimer = 0;
// Shake the ground (game container)
var shakeIntensity = 15;
var originalX = game.x;
var originalY = game.y;
// Create shake sequence
var _shakeSequence = function shakeSequence(count) {
if (count <= 0) {
// Return to original position
tween(game, {
x: originalX,
y: originalY
}, {
duration: 100,
easing: tween.easeOut
});
return;
}
// Random shake offset
var shakeX = originalX + (Math.random() - 0.5) * shakeIntensity;
var shakeY = originalY + (Math.random() - 0.5) * shakeIntensity;
tween(game, {
x: shakeX,
y: shakeY
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
_shakeSequence(count - 1);
}
});
};
// Start shake sequence with 8 shakes
_shakeSequence(8);
}
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
};
ışıklı büyü ışıltısı. In-Game asset. 2d. High contrast. No shadows
3 boyutlu alev
elinde asa olsun ve sinirli olsun
harry potter evreninde kötü bir yaratık. In-Game asset. 2d. High contrast. No shadows
harry potter temasında korkutucu bir yılan yeşil renkte. In-Game asset. 2d. High contrast. No shadows