User prompt
the color boxes are still there, there are red green yellox purpler boxes, remove them, also the hero stats text and the stat box isnt alligned, alling them properly. The stats are outside of the map rn fix it
User prompt
place hero stats text 5cm right, remove the color boxes next to the damage range speed stamina texts, just use icons
User prompt
make the hero stats more aesthetic, make a better looking one
User prompt
display the heros current stats on top right corner
User prompt
upgrade 1- increase your heros damage by 5, buyable 3 times, 40 coin cost 2- increase your heros range by 0.3 buyable 3times 30 coin cost 3- increase your heros attack speed by 0.3 cost 50 gold buyable 3 times. Add this system ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
place shop 3cm more lower
User prompt
add a shop 3 cm lower from the next wave button, use shop asset as the image, when you go inside the shop you can buy upgrades, create the shop first and then we will modify inside
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'damage')' in or related to this line: 'otherEnemy.health -= self.hangeInstance.damage * 0.5;' Line Number: 1894
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'damage')' in or related to this line: 'closestEnemy.health -= self.hangeInstance.damage;' Line Number: 1869
User prompt
also when hange attacks, use slashanim too ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
when armin tower attacks, switch to arminattack, then switch back ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
when towers attack enemies, send slashanim asset as an attack animation to the enemies, except sashatower ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
add slashing sword waves animation for all towers except for sashatower ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
remove shooting balls animation from all towers except sashatower
User prompt
reduce eren tower fire rate to 1.0/s increase its damage to 10
User prompt
after eren tower summoned, just like erwin tower, he will have a meter, after 10 seconds the meter is full, he transform into erentitan asset, after 10 second he transform backs to eren tower again. During eren tower form he cant attack, in erentitan form only he can attack ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
place the next wave ui 5cm right 10 cm low
User prompt
at the exact moment hange spawned, erwintower switches to erwinattack asset, after hange summoned, it switches back to erwintower asset immideatly, so erwinattack asset only persist for 1 second ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
at the exact moment erwin tower summons hange, it switchs to erwinattack, after hange summoned, it switched back to erwintower immideatly ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
switch back to erwintower asset when channeling, only switch to erwinattack asset when hange summoned ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
when erwin summon hange, swith to erwinattack asset, after summon finish switch back to erwintower asset again ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
lower the hanges fire rate by 0. 5
User prompt
lower the hanges fire rate 0.3
User prompt
Please fix the bug: 'Timeout.tick error: Cannot read properties of null (reading 'removeChild')' in or related to this line: 'self.hangeInstance.removeChild(attackGraphics);' Line Number: 1751
User prompt
when hange attacks, switch to hangeattack, after finish switch back to normal ↪💡 Consider importing and using the following plugins: @upit/tween.v1
/**** * 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.3; cellGraphics.tint = 0x8B4513; // Brown road color } else { cellGraphics.alpha = 0.3; cellGraphics.tint = 0x8B4513; // Brown road color } 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 = 'Armin'; } // 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; // Initialize channeling properties for Erwin tower self.isChanneling = false; self.channelingTimer = 0; self.hangeActive = false; self.hangeSummoned = false; self.hangeInstance = null; // Initialize transformation properties for Eren tower self.isTransforming = false; self.transformationTimer = 0; self.isTitan = false; self.titanDuration = 0; // Declare baseGraphics early to avoid undefined errors 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 }); } // Create channel bar for Erwin tower (initially hidden) self.channelBarBg = null; self.channelBar = null; if (self.id === 'slow') { // Create channel bar background self.channelBarBg = self.attachAsset('staminaBarBg', { anchorX: 0.5, anchorY: 0.5 }); self.channelBarBg.y = -baseGraphics.height / 2 - 40; self.channelBarBg.width = 150; self.channelBarBg.height = 16; self.channelBarBg.visible = false; // Create channel bar self.channelBar = self.attachAsset('staminaBar', { anchorX: 0, anchorY: 0.5 }); self.channelBar.x = -self.channelBarBg.width / 2; self.channelBar.y = -baseGraphics.height / 2 - 40; self.channelBar.width = 0; self.channelBar.height = 16; self.channelBar.tint = 0x00ff00; // Green color self.channelBar.visible = false; } // Create transformation meter for Eren tower (initially hidden) self.transformBarBg = null; self.transformBar = null; if (self.id === 'rapid') { // Create transformation bar background self.transformBarBg = self.attachAsset('staminaBarBg', { anchorX: 0.5, anchorY: 0.5 }); self.transformBarBg.y = -baseGraphics.height / 2 - 40; self.transformBarBg.width = 150; self.transformBarBg.height = 16; self.transformBarBg.visible = false; // Create transformation bar self.transformBar = self.attachAsset('staminaBar', { anchorX: 0, anchorY: 0.5 }); self.transformBar.x = -self.transformBarBg.width / 2; self.transformBar.y = -baseGraphics.height / 2 - 40; self.transformBar.width = 0; self.transformBar.height = 16; self.transformBar.tint = 0xff0000; // Red color for transformation self.transformBar.visible = false; } 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; } // 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 () { // Handle Eren tower transformation if (self.id === 'rapid') { if (!self.isTransforming && !self.isTitan) { // Normal Eren tower - start transformation timer after spawning self.transformationTimer++; // Show transformation meter if (self.transformBarBg && self.transformBar) { self.transformBarBg.visible = true; self.transformBar.visible = true; // Calculate progress (transformation takes 600 frames = 10 seconds) var progress = self.transformationTimer / 600; self.transformBar.width = self.transformBarBg.width * progress; } // Transform to titan after 10 seconds if (self.transformationTimer >= 600) { self.isTitan = true; self.titanDuration = 600; // Stay as titan for 10 seconds self.transformationTimer = 0; // Switch to titan asset baseGraphics.visible = false; var titanGraphics = self.attachAsset('erentitan', { anchorX: 0.5, anchorY: 0.5 }); self.titanGraphics = titanGraphics; // Hide transformation meter when transformed if (self.transformBarBg && self.transformBar) { self.transformBarBg.visible = false; self.transformBar.visible = false; } } } else if (self.isTitan) { // In titan form - countdown to transform back self.titanDuration--; if (self.titanDuration <= 0) { // Transform back to normal Eren tower self.isTitan = false; self.transformationTimer = 0; // Remove titan graphics and show normal graphics if (self.titanGraphics && self.titanGraphics.parent) { self.removeChild(self.titanGraphics); } baseGraphics.visible = true; } } } // Handle Erwin tower channeling and Hange summoning if (self.id === 'slow' && self.isChanneling) { self.channelingTimer--; // Update channel bar progress if (self.channelBarBg && self.channelBar) { self.channelBarBg.visible = true; self.channelBar.visible = true; // Calculate progress (channeling starts at 600 and counts down to 0) var progress = (600 - self.channelingTimer) / 600; self.channelBar.width = self.channelBarBg.width * progress; } // After 5 seconds (300 frames), summon Hange if (!self.hangeSummoned && self.channelingTimer <= 300) { self.hangeSummoned = true; self.hangeActive = true; // Switch Erwin tower to attack asset at the exact moment of spawning var attackGraphics = self.attachAsset('erwinattack', { anchorX: 0.5, anchorY: 0.5 }); // Hide the original tower graphics baseGraphics.visible = false; // Switch back to normal graphics after 1 second (60 frames) LK.setTimeout(function () { // Remove attack graphics if (attackGraphics && attackGraphics.parent) { self.removeChild(attackGraphics); } // Show original tower graphics again baseGraphics.visible = true; }, 1000); // 1000ms attack animation duration // Create Hange near the tower self.hangeInstance = new Container(); var hangeGraphics = self.hangeInstance.attachAsset('hange', { anchorX: 0.5, anchorY: 0.5 }); // Position Hange next to the tower self.hangeInstance.x = self.x + 50; self.hangeInstance.y = self.y; game.addChild(self.hangeInstance); // Initialize Hange properties self.hangeInstance.attackTimer = 0; self.hangeInstance.attackRate = 48; // Attack every 0.8 seconds self.hangeInstance.damage = self.damage * 2; // Hange deals double damage self.hangeInstance.range = self.getRange() * 1.5; // Hange has 1.5x range self.hangeInstance.lifetime = 300; // Dies after 5 seconds } // Handle Hange's attacks if summoned if (self.hangeSummoned && self.hangeInstance && self.hangeInstance.parent) { self.hangeInstance.attackTimer++; self.hangeInstance.lifetime--; // Hange attacks enemies if (self.hangeInstance.attackTimer >= self.hangeInstance.attackRate) { self.hangeInstance.attackTimer = 0; // Find closest enemy in range var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.hangeInstance.x; var dy = enemy.y - self.hangeInstance.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.hangeInstance.range && distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // Attack the closest enemy if (closestEnemy) { // Switch to attack asset var attackGraphics = self.hangeInstance.attachAsset('hangeattack', { anchorX: 0.5, anchorY: 0.5 }); // Hide the original hange graphics var hangeGraphics = self.hangeInstance.children[0]; hangeGraphics.visible = false; // Switch back to normal graphics after attack duration LK.setTimeout(function () { // Remove attack graphics if (attackGraphics && attackGraphics.parent && self.hangeInstance && self.hangeInstance.parent) { self.hangeInstance.removeChild(attackGraphics); } // Show original hange graphics again if (hangeGraphics && self.hangeInstance && self.hangeInstance.parent) { hangeGraphics.visible = true; } }, 500); // 500ms attack animation duration var bullet = new Bullet(self.hangeInstance.x, self.hangeInstance.y, closestEnemy, self.hangeInstance.damage, self.bulletSpeed * 1.5); bullet.type = 'splash'; // Hange uses splash attacks game.addChild(bullet); bullets.push(bullet); closestEnemy.bulletsTargetingThis.push(bullet); } } // Remove Hange after lifetime expires if (self.hangeInstance.lifetime <= 0) { game.removeChild(self.hangeInstance); self.hangeInstance = null; self.hangeActive = false; } } // End channeling after 10 seconds if (self.channelingTimer <= 0) { self.isChanneling = false; // Hide channel bar when channeling ends if (self.channelBarBg && self.channelBar) { self.channelBarBg.visible = false; self.channelBar.visible = false; } } return; // Skip normal tower behavior while channeling } // Hide channel bar when not channeling (for Erwin towers) if (self.id === 'slow' && !self.isChanneling && self.channelBarBg && self.channelBar) { self.channelBarBg.visible = false; self.channelBar.visible = false; } 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 () { // Special handling for Eren tower - can only attack in titan form if (self.id === 'rapid') { if (!self.isTitan) { return; // Cannot attack in normal form } } // Special handling for Erwin tower if (self.id === 'slow') { // Check if already channeling or if Hange is active if (self.isChanneling || self.hangeActive) { return; // Cannot attack while channeling or Hange is active } // Start channeling process self.isChanneling = true; self.channelingTimer = 600; // 10 seconds at 60 FPS self.hangeActive = false; self.hangeSummoned = false; // Show channel bar when starting to channel if (self.channelBarBg && self.channelBar) { self.channelBarBg.visible = true; self.channelBar.visible = true; self.channelBar.width = 0; // Start with empty bar } return; } 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; } // Handle Eren titan tower attack animation if (self.id === 'rapid' && self.isTitan) { // Switch to attack asset var attackGraphics = self.attachAsset('erentitanattack', { anchorX: 0.5, anchorY: 0.5 }); // Hide the titan graphics if (self.titanGraphics) { self.titanGraphics.visible = false; } // Switch back to titan graphics after attack duration LK.setTimeout(function () { // Remove attack graphics if (attackGraphics && attackGraphics.parent) { self.removeChild(attackGraphics); } // Show titan graphics again if (self.titanGraphics) { self.titanGraphics.visible = true; } }, 500); // 500ms attack animation duration } // Handle Sasha tower attack animation if (self.id === 'sniper') { // Switch to attack asset var attackGraphics = self.attachAsset('sashaattack', { anchorX: 0.5, anchorY: 0.5 }); // Hide the original tower graphics baseGraphics.visible = false; // Switch back to normal graphics after attack duration LK.setTimeout(function () { // Remove attack graphics if (attackGraphics && attackGraphics.parent) { self.removeChild(attackGraphics); } // Show original tower graphics again baseGraphics.visible = true; }, 500); // 500ms attack animation duration } // Handle Mikasa tower attack animation if (self.id === 'splash') { // Switch to attack asset var attackGraphics = self.attachAsset('mikasaattack', { anchorX: 0.5, anchorY: 0.5 }); // Hide the original tower graphics baseGraphics.visible = false; // Switch back to normal graphics after attack duration LK.setTimeout(function () { // Remove attack graphics if (attackGraphics && attackGraphics.parent) { self.removeChild(attackGraphics); } // Show original tower graphics again baseGraphics.visible = true; }, 500); // 500ms attack animation duration } // 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 () { // Check if tower fits in grid bounds and doesn't block path or overlap roads self.canPlace = self.gridX >= 0 && self.gridY >= 0 && self.gridX + 1 < grid.cells.length && self.gridY + 1 < grid.cells[0].length && !wouldBlockPath(self.gridX, self.gridY); 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 = 'Armin'; } // 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; // Move 1cm lower (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; // Show difficulty page instead of starting game directly showDifficultyPage(); } function showDifficultyPage() { // Hide main menu if (mainMenuContainer) { mainMenuContainer.visible = false; } // Create difficulty page container var difficultyContainer = new Container(); game.addChild(difficultyContainer); // Add black background var blackBackground = difficultyContainer.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 difficulty page background var difficultyBackground = difficultyContainer.attachAsset('Difficulty', { anchorX: 0.5, anchorY: 0.5 }); difficultyBackground.x = 2048 / 2; difficultyBackground.y = 2732 / 2; // Create difficulty buttons var difficulties = [{ name: "CADET", y: 862 // Move 40 pixels lower }, { name: "SQUAD LEADER", y: 1287 // 3cm higher (113 pixels) }, { name: "CAPTAIN", y: 1789 // Move 2cm lower (76 pixels) }, { name: "TATAKAE", y: 2253 // Move 3cm lower (113 pixels) }]; for (var i = 0; i < difficulties.length; i++) { var difficulty = difficulties[i]; var difficultyButton = new Container(); difficultyContainer.addChild(difficultyButton); // Add button background var buttonBg = difficultyButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBg.width = 600; buttonBg.height = 120; // Apply specific colors for difficulty buttons if (difficulty.name === "CADET") { buttonBg.tint = 0xD2B48C; // Tan color } else if (difficulty.name === "SQUAD LEADER") { buttonBg.tint = 0x93C47D; // Pistachio green color } else if (difficulty.name === "CAPTAIN") { buttonBg.tint = 0x8B0000; // Dark red color } else if (difficulty.name === "TATAKAE") { buttonBg.tint = 0x4682B4; // Darker steel blue color } else { buttonBg.tint = 0x2C3E50; } buttonBg.alpha = 0.8; var buttonText = new Text2(difficulty.name, { size: 52, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); difficultyButton.addChild(buttonText); difficultyButton.x = 2048 / 2; difficultyButton.y = difficulty.y; // Add tween animation for all difficulty buttons if (difficulty.name === "CADET") { // Start slightly higher and animate to final position difficultyButton.y = difficulty.y - 50; tween(difficultyButton, { y: difficulty.y }, { duration: 800, easing: tween.bounceOut }); } else if (difficulty.name === "SQUAD LEADER") { // Start slightly higher and animate to final position difficultyButton.y = difficulty.y - 50; tween(difficultyButton, { y: difficulty.y }, { duration: 800, easing: tween.bounceOut }); } else if (difficulty.name === "CAPTAIN") { // Start slightly higher and animate to final position difficultyButton.y = difficulty.y - 50; tween(difficultyButton, { y: difficulty.y }, { duration: 800, easing: tween.bounceOut }); } else if (difficulty.name === "TATAKAE") { // Start slightly higher and animate to final position difficultyButton.y = difficulty.y - 50; tween(difficultyButton, { y: difficulty.y }, { duration: 800, easing: tween.bounceOut }); } // Store difficulty name for button handler difficultyButton.difficultyName = difficulty.name; // Add hover animation for each difficulty button difficultyButton.move = function (x, y, obj) { // Check if mouse is within button bounds var buttonLeft = this.x - 300; // half of button width (600/2) var buttonRight = this.x + 300; var buttonTop = this.y - 60; // half of button height (120/2) var buttonBottom = this.y + 60; var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom; if (mouseInButton) { // Stop any existing scale tweens tween.stop(this, { scaleX: true, scaleY: true }); // Scale up on hover tween(this, { scaleX: 1.1, scaleY: 1.1 }, { duration: 200, easing: tween.easeOut }); } else { // Stop any existing scale tweens tween.stop(this, { scaleX: true, scaleY: true }); // Scale back to normal tween(this, { scaleX: 1.0, scaleY: 1.0 }, { duration: 200, easing: tween.easeOut }); } }; difficultyButton.down = function () { // Start game with selected difficulty startGameWithDifficulty(this.difficultyName); // Remove difficulty page difficultyContainer.destroy(); }; } } function startGameWithDifficulty(selectedDifficulty) { if (gameStarted) return; gameStarted = true; // Stop main menu music immediately LK.stopMusic(); // Store selected difficulty (can be used for game balancing) game.selectedDifficulty = selectedDifficulty; // Initialize game elements that were previously created at startup initializeGameElements(); // Start the first wave if (waveIndicator) { waveIndicator.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; } // Show difficulty selection notification var notification = game.addChild(new Notification("Difficulty: " + selectedDifficulty + " selected!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } 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 spawn point if (spawnPoint) { spawnPoint.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; // Create road path from spawn to base function createRoadPath() { // Clear existing path cells (set them to grass/floor) for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[0].length; j++) { var cell = grid.cells[i][j]; if (cell.type === 0) { // Convert floor cells to walls first cell.type = 1; } } } // Define the maze-like road path coordinates with longer turns var roadPath = [ // Start from spawn point (column 11) - extended downward { x: 11, y: 0 }, { x: 11, y: 1 }, { x: 11, y: 2 }, { x: 11, y: 3 }, { x: 11, y: 4 }, { x: 11, y: 5 }, { x: 11, y: 6 }, { x: 11, y: 7 }, { x: 11, y: 8 }, // First turn right - extended rightward { x: 12, y: 8 }, { x: 13, y: 8 }, { x: 14, y: 8 }, { x: 15, y: 8 }, { x: 16, y: 8 }, { x: 17, y: 8 }, // Turn down - extended downward { x: 17, y: 9 }, { x: 17, y: 10 }, { x: 17, y: 11 }, { x: 17, y: 12 }, { x: 17, y: 13 }, { x: 17, y: 14 }, // Turn left - extended leftward { x: 16, y: 14 }, { x: 15, y: 14 }, { x: 14, y: 14 }, { x: 13, y: 14 }, { x: 12, y: 14 }, { x: 11, y: 14 }, { x: 10, y: 14 }, { x: 9, y: 14 }, { x: 8, y: 14 }, { x: 7, y: 14 }, // Turn down - extended downward { x: 7, y: 15 }, { x: 7, y: 16 }, { x: 7, y: 17 }, { x: 7, y: 18 }, { x: 7, y: 19 }, // Turn right - extended rightward { x: 8, y: 19 }, { x: 9, y: 19 }, { x: 10, y: 19 }, { x: 11, y: 19 }, { x: 12, y: 19 }, { x: 13, y: 19 }, { x: 14, y: 19 }, { x: 15, y: 19 }, { x: 16, y: 19 }, { x: 17, y: 19 }, { x: 18, y: 19 }, // Turn down - extended downward { x: 18, y: 20 }, { x: 18, y: 21 }, { x: 18, y: 22 }, { x: 18, y: 23 }, { x: 18, y: 24 }, // Turn left - extended leftward { x: 17, y: 24 }, { x: 16, y: 24 }, { x: 15, y: 24 }, { x: 14, y: 24 }, { x: 13, y: 24 }, { x: 12, y: 24 }, // Turn down - extended downward { x: 12, y: 25 }, { x: 12, y: 26 }, { x: 12, y: 27 }, // Turn right towards base - extended rightward { x: 13, y: 27 }, { x: 14, y: 27 }, { x: 15, y: 27 }, { x: 16, y: 27 }, { x: 17, y: 27 }, { x: 18, y: 27 }, { x: 19, y: 27 }, { x: 20, y: 27 }, { x: 21, y: 27 }]; // Set road tiles along the path for (var i = 0; i < roadPath.length; i++) { var pathPoint = roadPath[i]; var cell = grid.getCell(pathPoint.x, pathPoint.y); if (cell) { cell.type = 0; // Set as passable road } } // Ensure spawn and goal areas remain correct for (var i = 11 - 3; i <= 11 + 3; i++) { var spawnCell = grid.getCell(i, 0); if (spawnCell) { spawnCell.type = 2; // Spawn } } // Set castle area as goal (2x2 area around castle position) var castleGridX = grid.cells.length - 3; // 2 cells from right edge var castleGridY = grid.cells[0].length - 8; // 7 cells up from bottom for (var ci = 0; ci < 2; ci++) { for (var cj = 0; cj < 2; cj++) { var castleCell = grid.getCell(castleGridX + ci, castleGridY + cj); if (castleCell) { castleCell.type = 3; // Set as goal } } } } // Create the road path createRoadPath(); grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); debugLayer.visible = true; // Show the debug layer so road tiles are visible 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) { // First check if any cell in the 2x2 tower footprint is a road tile (type 0) 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) { // Cannot build on road tiles return true; } } } 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 nextWaveButton.x = 189; // Move 5cm right (5cm * 37.8 pixels/cm) nextWaveButton.y = 378; // Move 10cm down (10cm * 37.8 pixels/cm) 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 spawn point image at the enemy spawn location var spawnPoint = LK.getAsset('spawnpoint', { anchorX: 0.5, anchorY: 0.5 }); spawnPoint.x = grid.x + 11 * CELL_SIZE + CELL_SIZE / 2 - 38; // Center of spawn area (middle of 6 tiles), moved 1cm left (38 pixels) spawnPoint.y = grid.y + 264 - 76; // Top of the grid where enemies spawn, moved 7cm (264 pixels) lower, then 2cm up (76 pixels) spawnPoint.visible = false; // Hide initially until game starts game.addChild(spawnPoint); // 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; } // Initialize enemy spawn tracking if not already set if (!game.enemySpawnQueue) { game.enemySpawnQueue = []; game.enemySpawnTimer = 0; game.enemySpawnDelay = 120; // 2 second delay (120 frames = 2 seconds at 60 FPS) } // Create spawn queue for this wave for (var i = 0; i < enemyCount; i++) { game.enemySpawnQueue.push({ waveType: waveType, waveNumber: currentWave }); } } 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; } } // Handle enemy spawning from queue one by one if (game.enemySpawnQueue && game.enemySpawnQueue.length > 0) { game.enemySpawnTimer++; if (game.enemySpawnTimer >= game.enemySpawnDelay) { game.enemySpawnTimer = 0; // Spawn one enemy from the queue var spawnData = game.enemySpawnQueue.shift(); var enemy = new Enemy(spawnData.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 var healthMultiplier = Math.pow(1.12, spawnData.waveNumber); // ~20% increase per wave enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Position enemy exactly on top of spawnpoint image var spawnX = 11; // Center column where spawnpoint is located var spawnY = 0; // Exactly at the spawnpoint position enemy.cellX = spawnX; enemy.cellY = spawnY; enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; // Set enemy position to match spawnpoint exactly, then move 3cm lower and 1cm right enemy.x = spawnPoint.x + 38; // Move 1cm right (1cm * 37.8 pixels/cm) enemy.y = spawnPoint.y + 113; // Move 3cm lower (3cm * 37.8 pixels/cm) enemy.waveNumber = spawnData.waveNumber; enemies.push(enemy); } } 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
@@ -1374,8 +1374,13 @@
self.channelingTimer = 0;
self.hangeActive = false;
self.hangeSummoned = false;
self.hangeInstance = null;
+ // Initialize transformation properties for Eren tower
+ self.isTransforming = false;
+ self.transformationTimer = 0;
+ self.isTitan = false;
+ self.titanDuration = 0;
// Declare baseGraphics early to avoid undefined errors
var baseGraphics;
switch (self.id) {
case 'rapid':
@@ -1438,8 +1443,33 @@
self.channelBar.height = 16;
self.channelBar.tint = 0x00ff00; // Green color
self.channelBar.visible = false;
}
+ // Create transformation meter for Eren tower (initially hidden)
+ self.transformBarBg = null;
+ self.transformBar = null;
+ if (self.id === 'rapid') {
+ // Create transformation bar background
+ self.transformBarBg = self.attachAsset('staminaBarBg', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ self.transformBarBg.y = -baseGraphics.height / 2 - 40;
+ self.transformBarBg.width = 150;
+ self.transformBarBg.height = 16;
+ self.transformBarBg.visible = false;
+ // Create transformation bar
+ self.transformBar = self.attachAsset('staminaBar', {
+ anchorX: 0,
+ anchorY: 0.5
+ });
+ self.transformBar.x = -self.transformBarBg.width / 2;
+ self.transformBar.y = -baseGraphics.height / 2 - 40;
+ self.transformBar.width = 0;
+ self.transformBar.height = 16;
+ self.transformBar.tint = 0xff0000; // Red color for transformation
+ self.transformBar.visible = false;
+ }
switch (self.id) {
case 'rapid':
self.fireRate = 30;
self.damage = 5;
@@ -1624,8 +1654,54 @@
}
return closestEnemy;
};
self.update = function () {
+ // Handle Eren tower transformation
+ if (self.id === 'rapid') {
+ if (!self.isTransforming && !self.isTitan) {
+ // Normal Eren tower - start transformation timer after spawning
+ self.transformationTimer++;
+ // Show transformation meter
+ if (self.transformBarBg && self.transformBar) {
+ self.transformBarBg.visible = true;
+ self.transformBar.visible = true;
+ // Calculate progress (transformation takes 600 frames = 10 seconds)
+ var progress = self.transformationTimer / 600;
+ self.transformBar.width = self.transformBarBg.width * progress;
+ }
+ // Transform to titan after 10 seconds
+ if (self.transformationTimer >= 600) {
+ self.isTitan = true;
+ self.titanDuration = 600; // Stay as titan for 10 seconds
+ self.transformationTimer = 0;
+ // Switch to titan asset
+ baseGraphics.visible = false;
+ var titanGraphics = self.attachAsset('erentitan', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ self.titanGraphics = titanGraphics;
+ // Hide transformation meter when transformed
+ if (self.transformBarBg && self.transformBar) {
+ self.transformBarBg.visible = false;
+ self.transformBar.visible = false;
+ }
+ }
+ } else if (self.isTitan) {
+ // In titan form - countdown to transform back
+ self.titanDuration--;
+ if (self.titanDuration <= 0) {
+ // Transform back to normal Eren tower
+ self.isTitan = false;
+ self.transformationTimer = 0;
+ // Remove titan graphics and show normal graphics
+ if (self.titanGraphics && self.titanGraphics.parent) {
+ self.removeChild(self.titanGraphics);
+ }
+ baseGraphics.visible = true;
+ }
+ }
+ }
// Handle Erwin tower channeling and Hange summoning
if (self.id === 'slow' && self.isChanneling) {
self.channelingTimer--;
// Update channel bar progress
@@ -1817,8 +1893,14 @@
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
+ // Special handling for Eren tower - can only attack in titan form
+ if (self.id === 'rapid') {
+ if (!self.isTitan) {
+ return; // Cannot attack in normal form
+ }
+ }
// Special handling for Erwin tower
if (self.id === 'slow') {
// Check if already channeling or if Hange is active
if (self.isChanneling || self.hangeActive) {
@@ -1851,8 +1933,31 @@
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
+ // Handle Eren titan tower attack animation
+ if (self.id === 'rapid' && self.isTitan) {
+ // Switch to attack asset
+ var attackGraphics = self.attachAsset('erentitanattack', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ // Hide the titan graphics
+ if (self.titanGraphics) {
+ self.titanGraphics.visible = false;
+ }
+ // Switch back to titan graphics after attack duration
+ LK.setTimeout(function () {
+ // Remove attack graphics
+ if (attackGraphics && attackGraphics.parent) {
+ self.removeChild(attackGraphics);
+ }
+ // Show titan graphics again
+ if (self.titanGraphics) {
+ self.titanGraphics.visible = true;
+ }
+ }, 500); // 500ms attack animation duration
+ }
// Handle Sasha tower attack animation
if (self.id === 'sniper') {
// Switch to attack asset
var attackGraphics = self.attachAsset('sashaattack', {
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