/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 1; // Use tower-specific damage self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function (dt) { 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); // Use scaledDt for consistent movement var scaledSpeed = self.speed * (dt || scaledDt); if (distance < scaledSpeed) { // 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, minimum 1) var splashDamage = Math.max(1, Math.floor(self.damage * 0.5)); otherEnemy.health -= splashDamage; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } self.destroy(); } else { var angle = Math.atan2(dy, dx); // Use scaledDt for consistent movement var scaledSpeed = self.speed * (dt || scaledDt); self.x += Math.cos(angle) * scaledSpeed; self.y += Math.sin(angle) * scaledSpeed; } }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case TYPE_ROAD: case TYPE_SLOW_ROAD: case TYPE_POISON_ROAD: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.visible = false; // Hide numbers for decorative roads cellGraphics.tint = 0x880000; return; } numberLabel.visible = false; // Hide numbers to make roads naked // Replace cell graphics with appropriate road asset if (!cellGraphics.isRoadAsset) { self.removeChild(cellGraphics); cellGraphics = self.attachAsset('roadNormal', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.isRoadAsset = true; } // Apply road texture and rotation based on connections renderRoadCell(cellGraphics, self.cell.x, self.cell.y); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; } else { // Reset to default road appearance cellGraphics.tint = 0xFFFFFF; } // Remove all arrows - roads should be naked while (debugArrows.length) { self.removeChild(debugArrows.pop()); } break; } case 2: { // spawn - enemies can walk on it but it doesn't count as road for scoring self.removeArrows(); // Replace cell graphics with enemyspawns asset if (!cellGraphics.isSpawnAsset) { self.removeChild(cellGraphics); cellGraphics = self.attachAsset('enemyspawns', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.isSpawnAsset = true; } cellGraphics.tint = 0xFFFFFF; // Reset tint for spawn asset numberLabel.visible = false; // Don't show score numbers for spawn point even though enemies use it for pathfinding break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); // Replace cell graphics with playerbase asset if (!cellGraphics.isBaseAsset) { self.removeChild(cellGraphics); cellGraphics = self.attachAsset('playerbase', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.isBaseAsset = true; } cellGraphics.tint = 0xFFFFFF; // Reset tint for base asset numberLabel.visible = false; break; } case 4: { // tower body self.removeArrows(); cellGraphics.tint = 0x555555; // dark gray numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 10000)); }; }); // 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 * 1.05; // 5% faster self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; // Initialize with base HP from table self.maxHealth = HP_TABLE[self.type] || HP_TABLE.normal; self.health = self.maxHealth; self.poisonTimer = 0; // For poison road damage tracking self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = HP_TABLE.fast; break; case 'immune': self.isImmune = true; self.maxHealth = HP_TABLE.immune; self.speed *= 0.9; // 10% slower break; case 'flying': self.isFlying = true; self.maxHealth = Math.floor(HP_TABLE.flying * 1.1); // 10% more HP self.speed *= 1.1; // 10% faster break; case 'swarm': self.maxHealth = HP_TABLE.swarm; self.speed *= 1.4; // 40% faster movement break; case 'normal': default: self.maxHealth = HP_TABLE.normal; break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies use HP scaling: first boss HP * boss number var bossNumber = currentWave / 10; // 1st boss = wave 10 (bossNumber = 1), 2nd boss = wave 20 (bossNumber = 2), etc. var firstBossHP = 200; // Base HP for first boss self.maxHealth = firstBossHP * bossNumber; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Add scaling animation to all enemies // Start with random scale between 0.8 and 1.2 var initialScale = 0.8 + Math.random() * 0.4; // Random between 0.8 and 1.2 enemyGraphics.scaleX = initialScale; enemyGraphics.scaleY = initialScale; // Create continuous scaling animation var scaleMin = 0.8; var scaleMax = 1.2; var _animateScale = function animateScale() { var targetScale = scaleMin + Math.random() * (scaleMax - scaleMin); var duration = 500 + Math.random() * 250; // 0.5-0.75 seconds tween(enemyGraphics, { scaleX: targetScale, scaleY: targetScale }, { duration: duration, easing: tween.easeInOut, onFinish: function onFinish() { if (enemyGraphics && enemyGraphics.parent) { _animateScale(); // Continue animation if enemy still exists } } }); }; // Start the scaling animation _animateScale(); // Scale up boss enemies (applied on top of the animation) if (self.isBoss) { // For bosses, scale the animation range up scaleMin = 1.4; // 0.8 * 1.8 = 1.44, rounded to 1.4 scaleMax = 2.2; // 1.2 * 1.8 = 2.16, rounded to 2.2 enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // Add scaling animation to shadow that matches the main enemy var shadowScaleMin = 0.8; var shadowScaleMax = 1.2; var _animateShadowScale = function animateShadowScale() { var targetScale = shadowScaleMin + Math.random() * (shadowScaleMax - shadowScaleMin); var duration = 500 + Math.random() * 250; // 0.5-0.75 seconds tween(shadowGraphics, { scaleX: targetScale, scaleY: targetScale }, { duration: duration, easing: tween.easeInOut, onFinish: function onFinish() { if (shadowGraphics && shadowGraphics.parent) { _animateShadowScale(); // Continue animation if shadow still exists } } }); }; // Start the shadow scaling animation _animateShadowScale(); // If this is a boss, scale up the shadow to match if (self.isBoss) { // For boss shadows, scale the animation range up shadowScaleMin = 1.4; shadowScaleMax = 2.2; 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 (dt) { 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 -= dt || scaledDt; 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; } // Initialize poison timer if not exists if (!self.poisonTimer) self.poisonTimer = 0; self.poisonTimer += dt || scaledDt; // Apply poison damage every 30 units (twice per second at 1x speed) if (self.poisonTimer >= 30) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; self.poisonTimer = 0; } self.poisonDuration -= dt || scaledDt; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Combine poison (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); // Calculate actual grid height accounting for bars var GRID_H_PX = 2732 - TOP_BAR_H - BOTTOM_BAR_H; var actualRows = Math.floor(GRID_H_PX / CELL_SIZE); gridHeight = Math.min(gridHeight, actualRows); 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]; // Default all cells to walls var cellType = 1; 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; } } } // Set spawn at center (10, 12) var centerCell = self.cells[10] && self.cells[10][12]; if (centerCell) { centerCell.type = 2; self.spawns = [centerCell]; } 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 != TYPE_WALL && !node.hasTower) { 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]; // Only process cardinal directions (up, down, left, right) var 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; } else { // Limit spawn to only target the spawn exit (down direction) self.spawns[a].targets = [spawnExit]; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // 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]); } } } }; // Re-render all road cells with updated corner/straight textures self.forEachRoadCell = function (renderFunc) { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; if (cell && (cell.type === TYPE_ROAD || cell.type === TYPE_SLOW_ROAD || cell.type === TYPE_POISON_ROAD)) { var debugCell = cell.debugCell; if (debugCell && debugCell.children[0]) { renderFunc(debugCell.children[0], i, j); } } } } }; self.poisonRoadParticles = {}; // Track particles per poison road cell self.slowRoadParticles = {}; // Track particles per slow road cell self.spawnPotionParticle = function (cellX, cellY) { var key = cellX + ',' + cellY; if (!self.poisonRoadParticles[key]) { self.poisonRoadParticles[key] = []; } // Clean up destroyed particles FIRST (before checking limit) // Check both parent !== null AND !destroyed flag self.poisonRoadParticles[key] = self.poisonRoadParticles[key].filter(function (p) { return p.parent !== null && !p.destroyed; }); // Check if we can spawn more particles (max 60 per cell - increased from 30) if (self.poisonRoadParticles[key].length >= 60) { return; } // Random position within the cell - use self.x instead of grid.x since we're inside the Grid class var particleX = self.x + cellX * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8; var particleY = self.y + cellY * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8; var particle = new PotionParticle(particleX, particleY); game.addChild(particle); self.poisonRoadParticles[key].push(particle); }; self.spawnSlowParticle = function (cellX, cellY) { var key = cellX + ',' + cellY; if (!self.slowRoadParticles[key]) { self.slowRoadParticles[key] = []; } // Clean up destroyed particles FIRST (before checking limit) // Check both parent !== null AND !destroyed flag self.slowRoadParticles[key] = self.slowRoadParticles[key].filter(function (p) { return p.parent !== null && !p.destroyed; }); // Check if we can spawn more particles (max 60 per cell) if (self.slowRoadParticles[key].length >= 60) { return; } // Random position within the cell - use self.x instead of grid.x since we're inside the Grid class var particleX = self.x + cellX * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8; var particleY = self.y + cellY * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8; var particle = new SlowParticle(particleX, particleY); game.addChild(particle); self.slowRoadParticles[key].push(particle); }; 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; } } // Handle flying enemies differently - they follow the road but don't get poison effects if (enemy.isFlying) { // Use the same pathfinding system as ground enemies but ignore poison effects // Handle normal pathfinding enemies using pathPoints array if (!enemy.currentTarget) { if (enemy.cellPathIndex === undefined) { enemy.cellPathIndex = 1; // Start with spawn exit } if (enemy.cellPathIndex < pathPoints.length) { enemy.currentTarget = pathPoints[enemy.cellPathIndex]; } } if (enemy.currentTarget) { var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); // Calculate local speed for this enemy var speed = enemy.speed; // Flying enemies are NOT affected by slow roads, so don't check for slow road // Scale by game speed (for fast forward/pause) var scaledSpeed = speed * gameSpeed; if (dist < scaledSpeed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); // Move to next point in path enemy.cellPathIndex++; if (enemy.cellPathIndex < pathPoints.length) { enemy.currentTarget = pathPoints[enemy.cellPathIndex]; } else { enemy.currentTarget = undefined; // Reached the goal return true; } } else { 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 }); } } enemy.currentCellX += Math.cos(angle) * scaledSpeed; enemy.currentCellY += Math.sin(angle) * scaledSpeed; } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Flying enemies are NOT affected by poison roads, so skip poison damage section return false; } // Apply road effects for ground enemies if (!enemy.isFlying) { var currentCell = grid.getCell(Math.floor(enemy.currentCellX), Math.floor(enemy.currentCellY)); if (currentCell) { // Apply poison damage if (currentCell.type === TYPE_POISON_ROAD) { enemy.poisonTimer += gameSpeed; if (enemy.poisonTimer >= 60) { // Once per second at 60 FPS enemy.health -= 0.5; enemy.poisonTimer = 0; if (enemy.health <= 0) { enemy.health = 0; } // Update health bar if (enemy.healthBar) { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } } } else { enemy.poisonTimer = 0; } } } // Handle normal pathfinding enemies using pathPoints array if (!enemy.currentTarget) { if (enemy.cellPathIndex === undefined) { enemy.cellPathIndex = 1; // Start with spawn exit } if (enemy.cellPathIndex < pathPoints.length) { enemy.currentTarget = pathPoints[enemy.cellPathIndex]; } } if (enemy.currentTarget) { var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); // Calculate local speed for this enemy var speed = enemy.speed; // Check if enemy is on slow road var currentCell = grid.getCell(Math.floor(enemy.currentCellX), Math.floor(enemy.currentCellY)); if (currentCell && currentCell.type === TYPE_SLOW_ROAD) { speed *= SLOW_MUL; // Apply slow multiplier } // Scale by game speed (for fast forward/pause) var scaledSpeed = speed * gameSpeed; if (dist < scaledSpeed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); // Move to next point in path enemy.cellPathIndex++; if (enemy.cellPathIndex < pathPoints.length) { enemy.currentTarget = pathPoints[enemy.cellPathIndex]; } else { enemy.currentTarget = undefined; } return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * scaledSpeed; enemy.currentCellY += Math.sin(angle) * scaledSpeed; } 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); // Create outline first (behind the main button) var buttonOutline = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonOutline.width = 450 + 8; // 50% bigger + outline thickness buttonOutline.height = 150 + 8; // 50% bigger + outline thickness buttonOutline.tint = 0x000000; // Black outline var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 450; // 50% bigger (300 * 1.5) buttonBackground.height = 150; // 50% bigger (100 * 1.5) buttonBackground.tint = 0xFFF8DC; // Cream white color var buttonText = new Text2("Next Wave", { size: 75, //{8q} // 50% bigger text (50 * 1.5) fill: 0x000000, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.pulseStartTime = 0; self.isPulsing = false; self.update = function () { var shouldBeEnabled = waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves && waveIsComplete; var shouldBeVisible = waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves && waveIsComplete; // Handle visibility changes with tween effects if (shouldBeVisible && !self.visible) { // Show button with zoom in effect self.visible = true; self.scaleX = 0.1; self.scaleY = 0.1; self.alpha = 0; self.pulseStartTime = LK.ticks; self.isPulsing = true; tween(self, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 300, easing: tween.elasticOut }); } else if (!shouldBeVisible && self.visible) { // Hide button with zoom out effect self.isPulsing = false; tween(self, { scaleX: 0.1, scaleY: 0.1, alpha: 0 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { self.visible = false; } }); } // Add continuous zoom in/out animation every 3 seconds when visible if (self.visible && self.isPulsing) { var timeSincePulse = LK.ticks - self.pulseStartTime; // 3 seconds = 180 ticks at 60 FPS if (timeSincePulse % 180 === 0 && timeSincePulse > 0) { // Zoom out then zoom in tween(self, { scaleX: 0.8, scaleY: 0.8 }, { duration: 400, easing: tween.easeInOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 400, easing: tween.easeInOut }); } }); } } if (shouldBeEnabled) { self.enabled = true; buttonBackground.tint = 0xFFF8DC; // Cream white color } else { self.enabled = false; buttonBackground.tint = 0x888888; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves && waveIsComplete) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Reset wave completion tracking for new wave waveIsComplete = false; totalEnemiesSpawnedThisWave = 0; totalEnemiesDeadThisWave = 0; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var PoisonRoadPreview = Container.expand(function () { var self = Container.call(this); self.canPlace = false; self.gridX = 0; self.gridY = 0; // Create purple semi-transparent square var previewGraphics = LK.getAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.tint = 0x8B4513; // Brown for poison road (same as normal road) previewGraphics.alpha = 0.5; previewGraphics.width = CELL_SIZE; previewGraphics.height = CELL_SIZE; self.addChild(previewGraphics); // Add pulsing effect self.pulsingTween = null; self.startPulsing = function () { if (self.pulsingTween) return; self.pulsingTween = tween(previewGraphics, { alpha: 0.8 }, { duration: 500, easing: tween.easeInOut, loop: true, yoyo: true }); }; self.stopPulsing = function () { if (self.pulsingTween) { tween.stop(previewGraphics, { alpha: true }); self.pulsingTween = null; previewGraphics.alpha = 0.5; } }; // Start pulsing immediately self.startPulsing(); self.updatePlacementStatus = function () { self.canPlace = false; // Check if placement would be in bottom bar if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) { self.canPlace = false; return; } var canAfford = gold >= POISON_ROAD_COST; // Check if this cell is valid for road placement var cell = grid.getCell(self.gridX, self.gridY); // Road can only be placed on wall cells (type 1) without towers if (!cell || cell.type !== TYPE_WALL || cell.hasTower) { self.canPlace = false; } else if (!canAfford) { self.canPlace = false; } else { // New yol bloğunun merkez koordinatı var cx = self.gridX + 0.5; var cy = self.gridY + 0.5; var spawn = pathPoints[0]; // First point is spawn var spawnCenter = { x: spawn.x + 0.5, y: spawn.y + 0.5 }; var base = pathPoints[pathPoints.length - 1]; var baseCenter = { x: base.x + 0.5, y: base.y + 0.5 }; // Spawn edge restrictions removed - roads can be placed adjacent to any spawn edge // Check base edge restrictions var isBaseEdge = sharesEdge({ x: cx, y: cy }, baseCenter); if (isBaseEdge) { var last = pathPoints[pathPoints.length - 1]; // base centre var prev = pathPoints[pathPoints.length - 2]; // önceki yol var backDir = getDirectionIndex(last.x, last.y, prev.x, prev.y); var dirHere = getDirectionIndex(baseCenter.x, baseCenter.y, cx, cy); if (dirHere === backDir) { // SADECE geri yasak self.canPlace = false; return; } // yan veya ileri 90° → serbest } // Check if adjacent to any existing road (forbidden - only base allowed) var dirsRoad = [[0, -1], [1, 0], [0, 1], [-1, 0]]; var adjacentToRoad = false; for (var d = 0; d < 4; d++) { var n = grid.getCell(self.gridX + dirsRoad[d][0], self.gridY + dirsRoad[d][1]); if (n && (n.type === TYPE_ROAD || n.type === TYPE_SLOW_ROAD || n.type === TYPE_POISON_ROAD)) { adjacentToRoad = true; break; } } // Roads can only be placed adjacent to base if (adjacentToRoad) { self.canPlace = false; } else { // Check if cell is adjacent to the base (end of current path) var base = pathPoints[pathPoints.length - 1]; var isAdjacentToBase = false; var baseNeighbors = [{ x: base.x, y: base.y - 1 }, { x: base.x + 1, y: base.y }, { x: base.x, y: base.y + 1 }, { x: base.x - 1, y: base.y }]; for (var i = 0; i < baseNeighbors.length; i++) { if (baseNeighbors[i].x === self.gridX && baseNeighbors[i].y === self.gridY) { isAdjacentToBase = true; break; } } // Check if this would be going backwards var isBackwards = false; if (isAdjacentToBase && pathPoints.length >= 2) { var previousPoint = pathPoints[pathPoints.length - 2]; var currentDirection = { x: base.x - previousPoint.x, y: base.y - previousPoint.y }; var proposedDirection = { x: self.gridX - base.x, y: self.gridY - base.y }; if (currentDirection.x === -proposedDirection.x && currentDirection.y === -proposedDirection.y) { isBackwards = true; } } self.canPlace = isAdjacentToBase && !isBackwards; } } // Update appearance if (self.canPlace) { previewGraphics.tint = 0x00FF00; // Green for placeable self.visible = true; } else { previewGraphics.tint = 0xFF0000; // Red for not placeable self.visible = true; } }; 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 - 72 + 10 + 4 + 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2; self.updatePlacementStatus(); }; return self; }); var PotionParticle = Container.expand(function (x, y) { var self = Container.call(this); self.x = x; self.y = y; var particleGraphics = self.attachAsset('potionparticle', { anchorX: 0.5, anchorY: 0.5 }); // Start with random alpha self.alpha = 0.3 + Math.random() * 0.5; // Random horizontal offset for zigzag self.zigzagAmplitude = 5 + Math.random() * 5; self.zigzagFrequency = 2 + Math.random() * 2; self.startX = x; self.lifetime = 0; // Animate upward movement with zigzag tween(self, { y: y - 80, alpha: 0 }, { duration: 2000, // Reduced from 3000ms to 2000ms easing: tween.linear, onFinish: function onFinish() { // Remove from parent before destroying if (self.parent) { self.parent.removeChild(self); } self.destroy(); } }); self.update = function () { self.lifetime += 1; // Create zigzag pattern var zigzagOffset = Math.sin(self.lifetime * 0.1 * self.zigzagFrequency) * self.zigzagAmplitude; self.x = self.startX + zigzagOffset; }; return self; }); var RoadPreview = Container.expand(function () { var self = Container.call(this); self.canPlace = false; self.gridX = 0; self.gridY = 0; // Create simple brown semi-transparent square var previewGraphics = LK.getAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.tint = 0x8B4513; // Brown for road previewGraphics.alpha = 0.5; previewGraphics.width = CELL_SIZE; previewGraphics.height = CELL_SIZE; self.addChild(previewGraphics); // Add pulsing effect - start with null reference self.pulsingTween = null; self.startPulsing = function () { if (self.pulsingTween) return; self.pulsingTween = tween(previewGraphics, { alpha: 0.8 }, { duration: 500, easing: tween.easeInOut, loop: true, yoyo: true }); }; self.stopPulsing = function () { if (self.pulsingTween) { tween.stop(previewGraphics, { alpha: true }); self.pulsingTween = null; previewGraphics.alpha = 0.5; } }; // Start pulsing immediately self.startPulsing(); self.updatePlacementStatus = function () { self.canPlace = false; // Check if placement would be in bottom bar if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) { self.canPlace = false; return; } var canAfford = gold >= ROAD_COST; // Check if this cell is valid for road placement var cell = grid.getCell(self.gridX, self.gridY); // Road can only be placed on wall cells (type 1) without towers if (!cell || cell.type !== 1 || cell.hasTower) { // Can't place on non-wall cells or cells with towers self.canPlace = false; } else if (!canAfford) { // Not enough gold self.canPlace = false; } else { // New road bloğunun merkez koordinatı var cx = self.gridX + 0.5; var cy = self.gridY + 0.5; var spawn = pathPoints[0]; // First point is spawn var spawnCenter = { x: spawn.x + 0.5, y: spawn.y + 0.5 }; var base = pathPoints[pathPoints.length - 1]; var baseCenter = { x: base.x + 0.5, y: base.y + 0.5 }; // Spawn edge restrictions removed - roads can be placed adjacent to any spawn edge // Check base edge restrictions var isBaseEdge = sharesEdge({ x: cx, y: cy }, baseCenter); if (isBaseEdge) { var last = pathPoints[pathPoints.length - 1]; // base centre var prev = pathPoints[pathPoints.length - 2]; // önceki yol var backDir = getDirectionIndex(last.x, last.y, prev.x, prev.y); var dirHere = getDirectionIndex(baseCenter.x, baseCenter.y, cx, cy); if (dirHere === backDir) { // SADECE geri yasak self.canPlace = false; return; } // yan veya ileri 90° → serbest } // Check if adjacent to any existing road (forbidden - only base allowed) var dirsRoad = [[0, -1], [1, 0], [0, 1], [-1, 0]]; var adjacentToRoad = false; for (var d = 0; d < 4; d++) { var n = grid.getCell(self.gridX + dirsRoad[d][0], self.gridY + dirsRoad[d][1]); if (n && (n.type === TYPE_ROAD || n.type === TYPE_SLOW_ROAD || n.type === TYPE_POISON_ROAD)) { // bitişik ROAD tespit adjacentToRoad = true; break; } } // Roads can only be placed adjacent to base if (adjacentToRoad) { self.canPlace = false; } else { // Check if cell is adjacent to the base (end of current path) var base = pathPoints[pathPoints.length - 1]; var isAdjacentToBase = false; // Check 4-directional neighbors from base var baseNeighbors = [{ x: base.x, y: base.y - 1 }, // up { x: base.x + 1, y: base.y }, // right { x: base.x, y: base.y + 1 }, // down { x: base.x - 1, y: base.y } // left ]; for (var i = 0; i < baseNeighbors.length; i++) { if (baseNeighbors[i].x === self.gridX && baseNeighbors[i].y === self.gridY) { isAdjacentToBase = true; break; } } // Check if this would be going backwards var isBackwards = false; if (isAdjacentToBase && pathPoints.length >= 2) { var previousPoint = pathPoints[pathPoints.length - 2]; var currentDirection = { x: base.x - previousPoint.x, y: base.y - previousPoint.y }; var proposedDirection = { x: self.gridX - base.x, y: self.gridY - base.y }; // Check if proposed direction is opposite to current direction if (currentDirection.x === -proposedDirection.x && currentDirection.y === -proposedDirection.y) { isBackwards = true; } } self.canPlace = isAdjacentToBase && !isBackwards; } } // Update appearance - green for placeable, red for not placeable if (self.canPlace) { previewGraphics.tint = 0x00FF00; // Green for placeable self.visible = true; } else { previewGraphics.tint = 0xFF0000; // Red for not placeable self.visible = true; } }; 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 - 72 + 10 + 4 + 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2; self.updatePlacementStatus(); }; return self; }); var SlowParticle = Container.expand(function (x, y) { var self = Container.call(this); self.x = x; self.y = y; var particleGraphics = self.attachAsset('slowparticle', { anchorX: 0.5, anchorY: 0.5 }); // Start with random alpha self.alpha = 0.3 + Math.random() * 0.5; // Random horizontal offset for zigzag self.zigzagAmplitude = 5 + Math.random() * 5; self.zigzagFrequency = 2 + Math.random() * 2; self.startX = x; self.lifetime = 0; // Animate upward movement with zigzag tween(self, { y: y - 80, alpha: 0 }, { duration: 2000, // Same duration as poison particles easing: tween.linear, onFinish: function onFinish() { // Remove from parent before destroying if (self.parent) { self.parent.removeChild(self); } self.destroy(); } }); self.update = function () { self.lifetime += 1; // Create zigzag pattern var zigzagOffset = Math.sin(self.lifetime * 0.1 * self.zigzagFrequency) * self.zigzagAmplitude; self.x = self.startX + zigzagOffset; }; return self; }); var SlowRoadPreview = Container.expand(function () { var self = Container.call(this); self.canPlace = false; self.gridX = 0; self.gridY = 0; // Create ice-blue semi-transparent square var previewGraphics = LK.getAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.tint = 0x8B4513; // Brown for slow road (same as normal road) previewGraphics.alpha = 0.5; previewGraphics.width = CELL_SIZE; previewGraphics.height = CELL_SIZE; self.addChild(previewGraphics); // Add pulsing effect self.pulsingTween = null; self.startPulsing = function () { if (self.pulsingTween) return; self.pulsingTween = tween(previewGraphics, { alpha: 0.8 }, { duration: 500, easing: tween.easeInOut, loop: true, yoyo: true }); }; self.stopPulsing = function () { if (self.pulsingTween) { tween.stop(previewGraphics, { alpha: true }); self.pulsingTween = null; previewGraphics.alpha = 0.5; } }; // Start pulsing immediately self.startPulsing(); self.updatePlacementStatus = function () { self.canPlace = false; // Check if placement would be in bottom bar if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) { self.canPlace = false; return; } var canAfford = gold >= SLOW_ROAD_COST; // Check if this cell is valid for road placement var cell = grid.getCell(self.gridX, self.gridY); // Road can only be placed on wall cells (type 1) without towers if (!cell || cell.type !== TYPE_WALL || cell.hasTower) { self.canPlace = false; } else if (!canAfford) { self.canPlace = false; } else { // New yol bloğunun merkez koordinatı var cx = self.gridX + 0.5; var cy = self.gridY + 0.5; var spawn = pathPoints[0]; // First point is spawn var spawnCenter = { x: spawn.x + 0.5, y: spawn.y + 0.5 }; var base = pathPoints[pathPoints.length - 1]; var baseCenter = { x: base.x + 0.5, y: base.y + 0.5 }; // Spawn edge restrictions removed - roads can be placed adjacent to any spawn edge // Check base edge restrictions var isBaseEdge = sharesEdge({ x: cx, y: cy }, baseCenter); if (isBaseEdge) { var last = pathPoints[pathPoints.length - 1]; // base centre var prev = pathPoints[pathPoints.length - 2]; // önceki yol var backDir = getDirectionIndex(last.x, last.y, prev.x, prev.y); var dirHere = getDirectionIndex(baseCenter.x, baseCenter.y, cx, cy); if (dirHere === backDir) { // SADECE geri yasak self.canPlace = false; return; } // yan veya ileri 90° → serbest } // Check if adjacent to any existing road (forbidden - only base allowed) var dirsRoad = [[0, -1], [1, 0], [0, 1], [-1, 0]]; var adjacentToRoad = false; for (var d = 0; d < 4; d++) { var n = grid.getCell(self.gridX + dirsRoad[d][0], self.gridY + dirsRoad[d][1]); if (n && (n.type === TYPE_ROAD || n.type === TYPE_SLOW_ROAD || n.type === TYPE_POISON_ROAD)) { adjacentToRoad = true; break; } } // Roads can only be placed adjacent to base if (adjacentToRoad) { self.canPlace = false; } else { // Check if cell is adjacent to the base (end of current path) var base = pathPoints[pathPoints.length - 1]; var isAdjacentToBase = false; var baseNeighbors = [{ x: base.x, y: base.y - 1 }, { x: base.x + 1, y: base.y }, { x: base.x, y: base.y + 1 }, { x: base.x - 1, y: base.y }]; for (var i = 0; i < baseNeighbors.length; i++) { if (baseNeighbors[i].x === self.gridX && baseNeighbors[i].y === self.gridY) { isAdjacentToBase = true; break; } } // Check if this would be going backwards var isBackwards = false; if (isAdjacentToBase && pathPoints.length >= 2) { var previousPoint = pathPoints[pathPoints.length - 2]; var currentDirection = { x: base.x - previousPoint.x, y: base.y - previousPoint.y }; var proposedDirection = { x: self.gridX - base.x, y: self.gridY - base.y }; if (currentDirection.x === -proposedDirection.x && currentDirection.y === -proposedDirection.y) { isBackwards = true; } } self.canPlace = isAdjacentToBase && !isBackwards; } } // Update appearance if (self.canPlace) { previewGraphics.tint = 0x00FF00; // Green for placeable self.visible = true; } else { previewGraphics.tint = 0xFF0000; // Red for not placeable self.visible = true; } }; 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 - 72 + 10 + 4 + 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2; self.updatePlacementStatus(); }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'basic'; // Increase size of base for easier touch var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.width = CELL_SIZE * 1.3 * 1.2; baseGraphics.height = CELL_SIZE * 1.3 * 1.2; switch (self.towerType) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'splash': baseGraphics.tint = 0x33CC00; break; default: baseGraphics.tint = 0xAAAAAA; } var towerCost = getTowerCost(self.towerType); // Get display name for tower type var displayName; switch (self.towerType) { case 'basic': displayName = ' Standard\n Cannon'; break; case 'rapid': displayName = ' Quick\nCannon'; break; case 'splash': displayName = ' Splash\nCannon'; break; default: displayName = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1); } // Add shadow for tower type label var typeLabelShadow = new Text2(displayName, { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = 0 + 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(displayName, { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = 0; // Position at center of tower self.addChild(typeLabel); // Add coin image shadow var coinImageShadow = self.attachAsset('goldimage', { anchorX: 0.5, anchorY: 0.5 }); coinImageShadow.width = 40; coinImageShadow.height = 40; coinImageShadow.x = -25 + 4; coinImageShadow.y = 99 + 12 + 15; coinImageShadow.tint = 0x000000; coinImageShadow.alpha = 0.5; // Add cost shadow var costLabelShadow = new Text2(' ' + towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 15 + 4; costLabelShadow.y = 99 + 12 + 15; self.addChild(costLabelShadow); // Add coin image var coinImage = self.attachAsset('goldimage', { anchorX: 0.5, anchorY: 0.5 }); coinImage.width = 40; coinImage.height = 40; coinImage.x = -25; coinImage.y = 95 + 12 + 15; // Add cost label var costLabel = new Text2(' ' + towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.x = 15; costLabel.y = 95 + 12 + 15; 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 || 'basic'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case '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; default: // Basic: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60 * 1.10; // 10% slower fire rate self.bulletSpeed = 5; self.damage = 1; // Default tower damage self.lastFired = 0; self.targetEnemy = null; // Set initial fire rate based on tower type and level self.updateFireRate = function () { switch (self.id) { case 'rapid': self.fireRate = 30 * 1.10; // 10% slower fire rate break; case 'splash': self.fireRate = 75 * 1.10; // 10% slower fire rate break; default: // basic tower // Level-based fire rate for basic tower // Convert shots per second to ticks between shots (60 FPS) var shotsPerSecond = [0.8, 1.1, 1.4, 1.7, 2.0, 2.5]; // rates for levels 1-6 var currentRate = shotsPerSecond[Math.min(self.level - 1, 5)]; // cap at level 6 self.fireRate = 60 / currentRate; // convert to ticks between shots break; } }; switch (self.id) { case 'rapid': self.fireRate = 30 * 1.10; // 10% slower fire rate self.damage = 1.5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'splash': self.fireRate = 75 * 1.10; // 10% slower fire rate self.damage = 2; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; default: // basic tower self.damage = 1; self.range = 3 * CELL_SIZE; self.bulletSpeed = 5; break; } // Set initial fire rate self.updateFireRate(); var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.width = CELL_SIZE; baseGraphics.height = CELL_SIZE; switch (self.id) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'splash': baseGraphics.tint = 0x33CC00; break; default: baseGraphics.tint = 0xAAAAAA; } var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 6; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -CELL_SIZE + dotSpacing * (i + 1) + 50; dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); var gunAsset = self.id === 'rapid' ? 'rapidtowercannon' : self.id === 'splash' ? 'splashcannontower' : 'defense'; var gunGraphics = gunContainer.attachAsset(gunAsset, { anchorX: 0.5, anchorY: 0.5 }); self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { switch (self.id) { case 'rapid': towerLevelIndicator.tint = 0x00AAFF; break; case 'splash': towerLevelIndicator.tint = 0x33CC00; break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type (damage remains constant at 1) 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.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.bulletSpeed = 7 + self.level * 0.7; } } else if (self.id === 'splash') { 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.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.bulletSpeed = 5 + self.level * 0.5; } } else { // Basic tower - use level-based fire rate system self.updateFireRate(); 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 (dt) { 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; // Use cooldown system with scaledDt if (!self.cooldown) self.cooldown = 0; self.cooldown -= dt || scaledDt; if (self.cooldown <= 0) { self.fire(); self.cooldown = self.fireRate; } } }; self.down = function (x, y, obj) { var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0x00AAFF; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'splash': bullet.children[0].tint = 0x33CC00; bullet.children[0].width = 40 * 0.65; bullet.children[0].height = 40 * 0.65; 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; // Position at exact grid cell center with offset of 56 pixels up and 56 pixels left (moved 14 pixels down and 12 pixels right total) self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2 - 58; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2 - 56; var cell = grid.getCell(gridX, gridY); if (cell) { cell.type = 4; // tower type cell.hasTower = true; // mark as occupied } self.refreshCellsInRange(); }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'basic'; 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; previewGraphics.height = CELL_SIZE; // Add pulsing effect to preview self.pulsingTween = null; self.startPulsing = function () { if (self.pulsingTween) return; self.pulsingTween = tween(previewGraphics, { alpha: 0.8 }, { duration: 500, easing: tween.easeInOut, loop: true, yoyo: true }); }; self.stopPulsing = function () { if (self.pulsingTween) { tween.stop(previewGraphics, { alpha: true }); self.pulsingTween = null; previewGraphics.alpha = 0.5; } }; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; break; case 'splash': previewGraphics.tint = 0x33CC00; break; default: previewGraphics.tint = 0xAAAAAA; } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } else { // Green tint for valid placement switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; break; case 'splash': previewGraphics.tint = 0x33CC00; break; default: previewGraphics.tint = 0xAAAAAA; } } }; self.updatePlacementStatus = function () { var validGridPlacement = true; // Check if placement would be in bottom bar if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) { validGridPlacement = false; self.canPlace = false; } if (self.gridY <= 4 || self.gridY >= grid.cells[0].length - 4) { validGridPlacement = false; } else { var cell = grid.getCell(self.gridX, self.gridY); // Towers can only be placed on wall cells (type 1) and must not have towers if (!cell || cell.type !== 1 || cell.hasTower) { validGridPlacement = false; } } 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.cellY === self.gridY) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX === self.gridX && targetY === self.gridY) { self.blockedByEnemy = true; break; } } } } } // Check if tower touches a road var touchesRoad = false; if (validGridPlacement) { var dirs = [[0, -1], [1, 0], [0, 1], [-1, 0]]; // 4 directions for (var d = 0; d < dirs.length; d++) { var nx = self.gridX + dirs[d][0]; var ny = self.gridY + dirs[d][1]; var nCell = grid.getCell(nx, ny); if (nCell && (nCell.type === TYPE_ROAD || nCell.type === TYPE_SLOW_ROAD || nCell.type === TYPE_POISON_ROAD || nCell.type === TYPE_SPAWN || nCell.type === TYPE_GOAL)) { touchesRoad = true; break; } } } self.canPlace = validGridPlacement && touchesRoad && !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); // Position at exact grid cell center - same as Tower self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2 - 72 + 10 + 4 + 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2; self.checkPlacement(); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + BULLET_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: ' + BULLET_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; var cell = grid.getCell(gridX, gridY); if (cell) { cell.type = 1; // reset to wall cell.hasTower = false; // mark as free 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(); if (typeof window.fixSpawnTargets === 'function') window.fixSpawnTargets(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (i === 5) { // Wave 6 - Mini Boss block.tint = 0xFF4500; waveType = "Mini Boss"; enemyType = "swarm"; enemyCount = 1; } 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; } } // Only handle wave progression if game is not paused if (gameSpeed > 0) { self.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { // Scale wave timer by game speed // When gameSpeed is 2x, timer progresses 2x faster // When gameSpeed is 0.5x, timer progresses 2x slower waveTimer += gameSpeed; // Direct multiplication by game speed if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; if (currentWave != 1) { var waveType = self.getWaveTypeName(currentWave); var enemyCount = self.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } } }; self.handleWaveProgression(); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } window.fixSpawnTargets = function fixSpawnTargets() { if (!grid || !grid.spawns || !grid.spawns[0]) return; // koruma var spawn = grid.spawns[0]; var spawnExit = grid.getCell(spawn.x, spawn.y + 1); spawn.targets = [spawnExit]; // yalnız aşağı }; // Helper function to check if a cell type is walkable road function isRoad(cellType) { return cellType === TYPE_ROAD || cellType === TYPE_SLOW_ROAD || cellType === TYPE_POISON_ROAD || cellType === TYPE_SPAWN || cellType === TYPE_GOAL; } // Classify block and return detailed info including directions function classifyBlock(x, y) { var kind = classifyRoadCell(x, y); var dirIn = 0; // Default up var dirOut = 0; // Default up var rotation = 0; if (kind === 'corner') { var _grid$getCell5, _grid$getCell6, _grid$getCell7, _grid$getCell8; var up = isRoad((_grid$getCell5 = grid.getCell(x, y - 1)) === null || _grid$getCell5 === void 0 ? void 0 : _grid$getCell5.type); var dn = isRoad((_grid$getCell6 = grid.getCell(x, y + 1)) === null || _grid$getCell6 === void 0 ? void 0 : _grid$getCell6.type); var lt = isRoad((_grid$getCell7 = grid.getCell(x - 1, y)) === null || _grid$getCell7 === void 0 ? void 0 : _grid$getCell7.type); var rt = isRoad((_grid$getCell8 = grid.getCell(x + 1, y)) === null || _grid$getCell8 === void 0 ? void 0 : _grid$getCell8.type); // Determine directions and rotation if (up && rt) { dirIn = 2; dirOut = 0; rotation = 0; // ┘ (from down to up) } else if (rt && dn) { dirIn = 3; dirOut = 1; rotation = Math.PI / 2; // └ (from left to right) } else if (dn && lt) { dirIn = 0; dirOut = 2; rotation = Math.PI; // ┌ (from up to down) } else if (lt && up) { dirIn = 1; dirOut = 3; rotation = -Math.PI / 2; // ┐ (from right to left) } } return { kind: kind, dirIn: dirIn, dirOut: dirOut, rotation: rotation }; } // Classify road cell as corner or straight based on connections function classifyRoadCell(x, y) { var _grid$getCell, _grid$getCell2, _grid$getCell3, _grid$getCell4; var n = (_grid$getCell = grid.getCell(x, y - 1)) === null || _grid$getCell === void 0 ? void 0 : _grid$getCell.type; // up var s = (_grid$getCell2 = grid.getCell(x, y + 1)) === null || _grid$getCell2 === void 0 ? void 0 : _grid$getCell2.type; // down var w = (_grid$getCell3 = grid.getCell(x - 1, y)) === null || _grid$getCell3 === void 0 ? void 0 : _grid$getCell3.type; // left var e = (_grid$getCell4 = grid.getCell(x + 1, y)) === null || _grid$getCell4 === void 0 ? void 0 : _grid$getCell4.type; // right var up = isRoad(n); var dn = isRoad(s); var lt = isRoad(w); var rt = isRoad(e); // Corner = at least one vertical AND at least one horizontal connection if ((up || dn) && (lt || rt)) return 'corner'; return 'straight'; } // Render road cell with appropriate texture and rotation function renderRoadCell(cellSprite, x, y) { var c = grid.getCell(x, y); // ① If already marked as corner, use that directly if (c.kind === 'corner') { var cornerAsset = LK.getAsset('roadCorner', {}); cellSprite.texture = cornerAsset.texture; cellSprite.rotation = c.cachedRotation || 0; // Use stored rotation return; // Done } // ② Otherwise continue with automatic analysis (straight/corner detection) var _grid$getCell5, _grid$getCell6, _grid$getCell7, _grid$getCell8; var up = isRoad((_grid$getCell5 = grid.getCell(x, y - 1)) === null || _grid$getCell5 === void 0 ? void 0 : _grid$getCell5.type); var dn = isRoad((_grid$getCell6 = grid.getCell(x, y + 1)) === null || _grid$getCell6 === void 0 ? void 0 : _grid$getCell6.type); var lt = isRoad((_grid$getCell7 = grid.getCell(x - 1, y)) === null || _grid$getCell7 === void 0 ? void 0 : _grid$getCell7.type); var rt = isRoad((_grid$getCell8 = grid.getCell(x + 1, y)) === null || _grid$getCell8 === void 0 ? void 0 : _grid$getCell8.type); // Count connections to determine if this is a corner var connectionCount = 0; var hasVertical = up || dn; var hasHorizontal = lt || rt; if (up) connectionCount++; if (dn) connectionCount++; if (lt) connectionCount++; if (rt) connectionCount++; // Use corner asset if we have exactly 2 connections AND they form a corner (one vertical + one horizontal) var isCorner = connectionCount === 2 && hasVertical && hasHorizontal; if (isCorner) { var cornerAsset = LK.getAsset('roadCorner', {}); cellSprite.texture = cornerAsset.texture; // Set rotation based on which two directions are connected if (up && rt) cellSprite.rotation = 0; // ┘ (up + right) else if (rt && dn) cellSprite.rotation = Math.PI / 2; // └ (right + down) else if (dn && lt) cellSprite.rotation = Math.PI; // ┌ (down + left) else if (lt && up) cellSprite.rotation = -Math.PI / 2; // ┐ (left + up) else cellSprite.rotation = 0; // fallback } else { var normalAsset = LK.getAsset('roadNormal', {}); cellSprite.texture = normalAsset.texture; cellSprite.rotation = 0; } } var CELL_SIZE = 114; var ROAD_COST = 5; var TOP_BAR_H = 38; var BOTTOM_BAR_H = -260; // Bullet damage constant var BULLET_DAMAGE = 1; // Enemy HP table var HP_TABLE = { normal: Math.floor(5 * 0.85 * 0.95), fast: Math.floor(6 * 0.85 * 0.95), flying: Math.floor(6 * 0.85 * 0.95), immune: Math.floor(12 * 0.85 * 0.95), swarm: Math.floor(3 * 0.85 * 0.95) - 1, boss: Math.floor(50 * 0.85 * 0.95) }; // Road type constants var TYPE_ROAD = 0; var TYPE_WALL = 1; var TYPE_SPAWN = 2; var TYPE_GOAL = 3; var TYPE_TOWER = 4; var TYPE_SLOW_ROAD = 5; var TYPE_POISON_ROAD = 6; // Slow road multiplier var SLOW_MUL = 0.75; // 25% slower // Road costs var SLOW_ROAD_COST = 10; var POISON_ROAD_COST = 15; // Direction constants for spawn/base edge filtering var DIRS = [{ dx: 0, dy: -1 }, // up (index 0) { dx: 1, dy: 0 }, // right (index 1) { dx: 0, dy: 1 }, // down (index 2) { dx: -1, dy: 0 } // left (index 3) ]; var SPAWN_EXIT_DIR = 2; // index 2 → down // Helper functions for spawn/base edge filtering function sharesEdge(a, b) { return Math.abs(a.x - b.x) === 1 && a.y === b.y || Math.abs(a.y - b.y) === 1 && a.x === b.x; } function getDirectionIndex(fromX, fromY, toX, toY) { if (toX > fromX) return 1; // right if (toX < fromX) return 3; // left if (toY > fromY) return 2; // down return 0; // up } var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; var gold = 80; var lives = 100; var score = 0; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave // Wave spawning state tracking var enemiesSpawnedThisWave = 0; var lastEnemySpawnTime = 0; var enemySpawnDelay = 78; // Default 1.3 seconds at 60 FPS (60 * 1.3 = 78) var totalEnemiesToSpawnThisWave = 0; var currentWaveType = 'normal'; // Wave completion tracking var totalEnemiesSpawnedThisWave = 0; var totalEnemiesDeadThisWave = 0; var waveIsComplete = false; // Function to get spawn delay based on enemy type function getEnemySpawnDelay(enemyType) { switch (enemyType) { case 'normal': return 84; // 1.4 seconds (60 * 1.4 = 84) case 'fast': return 96; // 1.6 seconds (60 * 1.6 = 96) case 'swarm': return 47; // 0.78 seconds (60 * 0.78 = 47) - 40% faster spawn rate case 'immune': return 84; // 1.4 seconds for immune enemies case 'flying': return 72; // 1.2 seconds for flying enemies default: return 72; // 1.2 seconds (60 * 1.2 = 72) for other enemies } } // Game speed control var gameSpeed = 1; var gameSpeedFactors = [1, 2, 5, 0.5, 0]; var currentSpeedIndex = 0; // Global scaled delta time var scaledDt = 1; // Initialize with default value var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('HP: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); // Create health bar var healthBarContainer = new Container(); healthBarContainer.visible = true; // Ensure container is visible var healthBarBg = healthBarContainer.attachAsset('healthBar', { anchorX: 0.5, anchorY: 0.5 }); healthBarBg.width = 106.9; // 5% smaller than 112.5 (112.5 * 0.95) healthBarBg.height = 10.7; // 5% smaller than 11.25 (11.25 * 0.95) healthBarBg.tint = 0x444444; // Dark gray background healthBarBg.visible = true; // Ensure background is visible var healthBarFill = healthBarContainer.attachAsset('healthBar', { anchorX: 0.5, anchorY: 0.5 }); healthBarFill.width = 106.9; // 5% smaller than 112.5 (112.5 * 0.95) healthBarFill.height = 10.7; // 5% smaller than 11.25 (11.25 * 0.95) healthBarFill.tint = 0x00FF00; // Green fill healthBarFill.visible = true; // Ensure fill is visible livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; var speedText = new Text2('1×', { size: 60, fill: 0xFFFFFF, weight: 800 }); speedText.anchor.set(0.5, 0.5); speedText.x = spacing + 30 + 200; speedText.y = topMargin; LK.gui.top.addChild(speedText); speedText.down = function () { currentSpeedIndex = (currentSpeedIndex + 1) % gameSpeedFactors.length; gameSpeed = gameSpeedFactors[currentSpeedIndex]; if (gameSpeed === 0) { speedText.setText('⏸'); } else if (gameSpeed === 0.5) { speedText.setText('0.5×'); } else if (gameSpeed === 1) { speedText.setText('1×'); } else if (gameSpeed === 2) { speedText.setText('2×'); } else if (gameSpeed === 5) { speedText.setText('5×'); } else { speedText.setText(gameSpeed + 'x'); } }; function updateHealthBarPos() { if (pathPoints && pathPoints.length > 0) { var base = pathPoints[pathPoints.length - 1]; // Last point is always the base healthBarContainer.x = grid.x + base.x * CELL_SIZE + CELL_SIZE / 2 - 58; // 58 pixels left (59 - 1) healthBarContainer.y = grid.y + base.y * CELL_SIZE + CELL_SIZE + 8 - 43; // 43 pixels up from 8px below base (40 + 3) } } function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('HP: ' + lives); scoreText.setText('Score: ' + score); // Update health bar var healthPercentage = lives / 100; // Assuming max health is 100 healthBarFill.width = 106.9 * healthPercentage; // Use new smaller size (106.9 instead of 112.5) // Align health bar fill to the left by adjusting anchor and position healthBarFill.anchorX = 0; // Change anchor to left healthBarFill.x = healthBarBg.x - healthBarBg.width / 2; // Position at left edge of background } function setGold(value) { gold = value; updateUI(); } // Create bottom bar var bottomBar = new Container(); var bottomBarGraphics = bottomBar.attachAsset('notification', { anchorX: 0, anchorY: 0 }); bottomBarGraphics.width = 2048; bottomBarGraphics.height = Math.abs(BOTTOM_BAR_H); bottomBarGraphics.tint = 0x222222; bottomBar.y = 2732 - BOTTOM_BAR_H; 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 = 60; grid.y = 200 - CELL_SIZE * 4; // Initialize the dynamic path function buildPath(spawnX, spawnY) { var pts = []; pts.push({ x: spawnX, y: spawnY }); // spawn pts.push({ x: spawnX, y: spawnY + 1 }); // 1 down for (var k = 1; k <= 3; k++) { // 3 left pts.push({ x: spawnX - k, y: spawnY + 1 }); } return pts; // last element is base } var pathPoints = buildPath(10, 12); // Define spawn and spawn exit for enemy pathfinding var spawn = grid.getCell(10, 12); var spawnExit = grid.getCell(10, 13); // Mark path cells in grid for (var i = 0; i < pathPoints.length; i++) { var point = pathPoints[i]; var cell = grid.getCell(point.x, point.y); if (cell) { if (i === 0) { cell.type = 2; // spawn } else if (i === pathPoints.length - 1) { cell.type = 3; // goal grid.goals.push(cell); // Add base to goals array } else { cell.type = TYPE_ROAD; // path } // Store sprite reference and block info for initial path (center cell) var blockInfo = classifyBlock(cell.x, cell.y); if (cell.debugCell && cell.debugCell.children[0]) { // Only store sprite reference if this is the center cell var HALF = 1; // Block center offset if (cell.x % 2 === 0 && cell.y % 2 === 1) { // Center cell for our grid layout cell.sprite = cell.debugCell.children[0]; cell.kind = blockInfo.kind; cell.dirIn = blockInfo.dirIn; cell.dirOut = blockInfo.dirOut; } } } } grid.pathFind(); // Fix spawn targets to only go downward if (typeof window.fixSpawnTargets === 'function') window.fixSpawnTargets(); grid.renderDebug(); // Position health bar below player base (at end of path) - moved here after pathPoints is initialized // NOTE: This will be executed after uiLayer is created // Update sprite references and block info after initial rendering for (var i = 0; i < pathPoints.length; i++) { var point = pathPoints[i]; var cell = grid.getCell(point.x, point.y); if (cell && cell.debugCell && cell.debugCell.children[0]) { var blockInfo = classifyBlock(cell.x, cell.y); // Only store sprite reference if this is the center cell var HALF = 1; // Block center offset if (cell.x % 2 === 0 && cell.y % 2 === 1) { // Center cell for our grid layout cell.sprite = cell.debugCell.children[0]; cell.kind = blockInfo.kind; cell.dirIn = blockInfo.dirIn; cell.dirOut = blockInfo.dirOut; } } } debugLayer.addChild(grid); game.addChild(bottomBar); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); // Add UI overlay layer that renders on top of everything var uiLayer = new Container(); game.addChild(uiLayer); // Position health bar below player base (at end of path) - moved here after uiLayer is created if (pathPoints && pathPoints.length > 0) { var basePoint = pathPoints[pathPoints.length - 1]; // Add health bar to UI layer instead of base cell uiLayer.addChild(healthBarContainer); // Position health bar in world coordinates healthBarContainer.x = grid.x + basePoint.x * CELL_SIZE + CELL_SIZE / 2; healthBarContainer.y = grid.y + basePoint.y * CELL_SIZE + CELL_SIZE + 8; // 8px below base // Ensure health bar is visible healthBarContainer.visible = true; healthBarBg.visible = true; healthBarFill.visible = true; } var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var roadPreview = new RoadPreview(); game.addChild(roadPreview); roadPreview.visible = false; var slowRoadPreview = new SlowRoadPreview(); game.addChild(slowRoadPreview); slowRoadPreview.visible = false; var poisonRoadPreview = new PoisonRoadPreview(); game.addChild(poisonRoadPreview); poisonRoadPreview.visible = false; var isDragging = false; var dragType = null; // 'tower' or 'road' function wouldBlockPath(gridX, gridY) { // Always return false as enemies can only walk on road tiles return false; } function getTowerCost(towerType) { switch (towerType) { case 'rapid': return 15; case 'splash': return 25; default: return 5; } } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'basic'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); grid.pathFind(); grid.renderDebug(); return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } // Check if clicking on road sources for (var i = 0; i < roadSources.length; i++) { var source = roadSources[i]; if (x >= source.x - source.width / 2 && x <= source.x + source.width / 2 && y >= source.y - source.height / 2 && y <= source.y + source.height / 2) { isDragging = true; if (source.data.type === 'road') { roadPreview.visible = true; dragType = 'road'; roadPreview.snapToGrid(x, y); } else if (source.data.type === 'slowRoad') { slowRoadPreview.visible = true; dragType = 'slowRoad'; slowRoadPreview.snapToGrid(x, y); } else if (source.data.type === 'poisonRoad') { poisonRoadPreview.visible = true; dragType = 'poisonRoad'; poisonRoadPreview.snapToGrid(x, y); } return; } } // Check tower sources 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; dragType = 'tower'; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); towerPreview.startPulsing(); // Apply the same offset as in move handler to ensure consistency when starting drag towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); break; } } }; game.move = function (x, y, obj) { if (isDragging) { // Shift the y position upward by 1.5 tiles to show preview above finger if (dragType === 'road') { roadPreview.snapToGrid(x, y); } else if (dragType === 'slowRoad') { slowRoadPreview.snapToGrid(x, y); } else if (dragType === 'poisonRoad') { poisonRoadPreview.snapToGrid(x, y); } else { towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } } }; game.up = function (x, y, obj) { // Ignore clicks in bottom bar area if (y > 2732 + BOTTOM_BAR_H) 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 (dragType === 'road' || dragType === 'slowRoad' || dragType === 'poisonRoad') { // Handle road placement var preview, cost, roadType; if (dragType === 'road') { preview = roadPreview; cost = ROAD_COST; roadType = TYPE_ROAD; } else if (dragType === 'slowRoad') { preview = slowRoadPreview; cost = SLOW_ROAD_COST; roadType = TYPE_SLOW_ROAD; } else if (dragType === 'poisonRoad') { preview = poisonRoadPreview; cost = POISON_ROAD_COST; roadType = TYPE_POISON_ROAD; } if (preview.canPlace && gold >= cost) { var cell = grid.getCell(preview.gridX, preview.gridY); if (cell) { // Deduct gold setGold(gold - cost); // Check if touching base to determine if this extends the path var base = pathPoints[pathPoints.length - 1]; var touchingBase = sharesEdge({ x: preview.gridX + 0.5, y: preview.gridY + 0.5 }, { x: base.x + 0.5, y: base.y + 0.5 }); if (touchingBase) { // L-turn detection and corner conversion if (pathPoints.length >= 2) { // Direction mapping: 0=up, 1=right, 2=down, 3=left var getDirection = function getDirection(dx, dy) { if (dx > 0) return 1; // → if (dx < 0) return 3; // ← if (dy > 0) return 2; // ↓ return 0; // ↑ }; var prevCell = grid.getCell(pathPoints[pathPoints.length - 2].x, pathPoints[pathPoints.length - 2].y); // A (old base) var newCell = { x: preview.gridX, y: preview.gridY }; // B (new base) // Calculate direction vectors var dx = newCell.x - prevCell.x; var dy = newCell.y - prevCell.y; var newDir = getDirection(dx, dy); // prevCell's exit direction var oldDir = prevCell.dirIn || 0; // prevCell's entry direction // Check if this is a turn (different axes: horizontal vs vertical) var isTurn = oldDir % 2 !== newDir % 2; if (isTurn && prevCell.sprite) { // Update cell data for corner prevCell.kind = 'corner'; prevCell.dirOut = newDir; // Break PIXI texture cache prevCell.sprite.texture = LK.getAsset('cell', {}).texture; // Apply corner texture prevCell.sprite.texture = LK.getAsset('roadCorner', {}).texture; // Apply correct rotation based on turn direction var rotationMap = {}; rotationMap['2-0'] = 0; // ↓ then ↑ (┘) rotationMap['3-1'] = Math.PI / 2; // ← then → (└) rotationMap['0-2'] = Math.PI; // ↑ then ↓ (┌) rotationMap['1-3'] = -Math.PI / 2; // → then ← (┐) var rotationKey = oldDir + '-' + newDir; var rotation = rotationMap[rotationKey] || 0; prevCell.sprite.rotation = rotation; // Cache the rotation for future renders prevCell.cachedRotation = rotation; } } // Extending the main path - old behavior var oldBase = grid.goals[0]; oldBase.type = TYPE_ROAD; // convert to normal road cell.type = TYPE_GOAL; // temporarily make this the new base cell.hasTower = false; // ensure it's not marked as having a tower grid.goals[0] = cell; // update goals array // Update pathPoints array for debug arrows pathPoints.push({ x: cell.x, y: cell.y }); // Update health bar position (it stays in uiLayer) updateHealthBarPos(); // Now convert the new base to the desired road type oldBase.type = roadType; // Store sprite reference and block info for the newly placed block (center cell) var blockInfo = classifyBlock(cell.x, cell.y); if (cell.debugCell && cell.debugCell.children[0]) { // Only store sprite reference if this is the center cell (for 2x2 blocks) var HALF = 1; // Block center offset if (cell.x % 2 === 0 && cell.y % 2 === 1) { // Center cell for our grid layout cell.sprite = cell.debugCell.children[0]; cell.kind = blockInfo.kind; cell.dirIn = blockInfo.dirIn; cell.dirOut = blockInfo.dirOut; } } } else { // Decorative road next to spawn - don't extend path cell.type = roadType; cell.hasTower = false; // ensure it's not marked as having a tower // Store sprite reference and block info for decorative road (center cell) var blockInfo = classifyBlock(cell.x, cell.y); if (cell.debugCell && cell.debugCell.children[0]) { // Only store sprite reference if this is the center cell var HALF = 1; // Block center offset if (cell.x % 2 === 0 && cell.y % 2 === 1) { // Center cell for our grid layout cell.sprite = cell.debugCell.children[0]; cell.kind = blockInfo.kind; cell.dirIn = blockInfo.dirIn; cell.dirOut = blockInfo.dirOut; } } } // Recalculate path and redraw grid.pathFind(); // Fix spawn targets to only go downward if (typeof window.fixSpawnTargets === 'function') window.fixSpawnTargets(); grid.renderDebug(); // Show gold deduction var goldIndicator = new GoldIndicator(-cost, preview.x, preview.y); game.addChild(goldIndicator); } } else if (gold < cost) { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { // Check specific reasons for failure var base = pathPoints[pathPoints.length - 1]; var isAdjacentToBase = false; var baseNeighbors = [{ x: base.x, y: base.y - 1 }, // up { x: base.x + 1, y: base.y }, // right { x: base.x, y: base.y + 1 }, // down { x: base.x - 1, y: base.y } // left ]; for (var i = 0; i < baseNeighbors.length; i++) { if (baseNeighbors[i].x === preview.gridX && baseNeighbors[i].y === preview.gridY) { isAdjacentToBase = true; break; } } if (!isAdjacentToBase) { var notification = game.addChild(new Notification("Road must connect to Base!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Road can't go backwards!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } roadPreview.visible = false; slowRoadPreview.visible = false; poisonRoadPreview.visible = false; } else { // Handle 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; towerPreview.stopPulsing(); } dragType = null; 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 - BOTTOM_BAR_H - 80 - 320 + 30 + 36; game.addChild(waveIndicator); var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200 - 500 - 300 - 10 - 7; // Move 817 pixels left total (500 + 300 + 10 + 7) nextWaveButton.y = 2732 - BOTTOM_BAR_H - 100 + 20 - 320 + 30 - 400 - 200 + 100 - 4 + 30 + 40; // Move 434 pixels up total (400 + 200 - 100 + 4 - 30 - 40) nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); var buildItems = ['basic', 'rapid', 'splash']; var sourceTowers = []; var towerSpacing = 300; // Increase spacing for larger towers // Account for road buttons in spacing calculation var totalItems = buildItems.length + 3; // +3 for road types var startX = 2048 / 2 - totalItems * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - BOTTOM_BAR_H - CELL_SIZE * 3 - 90 - 150 - 50 - 15 + 10; // Create road source buttons var roadSources = []; // Normal road var roadSource = new Container(); roadSource.interactive = true; roadSource.buttonMode = true; roadSource.data = { type: "road" }; var roadBg = roadSource.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); roadBg.width = CELL_SIZE * 1.3 * 1.2; roadBg.height = CELL_SIZE * 1.3 * 1.2; roadBg.tint = 0xF4A460; // Sand yellow color for road // Add shadow for road type label var roadTypeLabelShadow = new Text2('Basic\nRoad', { size: 50, fill: 0x000000, weight: 800 }); roadTypeLabelShadow.anchor.set(0.5, 0.5); roadTypeLabelShadow.x = 4; roadTypeLabelShadow.y = 4; roadSource.addChild(roadTypeLabelShadow); // Add road type label var roadTypeLabel = new Text2('Basic\nRoad', { size: 50, fill: 0xFFFFFF, weight: 800 }); roadTypeLabel.anchor.set(0.5, 0.5); roadTypeLabel.y = 0; roadSource.addChild(roadTypeLabel); // Add coin image var roadCoinImage = roadSource.attachAsset('goldimage', { anchorX: 0.5, anchorY: 0.5 }); roadCoinImage.width = 40; roadCoinImage.height = 40; roadCoinImage.x = -25; roadCoinImage.y = 105 + 15; // Add cost label var roadCostLabel = new Text2(' ' + ROAD_COST.toString(), { size: 50, fill: 0xFFD700, weight: 800 }); roadCostLabel.anchor.set(0.5, 0.5); roadCostLabel.x = 15; roadCostLabel.y = 105 + 15; roadSource.addChild(roadCostLabel); roadSource.x = startX; roadSource.y = towerY + 50; roadSource.update = function () { var canAfford = gold >= ROAD_COST; roadSource.alpha = canAfford ? 1 : 0.5; }; towerLayer.addChild(roadSource); roadSources.push(roadSource); // Slow road var slowRoadSource = new Container(); slowRoadSource.interactive = true; slowRoadSource.buttonMode = true; slowRoadSource.data = { type: "slowRoad" }; var slowRoadBg = slowRoadSource.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); slowRoadBg.width = CELL_SIZE * 1.3 * 1.2; slowRoadBg.height = CELL_SIZE * 1.3 * 1.2; slowRoadBg.tint = 0xF4A460; // Sand yellow for slow road // Add shadow for slow road type label var slowRoadTypeLabelShadow = new Text2('Slow\nRoad', { size: 50, fill: 0x000000, weight: 800 }); slowRoadTypeLabelShadow.anchor.set(0.5, 0.5); slowRoadTypeLabelShadow.x = 4; slowRoadTypeLabelShadow.y = 4; slowRoadSource.addChild(slowRoadTypeLabelShadow); // Add road type label var slowRoadTypeLabel = new Text2('Slow\nRoad', { size: 50, fill: 0xFFFFFF, weight: 800 }); slowRoadTypeLabel.anchor.set(0.5, 0.5); slowRoadTypeLabel.y = 0; slowRoadSource.addChild(slowRoadTypeLabel); // Add coin image var slowRoadCoinImage = slowRoadSource.attachAsset('goldimage', { anchorX: 0.5, anchorY: 0.5 }); slowRoadCoinImage.width = 40; slowRoadCoinImage.height = 40; slowRoadCoinImage.x = -25; slowRoadCoinImage.y = 105 + 15; // Add cost label var slowRoadCostLabel = new Text2(' ' + SLOW_ROAD_COST.toString(), { size: 50, fill: 0xFFD700, weight: 800 }); slowRoadCostLabel.anchor.set(0.5, 0.5); slowRoadCostLabel.x = 15; slowRoadCostLabel.y = 105 + 15; slowRoadSource.addChild(slowRoadCostLabel); slowRoadSource.x = startX + towerSpacing; slowRoadSource.y = towerY + 50; slowRoadSource.update = function () { var canAfford = gold >= SLOW_ROAD_COST; slowRoadSource.alpha = canAfford ? 1 : 0.5; }; towerLayer.addChild(slowRoadSource); roadSources.push(slowRoadSource); // Poison road var poisonRoadSource = new Container(); poisonRoadSource.interactive = true; poisonRoadSource.buttonMode = true; poisonRoadSource.data = { type: "poisonRoad" }; var poisonRoadBg = poisonRoadSource.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); poisonRoadBg.width = CELL_SIZE * 1.3 * 1.2; poisonRoadBg.height = CELL_SIZE * 1.3 * 1.2; poisonRoadBg.tint = 0xF4A460; // Sand yellow for poison road // Add shadow for poison road type label var poisonRoadTypeLabelShadow = new Text2('Poison\n Road', { size: 50, fill: 0x000000, weight: 800 }); poisonRoadTypeLabelShadow.anchor.set(0.5, 0.5); poisonRoadTypeLabelShadow.x = 4; poisonRoadTypeLabelShadow.y = 4; poisonRoadSource.addChild(poisonRoadTypeLabelShadow); // Add road type label var poisonRoadTypeLabel = new Text2('Poison\n Road', { size: 50, fill: 0xFFFFFF, weight: 800 }); poisonRoadTypeLabel.anchor.set(0.5, 0.5); poisonRoadTypeLabel.y = 0; poisonRoadSource.addChild(poisonRoadTypeLabel); // Add coin image var poisonRoadCoinImage = poisonRoadSource.attachAsset('goldimage', { anchorX: 0.5, anchorY: 0.5 }); poisonRoadCoinImage.width = 40; poisonRoadCoinImage.height = 40; poisonRoadCoinImage.x = -25; poisonRoadCoinImage.y = 105 + 15; // Add cost label var poisonRoadCostLabel = new Text2(' ' + POISON_ROAD_COST.toString(), { size: 50, fill: 0xFFD700, weight: 800 }); poisonRoadCostLabel.anchor.set(0.5, 0.5); poisonRoadCostLabel.x = 15; poisonRoadCostLabel.y = 105 + 15; poisonRoadSource.addChild(poisonRoadCostLabel); poisonRoadSource.x = startX + towerSpacing * 2; poisonRoadSource.y = towerY + 50; poisonRoadSource.update = function () { var canAfford = gold >= POISON_ROAD_COST; poisonRoadSource.alpha = canAfford ? 1 : 0.5; }; towerLayer.addChild(poisonRoadSource); roadSources.push(poisonRoadSource); // Add "ROADS" section header var roadsHeaderText = new Text2('ROADS', { size: 60, fill: 0xFFFFFF, weight: 800 }); roadsHeaderText.anchor.set(0.5, 0.5); roadsHeaderText.x = startX + towerSpacing; // Center over the road items roadsHeaderText.y = towerY - 130 + 50 - 30; // Position above the road items towerLayer.addChild(roadsHeaderText); // Add dash between poison and default var dashText = new Text2('-', { size: 80, fill: 0xFFFFFF, weight: 800 }); dashText.anchor.set(0.5, 0.5); dashText.x = startX + towerSpacing * 2.5; dashText.y = towerY + 50; towerLayer.addChild(dashText); // Add "TOWERS" section header var towersHeaderText = new Text2('TOWERS', { size: 60, fill: 0xFFFFFF, weight: 800 }); towersHeaderText.anchor.set(0.5, 0.5); towersHeaderText.x = startX + towerSpacing * 4; // Center over the tower items towersHeaderText.y = towerY - 130 + 50 - 30; // Position above the tower items towerLayer.addChild(towersHeaderText); // Create tower sources (offset by 3 to account for road types) for (var i = 0; i < buildItems.length; i++) { var tower = new SourceTower(buildItems[i]); tower.x = startX + (i + 3) * towerSpacing; tower.y = towerY + 50; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; game.update = function () { // Calculate global scaledDt scaledDt = 1 * gameSpeed; // 0.5, 1, 3, 5, etc. // Apply game speed (skip updates if paused) if (gameSpeed === 0) { return; // Game is paused } // Scale time-based updates by game speed var speedMultiplier = gameSpeed; if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Reset wave completion tracking totalEnemiesSpawnedThisWave = 0; totalEnemiesDeadThisWave = 0; waveIsComplete = false; // Get wave type and enemy count from the wave indicator currentWaveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; // Always get the enemy count from wave indicator first enemyCount = waveIndicator.getEnemyCount(currentWave); // Only override for boss waves if it's NOT a swarm wave if (isBossWave && currentWaveType !== '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; } else if (currentWaveType === 'swarm') { // For wave 6 (mini boss), spawn only 1 enemy if (currentWave === 6) { enemyCount = 1; } else { // Other swarm waves get 30 enemies enemyCount = 30; } } // Initialize wave spawning variables totalEnemiesToSpawnThisWave = enemyCount; enemiesSpawnedThisWave = 0; lastEnemySpawnTime = LK.ticks - getEnemySpawnDelay(currentWaveType); // Allow immediate first spawn } // Handle delayed enemy spawning with dynamic delays based on enemy type if (waveSpawned && enemiesSpawnedThisWave < totalEnemiesToSpawnThisWave) { var currentEnemySpawnDelay = getEnemySpawnDelay(currentWaveType); // Scale the spawn delay by game speed to maintain consistent real-time intervals var scaledSpawnDelay = Math.floor(currentEnemySpawnDelay / gameSpeed); if (LK.ticks - lastEnemySpawnTime >= scaledSpawnDelay) { // Spawn one enemy var enemy = new Enemy(currentWaveType); // 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 // Special case for wave 6 mini boss - 50x HP if (currentWave === 6 && currentWaveType === 'swarm') { healthMultiplier = healthMultiplier * 50; // 50x more HP for mini boss } enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // Spawn at the defined spawn point (center of grid) var spawnPoint = pathPoints[0]; enemy.cellX = spawnPoint.x; enemy.cellY = spawnPoint.y; enemy.currentCellX = spawnPoint.x; enemy.currentCellY = spawnPoint.y; enemy.x = grid.x + spawnPoint.x * CELL_SIZE + CELL_SIZE / 2; enemy.y = grid.y + spawnPoint.y * CELL_SIZE + CELL_SIZE / 2; enemy.waveNumber = currentWave; // Initialize enemy pathfinding - start with spawn exit as first target enemy.currentTarget = spawnExit; enemy.cellPathIndex = 1; // Index into pathPoints array enemies.push(enemy); // Update spawn tracking enemiesSpawnedThisWave++; totalEnemiesSpawnedThisWave++; lastEnemySpawnTime = LK.ticks; } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } // Check if wave is complete: all enemies spawned AND all enemies dead if (waveSpawned && enemiesSpawnedThisWave >= totalEnemiesToSpawnThisWave && totalEnemiesDeadThisWave >= totalEnemiesSpawnedThisWave) { if (!waveIsComplete) { waveIsComplete = true; var notification = game.addChild(new Notification("Wave " + currentWave + " Complete! Next wave unlocked!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } waveInProgress = false; waveSpawned = false; } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; enemy.update(scaledDt); if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Track enemy death for wave completion if (enemy.waveNumber === currentWave) { totalEnemiesDeadThisWave++; } // Boss enemies give more gold and score var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Track enemy death for wave completion (even if they reach base) if (enemy.waveNumber === currentWave) { totalEnemiesDeadThisWave++; } // 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 - enemy.health); updateUI(); if (lives <= 0) { LK.showGameOver(); } } } // Update towers with scaledDt for (var i = 0; i < towers.length; i++) { towers[i].update(scaledDt); } // Update bullets with scaledDt for (var i = 0; i < bullets.length; i++) { bullets[i].update(scaledDt); } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (roadPreview.visible) { roadPreview.updatePlacementStatus(); } if (slowRoadPreview.visible) { slowRoadPreview.updatePlacementStatus(); } if (poisonRoadPreview.visible) { poisonRoadPreview.updatePlacementStatus(); } // Update road source buttons for (var i = 0; i < roadSources.length; i++) { roadSources[i].update(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } // Spawn poison particles on poison roads continuously // No need to check for game over - particles have their own lifetime if (LK.ticks % 30 === 0) { // Spawn particles every 0.5 seconds for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var cell = grid.cells[i][j]; if (cell && cell.type === TYPE_POISON_ROAD) { // 100% chance to spawn a particle on each poison road cell grid.spawnPotionParticle(i, j); } if (cell && cell.type === TYPE_SLOW_ROAD) { // 100% chance to spawn a particle on each slow road cell grid.spawnSlowParticle(i, j); } } } } // Update health bar position every frame updateHealthBarPos(); };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 1; // Use tower-specific damage
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function (dt) {
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);
// Use scaledDt for consistent movement
var scaledSpeed = self.speed * (dt || scaledDt);
if (distance < scaledSpeed) {
// 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, minimum 1)
var splashDamage = Math.max(1, Math.floor(self.damage * 0.5));
otherEnemy.health -= splashDamage;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
// Use scaledDt for consistent movement
var scaledSpeed = self.speed * (dt || scaledDt);
self.x += Math.cos(angle) * scaledSpeed;
self.y += Math.sin(angle) * scaledSpeed;
}
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case TYPE_ROAD:
case TYPE_SLOW_ROAD:
case TYPE_POISON_ROAD:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.visible = false; // Hide numbers for decorative roads
cellGraphics.tint = 0x880000;
return;
}
numberLabel.visible = false; // Hide numbers to make roads naked
// Replace cell graphics with appropriate road asset
if (!cellGraphics.isRoadAsset) {
self.removeChild(cellGraphics);
cellGraphics = self.attachAsset('roadNormal', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.isRoadAsset = true;
}
// Apply road texture and rotation based on connections
renderRoadCell(cellGraphics, self.cell.x, self.cell.y);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff;
} else {
// Reset to default road appearance
cellGraphics.tint = 0xFFFFFF;
}
// Remove all arrows - roads should be naked
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
break;
}
case 2:
{
// spawn - enemies can walk on it but it doesn't count as road for scoring
self.removeArrows();
// Replace cell graphics with enemyspawns asset
if (!cellGraphics.isSpawnAsset) {
self.removeChild(cellGraphics);
cellGraphics = self.attachAsset('enemyspawns', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.isSpawnAsset = true;
}
cellGraphics.tint = 0xFFFFFF; // Reset tint for spawn asset
numberLabel.visible = false;
// Don't show score numbers for spawn point even though enemies use it for pathfinding
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
// Replace cell graphics with playerbase asset
if (!cellGraphics.isBaseAsset) {
self.removeChild(cellGraphics);
cellGraphics = self.attachAsset('playerbase', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.isBaseAsset = true;
}
cellGraphics.tint = 0xFFFFFF; // Reset tint for base asset
numberLabel.visible = false;
break;
}
case 4:
{
// tower body
self.removeArrows();
cellGraphics.tint = 0x555555; // dark gray
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 10000));
};
});
// 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 * 1.05; // 5% faster
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
// Initialize with base HP from table
self.maxHealth = HP_TABLE[self.type] || HP_TABLE.normal;
self.health = self.maxHealth;
self.poisonTimer = 0; // For poison road damage tracking
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = HP_TABLE.fast;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = HP_TABLE.immune;
self.speed *= 0.9; // 10% slower
break;
case 'flying':
self.isFlying = true;
self.maxHealth = Math.floor(HP_TABLE.flying * 1.1); // 10% more HP
self.speed *= 1.1; // 10% faster
break;
case 'swarm':
self.maxHealth = HP_TABLE.swarm;
self.speed *= 1.4; // 40% faster movement
break;
case 'normal':
default:
self.maxHealth = HP_TABLE.normal;
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies use HP scaling: first boss HP * boss number
var bossNumber = currentWave / 10; // 1st boss = wave 10 (bossNumber = 1), 2nd boss = wave 20 (bossNumber = 2), etc.
var firstBossHP = 200; // Base HP for first boss
self.maxHealth = firstBossHP * bossNumber;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Add scaling animation to all enemies
// Start with random scale between 0.8 and 1.2
var initialScale = 0.8 + Math.random() * 0.4; // Random between 0.8 and 1.2
enemyGraphics.scaleX = initialScale;
enemyGraphics.scaleY = initialScale;
// Create continuous scaling animation
var scaleMin = 0.8;
var scaleMax = 1.2;
var _animateScale = function animateScale() {
var targetScale = scaleMin + Math.random() * (scaleMax - scaleMin);
var duration = 500 + Math.random() * 250; // 0.5-0.75 seconds
tween(enemyGraphics, {
scaleX: targetScale,
scaleY: targetScale
}, {
duration: duration,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (enemyGraphics && enemyGraphics.parent) {
_animateScale(); // Continue animation if enemy still exists
}
}
});
};
// Start the scaling animation
_animateScale();
// Scale up boss enemies (applied on top of the animation)
if (self.isBoss) {
// For bosses, scale the animation range up
scaleMin = 1.4; // 0.8 * 1.8 = 1.44, rounded to 1.4
scaleMax = 2.2; // 1.2 * 1.8 = 2.16, rounded to 2.2
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Fall back to regular enemy asset if specific type asset not found
// Apply tint to differentiate enemy types
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
break;
case 'immune':
enemyGraphics.tint = 0xAA0000; // Red for immune enemies
break;
case 'flying':
enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
break;
case 'swarm':
enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
break;
}*/
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// Add scaling animation to shadow that matches the main enemy
var shadowScaleMin = 0.8;
var shadowScaleMax = 1.2;
var _animateShadowScale = function animateShadowScale() {
var targetScale = shadowScaleMin + Math.random() * (shadowScaleMax - shadowScaleMin);
var duration = 500 + Math.random() * 250; // 0.5-0.75 seconds
tween(shadowGraphics, {
scaleX: targetScale,
scaleY: targetScale
}, {
duration: duration,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (shadowGraphics && shadowGraphics.parent) {
_animateShadowScale(); // Continue animation if shadow still exists
}
}
});
};
// Start the shadow scaling animation
_animateShadowScale();
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
// For boss shadows, scale the animation range up
shadowScaleMin = 1.4;
shadowScaleMax = 2.2;
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 (dt) {
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 -= dt || scaledDt;
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;
}
// Initialize poison timer if not exists
if (!self.poisonTimer) self.poisonTimer = 0;
self.poisonTimer += dt || scaledDt;
// Apply poison damage every 30 units (twice per second at 1x speed)
if (self.poisonTimer >= 30) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
self.poisonTimer = 0;
}
self.poisonDuration -= dt || scaledDt;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
var GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2("+" + value, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
// Calculate actual grid height accounting for bars
var GRID_H_PX = 2732 - TOP_BAR_H - BOTTOM_BAR_H;
var actualRows = Math.floor(GRID_H_PX / CELL_SIZE);
gridHeight = Math.min(gridHeight, actualRows);
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];
// Default all cells to walls
var cellType = 1;
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;
}
}
}
// Set spawn at center (10, 12)
var centerCell = self.cells[10] && self.cells[10][12];
if (centerCell) {
centerCell.type = 2;
self.spawns = [centerCell];
}
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 != TYPE_WALL && !node.hasTower) {
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];
// Only process cardinal directions (up, down, left, right)
var 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;
} else {
// Limit spawn to only target the spawn exit (down direction)
self.spawns[a].targets = [spawnExit];
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// 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]);
}
}
}
};
// Re-render all road cells with updated corner/straight textures
self.forEachRoadCell = function (renderFunc) {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
if (cell && (cell.type === TYPE_ROAD || cell.type === TYPE_SLOW_ROAD || cell.type === TYPE_POISON_ROAD)) {
var debugCell = cell.debugCell;
if (debugCell && debugCell.children[0]) {
renderFunc(debugCell.children[0], i, j);
}
}
}
}
};
self.poisonRoadParticles = {}; // Track particles per poison road cell
self.slowRoadParticles = {}; // Track particles per slow road cell
self.spawnPotionParticle = function (cellX, cellY) {
var key = cellX + ',' + cellY;
if (!self.poisonRoadParticles[key]) {
self.poisonRoadParticles[key] = [];
}
// Clean up destroyed particles FIRST (before checking limit)
// Check both parent !== null AND !destroyed flag
self.poisonRoadParticles[key] = self.poisonRoadParticles[key].filter(function (p) {
return p.parent !== null && !p.destroyed;
});
// Check if we can spawn more particles (max 60 per cell - increased from 30)
if (self.poisonRoadParticles[key].length >= 60) {
return;
}
// Random position within the cell - use self.x instead of grid.x since we're inside the Grid class
var particleX = self.x + cellX * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8;
var particleY = self.y + cellY * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8;
var particle = new PotionParticle(particleX, particleY);
game.addChild(particle);
self.poisonRoadParticles[key].push(particle);
};
self.spawnSlowParticle = function (cellX, cellY) {
var key = cellX + ',' + cellY;
if (!self.slowRoadParticles[key]) {
self.slowRoadParticles[key] = [];
}
// Clean up destroyed particles FIRST (before checking limit)
// Check both parent !== null AND !destroyed flag
self.slowRoadParticles[key] = self.slowRoadParticles[key].filter(function (p) {
return p.parent !== null && !p.destroyed;
});
// Check if we can spawn more particles (max 60 per cell)
if (self.slowRoadParticles[key].length >= 60) {
return;
}
// Random position within the cell - use self.x instead of grid.x since we're inside the Grid class
var particleX = self.x + cellX * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8;
var particleY = self.y + cellY * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8;
var particle = new SlowParticle(particleX, particleY);
game.addChild(particle);
self.slowRoadParticles[key].push(particle);
};
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;
}
}
// Handle flying enemies differently - they follow the road but don't get poison effects
if (enemy.isFlying) {
// Use the same pathfinding system as ground enemies but ignore poison effects
// Handle normal pathfinding enemies using pathPoints array
if (!enemy.currentTarget) {
if (enemy.cellPathIndex === undefined) {
enemy.cellPathIndex = 1; // Start with spawn exit
}
if (enemy.cellPathIndex < pathPoints.length) {
enemy.currentTarget = pathPoints[enemy.cellPathIndex];
}
}
if (enemy.currentTarget) {
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
// Calculate local speed for this enemy
var speed = enemy.speed;
// Flying enemies are NOT affected by slow roads, so don't check for slow road
// Scale by game speed (for fast forward/pause)
var scaledSpeed = speed * gameSpeed;
if (dist < scaledSpeed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
// Move to next point in path
enemy.cellPathIndex++;
if (enemy.cellPathIndex < pathPoints.length) {
enemy.currentTarget = pathPoints[enemy.cellPathIndex];
} else {
enemy.currentTarget = undefined;
// Reached the goal
return true;
}
} else {
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
});
}
}
enemy.currentCellX += Math.cos(angle) * scaledSpeed;
enemy.currentCellY += Math.sin(angle) * scaledSpeed;
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Flying enemies are NOT affected by poison roads, so skip poison damage section
return false;
}
// Apply road effects for ground enemies
if (!enemy.isFlying) {
var currentCell = grid.getCell(Math.floor(enemy.currentCellX), Math.floor(enemy.currentCellY));
if (currentCell) {
// Apply poison damage
if (currentCell.type === TYPE_POISON_ROAD) {
enemy.poisonTimer += gameSpeed;
if (enemy.poisonTimer >= 60) {
// Once per second at 60 FPS
enemy.health -= 0.5;
enemy.poisonTimer = 0;
if (enemy.health <= 0) {
enemy.health = 0;
}
// Update health bar
if (enemy.healthBar) {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
}
} else {
enemy.poisonTimer = 0;
}
}
}
// Handle normal pathfinding enemies using pathPoints array
if (!enemy.currentTarget) {
if (enemy.cellPathIndex === undefined) {
enemy.cellPathIndex = 1; // Start with spawn exit
}
if (enemy.cellPathIndex < pathPoints.length) {
enemy.currentTarget = pathPoints[enemy.cellPathIndex];
}
}
if (enemy.currentTarget) {
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
// Calculate local speed for this enemy
var speed = enemy.speed;
// Check if enemy is on slow road
var currentCell = grid.getCell(Math.floor(enemy.currentCellX), Math.floor(enemy.currentCellY));
if (currentCell && currentCell.type === TYPE_SLOW_ROAD) {
speed *= SLOW_MUL; // Apply slow multiplier
}
// Scale by game speed (for fast forward/pause)
var scaledSpeed = speed * gameSpeed;
if (dist < scaledSpeed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
// Move to next point in path
enemy.cellPathIndex++;
if (enemy.cellPathIndex < pathPoints.length) {
enemy.currentTarget = pathPoints[enemy.cellPathIndex];
} else {
enemy.currentTarget = undefined;
}
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * scaledSpeed;
enemy.currentCellY += Math.sin(angle) * scaledSpeed;
}
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);
// Create outline first (behind the main button)
var buttonOutline = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonOutline.width = 450 + 8; // 50% bigger + outline thickness
buttonOutline.height = 150 + 8; // 50% bigger + outline thickness
buttonOutline.tint = 0x000000; // Black outline
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 450; // 50% bigger (300 * 1.5)
buttonBackground.height = 150; // 50% bigger (100 * 1.5)
buttonBackground.tint = 0xFFF8DC; // Cream white color
var buttonText = new Text2("Next Wave", {
size: 75,
//{8q} // 50% bigger text (50 * 1.5)
fill: 0x000000,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.pulseStartTime = 0;
self.isPulsing = false;
self.update = function () {
var shouldBeEnabled = waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves && waveIsComplete;
var shouldBeVisible = waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves && waveIsComplete;
// Handle visibility changes with tween effects
if (shouldBeVisible && !self.visible) {
// Show button with zoom in effect
self.visible = true;
self.scaleX = 0.1;
self.scaleY = 0.1;
self.alpha = 0;
self.pulseStartTime = LK.ticks;
self.isPulsing = true;
tween(self, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 300,
easing: tween.elasticOut
});
} else if (!shouldBeVisible && self.visible) {
// Hide button with zoom out effect
self.isPulsing = false;
tween(self, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.visible = false;
}
});
}
// Add continuous zoom in/out animation every 3 seconds when visible
if (self.visible && self.isPulsing) {
var timeSincePulse = LK.ticks - self.pulseStartTime;
// 3 seconds = 180 ticks at 60 FPS
if (timeSincePulse % 180 === 0 && timeSincePulse > 0) {
// Zoom out then zoom in
tween(self, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 400,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 400,
easing: tween.easeInOut
});
}
});
}
}
if (shouldBeEnabled) {
self.enabled = true;
buttonBackground.tint = 0xFFF8DC; // Cream white color
} else {
self.enabled = false;
buttonBackground.tint = 0x888888;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves && waveIsComplete) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Reset wave completion tracking for new wave
waveIsComplete = false;
totalEnemiesSpawnedThisWave = 0;
totalEnemiesDeadThisWave = 0;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 0x000000,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var PoisonRoadPreview = Container.expand(function () {
var self = Container.call(this);
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
// Create purple semi-transparent square
var previewGraphics = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.tint = 0x8B4513; // Brown for poison road (same as normal road)
previewGraphics.alpha = 0.5;
previewGraphics.width = CELL_SIZE;
previewGraphics.height = CELL_SIZE;
self.addChild(previewGraphics);
// Add pulsing effect
self.pulsingTween = null;
self.startPulsing = function () {
if (self.pulsingTween) return;
self.pulsingTween = tween(previewGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
};
self.stopPulsing = function () {
if (self.pulsingTween) {
tween.stop(previewGraphics, {
alpha: true
});
self.pulsingTween = null;
previewGraphics.alpha = 0.5;
}
};
// Start pulsing immediately
self.startPulsing();
self.updatePlacementStatus = function () {
self.canPlace = false;
// Check if placement would be in bottom bar
if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) {
self.canPlace = false;
return;
}
var canAfford = gold >= POISON_ROAD_COST;
// Check if this cell is valid for road placement
var cell = grid.getCell(self.gridX, self.gridY);
// Road can only be placed on wall cells (type 1) without towers
if (!cell || cell.type !== TYPE_WALL || cell.hasTower) {
self.canPlace = false;
} else if (!canAfford) {
self.canPlace = false;
} else {
// New yol bloğunun merkez koordinatı
var cx = self.gridX + 0.5;
var cy = self.gridY + 0.5;
var spawn = pathPoints[0]; // First point is spawn
var spawnCenter = {
x: spawn.x + 0.5,
y: spawn.y + 0.5
};
var base = pathPoints[pathPoints.length - 1];
var baseCenter = {
x: base.x + 0.5,
y: base.y + 0.5
};
// Spawn edge restrictions removed - roads can be placed adjacent to any spawn edge
// Check base edge restrictions
var isBaseEdge = sharesEdge({
x: cx,
y: cy
}, baseCenter);
if (isBaseEdge) {
var last = pathPoints[pathPoints.length - 1]; // base centre
var prev = pathPoints[pathPoints.length - 2]; // önceki yol
var backDir = getDirectionIndex(last.x, last.y, prev.x, prev.y);
var dirHere = getDirectionIndex(baseCenter.x, baseCenter.y, cx, cy);
if (dirHere === backDir) {
// SADECE geri yasak
self.canPlace = false;
return;
}
// yan veya ileri 90° → serbest
}
// Check if adjacent to any existing road (forbidden - only base allowed)
var dirsRoad = [[0, -1], [1, 0], [0, 1], [-1, 0]];
var adjacentToRoad = false;
for (var d = 0; d < 4; d++) {
var n = grid.getCell(self.gridX + dirsRoad[d][0], self.gridY + dirsRoad[d][1]);
if (n && (n.type === TYPE_ROAD || n.type === TYPE_SLOW_ROAD || n.type === TYPE_POISON_ROAD)) {
adjacentToRoad = true;
break;
}
}
// Roads can only be placed adjacent to base
if (adjacentToRoad) {
self.canPlace = false;
} else {
// Check if cell is adjacent to the base (end of current path)
var base = pathPoints[pathPoints.length - 1];
var isAdjacentToBase = false;
var baseNeighbors = [{
x: base.x,
y: base.y - 1
}, {
x: base.x + 1,
y: base.y
}, {
x: base.x,
y: base.y + 1
}, {
x: base.x - 1,
y: base.y
}];
for (var i = 0; i < baseNeighbors.length; i++) {
if (baseNeighbors[i].x === self.gridX && baseNeighbors[i].y === self.gridY) {
isAdjacentToBase = true;
break;
}
}
// Check if this would be going backwards
var isBackwards = false;
if (isAdjacentToBase && pathPoints.length >= 2) {
var previousPoint = pathPoints[pathPoints.length - 2];
var currentDirection = {
x: base.x - previousPoint.x,
y: base.y - previousPoint.y
};
var proposedDirection = {
x: self.gridX - base.x,
y: self.gridY - base.y
};
if (currentDirection.x === -proposedDirection.x && currentDirection.y === -proposedDirection.y) {
isBackwards = true;
}
}
self.canPlace = isAdjacentToBase && !isBackwards;
}
}
// Update appearance
if (self.canPlace) {
previewGraphics.tint = 0x00FF00; // Green for placeable
self.visible = true;
} else {
previewGraphics.tint = 0xFF0000; // Red for not placeable
self.visible = true;
}
};
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 - 72 + 10 + 4 + 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2;
self.updatePlacementStatus();
};
return self;
});
var PotionParticle = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
var particleGraphics = self.attachAsset('potionparticle', {
anchorX: 0.5,
anchorY: 0.5
});
// Start with random alpha
self.alpha = 0.3 + Math.random() * 0.5;
// Random horizontal offset for zigzag
self.zigzagAmplitude = 5 + Math.random() * 5;
self.zigzagFrequency = 2 + Math.random() * 2;
self.startX = x;
self.lifetime = 0;
// Animate upward movement with zigzag
tween(self, {
y: y - 80,
alpha: 0
}, {
duration: 2000,
// Reduced from 3000ms to 2000ms
easing: tween.linear,
onFinish: function onFinish() {
// Remove from parent before destroying
if (self.parent) {
self.parent.removeChild(self);
}
self.destroy();
}
});
self.update = function () {
self.lifetime += 1;
// Create zigzag pattern
var zigzagOffset = Math.sin(self.lifetime * 0.1 * self.zigzagFrequency) * self.zigzagAmplitude;
self.x = self.startX + zigzagOffset;
};
return self;
});
var RoadPreview = Container.expand(function () {
var self = Container.call(this);
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
// Create simple brown semi-transparent square
var previewGraphics = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.tint = 0x8B4513; // Brown for road
previewGraphics.alpha = 0.5;
previewGraphics.width = CELL_SIZE;
previewGraphics.height = CELL_SIZE;
self.addChild(previewGraphics);
// Add pulsing effect - start with null reference
self.pulsingTween = null;
self.startPulsing = function () {
if (self.pulsingTween) return;
self.pulsingTween = tween(previewGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
};
self.stopPulsing = function () {
if (self.pulsingTween) {
tween.stop(previewGraphics, {
alpha: true
});
self.pulsingTween = null;
previewGraphics.alpha = 0.5;
}
};
// Start pulsing immediately
self.startPulsing();
self.updatePlacementStatus = function () {
self.canPlace = false;
// Check if placement would be in bottom bar
if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) {
self.canPlace = false;
return;
}
var canAfford = gold >= ROAD_COST;
// Check if this cell is valid for road placement
var cell = grid.getCell(self.gridX, self.gridY);
// Road can only be placed on wall cells (type 1) without towers
if (!cell || cell.type !== 1 || cell.hasTower) {
// Can't place on non-wall cells or cells with towers
self.canPlace = false;
} else if (!canAfford) {
// Not enough gold
self.canPlace = false;
} else {
// New road bloğunun merkez koordinatı
var cx = self.gridX + 0.5;
var cy = self.gridY + 0.5;
var spawn = pathPoints[0]; // First point is spawn
var spawnCenter = {
x: spawn.x + 0.5,
y: spawn.y + 0.5
};
var base = pathPoints[pathPoints.length - 1];
var baseCenter = {
x: base.x + 0.5,
y: base.y + 0.5
};
// Spawn edge restrictions removed - roads can be placed adjacent to any spawn edge
// Check base edge restrictions
var isBaseEdge = sharesEdge({
x: cx,
y: cy
}, baseCenter);
if (isBaseEdge) {
var last = pathPoints[pathPoints.length - 1]; // base centre
var prev = pathPoints[pathPoints.length - 2]; // önceki yol
var backDir = getDirectionIndex(last.x, last.y, prev.x, prev.y);
var dirHere = getDirectionIndex(baseCenter.x, baseCenter.y, cx, cy);
if (dirHere === backDir) {
// SADECE geri yasak
self.canPlace = false;
return;
}
// yan veya ileri 90° → serbest
}
// Check if adjacent to any existing road (forbidden - only base allowed)
var dirsRoad = [[0, -1], [1, 0], [0, 1], [-1, 0]];
var adjacentToRoad = false;
for (var d = 0; d < 4; d++) {
var n = grid.getCell(self.gridX + dirsRoad[d][0], self.gridY + dirsRoad[d][1]);
if (n && (n.type === TYPE_ROAD || n.type === TYPE_SLOW_ROAD || n.type === TYPE_POISON_ROAD)) {
// bitişik ROAD tespit
adjacentToRoad = true;
break;
}
}
// Roads can only be placed adjacent to base
if (adjacentToRoad) {
self.canPlace = false;
} else {
// Check if cell is adjacent to the base (end of current path)
var base = pathPoints[pathPoints.length - 1];
var isAdjacentToBase = false;
// Check 4-directional neighbors from base
var baseNeighbors = [{
x: base.x,
y: base.y - 1
},
// up
{
x: base.x + 1,
y: base.y
},
// right
{
x: base.x,
y: base.y + 1
},
// down
{
x: base.x - 1,
y: base.y
} // left
];
for (var i = 0; i < baseNeighbors.length; i++) {
if (baseNeighbors[i].x === self.gridX && baseNeighbors[i].y === self.gridY) {
isAdjacentToBase = true;
break;
}
}
// Check if this would be going backwards
var isBackwards = false;
if (isAdjacentToBase && pathPoints.length >= 2) {
var previousPoint = pathPoints[pathPoints.length - 2];
var currentDirection = {
x: base.x - previousPoint.x,
y: base.y - previousPoint.y
};
var proposedDirection = {
x: self.gridX - base.x,
y: self.gridY - base.y
};
// Check if proposed direction is opposite to current direction
if (currentDirection.x === -proposedDirection.x && currentDirection.y === -proposedDirection.y) {
isBackwards = true;
}
}
self.canPlace = isAdjacentToBase && !isBackwards;
}
}
// Update appearance - green for placeable, red for not placeable
if (self.canPlace) {
previewGraphics.tint = 0x00FF00; // Green for placeable
self.visible = true;
} else {
previewGraphics.tint = 0xFF0000; // Red for not placeable
self.visible = true;
}
};
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 - 72 + 10 + 4 + 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2;
self.updatePlacementStatus();
};
return self;
});
var SlowParticle = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
var particleGraphics = self.attachAsset('slowparticle', {
anchorX: 0.5,
anchorY: 0.5
});
// Start with random alpha
self.alpha = 0.3 + Math.random() * 0.5;
// Random horizontal offset for zigzag
self.zigzagAmplitude = 5 + Math.random() * 5;
self.zigzagFrequency = 2 + Math.random() * 2;
self.startX = x;
self.lifetime = 0;
// Animate upward movement with zigzag
tween(self, {
y: y - 80,
alpha: 0
}, {
duration: 2000,
// Same duration as poison particles
easing: tween.linear,
onFinish: function onFinish() {
// Remove from parent before destroying
if (self.parent) {
self.parent.removeChild(self);
}
self.destroy();
}
});
self.update = function () {
self.lifetime += 1;
// Create zigzag pattern
var zigzagOffset = Math.sin(self.lifetime * 0.1 * self.zigzagFrequency) * self.zigzagAmplitude;
self.x = self.startX + zigzagOffset;
};
return self;
});
var SlowRoadPreview = Container.expand(function () {
var self = Container.call(this);
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
// Create ice-blue semi-transparent square
var previewGraphics = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.tint = 0x8B4513; // Brown for slow road (same as normal road)
previewGraphics.alpha = 0.5;
previewGraphics.width = CELL_SIZE;
previewGraphics.height = CELL_SIZE;
self.addChild(previewGraphics);
// Add pulsing effect
self.pulsingTween = null;
self.startPulsing = function () {
if (self.pulsingTween) return;
self.pulsingTween = tween(previewGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
};
self.stopPulsing = function () {
if (self.pulsingTween) {
tween.stop(previewGraphics, {
alpha: true
});
self.pulsingTween = null;
previewGraphics.alpha = 0.5;
}
};
// Start pulsing immediately
self.startPulsing();
self.updatePlacementStatus = function () {
self.canPlace = false;
// Check if placement would be in bottom bar
if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) {
self.canPlace = false;
return;
}
var canAfford = gold >= SLOW_ROAD_COST;
// Check if this cell is valid for road placement
var cell = grid.getCell(self.gridX, self.gridY);
// Road can only be placed on wall cells (type 1) without towers
if (!cell || cell.type !== TYPE_WALL || cell.hasTower) {
self.canPlace = false;
} else if (!canAfford) {
self.canPlace = false;
} else {
// New yol bloğunun merkez koordinatı
var cx = self.gridX + 0.5;
var cy = self.gridY + 0.5;
var spawn = pathPoints[0]; // First point is spawn
var spawnCenter = {
x: spawn.x + 0.5,
y: spawn.y + 0.5
};
var base = pathPoints[pathPoints.length - 1];
var baseCenter = {
x: base.x + 0.5,
y: base.y + 0.5
};
// Spawn edge restrictions removed - roads can be placed adjacent to any spawn edge
// Check base edge restrictions
var isBaseEdge = sharesEdge({
x: cx,
y: cy
}, baseCenter);
if (isBaseEdge) {
var last = pathPoints[pathPoints.length - 1]; // base centre
var prev = pathPoints[pathPoints.length - 2]; // önceki yol
var backDir = getDirectionIndex(last.x, last.y, prev.x, prev.y);
var dirHere = getDirectionIndex(baseCenter.x, baseCenter.y, cx, cy);
if (dirHere === backDir) {
// SADECE geri yasak
self.canPlace = false;
return;
}
// yan veya ileri 90° → serbest
}
// Check if adjacent to any existing road (forbidden - only base allowed)
var dirsRoad = [[0, -1], [1, 0], [0, 1], [-1, 0]];
var adjacentToRoad = false;
for (var d = 0; d < 4; d++) {
var n = grid.getCell(self.gridX + dirsRoad[d][0], self.gridY + dirsRoad[d][1]);
if (n && (n.type === TYPE_ROAD || n.type === TYPE_SLOW_ROAD || n.type === TYPE_POISON_ROAD)) {
adjacentToRoad = true;
break;
}
}
// Roads can only be placed adjacent to base
if (adjacentToRoad) {
self.canPlace = false;
} else {
// Check if cell is adjacent to the base (end of current path)
var base = pathPoints[pathPoints.length - 1];
var isAdjacentToBase = false;
var baseNeighbors = [{
x: base.x,
y: base.y - 1
}, {
x: base.x + 1,
y: base.y
}, {
x: base.x,
y: base.y + 1
}, {
x: base.x - 1,
y: base.y
}];
for (var i = 0; i < baseNeighbors.length; i++) {
if (baseNeighbors[i].x === self.gridX && baseNeighbors[i].y === self.gridY) {
isAdjacentToBase = true;
break;
}
}
// Check if this would be going backwards
var isBackwards = false;
if (isAdjacentToBase && pathPoints.length >= 2) {
var previousPoint = pathPoints[pathPoints.length - 2];
var currentDirection = {
x: base.x - previousPoint.x,
y: base.y - previousPoint.y
};
var proposedDirection = {
x: self.gridX - base.x,
y: self.gridY - base.y
};
if (currentDirection.x === -proposedDirection.x && currentDirection.y === -proposedDirection.y) {
isBackwards = true;
}
}
self.canPlace = isAdjacentToBase && !isBackwards;
}
}
// Update appearance
if (self.canPlace) {
previewGraphics.tint = 0x00FF00; // Green for placeable
self.visible = true;
} else {
previewGraphics.tint = 0xFF0000; // Red for not placeable
self.visible = true;
}
};
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 - 72 + 10 + 4 + 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2;
self.updatePlacementStatus();
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'basic';
// Increase size of base for easier touch
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
baseGraphics.width = CELL_SIZE * 1.3 * 1.2;
baseGraphics.height = CELL_SIZE * 1.3 * 1.2;
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
// Get display name for tower type
var displayName;
switch (self.towerType) {
case 'basic':
displayName = ' Standard\n Cannon';
break;
case 'rapid':
displayName = ' Quick\nCannon';
break;
case 'splash':
displayName = ' Splash\nCannon';
break;
default:
displayName = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
}
// Add shadow for tower type label
var typeLabelShadow = new Text2(displayName, {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = 0 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var typeLabel = new Text2(displayName, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = 0; // Position at center of tower
self.addChild(typeLabel);
// Add coin image shadow
var coinImageShadow = self.attachAsset('goldimage', {
anchorX: 0.5,
anchorY: 0.5
});
coinImageShadow.width = 40;
coinImageShadow.height = 40;
coinImageShadow.x = -25 + 4;
coinImageShadow.y = 99 + 12 + 15;
coinImageShadow.tint = 0x000000;
coinImageShadow.alpha = 0.5;
// Add cost shadow
var costLabelShadow = new Text2(' ' + towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 15 + 4;
costLabelShadow.y = 99 + 12 + 15;
self.addChild(costLabelShadow);
// Add coin image
var coinImage = self.attachAsset('goldimage', {
anchorX: 0.5,
anchorY: 0.5
});
coinImage.width = 40;
coinImage.height = 40;
coinImage.x = -25;
coinImage.y = 95 + 12 + 15;
// Add cost label
var costLabel = new Text2(' ' + towerCost, {
size: 50,
fill: 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.x = 15;
costLabel.y = 95 + 12 + 15;
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 || 'basic';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// Standardized method to get the current range of the tower
self.getRange = function () {
// Always calculate range based on tower type and level
switch (self.id) {
case '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;
default:
// Basic: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60 * 1.10; // 10% slower fire rate
self.bulletSpeed = 5;
self.damage = 1; // Default tower damage
self.lastFired = 0;
self.targetEnemy = null;
// Set initial fire rate based on tower type and level
self.updateFireRate = function () {
switch (self.id) {
case 'rapid':
self.fireRate = 30 * 1.10; // 10% slower fire rate
break;
case 'splash':
self.fireRate = 75 * 1.10; // 10% slower fire rate
break;
default:
// basic tower
// Level-based fire rate for basic tower
// Convert shots per second to ticks between shots (60 FPS)
var shotsPerSecond = [0.8, 1.1, 1.4, 1.7, 2.0, 2.5]; // rates for levels 1-6
var currentRate = shotsPerSecond[Math.min(self.level - 1, 5)]; // cap at level 6
self.fireRate = 60 / currentRate; // convert to ticks between shots
break;
}
};
switch (self.id) {
case 'rapid':
self.fireRate = 30 * 1.10; // 10% slower fire rate
self.damage = 1.5;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'splash':
self.fireRate = 75 * 1.10; // 10% slower fire rate
self.damage = 2;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
default:
// basic tower
self.damage = 1;
self.range = 3 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
// Set initial fire rate
self.updateFireRate();
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
baseGraphics.width = CELL_SIZE;
baseGraphics.height = CELL_SIZE;
switch (self.id) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1) + 50;
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
var gunAsset = self.id === 'rapid' ? 'rapidtowercannon' : self.id === 'splash' ? 'splashcannontower' : 'defense';
var gunGraphics = gunContainer.attachAsset(gunAsset, {
anchorX: 0.5,
anchorY: 0.5
});
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// Make last upgrade level extra expensive
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type (damage remains constant at 1)
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.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.bulletSpeed = 7 + self.level * 0.7;
}
} else if (self.id === 'splash') {
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.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.bulletSpeed = 5 + self.level * 0.5;
}
} else {
// Basic tower - use level-based fire rate system
self.updateFireRate();
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 (dt) {
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;
// Use cooldown system with scaledDt
if (!self.cooldown) self.cooldown = 0;
self.cooldown -= dt || scaledDt;
if (self.cooldown <= 0) {
self.fire();
self.cooldown = self.fireRate;
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
game.addChild(rangeIndicator);
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// Customize bullet appearance based on tower type
switch (self.id) {
case 'rapid':
bullet.children[0].tint = 0x00AAFF;
bullet.children[0].width = 20;
bullet.children[0].height = 20;
break;
case 'splash':
bullet.children[0].tint = 0x33CC00;
bullet.children[0].width = 40 * 0.65;
bullet.children[0].height = 40 * 0.65;
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;
// Position at exact grid cell center with offset of 56 pixels up and 56 pixels left (moved 14 pixels down and 12 pixels right total)
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2 - 58;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2 - 56;
var cell = grid.getCell(gridX, gridY);
if (cell) {
cell.type = 4; // tower type
cell.hasTower = true; // mark as occupied
}
self.refreshCellsInRange();
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'basic';
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;
previewGraphics.height = CELL_SIZE;
// Add pulsing effect to preview
self.pulsingTween = null;
self.startPulsing = function () {
if (self.pulsingTween) return;
self.pulsingTween = tween(previewGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
};
self.stopPulsing = function () {
if (self.pulsingTween) {
tween.stop(previewGraphics, {
alpha: true
});
self.pulsingTween = null;
previewGraphics.alpha = 0.5;
}
};
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
switch (self.towerType) {
case 'rapid':
previewGraphics.tint = 0x00AAFF;
break;
case 'splash':
previewGraphics.tint = 0x33CC00;
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
} else {
// Green tint for valid placement
switch (self.towerType) {
case 'rapid':
previewGraphics.tint = 0x00AAFF;
break;
case 'splash':
previewGraphics.tint = 0x33CC00;
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
// Check if placement would be in bottom bar
if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) {
validGridPlacement = false;
self.canPlace = false;
}
if (self.gridY <= 4 || self.gridY >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
var cell = grid.getCell(self.gridX, self.gridY);
// Towers can only be placed on wall cells (type 1) and must not have towers
if (!cell || cell.type !== 1 || cell.hasTower) {
validGridPlacement = false;
}
}
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.cellY === self.gridY) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX === self.gridX && targetY === self.gridY) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
// Check if tower touches a road
var touchesRoad = false;
if (validGridPlacement) {
var dirs = [[0, -1], [1, 0], [0, 1], [-1, 0]]; // 4 directions
for (var d = 0; d < dirs.length; d++) {
var nx = self.gridX + dirs[d][0];
var ny = self.gridY + dirs[d][1];
var nCell = grid.getCell(nx, ny);
if (nCell && (nCell.type === TYPE_ROAD || nCell.type === TYPE_SLOW_ROAD || nCell.type === TYPE_POISON_ROAD || nCell.type === TYPE_SPAWN || nCell.type === TYPE_GOAL)) {
touchesRoad = true;
break;
}
}
}
self.canPlace = validGridPlacement && touchesRoad && !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);
// Position at exact grid cell center - same as Tower
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2 - 72 + 10 + 4 + 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2;
self.checkPlacement();
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + BULLET_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: ' + BULLET_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;
var cell = grid.getCell(gridX, gridY);
if (cell) {
cell.type = 1; // reset to wall
cell.hasTower = false; // mark as free
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();
if (typeof window.fixSpawnTargets === 'function') window.fixSpawnTargets();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2;
startBlock.tint = 0x00AA00;
// Add shadow for start text
var startTextShadow = new Text2("Start Game", {
size: 50,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 4;
startTextShadow.y = 4;
startMarker.addChild(startTextShadow);
var startText = new Text2("Start Game", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startMarker.addChild(startText);
startMarker.x = -self.indicatorWidth;
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (i === 5) {
// Wave 6 - Mini Boss
block.tint = 0xFF4500;
waveType = "Mini Boss";
enemyType = "swarm";
enemyCount = 1;
} 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;
}
}
// Only handle wave progression if game is not paused
if (gameSpeed > 0) {
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
// Scale wave timer by game speed
// When gameSpeed is 2x, timer progresses 2x faster
// When gameSpeed is 0.5x, timer progresses 2x slower
waveTimer += gameSpeed; // Direct multiplication by game speed
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
window.fixSpawnTargets = function fixSpawnTargets() {
if (!grid || !grid.spawns || !grid.spawns[0]) return; // koruma
var spawn = grid.spawns[0];
var spawnExit = grid.getCell(spawn.x, spawn.y + 1);
spawn.targets = [spawnExit]; // yalnız aşağı
};
// Helper function to check if a cell type is walkable road
function isRoad(cellType) {
return cellType === TYPE_ROAD || cellType === TYPE_SLOW_ROAD || cellType === TYPE_POISON_ROAD || cellType === TYPE_SPAWN || cellType === TYPE_GOAL;
}
// Classify block and return detailed info including directions
function classifyBlock(x, y) {
var kind = classifyRoadCell(x, y);
var dirIn = 0; // Default up
var dirOut = 0; // Default up
var rotation = 0;
if (kind === 'corner') {
var _grid$getCell5, _grid$getCell6, _grid$getCell7, _grid$getCell8;
var up = isRoad((_grid$getCell5 = grid.getCell(x, y - 1)) === null || _grid$getCell5 === void 0 ? void 0 : _grid$getCell5.type);
var dn = isRoad((_grid$getCell6 = grid.getCell(x, y + 1)) === null || _grid$getCell6 === void 0 ? void 0 : _grid$getCell6.type);
var lt = isRoad((_grid$getCell7 = grid.getCell(x - 1, y)) === null || _grid$getCell7 === void 0 ? void 0 : _grid$getCell7.type);
var rt = isRoad((_grid$getCell8 = grid.getCell(x + 1, y)) === null || _grid$getCell8 === void 0 ? void 0 : _grid$getCell8.type);
// Determine directions and rotation
if (up && rt) {
dirIn = 2;
dirOut = 0;
rotation = 0; // ┘ (from down to up)
} else if (rt && dn) {
dirIn = 3;
dirOut = 1;
rotation = Math.PI / 2; // └ (from left to right)
} else if (dn && lt) {
dirIn = 0;
dirOut = 2;
rotation = Math.PI; // ┌ (from up to down)
} else if (lt && up) {
dirIn = 1;
dirOut = 3;
rotation = -Math.PI / 2; // ┐ (from right to left)
}
}
return {
kind: kind,
dirIn: dirIn,
dirOut: dirOut,
rotation: rotation
};
}
// Classify road cell as corner or straight based on connections
function classifyRoadCell(x, y) {
var _grid$getCell, _grid$getCell2, _grid$getCell3, _grid$getCell4;
var n = (_grid$getCell = grid.getCell(x, y - 1)) === null || _grid$getCell === void 0 ? void 0 : _grid$getCell.type; // up
var s = (_grid$getCell2 = grid.getCell(x, y + 1)) === null || _grid$getCell2 === void 0 ? void 0 : _grid$getCell2.type; // down
var w = (_grid$getCell3 = grid.getCell(x - 1, y)) === null || _grid$getCell3 === void 0 ? void 0 : _grid$getCell3.type; // left
var e = (_grid$getCell4 = grid.getCell(x + 1, y)) === null || _grid$getCell4 === void 0 ? void 0 : _grid$getCell4.type; // right
var up = isRoad(n);
var dn = isRoad(s);
var lt = isRoad(w);
var rt = isRoad(e);
// Corner = at least one vertical AND at least one horizontal connection
if ((up || dn) && (lt || rt)) return 'corner';
return 'straight';
}
// Render road cell with appropriate texture and rotation
function renderRoadCell(cellSprite, x, y) {
var c = grid.getCell(x, y);
// ① If already marked as corner, use that directly
if (c.kind === 'corner') {
var cornerAsset = LK.getAsset('roadCorner', {});
cellSprite.texture = cornerAsset.texture;
cellSprite.rotation = c.cachedRotation || 0; // Use stored rotation
return; // Done
}
// ② Otherwise continue with automatic analysis (straight/corner detection)
var _grid$getCell5, _grid$getCell6, _grid$getCell7, _grid$getCell8;
var up = isRoad((_grid$getCell5 = grid.getCell(x, y - 1)) === null || _grid$getCell5 === void 0 ? void 0 : _grid$getCell5.type);
var dn = isRoad((_grid$getCell6 = grid.getCell(x, y + 1)) === null || _grid$getCell6 === void 0 ? void 0 : _grid$getCell6.type);
var lt = isRoad((_grid$getCell7 = grid.getCell(x - 1, y)) === null || _grid$getCell7 === void 0 ? void 0 : _grid$getCell7.type);
var rt = isRoad((_grid$getCell8 = grid.getCell(x + 1, y)) === null || _grid$getCell8 === void 0 ? void 0 : _grid$getCell8.type);
// Count connections to determine if this is a corner
var connectionCount = 0;
var hasVertical = up || dn;
var hasHorizontal = lt || rt;
if (up) connectionCount++;
if (dn) connectionCount++;
if (lt) connectionCount++;
if (rt) connectionCount++;
// Use corner asset if we have exactly 2 connections AND they form a corner (one vertical + one horizontal)
var isCorner = connectionCount === 2 && hasVertical && hasHorizontal;
if (isCorner) {
var cornerAsset = LK.getAsset('roadCorner', {});
cellSprite.texture = cornerAsset.texture;
// Set rotation based on which two directions are connected
if (up && rt) cellSprite.rotation = 0; // ┘ (up + right)
else if (rt && dn) cellSprite.rotation = Math.PI / 2; // └ (right + down)
else if (dn && lt) cellSprite.rotation = Math.PI; // ┌ (down + left)
else if (lt && up) cellSprite.rotation = -Math.PI / 2; // ┐ (left + up)
else cellSprite.rotation = 0; // fallback
} else {
var normalAsset = LK.getAsset('roadNormal', {});
cellSprite.texture = normalAsset.texture;
cellSprite.rotation = 0;
}
}
var CELL_SIZE = 114;
var ROAD_COST = 5;
var TOP_BAR_H = 38;
var BOTTOM_BAR_H = -260;
// Bullet damage constant
var BULLET_DAMAGE = 1;
// Enemy HP table
var HP_TABLE = {
normal: Math.floor(5 * 0.85 * 0.95),
fast: Math.floor(6 * 0.85 * 0.95),
flying: Math.floor(6 * 0.85 * 0.95),
immune: Math.floor(12 * 0.85 * 0.95),
swarm: Math.floor(3 * 0.85 * 0.95) - 1,
boss: Math.floor(50 * 0.85 * 0.95)
};
// Road type constants
var TYPE_ROAD = 0;
var TYPE_WALL = 1;
var TYPE_SPAWN = 2;
var TYPE_GOAL = 3;
var TYPE_TOWER = 4;
var TYPE_SLOW_ROAD = 5;
var TYPE_POISON_ROAD = 6;
// Slow road multiplier
var SLOW_MUL = 0.75; // 25% slower
// Road costs
var SLOW_ROAD_COST = 10;
var POISON_ROAD_COST = 15;
// Direction constants for spawn/base edge filtering
var DIRS = [{
dx: 0,
dy: -1
},
// up (index 0)
{
dx: 1,
dy: 0
},
// right (index 1)
{
dx: 0,
dy: 1
},
// down (index 2)
{
dx: -1,
dy: 0
} // left (index 3)
];
var SPAWN_EXIT_DIR = 2; // index 2 → down
// Helper functions for spawn/base edge filtering
function sharesEdge(a, b) {
return Math.abs(a.x - b.x) === 1 && a.y === b.y || Math.abs(a.y - b.y) === 1 && a.x === b.x;
}
function getDirectionIndex(fromX, fromY, toX, toY) {
if (toX > fromX) return 1; // right
if (toX < fromX) return 3; // left
if (toY > fromY) return 2; // down
return 0; // up
}
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
var gold = 80;
var lives = 100;
var score = 0;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
// Wave spawning state tracking
var enemiesSpawnedThisWave = 0;
var lastEnemySpawnTime = 0;
var enemySpawnDelay = 78; // Default 1.3 seconds at 60 FPS (60 * 1.3 = 78)
var totalEnemiesToSpawnThisWave = 0;
var currentWaveType = 'normal';
// Wave completion tracking
var totalEnemiesSpawnedThisWave = 0;
var totalEnemiesDeadThisWave = 0;
var waveIsComplete = false;
// Function to get spawn delay based on enemy type
function getEnemySpawnDelay(enemyType) {
switch (enemyType) {
case 'normal':
return 84;
// 1.4 seconds (60 * 1.4 = 84)
case 'fast':
return 96;
// 1.6 seconds (60 * 1.6 = 96)
case 'swarm':
return 47;
// 0.78 seconds (60 * 0.78 = 47) - 40% faster spawn rate
case 'immune':
return 84;
// 1.4 seconds for immune enemies
case 'flying':
return 72;
// 1.2 seconds for flying enemies
default:
return 72;
// 1.2 seconds (60 * 1.2 = 72) for other enemies
}
}
// Game speed control
var gameSpeed = 1;
var gameSpeedFactors = [1, 2, 5, 0.5, 0];
var currentSpeedIndex = 0;
// Global scaled delta time
var scaledDt = 1; // Initialize with default value
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('HP: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
// Create health bar
var healthBarContainer = new Container();
healthBarContainer.visible = true; // Ensure container is visible
var healthBarBg = healthBarContainer.attachAsset('healthBar', {
anchorX: 0.5,
anchorY: 0.5
});
healthBarBg.width = 106.9; // 5% smaller than 112.5 (112.5 * 0.95)
healthBarBg.height = 10.7; // 5% smaller than 11.25 (11.25 * 0.95)
healthBarBg.tint = 0x444444; // Dark gray background
healthBarBg.visible = true; // Ensure background is visible
var healthBarFill = healthBarContainer.attachAsset('healthBar', {
anchorX: 0.5,
anchorY: 0.5
});
healthBarFill.width = 106.9; // 5% smaller than 112.5 (112.5 * 0.95)
healthBarFill.height = 10.7; // 5% smaller than 11.25 (11.25 * 0.95)
healthBarFill.tint = 0x00FF00; // Green fill
healthBarFill.visible = true; // Ensure fill is visible
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(livesText);
LK.gui.top.addChild(scoreText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
var speedText = new Text2('1×', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
speedText.anchor.set(0.5, 0.5);
speedText.x = spacing + 30 + 200;
speedText.y = topMargin;
LK.gui.top.addChild(speedText);
speedText.down = function () {
currentSpeedIndex = (currentSpeedIndex + 1) % gameSpeedFactors.length;
gameSpeed = gameSpeedFactors[currentSpeedIndex];
if (gameSpeed === 0) {
speedText.setText('⏸');
} else if (gameSpeed === 0.5) {
speedText.setText('0.5×');
} else if (gameSpeed === 1) {
speedText.setText('1×');
} else if (gameSpeed === 2) {
speedText.setText('2×');
} else if (gameSpeed === 5) {
speedText.setText('5×');
} else {
speedText.setText(gameSpeed + 'x');
}
};
function updateHealthBarPos() {
if (pathPoints && pathPoints.length > 0) {
var base = pathPoints[pathPoints.length - 1]; // Last point is always the base
healthBarContainer.x = grid.x + base.x * CELL_SIZE + CELL_SIZE / 2 - 58; // 58 pixels left (59 - 1)
healthBarContainer.y = grid.y + base.y * CELL_SIZE + CELL_SIZE + 8 - 43; // 43 pixels up from 8px below base (40 + 3)
}
}
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('HP: ' + lives);
scoreText.setText('Score: ' + score);
// Update health bar
var healthPercentage = lives / 100; // Assuming max health is 100
healthBarFill.width = 106.9 * healthPercentage; // Use new smaller size (106.9 instead of 112.5)
// Align health bar fill to the left by adjusting anchor and position
healthBarFill.anchorX = 0; // Change anchor to left
healthBarFill.x = healthBarBg.x - healthBarBg.width / 2; // Position at left edge of background
}
function setGold(value) {
gold = value;
updateUI();
}
// Create bottom bar
var bottomBar = new Container();
var bottomBarGraphics = bottomBar.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
bottomBarGraphics.width = 2048;
bottomBarGraphics.height = Math.abs(BOTTOM_BAR_H);
bottomBarGraphics.tint = 0x222222;
bottomBar.y = 2732 - BOTTOM_BAR_H;
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 = 60;
grid.y = 200 - CELL_SIZE * 4;
// Initialize the dynamic path
function buildPath(spawnX, spawnY) {
var pts = [];
pts.push({
x: spawnX,
y: spawnY
}); // spawn
pts.push({
x: spawnX,
y: spawnY + 1
}); // 1 down
for (var k = 1; k <= 3; k++) {
// 3 left
pts.push({
x: spawnX - k,
y: spawnY + 1
});
}
return pts; // last element is base
}
var pathPoints = buildPath(10, 12);
// Define spawn and spawn exit for enemy pathfinding
var spawn = grid.getCell(10, 12);
var spawnExit = grid.getCell(10, 13);
// Mark path cells in grid
for (var i = 0; i < pathPoints.length; i++) {
var point = pathPoints[i];
var cell = grid.getCell(point.x, point.y);
if (cell) {
if (i === 0) {
cell.type = 2; // spawn
} else if (i === pathPoints.length - 1) {
cell.type = 3; // goal
grid.goals.push(cell); // Add base to goals array
} else {
cell.type = TYPE_ROAD; // path
}
// Store sprite reference and block info for initial path (center cell)
var blockInfo = classifyBlock(cell.x, cell.y);
if (cell.debugCell && cell.debugCell.children[0]) {
// Only store sprite reference if this is the center cell
var HALF = 1; // Block center offset
if (cell.x % 2 === 0 && cell.y % 2 === 1) {
// Center cell for our grid layout
cell.sprite = cell.debugCell.children[0];
cell.kind = blockInfo.kind;
cell.dirIn = blockInfo.dirIn;
cell.dirOut = blockInfo.dirOut;
}
}
}
}
grid.pathFind();
// Fix spawn targets to only go downward
if (typeof window.fixSpawnTargets === 'function') window.fixSpawnTargets();
grid.renderDebug();
// Position health bar below player base (at end of path) - moved here after pathPoints is initialized
// NOTE: This will be executed after uiLayer is created
// Update sprite references and block info after initial rendering
for (var i = 0; i < pathPoints.length; i++) {
var point = pathPoints[i];
var cell = grid.getCell(point.x, point.y);
if (cell && cell.debugCell && cell.debugCell.children[0]) {
var blockInfo = classifyBlock(cell.x, cell.y);
// Only store sprite reference if this is the center cell
var HALF = 1; // Block center offset
if (cell.x % 2 === 0 && cell.y % 2 === 1) {
// Center cell for our grid layout
cell.sprite = cell.debugCell.children[0];
cell.kind = blockInfo.kind;
cell.dirIn = blockInfo.dirIn;
cell.dirOut = blockInfo.dirOut;
}
}
}
debugLayer.addChild(grid);
game.addChild(bottomBar);
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
// Add UI overlay layer that renders on top of everything
var uiLayer = new Container();
game.addChild(uiLayer);
// Position health bar below player base (at end of path) - moved here after uiLayer is created
if (pathPoints && pathPoints.length > 0) {
var basePoint = pathPoints[pathPoints.length - 1];
// Add health bar to UI layer instead of base cell
uiLayer.addChild(healthBarContainer);
// Position health bar in world coordinates
healthBarContainer.x = grid.x + basePoint.x * CELL_SIZE + CELL_SIZE / 2;
healthBarContainer.y = grid.y + basePoint.y * CELL_SIZE + CELL_SIZE + 8; // 8px below base
// Ensure health bar is visible
healthBarContainer.visible = true;
healthBarBg.visible = true;
healthBarFill.visible = true;
}
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var roadPreview = new RoadPreview();
game.addChild(roadPreview);
roadPreview.visible = false;
var slowRoadPreview = new SlowRoadPreview();
game.addChild(slowRoadPreview);
slowRoadPreview.visible = false;
var poisonRoadPreview = new PoisonRoadPreview();
game.addChild(poisonRoadPreview);
poisonRoadPreview.visible = false;
var isDragging = false;
var dragType = null; // 'tower' or 'road'
function wouldBlockPath(gridX, gridY) {
// Always return false as enemies can only walk on road tiles
return false;
}
function getTowerCost(towerType) {
switch (towerType) {
case 'rapid':
return 15;
case 'splash':
return 25;
default:
return 5;
}
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'basic');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
grid.pathFind();
grid.renderDebug();
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Check if clicking on road sources
for (var i = 0; i < roadSources.length; i++) {
var source = roadSources[i];
if (x >= source.x - source.width / 2 && x <= source.x + source.width / 2 && y >= source.y - source.height / 2 && y <= source.y + source.height / 2) {
isDragging = true;
if (source.data.type === 'road') {
roadPreview.visible = true;
dragType = 'road';
roadPreview.snapToGrid(x, y);
} else if (source.data.type === 'slowRoad') {
slowRoadPreview.visible = true;
dragType = 'slowRoad';
slowRoadPreview.snapToGrid(x, y);
} else if (source.data.type === 'poisonRoad') {
poisonRoadPreview.visible = true;
dragType = 'poisonRoad';
poisonRoadPreview.snapToGrid(x, y);
}
return;
}
}
// Check tower sources
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;
dragType = 'tower';
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
towerPreview.startPulsing();
// Apply the same offset as in move handler to ensure consistency when starting drag
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
break;
}
}
};
game.move = function (x, y, obj) {
if (isDragging) {
// Shift the y position upward by 1.5 tiles to show preview above finger
if (dragType === 'road') {
roadPreview.snapToGrid(x, y);
} else if (dragType === 'slowRoad') {
slowRoadPreview.snapToGrid(x, y);
} else if (dragType === 'poisonRoad') {
poisonRoadPreview.snapToGrid(x, y);
} else {
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
}
}
};
game.up = function (x, y, obj) {
// Ignore clicks in bottom bar area
if (y > 2732 + BOTTOM_BAR_H) 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 (dragType === 'road' || dragType === 'slowRoad' || dragType === 'poisonRoad') {
// Handle road placement
var preview, cost, roadType;
if (dragType === 'road') {
preview = roadPreview;
cost = ROAD_COST;
roadType = TYPE_ROAD;
} else if (dragType === 'slowRoad') {
preview = slowRoadPreview;
cost = SLOW_ROAD_COST;
roadType = TYPE_SLOW_ROAD;
} else if (dragType === 'poisonRoad') {
preview = poisonRoadPreview;
cost = POISON_ROAD_COST;
roadType = TYPE_POISON_ROAD;
}
if (preview.canPlace && gold >= cost) {
var cell = grid.getCell(preview.gridX, preview.gridY);
if (cell) {
// Deduct gold
setGold(gold - cost);
// Check if touching base to determine if this extends the path
var base = pathPoints[pathPoints.length - 1];
var touchingBase = sharesEdge({
x: preview.gridX + 0.5,
y: preview.gridY + 0.5
}, {
x: base.x + 0.5,
y: base.y + 0.5
});
if (touchingBase) {
// L-turn detection and corner conversion
if (pathPoints.length >= 2) {
// Direction mapping: 0=up, 1=right, 2=down, 3=left
var getDirection = function getDirection(dx, dy) {
if (dx > 0) return 1; // →
if (dx < 0) return 3; // ←
if (dy > 0) return 2; // ↓
return 0; // ↑
};
var prevCell = grid.getCell(pathPoints[pathPoints.length - 2].x, pathPoints[pathPoints.length - 2].y); // A (old base)
var newCell = {
x: preview.gridX,
y: preview.gridY
}; // B (new base)
// Calculate direction vectors
var dx = newCell.x - prevCell.x;
var dy = newCell.y - prevCell.y;
var newDir = getDirection(dx, dy); // prevCell's exit direction
var oldDir = prevCell.dirIn || 0; // prevCell's entry direction
// Check if this is a turn (different axes: horizontal vs vertical)
var isTurn = oldDir % 2 !== newDir % 2;
if (isTurn && prevCell.sprite) {
// Update cell data for corner
prevCell.kind = 'corner';
prevCell.dirOut = newDir;
// Break PIXI texture cache
prevCell.sprite.texture = LK.getAsset('cell', {}).texture;
// Apply corner texture
prevCell.sprite.texture = LK.getAsset('roadCorner', {}).texture;
// Apply correct rotation based on turn direction
var rotationMap = {};
rotationMap['2-0'] = 0; // ↓ then ↑ (┘)
rotationMap['3-1'] = Math.PI / 2; // ← then → (└)
rotationMap['0-2'] = Math.PI; // ↑ then ↓ (┌)
rotationMap['1-3'] = -Math.PI / 2; // → then ← (┐)
var rotationKey = oldDir + '-' + newDir;
var rotation = rotationMap[rotationKey] || 0;
prevCell.sprite.rotation = rotation;
// Cache the rotation for future renders
prevCell.cachedRotation = rotation;
}
}
// Extending the main path - old behavior
var oldBase = grid.goals[0];
oldBase.type = TYPE_ROAD; // convert to normal road
cell.type = TYPE_GOAL; // temporarily make this the new base
cell.hasTower = false; // ensure it's not marked as having a tower
grid.goals[0] = cell; // update goals array
// Update pathPoints array for debug arrows
pathPoints.push({
x: cell.x,
y: cell.y
});
// Update health bar position (it stays in uiLayer)
updateHealthBarPos();
// Now convert the new base to the desired road type
oldBase.type = roadType;
// Store sprite reference and block info for the newly placed block (center cell)
var blockInfo = classifyBlock(cell.x, cell.y);
if (cell.debugCell && cell.debugCell.children[0]) {
// Only store sprite reference if this is the center cell (for 2x2 blocks)
var HALF = 1; // Block center offset
if (cell.x % 2 === 0 && cell.y % 2 === 1) {
// Center cell for our grid layout
cell.sprite = cell.debugCell.children[0];
cell.kind = blockInfo.kind;
cell.dirIn = blockInfo.dirIn;
cell.dirOut = blockInfo.dirOut;
}
}
} else {
// Decorative road next to spawn - don't extend path
cell.type = roadType;
cell.hasTower = false; // ensure it's not marked as having a tower
// Store sprite reference and block info for decorative road (center cell)
var blockInfo = classifyBlock(cell.x, cell.y);
if (cell.debugCell && cell.debugCell.children[0]) {
// Only store sprite reference if this is the center cell
var HALF = 1; // Block center offset
if (cell.x % 2 === 0 && cell.y % 2 === 1) {
// Center cell for our grid layout
cell.sprite = cell.debugCell.children[0];
cell.kind = blockInfo.kind;
cell.dirIn = blockInfo.dirIn;
cell.dirOut = blockInfo.dirOut;
}
}
}
// Recalculate path and redraw
grid.pathFind();
// Fix spawn targets to only go downward
if (typeof window.fixSpawnTargets === 'function') window.fixSpawnTargets();
grid.renderDebug();
// Show gold deduction
var goldIndicator = new GoldIndicator(-cost, preview.x, preview.y);
game.addChild(goldIndicator);
}
} else if (gold < cost) {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
// Check specific reasons for failure
var base = pathPoints[pathPoints.length - 1];
var isAdjacentToBase = false;
var baseNeighbors = [{
x: base.x,
y: base.y - 1
},
// up
{
x: base.x + 1,
y: base.y
},
// right
{
x: base.x,
y: base.y + 1
},
// down
{
x: base.x - 1,
y: base.y
} // left
];
for (var i = 0; i < baseNeighbors.length; i++) {
if (baseNeighbors[i].x === preview.gridX && baseNeighbors[i].y === preview.gridY) {
isAdjacentToBase = true;
break;
}
}
if (!isAdjacentToBase) {
var notification = game.addChild(new Notification("Road must connect to Base!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
var notification = game.addChild(new Notification("Road can't go backwards!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
roadPreview.visible = false;
slowRoadPreview.visible = false;
poisonRoadPreview.visible = false;
} else {
// Handle 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;
towerPreview.stopPulsing();
}
dragType = null;
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 - BOTTOM_BAR_H - 80 - 320 + 30 + 36;
game.addChild(waveIndicator);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200 - 500 - 300 - 10 - 7; // Move 817 pixels left total (500 + 300 + 10 + 7)
nextWaveButton.y = 2732 - BOTTOM_BAR_H - 100 + 20 - 320 + 30 - 400 - 200 + 100 - 4 + 30 + 40; // Move 434 pixels up total (400 + 200 - 100 + 4 - 30 - 40)
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
var buildItems = ['basic', 'rapid', 'splash'];
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
// Account for road buttons in spacing calculation
var totalItems = buildItems.length + 3; // +3 for road types
var startX = 2048 / 2 - totalItems * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - BOTTOM_BAR_H - CELL_SIZE * 3 - 90 - 150 - 50 - 15 + 10;
// Create road source buttons
var roadSources = [];
// Normal road
var roadSource = new Container();
roadSource.interactive = true;
roadSource.buttonMode = true;
roadSource.data = {
type: "road"
};
var roadBg = roadSource.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
roadBg.width = CELL_SIZE * 1.3 * 1.2;
roadBg.height = CELL_SIZE * 1.3 * 1.2;
roadBg.tint = 0xF4A460; // Sand yellow color for road
// Add shadow for road type label
var roadTypeLabelShadow = new Text2('Basic\nRoad', {
size: 50,
fill: 0x000000,
weight: 800
});
roadTypeLabelShadow.anchor.set(0.5, 0.5);
roadTypeLabelShadow.x = 4;
roadTypeLabelShadow.y = 4;
roadSource.addChild(roadTypeLabelShadow);
// Add road type label
var roadTypeLabel = new Text2('Basic\nRoad', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
roadTypeLabel.anchor.set(0.5, 0.5);
roadTypeLabel.y = 0;
roadSource.addChild(roadTypeLabel);
// Add coin image
var roadCoinImage = roadSource.attachAsset('goldimage', {
anchorX: 0.5,
anchorY: 0.5
});
roadCoinImage.width = 40;
roadCoinImage.height = 40;
roadCoinImage.x = -25;
roadCoinImage.y = 105 + 15;
// Add cost label
var roadCostLabel = new Text2(' ' + ROAD_COST.toString(), {
size: 50,
fill: 0xFFD700,
weight: 800
});
roadCostLabel.anchor.set(0.5, 0.5);
roadCostLabel.x = 15;
roadCostLabel.y = 105 + 15;
roadSource.addChild(roadCostLabel);
roadSource.x = startX;
roadSource.y = towerY + 50;
roadSource.update = function () {
var canAfford = gold >= ROAD_COST;
roadSource.alpha = canAfford ? 1 : 0.5;
};
towerLayer.addChild(roadSource);
roadSources.push(roadSource);
// Slow road
var slowRoadSource = new Container();
slowRoadSource.interactive = true;
slowRoadSource.buttonMode = true;
slowRoadSource.data = {
type: "slowRoad"
};
var slowRoadBg = slowRoadSource.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
slowRoadBg.width = CELL_SIZE * 1.3 * 1.2;
slowRoadBg.height = CELL_SIZE * 1.3 * 1.2;
slowRoadBg.tint = 0xF4A460; // Sand yellow for slow road
// Add shadow for slow road type label
var slowRoadTypeLabelShadow = new Text2('Slow\nRoad', {
size: 50,
fill: 0x000000,
weight: 800
});
slowRoadTypeLabelShadow.anchor.set(0.5, 0.5);
slowRoadTypeLabelShadow.x = 4;
slowRoadTypeLabelShadow.y = 4;
slowRoadSource.addChild(slowRoadTypeLabelShadow);
// Add road type label
var slowRoadTypeLabel = new Text2('Slow\nRoad', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
slowRoadTypeLabel.anchor.set(0.5, 0.5);
slowRoadTypeLabel.y = 0;
slowRoadSource.addChild(slowRoadTypeLabel);
// Add coin image
var slowRoadCoinImage = slowRoadSource.attachAsset('goldimage', {
anchorX: 0.5,
anchorY: 0.5
});
slowRoadCoinImage.width = 40;
slowRoadCoinImage.height = 40;
slowRoadCoinImage.x = -25;
slowRoadCoinImage.y = 105 + 15;
// Add cost label
var slowRoadCostLabel = new Text2(' ' + SLOW_ROAD_COST.toString(), {
size: 50,
fill: 0xFFD700,
weight: 800
});
slowRoadCostLabel.anchor.set(0.5, 0.5);
slowRoadCostLabel.x = 15;
slowRoadCostLabel.y = 105 + 15;
slowRoadSource.addChild(slowRoadCostLabel);
slowRoadSource.x = startX + towerSpacing;
slowRoadSource.y = towerY + 50;
slowRoadSource.update = function () {
var canAfford = gold >= SLOW_ROAD_COST;
slowRoadSource.alpha = canAfford ? 1 : 0.5;
};
towerLayer.addChild(slowRoadSource);
roadSources.push(slowRoadSource);
// Poison road
var poisonRoadSource = new Container();
poisonRoadSource.interactive = true;
poisonRoadSource.buttonMode = true;
poisonRoadSource.data = {
type: "poisonRoad"
};
var poisonRoadBg = poisonRoadSource.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
poisonRoadBg.width = CELL_SIZE * 1.3 * 1.2;
poisonRoadBg.height = CELL_SIZE * 1.3 * 1.2;
poisonRoadBg.tint = 0xF4A460; // Sand yellow for poison road
// Add shadow for poison road type label
var poisonRoadTypeLabelShadow = new Text2('Poison\n Road', {
size: 50,
fill: 0x000000,
weight: 800
});
poisonRoadTypeLabelShadow.anchor.set(0.5, 0.5);
poisonRoadTypeLabelShadow.x = 4;
poisonRoadTypeLabelShadow.y = 4;
poisonRoadSource.addChild(poisonRoadTypeLabelShadow);
// Add road type label
var poisonRoadTypeLabel = new Text2('Poison\n Road', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
poisonRoadTypeLabel.anchor.set(0.5, 0.5);
poisonRoadTypeLabel.y = 0;
poisonRoadSource.addChild(poisonRoadTypeLabel);
// Add coin image
var poisonRoadCoinImage = poisonRoadSource.attachAsset('goldimage', {
anchorX: 0.5,
anchorY: 0.5
});
poisonRoadCoinImage.width = 40;
poisonRoadCoinImage.height = 40;
poisonRoadCoinImage.x = -25;
poisonRoadCoinImage.y = 105 + 15;
// Add cost label
var poisonRoadCostLabel = new Text2(' ' + POISON_ROAD_COST.toString(), {
size: 50,
fill: 0xFFD700,
weight: 800
});
poisonRoadCostLabel.anchor.set(0.5, 0.5);
poisonRoadCostLabel.x = 15;
poisonRoadCostLabel.y = 105 + 15;
poisonRoadSource.addChild(poisonRoadCostLabel);
poisonRoadSource.x = startX + towerSpacing * 2;
poisonRoadSource.y = towerY + 50;
poisonRoadSource.update = function () {
var canAfford = gold >= POISON_ROAD_COST;
poisonRoadSource.alpha = canAfford ? 1 : 0.5;
};
towerLayer.addChild(poisonRoadSource);
roadSources.push(poisonRoadSource);
// Add "ROADS" section header
var roadsHeaderText = new Text2('ROADS', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
roadsHeaderText.anchor.set(0.5, 0.5);
roadsHeaderText.x = startX + towerSpacing; // Center over the road items
roadsHeaderText.y = towerY - 130 + 50 - 30; // Position above the road items
towerLayer.addChild(roadsHeaderText);
// Add dash between poison and default
var dashText = new Text2('-', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
dashText.anchor.set(0.5, 0.5);
dashText.x = startX + towerSpacing * 2.5;
dashText.y = towerY + 50;
towerLayer.addChild(dashText);
// Add "TOWERS" section header
var towersHeaderText = new Text2('TOWERS', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
towersHeaderText.anchor.set(0.5, 0.5);
towersHeaderText.x = startX + towerSpacing * 4; // Center over the tower items
towersHeaderText.y = towerY - 130 + 50 - 30; // Position above the tower items
towerLayer.addChild(towersHeaderText);
// Create tower sources (offset by 3 to account for road types)
for (var i = 0; i < buildItems.length; i++) {
var tower = new SourceTower(buildItems[i]);
tower.x = startX + (i + 3) * towerSpacing;
tower.y = towerY + 50;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
game.update = function () {
// Calculate global scaledDt
scaledDt = 1 * gameSpeed; // 0.5, 1, 3, 5, etc.
// Apply game speed (skip updates if paused)
if (gameSpeed === 0) {
return; // Game is paused
}
// Scale time-based updates by game speed
var speedMultiplier = gameSpeed;
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
// Reset wave completion tracking
totalEnemiesSpawnedThisWave = 0;
totalEnemiesDeadThisWave = 0;
waveIsComplete = false;
// Get wave type and enemy count from the wave indicator
currentWaveType = waveIndicator.getWaveType(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
// Check if this is a boss wave
var isBossWave = currentWave % 10 === 0 && currentWave > 0;
// Always get the enemy count from wave indicator first
enemyCount = waveIndicator.getEnemyCount(currentWave);
// Only override for boss waves if it's NOT a swarm wave
if (isBossWave && currentWaveType !== '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;
} else if (currentWaveType === 'swarm') {
// For wave 6 (mini boss), spawn only 1 enemy
if (currentWave === 6) {
enemyCount = 1;
} else {
// Other swarm waves get 30 enemies
enemyCount = 30;
}
}
// Initialize wave spawning variables
totalEnemiesToSpawnThisWave = enemyCount;
enemiesSpawnedThisWave = 0;
lastEnemySpawnTime = LK.ticks - getEnemySpawnDelay(currentWaveType); // Allow immediate first spawn
}
// Handle delayed enemy spawning with dynamic delays based on enemy type
if (waveSpawned && enemiesSpawnedThisWave < totalEnemiesToSpawnThisWave) {
var currentEnemySpawnDelay = getEnemySpawnDelay(currentWaveType);
// Scale the spawn delay by game speed to maintain consistent real-time intervals
var scaledSpawnDelay = Math.floor(currentEnemySpawnDelay / gameSpeed);
if (LK.ticks - lastEnemySpawnTime >= scaledSpawnDelay) {
// Spawn one enemy
var enemy = new Enemy(currentWaveType);
// 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
// Special case for wave 6 mini boss - 50x HP
if (currentWave === 6 && currentWaveType === 'swarm') {
healthMultiplier = healthMultiplier * 50; // 50x more HP for mini boss
}
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// Spawn at the defined spawn point (center of grid)
var spawnPoint = pathPoints[0];
enemy.cellX = spawnPoint.x;
enemy.cellY = spawnPoint.y;
enemy.currentCellX = spawnPoint.x;
enemy.currentCellY = spawnPoint.y;
enemy.x = grid.x + spawnPoint.x * CELL_SIZE + CELL_SIZE / 2;
enemy.y = grid.y + spawnPoint.y * CELL_SIZE + CELL_SIZE / 2;
enemy.waveNumber = currentWave;
// Initialize enemy pathfinding - start with spawn exit as first target
enemy.currentTarget = spawnExit;
enemy.cellPathIndex = 1; // Index into pathPoints array
enemies.push(enemy);
// Update spawn tracking
enemiesSpawnedThisWave++;
totalEnemiesSpawnedThisWave++;
lastEnemySpawnTime = LK.ticks;
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
// Check if wave is complete: all enemies spawned AND all enemies dead
if (waveSpawned && enemiesSpawnedThisWave >= totalEnemiesToSpawnThisWave && totalEnemiesDeadThisWave >= totalEnemiesSpawnedThisWave) {
if (!waveIsComplete) {
waveIsComplete = true;
var notification = game.addChild(new Notification("Wave " + currentWave + " Complete! Next wave unlocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
waveInProgress = false;
waveSpawned = false;
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
enemy.update(scaledDt);
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Track enemy death for wave completion
if (enemy.waveNumber === currentWave) {
totalEnemiesDeadThisWave++;
}
// Boss enemies give more gold and score
var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5);
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Track enemy death for wave completion (even if they reach base)
if (enemy.waveNumber === currentWave) {
totalEnemiesDeadThisWave++;
}
// 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 - enemy.health);
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
}
}
// Update towers with scaledDt
for (var i = 0; i < towers.length; i++) {
towers[i].update(scaledDt);
}
// Update bullets with scaledDt
for (var i = 0; i < bullets.length; i++) {
bullets[i].update(scaledDt);
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
if (roadPreview.visible) {
roadPreview.updatePlacementStatus();
}
if (slowRoadPreview.visible) {
slowRoadPreview.updatePlacementStatus();
}
if (poisonRoadPreview.visible) {
poisonRoadPreview.updatePlacementStatus();
}
// Update road source buttons
for (var i = 0; i < roadSources.length; i++) {
roadSources[i].update();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
// Spawn poison particles on poison roads continuously
// No need to check for game over - particles have their own lifetime
if (LK.ticks % 30 === 0) {
// Spawn particles every 0.5 seconds
for (var i = 0; i < grid.cells.length; i++) {
for (var j = 0; j < grid.cells[i].length; j++) {
var cell = grid.cells[i][j];
if (cell && cell.type === TYPE_POISON_ROAD) {
// 100% chance to spawn a particle on each poison road cell
grid.spawnPotionParticle(i, j);
}
if (cell && cell.type === TYPE_SLOW_ROAD) {
// 100% chance to spawn a particle on each slow road cell
grid.spawnSlowParticle(i, j);
}
}
}
}
// Update health bar position every frame
updateHealthBarPos();
};
17. century tower castle square shaped 1x1 medeval military base,grey coloured top-down look , simple design, medieval-semirealistic.. In-Game asset. 2d. High contrast. No shadows
17.th century cannon bullet semirealistic medieval cannon bullet. In-Game asset. 2d. High contrast. No shadows
top down look for basic cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
red coloured top down look for quick cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows.
armor grey coloured top down look for armoured basic cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
swarm enemy light green coloured top down look for quick cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows.