User prompt
replace jean text in ui with Armin
User prompt
after a wave completes, wave completed text will be shown, for example: wave 3 completed! after it there will be a text says wave 4! indicating the next wave is starting ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
you cant build turrets on the roads
User prompt
make the turns longer by length
User prompt
give the road a little more turns
User prompt
make a maze like pathfinding with road tiles, it starts from spawn point and ends on base, enemies will follow that path always
User prompt
make a maze like pathfinding with road tiles, it starts from spawn point and ends on base, enemies will follow that path always, you cannot place towers on the roads, you can only place towers near the road, enemies will always go on top of that road to the base
User prompt
the issue persist, make a path system from the sctrach, make a maze like road where the enemies follow it
User prompt
the enemies starts following the road in first 3 seconds, but after that when they need to go to left they just stop and stuck, fix it and make a path for it
User prompt
make a pathfinding where the enemies will only go on the road path
User prompt
make it even more complex, and big, use most of the map
User prompt
make it even more complex, and big, use every inch of the map, make the most complex and long road you can make
User prompt
you cannot place towers on the roads, you can only place towers near the road, also make the road longer and more maze and comples, enemies will always go on top of that road to the base
User prompt
make a maze like pathfinding with road tiles, it starts from spawn point and ends on base, enemies will follow that path always,
User prompt
move the spawnpoint image asset 1 cm left 2 cm up
User prompt
make the enemises spawn 3 cm lower 1 cm right
User prompt
make the enemies spawn exactly on top of the spawnpoint image asset, make them spawn one by one with 2 second delay
User prompt
move spawnpoint 3 cm upper, make enemies spawn one by one fastly on top of the spawnpoint
User prompt
move spawnpoint 10cm lower
User prompt
add and place spawnpoint image asset as the starting point for the enemies to spawn
User prompt
place the spawnpoint image asset on enemies spawning starting point
User prompt
i want you to remove only ui, but you are removing the whole spawn system
User prompt
remove the ui which shows what wave and what enemy is coming, only remove the ui, dont remove the enemies spawn, only remove the ui aspect
User prompt
remove the ui which shows what wave and what enemy is coming
User prompt
place the SpawnPoint image asset on top of the enemies spawn starting point
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var BaseCastle = Container.expand(function () { var self = Container.call(this); // Castle stats self.maxHealth = 100; self.health = self.maxHealth; // Create castle graphics var castleGraphics = self.attachAsset('base', { anchorX: 0.5, anchorY: 0.5 }); // Create health bar background var healthBarBg = self.attachAsset('castleHealthBarBg', { anchorX: 0.5, anchorY: 0.5 }); healthBarBg.y = -castleGraphics.height / 2 - 40; // Create health bar var healthBar = self.attachAsset('castleHealthBar', { anchorX: 0, anchorY: 0.5 }); healthBar.x = -healthBarBg.width / 2; healthBar.y = -castleGraphics.height / 2 - 40; self.takeDamage = function (damage) { self.health -= damage; if (self.health <= 0) { self.health = 0; // Play destruction sound LK.getSound('castle_destroy').play(); // Game over - castle destroyed LK.showGameOver(); } else { // Play damage sound LK.getSound('castle_damage').play(); // Flash castle red when taking damage LK.effects.flashObject(self, 0xff0000, 500); } // Update health bar visual var healthPercent = self.health / self.maxHealth; healthBar.width = healthBarBg.width * healthPercent; // Change health bar color based on health level if (healthPercent > 0.6) { healthBar.tint = 0x00ff00; // Green } else if (healthPercent > 0.3) { healthBar.tint = 0xffff00; // Yellow } else { healthBar.tint = 0xff0000; // Red } }; return self; }); var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Apply slow effect // Make slow percentage scale with tower level (default 50%, up to 80% at max level) var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6 var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; // Slow by X% self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { self.targetEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'poison') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual poison effect var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Apply poison effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); cellGraphics.tint = 0x880000; return; } numberLabel.visible = false; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.alpha = 0; } else { cellGraphics.alpha = 0; } while (debugArrows.length > data.targets.length) { self.removeChild(debugArrows.pop()); } // Remove arrow rendering - arrows no longer displayed break; } case 1: { self.removeArrows(); cellGraphics.alpha = 0; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.alpha = 0; numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'slash': effectGraphics.tint = 0xFFFFFF; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Combine poison (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; if (i > 11 - 3 && i <= 11 + 3) { if (j === 0) { cellType = 2; self.spawns.push(cell); } else if (j <= 4) { cellType = 0; } else if (j === gridHeight - 1) { cellType = 3; self.goals.push(cell); } else if (j >= gridHeight - 4) { cellType = 0; } } // Set castle area as goal (2x2 area around castle position) var castleGridX = gridWidth - 3; // 2 cells from right edge var castleGridY = gridHeight - 8; // 7 cells up from bottom for (var ci = 0; ci < 2; ci++) { for (var cj = 0; cj < 2; cj++) { var castleCell = self.cells[castleGridX + ci] && self.cells[castleGridX + ci][castleGridY + cj]; if (castleCell) { castleCell.type = 3; // Set as goal self.goals.push(castleCell); } } } cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 1 && node.left.type != 1) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.right.type != 1) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.right.type != 1) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.left.type != 1) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { // Enemy reached castle - damage it if (baseCastle) { var damage = enemy.isBoss ? 20 : 5; // Boss enemies deal more damage baseCastle.takeDamage(damage); // Show damage indicator var damageIndicator = new Notification("-" + damage + " Castle HP"); damageIndicator.x = baseCastle.x; damageIndicator.y = baseCastle.y - 100; game.addChild(damageIndicator); } return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed; // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) var angle = Math.PI / 2; if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update enemy's position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // If enemy has now reached the entry area, update cell coordinates if (enemy.currentCellY >= 4) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { if (cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var Player = Container.expand(function () { var self = Container.call(this); // Player stats self.maxStamina = 80; self.stamina = self.maxStamina; self.staminaRegenRate = 0.2; // Points per frame when not attacking self.attackCost = 25; // Stamina cost per attack self.attackDamage = 15; self.attackRange = CELL_SIZE * 1.0; // Attack range in pixels self.attackCooldown = 0; self.attackCooldownMax = 30; // Frames between attacks self.moveSpeed = 3; // Movement bounds (keep player within map area) self.minX = grid.x + CELL_SIZE; self.maxX = grid.x + grid.cells.length * CELL_SIZE - CELL_SIZE; self.minY = grid.y + CELL_SIZE * 4; self.maxY = grid.y + (grid.cells[0].length - 4) * CELL_SIZE - CELL_SIZE; // Create player graphics var playerGraphics = self.attachAsset('PlayerCh', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8 }); // Create stamina bar background var staminaBarBg = self.attachAsset('staminaBarBg', { anchorX: 0.5, anchorY: 0.5 }); staminaBarBg.y = -playerGraphics.height / 2 - 30; // Create stamina bar var staminaBar = self.attachAsset('staminaBar', { anchorX: 0, anchorY: 0.5 }); staminaBar.x = -staminaBarBg.width / 2; staminaBar.y = -playerGraphics.height / 2 - 30; // Movement state self.targetX = 0; self.targetY = 0; self.isMoving = false; self.update = function () { // Handle movement if (self.isMoving) { var dx = self.targetX - self.x; var dy = self.targetY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > self.moveSpeed) { // Move towards target var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.moveSpeed; self.y += Math.sin(angle) * self.moveSpeed; // Rotate player to face movement direction playerGraphics.rotation = angle; } else { // Reached target self.x = self.targetX; self.y = self.targetY; self.isMoving = false; } } // Handle attack cooldown if (self.attackCooldown > 0) { self.attackCooldown--; } // Regenerate stamina when not attacking if (self.attackCooldown <= 0 && self.stamina < self.maxStamina) { self.stamina = Math.min(self.maxStamina, self.stamina + self.staminaRegenRate); } // Update stamina bar visual var staminaPercent = self.stamina / self.maxStamina; staminaBar.width = staminaBarBg.width * staminaPercent; // Change stamina bar color based on stamina level if (staminaPercent > 0.6) { staminaBar.tint = 0x00ff00; // Green } else if (staminaPercent > 0.3) { staminaBar.tint = 0xffff00; // Yellow } else { staminaBar.tint = 0xff0000; // Red } }; self.moveTo = function (x, y) { // Clamp movement within bounds self.targetX = Math.max(self.minX, Math.min(self.maxX, x)); self.targetY = Math.max(self.minY, Math.min(self.maxY, y)); self.isMoving = true; }; self.attack = function () { // Check if can attack (has stamina and not on cooldown) if (self.stamina >= self.attackCost && self.attackCooldown <= 0) { // Consume stamina self.stamina -= self.attackCost; self.attackCooldown = self.attackCooldownMax; // Find enemies in range var enemiesHit = []; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.attackRange) { enemiesHit.push(enemy); } } // Switch to attack animation asset var attackGraphics = self.attachAsset('Attackanim', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8 }); // Hide the original player graphics playerGraphics.visible = false; // Attack all enemies in range for (var i = 0; i < enemiesHit.length; i++) { var enemy = enemiesHit[i]; enemy.health -= self.attackDamage; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } // Create attack effect var attackEffect = new EffectIndicator(enemy.x, enemy.y, 'slash'); game.addChild(attackEffect); } // Create slash animation around player var slashEffect = new EffectIndicator(self.x, self.y, 'slash'); game.addChild(slashEffect); // After attack animation duration, switch back to normal graphics LK.setTimeout(function () { // Remove attack graphics if (attackGraphics && attackGraphics.parent) { self.removeChild(attackGraphics); } // Show original player graphics again playerGraphics.visible = true; }, 500); // 500ms attack animation duration return true; // Attack successful } return false; // Attack failed (no stamina or on cooldown) }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Increase size of base for easier touch var baseGraphics; switch (self.towerType) { case 'rapid': baseGraphics = self.attachAsset('eren', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); break; case 'sniper': baseGraphics = self.attachAsset('sasha', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); break; case 'splash': baseGraphics = self.attachAsset('mikasa', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); break; case 'slow': baseGraphics = self.attachAsset('Erwin', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); break; case 'poison': baseGraphics = self.attachAsset('levi', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); break; default: baseGraphics = self.attachAsset('jean', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } var towerCost = getTowerCost(self.towerType); // Get character name based on tower type var characterName; switch (self.towerType) { case 'rapid': characterName = 'Eren'; break; case 'sniper': characterName = 'Sasha'; break; case 'splash': characterName = 'Mikasa'; break; case 'slow': characterName = 'Erwin'; break; case 'poison': characterName = 'Levi'; break; default: characterName = 'Jean'; } // Create decorative background for name var nameBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); nameBackground.width = 220; nameBackground.height = 80; nameBackground.y = -180; // Move higher for Mikasa and Levi // Set background color based on tower type with more aesthetic colors switch (self.towerType) { case 'rapid': nameBackground.tint = 0x1B4F72; // Darker Rich Blue nameBackground.y = -185; // Adjust for Eren break; case 'sniper': nameBackground.tint = 0x1E8449; // Darker Forest Green nameBackground.y = -190; // Move higher for Sasha break; case 'splash': nameBackground.tint = 0x922B21; // Deeper Red for Mikasa break; case 'slow': nameBackground.tint = 0x6C3483; // Darker Royal Purple break; case 'poison': nameBackground.tint = 0x0E6655; // Deeper Teal for Levi nameBackground.y = -190; // Move higher for Levi break; default: nameBackground.tint = 0x424949; // Darker Steel Gray nameBackground.y = -185; // Adjust for Jean } nameBackground.alpha = 0.95; // Add shadow for tower type label var typeLabelShadow = new Text2(characterName, { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 3; // Adjust shadow position based on tower type if (self.towerType === 'rapid' || self.towerType === 'default') { typeLabelShadow.y = -182; } else if (self.towerType === 'sniper' || self.towerType === 'poison') { typeLabelShadow.y = -187; // Higher position for Sasha and Levi } else { typeLabelShadow.y = -177; } self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(characterName, { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); // Adjust text position based on tower type if (self.towerType === 'rapid' || self.towerType === 'default') { typeLabel.y = -185; // Position higher for Eren and Jean } else if (self.towerType === 'sniper' || self.towerType === 'poison') { typeLabel.y = -190; // Position even higher for Sasha and Levi } else { typeLabel.y = -180; // Standard position for others } self.addChild(typeLabel); // Create decorative background for cost var costBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); costBackground.width = 140; costBackground.height = 60; costBackground.y = 160; // Move higher costBackground.tint = 0x1C2833; // Darker, more elegant background costBackground.alpha = 0.95; // Add decorative gold coin squares around cost var leftCoin = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftCoin.width = 18; leftCoin.height = 18; leftCoin.tint = 0xF1C40F; // Brighter gold leftCoin.x = -55; leftCoin.y = 160; var rightCoin = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightCoin.width = 18; rightCoin.height = 18; rightCoin.tint = 0xF1C40F; // Brighter gold rightCoin.x = 55; rightCoin.y = 160; // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 44, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 3; costLabelShadow.y = 163; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost, { size: 44, fill: 0xF1C40F, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 160; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case 'sniper': // Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost if (self.level === self.maxLevel) { return 12 * CELL_SIZE; // Significantly increased range for max level } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case 'splash': // Splash: base 2, +0.2 per level (max ~4 blocks at max level) return (2 + (self.level - 1) * 0.2) * CELL_SIZE; case 'rapid': // Rapid: base 2.5, +0.5 per level return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'slow': // Slow: base 3.5, +0.5 per level return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'poison': // Poison: base 3.2, +0.5 per level return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; default: // Default: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 30; self.damage = 5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'sniper': self.fireRate = 90; self.damage = 25; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'splash': self.fireRate = 75; self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; case 'slow': self.fireRate = 50; self.damage = 8; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'poison': self.fireRate = 70; self.damage = 12; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 5; break; } var baseGraphics; switch (self.id) { case 'rapid': baseGraphics = self.attachAsset('erentower', { anchorX: 0.5, anchorY: 0.5 }); break; case 'sniper': baseGraphics = self.attachAsset('sashatower', { anchorX: 0.5, anchorY: 0.5 }); break; case 'splash': baseGraphics = self.attachAsset('mikasatower', { anchorX: 0.5, anchorY: 0.5 }); break; case 'slow': baseGraphics = self.attachAsset('erwintower', { anchorX: 0.5, anchorY: 0.5 }); break; case 'poison': baseGraphics = self.attachAsset('levitower', { anchorX: 0.5, anchorY: 0.5 }); break; default: baseGraphics = self.attachAsset('jeantower', { anchorX: 0.5, anchorY: 0.5 }); } // Remove level indicators - all towers now use only character images without dots var levelIndicators = []; // Keep empty array for existing code compatibility // Remove gunContainer - all towers now use only character images without arrows var gunContainer = new Container(); // Keep reference for existing code but don't add graphics self.updateLevelIndicators = function () { // No level indicators for any towers - all use only character images return; }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rapid') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect self.damage = 5 + self.level * 10; // double the effect self.bulletSpeed = 7 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades self.damage = 5 + self.level * 3; self.bulletSpeed = 7 + self.level * 0.7; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } self.refreshCellsInRange(); self.updateLevelIndicators(); // No upgrade animation needed - all towers use only character images without level indicators return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range if (distance <= self.getRange()) { // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { self.targetEnemy = self.findTarget(); if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); // All towers now use static character images without rotation if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; // Show upgrade menu without creating range indicators that block dragging var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; } // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0x00AAFF; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'sniper': bullet.children[0].tint = 0xFF5500; bullet.children[0].width = 15; bullet.children[0].height = 15; break; case 'splash': bullet.children[0].tint = 0x33CC00; bullet.children[0].width = 40; bullet.children[0].height = 40; break; case 'slow': bullet.children[0].tint = 0x9900FF; bullet.children[0].width = 35; bullet.children[0].height = 35; break; case 'poison': bullet.children[0].tint = 0x00FFAA; bullet.children[0].width = 35; bullet.children[0].height = 35; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; } } } self.refreshCellsInRange(); }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; self.hasEnoughGold = true; var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; // Reset tint first previewGraphics.tint = 0xFFFFFF; // Apply red tint only if cannot place OR not enough gold // Apply green tint if can place AND has enough gold if (self.canPlace && self.hasEnoughGold) { previewGraphics.tint = 0x00FF00; // Green for valid placement } else { previewGraphics.tint = 0xFF0000; // Red for invalid placement or insufficient gold } }; self.updatePlacementStatus = function () { // Very basic placement check - only verify tower fits in grid bounds self.canPlace = self.gridX >= 0 && self.gridY >= 0 && self.gridX + 1 < grid.cells.length && self.gridY + 1 < grid.cells[0].length; self.blockedByEnemy = false; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; self.checkPlacement(); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; // Get character name based on tower type var characterName; switch (self.tower.id) { case 'rapid': characterName = 'Eren'; break; case 'sniper': characterName = 'Sasha'; break; case 'splash': characterName = 'Mikasa'; break; case 'slow': characterName = 'Erwin'; break; case 'poison': characterName = 'Levi'; break; default: characterName = 'Jean'; } // Add background for tower type text for better readability var towerTypeBackground = self.attachAsset('notification', { anchorX: 0, anchorY: 0.5 }); towerTypeBackground.width = 600; towerTypeBackground.height = 100; towerTypeBackground.x = -870; towerTypeBackground.y = -160; towerTypeBackground.tint = 0x2C3E50; towerTypeBackground.alpha = 0.9; var towerTypeText = new Text2(characterName + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0.5); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0xFFFF00; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Boss Normal"; break; case 'fast': block.tint = 0x00AAFF; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xAA0000; waveType = "Boss Immune"; break; case 'flying': block.tint = 0xFFFF00; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the color of the wave type switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; break; case 'fast': block.tint = 0x00AAFF; break; case 'immune': block.tint = 0xAA0000; break; case 'flying': block.tint = 0xFFFF00; break; default: block.tint = 0xFF0000; break; } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a crown or some indicator to the wave marker for boss waves var bossIndicator = marker.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); bossIndicator.width = 30; bossIndicator.height = 30; bossIndicator.tint = 0xFFD700; // Gold color bossIndicator.y = -block.height / 2 - 15; // Change the wave type text to indicate boss waveType = "BOSS"; } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2(waveType, { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (every 10th wave) if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; for (var i = 0; i < totalWaves + 1; i++) { var marker = self.waveMarkers[i]; if (i === 0) { continue; } var block = marker.children[0]; if (i - 1 < currentWave) { block.alpha = .5; } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { waveTimer++; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; if (currentWave != 1) { var waveType = self.getWaveTypeName(currentWave); var enemyCount = self.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } } }; self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ var isHidingUpgradeMenu = false; var gameStarted = false; var mainMenuContainer = null; 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; } }); } function createMainMenu() { mainMenuContainer = new Container(); game.addChild(mainMenuContainer); // Add black background behind main menu var blackBackground = mainMenuContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); blackBackground.width = 2048; blackBackground.height = 2732; blackBackground.tint = 0x000000; blackBackground.x = 2048 / 2; blackBackground.y = 2732 / 2; // Add main menu background var menuBackground = mainMenuContainer.attachAsset('Mainmenu', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.x = 2048 / 2; menuBackground.y = 2732 / 2; // Create start button var startButton = new Container(); mainMenuContainer.addChild(startButton); // Add button background for better visual feedback var startButtonBg = startButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startButtonBg.width = 350; startButtonBg.height = 120; startButtonBg.tint = 0x2C3E50; startButtonBg.alpha = 0.8; var startText = new Text2("START", { size: 60, fill: 0xFFFFFF, weight: 800, font: "'Copperplate Gothic'" }); startText.anchor.set(0.5, 0.5); startButton.addChild(startText); // Add hover effect asset var hoverEffect = startButton.attachAsset('menuhover', { anchorX: 0.5, anchorY: 0.5 }); hoverEffect.visible = false; // Initially hidden hoverEffect.width = 370; hoverEffect.height = 140; startButton.x = 2048 - 150 - 189 + 76; // Move 2cm right (2cm * 37.8 pixels/cm) startButton.y = 2732 - 150 - 567 - 189 - 189 + 76 - 38; // Move 1cm higher (1cm * 37.8 pixels/cm) // Remove the button's move handler - we'll handle everything in the global handler // Move handling is now done in the main game.move handler startButton.down = function () { startGame(); }; // Create options button var optionsButton = new Container(); mainMenuContainer.addChild(optionsButton); // Add button background for options var optionsButtonBg = optionsButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); optionsButtonBg.width = 350; optionsButtonBg.height = 120; optionsButtonBg.tint = 0x2C3E50; optionsButtonBg.alpha = 0.8; var optionsText = new Text2("OPTIONS", { size: 60, fill: 0xFFFFFF, weight: 800, font: "'Copperplate Gothic'" }); optionsText.anchor.set(0.5, 0.5); optionsButton.addChild(optionsText); // Add hover effect asset for options button var optionsHoverEffect = optionsButton.attachAsset('menuhover', { anchorX: 0.5, anchorY: 0.5 }); optionsHoverEffect.visible = false; // Initially hidden optionsHoverEffect.width = 370; optionsHoverEffect.height = 140; optionsButton.x = 2048 - 150 - 189 + 76; // Same X position as start button optionsButton.y = 2732 - 150 - 567 - 189 - 189 + 76 - 38 + 189; // 5cm (189 pixels) lower than start button optionsButton.down = function () { // Options functionality can be added here later var notification = game.addChild(new Notification("Options menu coming soon!")); notification.x = 2048 / 2; notification.y = grid.height - 150; }; // Create story button var storyButton = new Container(); mainMenuContainer.addChild(storyButton); // Add button background for story var storyButtonBg = storyButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); storyButtonBg.width = 350; storyButtonBg.height = 120; storyButtonBg.tint = 0x2C3E50; storyButtonBg.alpha = 0.8; var storyText = new Text2("STORY", { size: 60, fill: 0xFFFFFF, weight: 800, font: "'Coplerplate Gothic'" }); storyText.anchor.set(0.5, 0.5); storyButton.addChild(storyText); // Add hover effect asset for story button var storyHoverEffect = storyButton.attachAsset('menuhover', { anchorX: 0.5, anchorY: 0.5 }); storyHoverEffect.visible = false; // Initially hidden storyHoverEffect.width = 370; storyHoverEffect.height = 140; storyButton.x = 2048 - 150 - 189 + 76; // Same X position as start and options buttons storyButton.y = 2732 - 150 - 567 - 189 - 189 + 76 - 38 + 189 + 189; // 5cm (189 pixels) lower than options button storyButton.down = function () { // Story functionality can be added here later var notification = game.addChild(new Notification("Story mode coming soon!")); notification.x = 2048 / 2; notification.y = grid.height - 150; }; } function startGame() { if (gameStarted) return; gameStarted = true; // Stop main menu music immediately LK.stopMusic(); // Hide main menu if (mainMenuContainer) { mainMenuContainer.visible = false; } // Initialize game elements that were previously created at startup initializeGameElements(); // Start the first wave if (waveIndicator) { waveIndicator.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; } } function initializeGameElements() { // Show previously hidden UI elements // waveIndicator.visible = true; // Removed - keep wave indicator hidden nextWaveButton.visible = true; towerLayer.visible = true; enemyLayer.visible = true; goldImage.visible = true; goldText.visible = true; // Update gold text to show current gold amount updateUI(); // Show source towers for (var i = 0; i < sourceTowers.length; i++) { sourceTowers[i].visible = true; } // Show player character if (player) { player.visible = true; } // Show base castle if (baseCastle) { baseCastle.visible = true; } // Any other game initialization that should happen when starting } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; var gold = 60; var lives = 20; var score = 0; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave var goldImage = LK.getAsset('gold', { anchorX: 0.5, anchorY: 0.5 }); goldImage.visible = false; // Hide gold image initially LK.gui.left.addChild(goldImage); goldImage.x = 38; // Move 1 cm right (1cm * 37.8 pixels/cm) goldImage.y = 0; var goldText = new Text2('0', { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0, 0.5); goldText.visible = false; // Hide gold text initially LK.gui.left.addChild(goldText); goldText.x = goldImage.x + goldImage.width / 2 + 10; // Position next to gold image with small gap goldText.y = 0; function updateUI() { goldText.setText(gold.toString()); } function setGold(value) { gold = value; updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); debugLayer.visible = false; game.addChild(debugLayer); towerLayer.visible = false; // Hide tower layer initially enemyLayer.visible = false; // Hide enemy layer initially game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY) { var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 5; switch (towerType) { case 'rapid': cost = 15; break; case 'sniper': cost = 25; break; case 'splash': cost = 35; break; case 'slow': cost = 45; break; case 'poison': cost = 55; break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); grid.pathFind(); grid.renderDebug(); return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } var draggedTower = null; var dragStartTime = 0; game.down = function (x, y, obj) { if (!gameStarted) { return; } var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } // Record drag start time for distinguishing between drag and click dragStartTime = LK.ticks; // Handle contextual player actions based on distance if (player && player.visible) { // Check if clicking in a valid game area (not on UI elements) var inGameArea = x >= grid.x && x <= grid.x + grid.cells.length * CELL_SIZE && y >= grid.y + CELL_SIZE * 4 && y <= grid.y + (grid.cells[0].length - 4) * CELL_SIZE; if (inGameArea) { // Calculate distance from player to tap location var dx = x - player.x; var dy = y - player.y; var distanceToTap = Math.sqrt(dx * dx + dy * dy); // If tapping close to hero (within attack range): attack if (distanceToTap <= player.attackRange) { // Trigger attack if (player.attack()) { // Attack successful - show feedback var notification = game.addChild(new Notification("Slash attack!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { // Attack failed - not enough stamina var notification = game.addChild(new Notification("Not enough stamina!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else { // If tapping far from hero: move to that location player.moveTo(x, y); } } } // Check if clicking on existing towers (no dragging allowed - towers are permanent) 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) { // Store the clicked tower for upgrade menu only (no dragging) draggedTower = tower; return; // Exit early to prevent other interactions } } 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) { // Check if player can afford this tower type first if (gold >= getTowerCost(tower.towerType)) { // Initialize dragging state isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.visible = true; // Position the preview at the initial drag position and update its status towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); towerPreview.updatePlacementStatus(); } else { // Show notification if can't afford var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } break; } } }; game.move = function (x, y, obj) { if (!gameStarted) { // Handle main menu hover effects when game hasn't started if (mainMenuContainer && mainMenuContainer.visible) { // Get button references from the main menu container var startButton = mainMenuContainer.children[2]; // third child after backgrounds var optionsButton = mainMenuContainer.children[3]; // fourth child var storyButton = mainMenuContainer.children[4]; // fifth child var startText = startButton.children[1]; // text is second child after background var optionsText = optionsButton.children[1]; // text is second child after background var storyText = storyButton.children[1]; // text is second child after background var hoverEffect = startButton.children[2]; // hover effect is third child var optionsHoverEffect = optionsButton.children[2]; // hover effect is third child var storyHoverEffect = storyButton.children[2]; // hover effect is third child // Calculate start button bounds more precisely - use button background size var buttonLeft = startButton.x - 175; // half of button background width (350/2) var buttonRight = startButton.x + 175; var buttonTop = startButton.y - 60; // half of button background height (120/2) var buttonBottom = startButton.y + 60; // Check if mouse is within start button bounds var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom; // Only show hover effect when mouse is precisely over the button hoverEffect.visible = mouseInButton; // Animate text color change with tween if (mouseInButton) { // Stop any existing tween on the text tween.stop(startText, { tint: true }); // Tween to red color tween(startText, { tint: 0xFF0000 }, { duration: 200, easing: tween.easeOut }); } else { // Stop any existing tween on the text tween.stop(startText, { tint: true }); // Tween back to white color tween(startText, { tint: 0xFFFFFF }, { duration: 200, easing: tween.easeOut }); } // Calculate options button bounds more precisely var optionsButtonLeft = optionsButton.x - 175; var optionsButtonRight = optionsButton.x + 175; var optionsButtonTop = optionsButton.y - 60; var optionsButtonBottom = optionsButton.y + 60; // Check if mouse is within options button bounds var mouseInOptionsButton = x >= optionsButtonLeft && x <= optionsButtonRight && y >= optionsButtonTop && y <= optionsButtonBottom; // Only show hover effect when mouse is precisely over the options button optionsHoverEffect.visible = mouseInOptionsButton; // Calculate story button bounds more precisely var storyButtonLeft = storyButton.x - 175; var storyButtonRight = storyButton.x + 175; var storyButtonTop = storyButton.y - 60; var storyButtonBottom = storyButton.y + 60; // Check if mouse is within story button bounds var mouseInStoryButton = x >= storyButtonLeft && x <= storyButtonRight && y >= storyButtonTop && y <= storyButtonBottom; // Only show hover effect when mouse is precisely over the story button storyHoverEffect.visible = mouseInStoryButton; // Animate options text color change with tween if (mouseInOptionsButton) { // Stop any existing tween on the options text tween.stop(optionsText, { tint: true }); // Tween to green color tween(optionsText, { tint: 0x00FF00 }, { duration: 200, easing: tween.easeOut }); } else { // Stop any existing tween on the options text tween.stop(optionsText, { tint: true }); // Tween back to white color tween(optionsText, { tint: 0xFFFFFF }, { duration: 200, easing: tween.easeOut }); } // Animate story text color change with tween if (mouseInStoryButton) { // Stop any existing tween on the story text tween.stop(storyText, { tint: true }); // Tween to blue color tween(storyText, { tint: 0x4444FF }, { duration: 200, easing: tween.easeOut }); } else { // Stop any existing tween on the story text tween.stop(storyText, { tint: true }); // Tween back to white color tween(storyText, { tint: 0xFFFFFF }, { duration: 200, easing: tween.easeOut }); } } else { // Hide hover effect when not in main menu - but check if elements exist first if (mainMenuContainer && mainMenuContainer.children.length > 2) { var startButton = mainMenuContainer.children[2]; var optionsButton = mainMenuContainer.children[3]; var storyButton = mainMenuContainer.children[4]; if (startButton && startButton.children.length > 2) { startButton.children[2].visible = false; // hoverEffect } if (optionsButton && optionsButton.children.length > 2) { optionsButton.children[2].visible = false; // optionsHoverEffect } if (storyButton && storyButton.children.length > 2) { storyButton.children[2].visible = false; // storyHoverEffect } } } return; } // Handle tower dragging when game is started if (isDragging && towerPreview.visible) { // Shift the y position upward by 1.5 tiles to show preview above finger towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); // Force update the preview appearance after position change towerPreview.updatePlacementStatus(); } }; game.up = function (x, y, obj) { if (!gameStarted) { return; } var dragDuration = LK.ticks - dragStartTime; var wasQuickClick = dragDuration < 10; // Less than ~1/6 second is considered a click var clickedOnTower = false; var clickedTower = null; 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; clickedTower = tower; 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; // Only allow placing new towers from source towers (no tower repositioning) if (!draggedTower && towerPreview.canPlace && towerPreview.hasEnoughGold) { // This is placing a new tower from source towers placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else if (!towerPreview.hasEnoughGold) { var notification = game.addChild(new Notification("Not enough gold!")); 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; // For existing towers, show upgrade menu on quick click if (draggedTower && wasQuickClick) { clickedTower = draggedTower; clickedOnTower = true; } draggedTower = null; var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } // Handle tower click for upgrade menu (only if it was a quick click) if (clickedOnTower && clickedTower && wasQuickClick) { // Trigger the tower's down method to show upgrade menu clickedTower.down(x, y, obj); } }; var waveIndicator = new WaveIndicator(); waveIndicator.visible = false; // Hide the wave indicator UI - permanently hidden game.addChild(waveIndicator); var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.visible = false; // Hide the next wave button nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); // Start playing main menu music immediately LK.playMusic('MainMenuSt'); // Create main menu when game loads createMainMenu(); var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; var sourceTowers = []; var towerSpacing = 300; // Increase spacing for larger towers var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; tower.visible = false; // Hide source towers initially towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; // Create base castle - positioned 7cm (264 pixels) up from bottom right var baseCastle = new BaseCastle(); baseCastle.x = grid.x + (grid.cells.length - 2) * CELL_SIZE; // 2 cells from right edge baseCastle.y = grid.y + (grid.cells[0].length - 7) * CELL_SIZE; // 7 cells up from bottom baseCastle.visible = false; // Hide initially until game starts game.addChild(baseCastle); // Create player character var player = new Player(); player.x = grid.x + grid.cells.length * CELL_SIZE / 2; // Center X of the map player.y = grid.y + grid.cells[0].length * CELL_SIZE / 2; // Center Y of the map player.visible = false; // Hide initially until game starts game.addChild(player); game.update = function () { if (!gameStarted) { return; } if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; if (isBossWave && waveType !== 'swarm') { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy = new Enemy(waveType); // Add enemy to the appropriate layer based on type if (enemy.isFlying) { // Add flying enemy to the top layer enemyLayerTop.addChild(enemy); // If it's a flying enemy, add its shadow to the middle layer if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else { // Add normal/ground enemies to the bottom layer enemyLayerBottom.addChild(enemy); } // Scale difficulty with wave number but don't apply to boss // as bosses already have their health multiplier // Use exponential scaling for health var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // All enemy types now spawn in the middle 6 tiles at the top spacing var gridWidth = 24; var midPoint = Math.floor(gridWidth / 2); // 12 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading enemy.cellX = spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; enemy.waveNumber = currentWave; enemies.push(enemy); // Add spawnpoint indicator at enemy spawn location (only for first enemy of each wave) if (i === 0) { var spawnIndicator = LK.getAsset('spawnpoint', { anchorX: 0.5, anchorY: 0.5 }); spawnIndicator.x = grid.x + spawnX * CELL_SIZE + CELL_SIZE / 2; spawnIndicator.y = grid.y + 0 * CELL_SIZE + CELL_SIZE / 2; // Position at top of grid spawnIndicator.alpha = 0.8; game.addChild(spawnIndicator); // Fade out the spawn indicator after 2 seconds tween(spawnIndicator, { alpha: 0 }, { duration: 2000, delay: 1000, onFinish: function onFinish() { if (spawnIndicator.parent) { game.removeChild(spawnIndicator); } } }); } } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Boss enemies give more gold and score var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
===================================================================
--- original.js
+++ change.js
@@ -3220,8 +3220,31 @@
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
enemy.waveNumber = currentWave;
enemies.push(enemy);
+ // Add spawnpoint indicator at enemy spawn location (only for first enemy of each wave)
+ if (i === 0) {
+ var spawnIndicator = LK.getAsset('spawnpoint', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ spawnIndicator.x = grid.x + spawnX * CELL_SIZE + CELL_SIZE / 2;
+ spawnIndicator.y = grid.y + 0 * CELL_SIZE + CELL_SIZE / 2; // Position at top of grid
+ spawnIndicator.alpha = 0.8;
+ game.addChild(spawnIndicator);
+ // Fade out the spawn indicator after 2 seconds
+ tween(spawnIndicator, {
+ alpha: 0
+ }, {
+ duration: 2000,
+ delay: 1000,
+ onFinish: function onFinish() {
+ if (spawnIndicator.parent) {
+ game.removeChild(spawnIndicator);
+ }
+ }
+ });
+ }
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
pixel art hearth icon. In-Game asset. 2d. High contrast. No shadows
attack on titan world game background. In-Game asset. 2d. High contrast. No shadows
armor pixel art. In-Game asset. 2d. High contrast. No shadows
a hero man on the horse character image. In-Game asset. 2d. High contrast. No shadows
hero swordsman. In-Game asset. 2d. High contrast. No shadows
sniper girl hero. In-Game asset. 2d. High contrast. No shadows
swordsman hero. In-Game asset. 2d. High contrast. No shadows
2d hero swordsman pixel sprite. In-Game asset. 2d. High contrast. No shadows
2d hero swordsman pixel sprite. In-Game asset. 2d. High contrast. No shadows
2d hero swordsman pixel sprite. sending shockwaves. In-Game asset. 2d. High contrast. No shadows
swordsman hero girl. In-Game asset. 2d. High contrast. No shadows
pixel art, swordsman attack, send shockwaves. In-Game asset. 2d. High contrast. No shadows
a hero, channeling, pixel art. In-Game asset. 2d. High contrast. No shadows
colossus monster, pixel art, standing, attacking. In-Game asset. 2d. High contrast. No shadows
colossus monster, pixel art, punching the ground. In-Game asset. 2d. High contrast. No shadows
swordsgirl hero attack, sending shockwaves, pixel art, no background. In-Game asset. 2d. High contrast. No shadows
swords girl hero, standing, pixel art, no backgroun. In-Game asset. 2d. High contrast. No shadows
monster giant, pixel art, no background. In-Game asset. 2d. High contrast. No shadows
monster giant with wings, flying , pixel art, no background. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows
castle gun, siege gun, pixel art. In-Game asset. 2d. High contrast. No shadows
castle base, pixel art. In-Game asset. 2d. High contrast. No shadows