User prompt
başlangıçta 130 altın ver
User prompt
snake in yılanı, assets deki yilan resmine benzesin
User prompt
snake in yılanı şuan mor görünüyor. bunun yerine gözleri olan yeşil renkte hareket eden gerçekçi bir yılan figürü yap
User prompt
snake in yılanı daha büyük olsun
User prompt
snape ateş ettiğinde bir yılan oluşsun ve düşmanların üzerinde dolaşarak ateş etsin. ağzından alev çıksın
User prompt
harry ateş ettiğinde düşmanlar ikiye bölünsün ve çoğalsın ama canları yarım güçte olsun daha kolay ölsünler
User prompt
harmony nin yavaşlatma gücü dolduğunda karakterin yanında ingilizce ‘ üzerine bas ve yavaşlat’ yazısı çıksın
User prompt
100 resmi ekranın tamamını kaplasın
User prompt
neden karakterler büyüdü eski hallerine getir
User prompt
resimleri netleştir 100 resmindeki oyundaki resimleri örnek al kendi yapay zekanı da kullan
User prompt
100 resmini kendi yapay zekanı kullanarak biraz daha netleştir bozmuşsun
User prompt
100 resmini ekranın tamamına kaplat
User prompt
100 resmini hogwartsundderattack resminden sonra göster. ona tıkladıktan sonra oyun başlasın
User prompt
neden görünmüyor 100 resmi
User prompt
100 resmi hagwardunderattack resminden sonra görünsün
User prompt
2. sırada 100 resmi görünsün
User prompt
100 resmine tıklayınca hogwartsunderataack resmine geç
User prompt
100 resmini kendi yapay zekanı kullanarak ekranın tamamına kapla görüntü kalitesini yükselt yazıları netleştir
User prompt
hogwartsunderattact resminden önce 100 resmi görünsün
User prompt
oyun ilk açıldığında 100 resmini göster
User prompt
oyunda ekranı karartmayı iptal et
User prompt
oyunda bir sorun var düzgün ilerlemiyor
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'self.fastWaveWarningShown')' in or related to this line: 'if (isFastWave && !self.fastWaveWarningShown) {' Line Number: 3930
User prompt
Before you go to fast wave, show a warning that the enemies that will come are very fast, you can change the position of the characters, and you have a slowing feature with hormony. After this warning, wait three seconds and the enemies will come.
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'self.fastWaveWarningShown')' in or related to this line: 'if (currentWave === 2 && !self.fastWaveWarningShown) {' Line Number: 3914
/**** * 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; } // 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 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 } else { // Ready to use self.cooldownIndicator.visible = true; self.cooldownFill.scaleX = 1; self.cooldownFill.scaleY = 1; self.cooldownFill.tint = 0x00FF00; // Green when ready } } 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(); // 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 selectedTower = null; var gold = 200; 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 fastWaveWarningShown = false; // Track if fast wave warning has been shown 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() { // Start the actual game immediately 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; fastWaveWarningShown = false; // Reset fast wave warning flag // 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) { // Check if this is the fast wave (wave 2) and show warning if (currentWave === 2 && !fastWaveWarningShown) { fastWaveWarningShown = true; waveSpawned = false; // Delay spawning waveInProgress = false; // Pause wave progression // Create warning container var warningContainer = new Container(); game.addChild(warningContainer); // Create background overlay var overlay = warningContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); overlay.width = 1600; overlay.height = 800; overlay.tint = 0x000000; overlay.alpha = 0.9; warningContainer.x = 2048 / 2; warningContainer.y = 2732 / 2; // Add warning text var warningText = new Text2("⚠️ DİKKAT! ⚠️\n\nGelecek düşmanlar ÇOK HIZLI!\n\nKarakterlerin yerini sürükleyerek\ndeğiştirebilirsiniz!\n\nHarmony kullanarak düşmanları\nyavaşlatabilirsiniz!", { size: 70, fill: 0xFFFF00, weight: 800 }); warningText.anchor.set(0.5, 0.5); warningText.y = -100; warningContainer.addChild(warningText); // Wait 2 seconds then show ready button LK.setTimeout(function () { // Add ready button var readyButton = new Container(); warningContainer.addChild(readyButton); var buttonBg = readyButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBg.width = 300; buttonBg.height = 100; buttonBg.tint = 0x00FF00; readyButton.y = 250; var buttonText = new Text2("HAZIR!", { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); readyButton.addChild(buttonText); // Add pulsing animation to button var _pulseButton = function pulseButton() { if (readyButton.parent) { tween(readyButton, { scaleX: 1.1, scaleY: 1.1 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { if (readyButton.parent) { tween(readyButton, { scaleX: 1, scaleY: 1 }, { duration: 500, easing: tween.easeInOut, onFinish: _pulseButton }); } } }); } }; _pulseButton(); readyButton.down = function () { // Remove warning and continue wave warningContainer.destroy(); waveInProgress = true; waveSpawned = false; }; }, 2000); return; // Don't spawn enemies yet } 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); } } 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 dark overlay for lightning effect var darkOverlay = new Container(); var overlayGraphics = darkOverlay.attachAsset('notification', { anchorX: 0, anchorY: 0 }); overlayGraphics.width = 2048; overlayGraphics.height = 2732; overlayGraphics.tint = 0x000000; darkOverlay.alpha = 0; game.addChild(darkOverlay); // Create smoke particles from edges 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); // Animate the darkness effect with slower fade-in for creeping horror tween(darkOverlay, { alpha: 0.9 }, { duration: 800, easing: tween.easeIn, onFinish: function onFinish() { // Hold the dark state for longer to see smoke effect LK.setTimeout(function () { tween(darkOverlay, { alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { if (darkOverlay.parent) { darkOverlay.destroy(); } // Don't immediately clean up smoke particles - let them fade naturally } }); }, 1500); // Hold darkness for 1.5 seconds } }); } } // 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;
}
// 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 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
} else {
// Ready to use
self.cooldownIndicator.visible = true;
self.cooldownFill.scaleX = 1;
self.cooldownFill.scaleY = 1;
self.cooldownFill.tint = 0x00FF00; // Green when ready
}
}
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();
// 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 selectedTower = null;
var gold = 200;
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 fastWaveWarningShown = false; // Track if fast wave warning has been shown
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() {
// Start the actual game immediately
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;
fastWaveWarningShown = false; // Reset fast wave warning flag
// 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) {
// Check if this is the fast wave (wave 2) and show warning
if (currentWave === 2 && !fastWaveWarningShown) {
fastWaveWarningShown = true;
waveSpawned = false; // Delay spawning
waveInProgress = false; // Pause wave progression
// Create warning container
var warningContainer = new Container();
game.addChild(warningContainer);
// Create background overlay
var overlay = warningContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
overlay.width = 1600;
overlay.height = 800;
overlay.tint = 0x000000;
overlay.alpha = 0.9;
warningContainer.x = 2048 / 2;
warningContainer.y = 2732 / 2;
// Add warning text
var warningText = new Text2("⚠️ DİKKAT! ⚠️\n\nGelecek düşmanlar ÇOK HIZLI!\n\nKarakterlerin yerini sürükleyerek\ndeğiştirebilirsiniz!\n\nHarmony kullanarak düşmanları\nyavaşlatabilirsiniz!", {
size: 70,
fill: 0xFFFF00,
weight: 800
});
warningText.anchor.set(0.5, 0.5);
warningText.y = -100;
warningContainer.addChild(warningText);
// Wait 2 seconds then show ready button
LK.setTimeout(function () {
// Add ready button
var readyButton = new Container();
warningContainer.addChild(readyButton);
var buttonBg = readyButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 300;
buttonBg.height = 100;
buttonBg.tint = 0x00FF00;
readyButton.y = 250;
var buttonText = new Text2("HAZIR!", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
readyButton.addChild(buttonText);
// Add pulsing animation to button
var _pulseButton = function pulseButton() {
if (readyButton.parent) {
tween(readyButton, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (readyButton.parent) {
tween(readyButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: _pulseButton
});
}
}
});
}
};
_pulseButton();
readyButton.down = function () {
// Remove warning and continue wave
warningContainer.destroy();
waveInProgress = true;
waveSpawned = false;
};
}, 2000);
return; // Don't spawn enemies yet
}
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);
}
}
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 dark overlay for lightning effect
var darkOverlay = new Container();
var overlayGraphics = darkOverlay.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
overlayGraphics.width = 2048;
overlayGraphics.height = 2732;
overlayGraphics.tint = 0x000000;
darkOverlay.alpha = 0;
game.addChild(darkOverlay);
// Create smoke particles from edges
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);
// Animate the darkness effect with slower fade-in for creeping horror
tween(darkOverlay, {
alpha: 0.9
}, {
duration: 800,
easing: tween.easeIn,
onFinish: function onFinish() {
// Hold the dark state for longer to see smoke effect
LK.setTimeout(function () {
tween(darkOverlay, {
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (darkOverlay.parent) {
darkOverlay.destroy();
}
// Don't immediately clean up smoke particles - let them fade naturally
}
});
}, 1500); // Hold darkness for 1.5 seconds
}
});
}
}
// 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