User prompt
make a kill count ui. Its for the kill count and scoreboard. apply it
User prompt
reduce the dificulty just a litlle bit for all levels
User prompt
increase the difficulty a little bit for all levels
User prompt
the time between waves are too much, reduce the time between the waves for all difficulties
User prompt
lets adjust the difficulties, right now they are all the same, cadet-easy squad leader-medium captain-hard tatakae-extreme, increase enemies number,health according to the difficulties
User prompt
make a box or lines for the towers section, seperate the towers section and make an aesthetic ui
User prompt
chamge the background transparency to 55 percent
User prompt
move the gold ui 10cm upper
User prompt
move the gold ui 20cm upper
User prompt
move gold ui 2 cm under the shop ui
User prompt
move gold ui under the shop ui
User prompt
make the background 70 percent transperent
User prompt
replace the background with the background asset
User prompt
for the story button in the menu, when you click it there is a info about the story, you can click back and go back to the menu. the story text: Story Humanity is on the brink of extinction. The Titans have breached the final wall. The last remnants of human civilization are under siege — and this time, there is no cavalry coming. The cities are burning, the soldiers are scattered, and the people cry out in fear. Only one defense remains: you. You stand alone on the last line of resistance, entrusted with the power to command the greatest warriors humanity has ever known. As the Titans march forward in waves of destruction, you must deploy Armin, Sasha, Erwin, Levi, Mikasa, and Eren — each with their own unique strengths, tactics, and abilities — to push back the horde. This is not just a battle. This is a last stand. Use the terrain, upgrade your defenses, and make every decision count. The fate of humanity lies in your hands. Hold the line. Command the legends. Stop the Titans.
User prompt
change the enemies names, enemy-aberrant titans, enemyimmune-female titan, enemyflying- annie enemyfast-armoredtitan, enemyswarm- ymir titan
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'health')' in or related to this line: 'self.targetEnemy.health -= self.damage;' Line Number: 2116
User prompt
increase levis damage to 13, increase erens damage to 18, increase all towers cost by 5
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'health')' in or related to this line: 'self.targetEnemy.health -= self.damage;' Line Number: 2113
User prompt
when levitower attacks, switch to leviattack, then switch to normal ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
the towers cannot build on top of each other, they cant overlap
User prompt
instead of a red arrow next to the bases health ui, use healthui asset
User prompt
Change castle health upgrade cost to 35 gold ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
make the castle health upgrade 35 gold ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
change castle health upgrade to 30 gold ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
place a base health ui for players to see the health of the baes with a heart icon
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var BaseCastle = Container.expand(function () { var self = Container.call(this); // Castle stats self.maxHealth = 100; self.health = self.maxHealth; // Attack properties self.canAttack = false; self.attackDamage = 20; self.attackRange = CELL_SIZE * 4; self.fireRate = 120; // Attack every 2 seconds self.lastFired = 0; self.targetEnemy = null; // 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 } }; self.findTarget = function () { if (!self.canAttack) return null; var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.attackRange && distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } return closestEnemy; }; self.update = function () { if (!self.canAttack) return; self.targetEnemy = self.findTarget(); if (self.targetEnemy && LK.ticks - self.lastFired >= self.fireRate) { // Create bullet to attack enemy var bullet = new Bullet(self.x, self.y, self.targetEnemy, self.attackDamage, 8); bullet.type = 'castle'; // Make castle bullets more visible bullet.children[0].tint = 0xFF0000; bullet.children[0].width = 20; bullet.children[0].height = 20; game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); self.lastFired = LK.ticks; } }; 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) { // Check if target enemy still exists before applying damage if (self.targetEnemy && self.targetEnemy.health !== undefined) { // 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: 28, fill: 0xFFFFFF, weight: 700 }); 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-aberrant_titans'; if (self.type !== 'normal') { switch (self.type) { case 'fast': assetId = 'enemyfast-armoredtitan'; break; case 'flying': assetId = 'enemyflying-annie'; break; case 'immune': assetId = 'enemyimmune-female_titan'; break; case 'swarm': assetId = 'enemyswarm-ymir_titan'; break; default: assetId = 'enemy-aberrant_titans'; } } 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-aberrant_titans', { 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.toString(), { size: 44, 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.toString(), { size: 44, 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.toString(), { size: 48, 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: 48, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 2; typeLabelShadow.y = -182; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(characterName, { size: 48, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -180; 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.toString(), { size: 42, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 2; costLabelShadow.y = 162; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost.toString(), { size: 42, 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 = 60; self.damage = 18; 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 = 13; 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 // Send slashanim asset to enemy instead of bullet var slashAnim = LK.getAsset('slashanim', { anchorX: 0.5, anchorY: 0.5 }); slashAnim.x = self.hangeInstance.x; slashAnim.y = self.hangeInstance.y; game.addChild(slashAnim); // Animate slashanim to enemy using tween tween(slashAnim, { x: closestEnemy.x, y: closestEnemy.y }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { // Apply damage when animation reaches enemy if (self.hangeInstance && self.hangeInstance.parent) { closestEnemy.health -= self.hangeInstance.damage; } else { // Use default damage if hangeInstance is no longer available closestEnemy.health -= self.damage * 2; // Hange deals double damage } if (closestEnemy.health <= 0) { closestEnemy.health = 0; } else { closestEnemy.healthBar.width = closestEnemy.health / closestEnemy.maxHealth * 70; } // Apply splash damage effects since Hange uses splash attacks // Create visual splash effect var splashEffect = new EffectIndicator(closestEnemy.x, closestEnemy.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 !== closestEnemy) { var splashDx = otherEnemy.x - closestEnemy.x; var splashDy = otherEnemy.y - closestEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) if (self.hangeInstance && self.hangeInstance.parent) { otherEnemy.health -= self.hangeInstance.damage * 0.5; } else { // Use default damage if hangeInstance is no longer available otherEnemy.health -= self.damage * 2 * 0.5; // Hange deals double damage } if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } // Remove the slashanim asset after applying effects slashAnim.destroy(); } }); } } // 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) { // Only create bullets for Sasha tower (sniper) if (self.id === 'sniper') { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // Customize bullet appearance for sniper bullet.children[0].tint = 0xFF5500; bullet.children[0].width = 15; bullet.children[0].height = 15; game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); } else { // For all other towers, send slashanim asset to enemy var slashAnim = LK.getAsset('slashanim', { anchorX: 0.5, anchorY: 0.5 }); slashAnim.x = self.x; slashAnim.y = self.y; game.addChild(slashAnim); // Animate slashanim to enemy using tween tween(slashAnim, { x: self.targetEnemy.x, y: self.targetEnemy.y }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { // Apply damage when animation reaches enemy // Check if target enemy still exists before accessing health property if (self.targetEnemy && self.targetEnemy.health !== undefined) { 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 tower type if (self.id === '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.id === '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 var slowPct = 0.5; if (self.level !== undefined) { var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.level - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; } else { self.targetEnemy.slowDuration = 180; } } } else if (self.id === '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; self.targetEnemy.poisonDuration = 300; } } // Remove the slashanim asset after applying effects slashAnim.destroy(); } }); } // Handle tower attack animations and add sword wave effects for all towers except Sasha 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; } // Create sword wave animation var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash'); game.addChild(slashWave); // 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 Levi tower attack animation if (self.id === 'poison') { // Switch to attack asset var attackGraphics = self.attachAsset('leviattack', { 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 Sasha tower attack animation (no sword wave for sniper) 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; // Create sword wave animation var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash'); game.addChild(slashWave); // 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 Armin tower attack animation (default type) if (self.id === 'default') { // Switch to attack asset var attackGraphics = self.attachAsset('arminattack', { anchorX: 0.5, anchorY: 0.5 }); // Hide the original tower graphics baseGraphics.visible = false; // Create sword wave animation var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash'); game.addChild(slashWave); // 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 other tower types (slow, poison) - add sword wave animations if (self.id === 'slow' || self.id === 'poison') { // Create sword wave animation for these tower types var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash'); game.addChild(slashWave); } } } }; 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.toString() + '/' + self.tower.maxLevel.toString() + '\nDamage: ' + self.tower.damage.toString() + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 68, fill: 0xFFFFFF, weight: 600 }); 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)); } if (statsText && statsText.setText) { statsText.setText('Level: ' + self.tower.level.toString() + '/' + self.tower.maxLevel.toString() + '\nDamage: ' + self.tower.damage.toString() + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); } if (buttonText && buttonText.setText) { buttonText.setText('Upgrade: ' + upgradeCost.toString() + ' gold'); } var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); if (sellButtonText && sellButtonText.setText) { sellButtonText.setText('Sell: +' + sellValue.toString() + ' gold'); } if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; if (buttonText && buttonText.setText) { 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 && buttonText.setText) { 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.toString() + ' gold'; if (buttonText && buttonText.setText) { 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: 48, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 2; startTextShadow.y = 2; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 48, 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 ****/ // Using defense icon as heart placeholder 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: "'Copperplate 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 () { showStoryScreen(); }; } function startGame() { if (gameStarted) return; // Show difficulty page instead of starting game directly showDifficultyPage(); } function showStoryScreen() { // Hide main menu if (mainMenuContainer) { mainMenuContainer.visible = false; } // Create story screen container var storyContainer = new Container(); game.addChild(storyContainer); // Add black background var blackBackground = storyContainer.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 story content background var storyBackground = storyContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); storyBackground.width = 1800; storyBackground.height = 2200; storyBackground.tint = 0x2C3E50; storyBackground.alpha = 0.95; storyBackground.x = 2048 / 2; storyBackground.y = 2732 / 2; // Add story title var storyTitle = new Text2("STORY", { size: 120, fill: 0xFFD700, weight: 900 }); storyTitle.anchor.set(0.5, 0.5); storyTitle.x = 2048 / 2; storyTitle.y = 400; storyContainer.addChild(storyTitle); // Add story text with proper line breaks and formatting var storyText = "Humanity is on the brink of extinction.\n\n" + "The Titans have breached the final wall. The last remnants of human\n" + "civilization are under siege — and this time, there is no cavalry coming.\n" + "The cities are burning, the soldiers are scattered, and the people cry\n" + "out in fear.\n\n" + "Only one defense remains: you.\n\n" + "You stand alone on the last line of resistance, entrusted with the power\n" + "to command the greatest warriors humanity has ever known. As the Titans\n" + "march forward in waves of destruction, you must deploy Armin, Sasha,\n" + "Erwin, Levi, Mikasa, and Eren — each with their own unique strengths,\n" + "tactics, and abilities — to push back the horde.\n\n" + "This is not just a battle.\n" + "This is a last stand.\n\n" + "Use the terrain, upgrade your defenses, and make every decision count.\n" + "The fate of humanity lies in your hands.\n\n" + "Hold the line. Command the legends. Stop the Titans."; var storyContentText = new Text2(storyText, { size: 46, fill: 0xFFFFFF, weight: 600 }); storyContentText.anchor.set(0.5, 0.5); storyContentText.x = 2048 / 2; storyContentText.y = 1400; storyContainer.addChild(storyContentText); // Create back button var backButton = new Container(); storyContainer.addChild(backButton); // Add button background var backButtonBg = backButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); backButtonBg.width = 400; backButtonBg.height = 120; backButtonBg.tint = 0x8B0000; backButtonBg.alpha = 0.9; var backText = new Text2("BACK", { size: 60, fill: 0xFFFFFF, weight: 800 }); backText.anchor.set(0.5, 0.5); backButton.addChild(backText); backButton.x = 2048 / 2; backButton.y = 2500; // Add hover effect for back button var backHoverEffect = backButton.attachAsset('menuhover', { anchorX: 0.5, anchorY: 0.5 }); backHoverEffect.visible = false; backHoverEffect.width = 420; backHoverEffect.height = 140; // Back button functionality backButton.down = function () { // Remove story screen storyContainer.destroy(); // Show main menu again if (mainMenuContainer) { mainMenuContainer.visible = true; } }; // Handle hover effects for back button backButton.move = function (x, y, obj) { var buttonLeft = this.x - 200; var buttonRight = this.x + 200; var buttonTop = this.y - 60; var buttonBottom = this.y + 60; var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom; backHoverEffect.visible = mouseInButton; if (mouseInButton) { tween.stop(backText, { tint: true }); tween(backText, { tint: 0xFF0000 }, { duration: 200, easing: tween.easeOut }); } else { tween.stop(backText, { tint: true }); tween(backText, { tint: 0xFFFFFF }, { duration: 200, easing: tween.easeOut }); } }; } 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; // Reset upgrades to zero and clear storage to prevent stacking upgrades = { damage: 0, range: 0, attackSpeed: 0, stamina: 0, castleHealth: 0, castleAttack: 0 }; // Clear all stored upgrade values storage.damageUpgrades = 0; storage.rangeUpgrades = 0; storage.attackSpeedUpgrades = 0; storage.staminaUpgrades = 0; storage.castleHealthUpgrades = 0; storage.castleAttackUpgrades = 0; // 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 showShopInterface() { // Create shop interface container var shopInterface = new Container(); game.addChild(shopInterface); // Add black semi-transparent background var shopBackground = shopInterface.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); shopBackground.width = 2048; shopBackground.height = 2732; shopBackground.tint = 0x000000; shopBackground.alpha = 0.7; shopBackground.x = 2048 / 2; shopBackground.y = 2732 / 2; // Add shop window var shopWindow = shopInterface.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); shopWindow.width = 1600; shopWindow.height = 1200; shopWindow.tint = 0x2C3E50; shopWindow.alpha = 0.95; shopWindow.x = 2048 / 2; shopWindow.y = 2732 / 2; // Add shop title var shopTitle = new Text2("UPGRADE SHOP", { size: 80, fill: 0xFFFFFF, weight: 800 }); shopTitle.anchor.set(0.5, 0.5); shopTitle.x = 2048 / 2; shopTitle.y = 2732 / 2 - 500; shopInterface.addChild(shopTitle); // Add close button var closeButton = new Container(); shopInterface.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 120; closeBackground.height = 120; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 80, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = 2048 / 2 + 740; // Right side of shop window closeButton.y = 2732 / 2 - 540; // Top of shop window closeButton.down = function () { shopInterface.destroy(); }; // Create upgrade buttons container var upgradesContainer = new Container(); upgradesContainer.x = 2048 / 2; upgradesContainer.y = 2732 / 2 - 150; shopInterface.addChild(upgradesContainer); // Damage upgrade var damageUpgradeContainer = new Container(); upgradesContainer.addChild(damageUpgradeContainer); damageUpgradeContainer.y = -200; var damageUpgradeBg = damageUpgradeContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); damageUpgradeBg.width = 1400; damageUpgradeBg.height = 120; damageUpgradeBg.tint = upgrades.damage >= upgradeMaxLevels.damage ? 0x555555 : gold >= upgradeCosts.damage ? 0x2E8B57 : 0x8B0000; damageUpgradeBg.alpha = 0.9; var damageText = new Text2("Damage +5 (" + upgrades.damage + "/" + upgradeMaxLevels.damage + ") - Cost: " + upgradeCosts.damage + " gold", { size: 50, fill: 0xFFFFFF, weight: 800 }); damageText.anchor.set(0.5, 0.5); damageUpgradeContainer.addChild(damageText); damageUpgradeContainer.update = function () { var isMaxLevel = upgrades.damage >= upgradeMaxLevels.damage; var canAfford = gold >= upgradeCosts.damage; damageUpgradeBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x2E8B57 : 0x8B0000; damageText.setText("Damage +5 (" + upgrades.damage + "/" + upgradeMaxLevels.damage + ") - Cost: " + upgradeCosts.damage + " gold"); }; damageUpgradeContainer.down = function () { if (upgrades.damage >= upgradeMaxLevels.damage) { var notification = game.addChild(new Notification("Damage upgrade maxed out!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (gold >= upgradeCosts.damage) { setGold(gold - upgradeCosts.damage); upgrades.damage++; storage.damageUpgrades = upgrades.damage; // Apply upgrade to player if (player) { player.attackDamage += 5; } var notification = game.addChild(new Notification("Damage upgraded! +" + upgrades.damage * 5 + " total damage")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; // Range upgrade var rangeUpgradeContainer = new Container(); upgradesContainer.addChild(rangeUpgradeContainer); rangeUpgradeContainer.y = -50; var rangeUpgradeBg = rangeUpgradeContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); rangeUpgradeBg.width = 1400; rangeUpgradeBg.height = 120; rangeUpgradeBg.tint = upgrades.range >= upgradeMaxLevels.range ? 0x555555 : gold >= upgradeCosts.range ? 0x2E8B57 : 0x8B0000; rangeUpgradeBg.alpha = 0.9; var rangeText = new Text2("Range +0.3 (" + upgrades.range + "/" + upgradeMaxLevels.range + ") - Cost: " + upgradeCosts.range + " gold", { size: 50, fill: 0xFFFFFF, weight: 800 }); rangeText.anchor.set(0.5, 0.5); rangeUpgradeContainer.addChild(rangeText); rangeUpgradeContainer.update = function () { var isMaxLevel = upgrades.range >= upgradeMaxLevels.range; var canAfford = gold >= upgradeCosts.range; rangeUpgradeBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x2E8B57 : 0x8B0000; rangeText.setText("Range +0.3 (" + upgrades.range + "/" + upgradeMaxLevels.range + ") - Cost: " + upgradeCosts.range + " gold"); }; rangeUpgradeContainer.down = function () { if (upgrades.range >= upgradeMaxLevels.range) { var notification = game.addChild(new Notification("Range upgrade maxed out!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (gold >= upgradeCosts.range) { setGold(gold - upgradeCosts.range); upgrades.range++; storage.rangeUpgrades = upgrades.range; // Apply upgrade to player if (player) { player.attackRange += 0.3 * CELL_SIZE; } var notification = game.addChild(new Notification("Range upgraded! +" + (upgrades.range * 0.3).toFixed(1) + " range")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; // Attack Speed upgrade var attackSpeedUpgradeContainer = new Container(); upgradesContainer.addChild(attackSpeedUpgradeContainer); attackSpeedUpgradeContainer.y = 100; var attackSpeedUpgradeBg = attackSpeedUpgradeContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); attackSpeedUpgradeBg.width = 1400; attackSpeedUpgradeBg.height = 120; attackSpeedUpgradeBg.tint = upgrades.attackSpeed >= upgradeMaxLevels.attackSpeed ? 0x555555 : gold >= upgradeCosts.attackSpeed ? 0x2E8B57 : 0x8B0000; attackSpeedUpgradeBg.alpha = 0.9; var attackSpeedText = new Text2("Attack Speed +0.3 (" + upgrades.attackSpeed + "/" + upgradeMaxLevels.attackSpeed + ") - Cost: " + upgradeCosts.attackSpeed + " gold", { size: 50, fill: 0xFFFFFF, weight: 800 }); attackSpeedText.anchor.set(0.5, 0.5); attackSpeedUpgradeContainer.addChild(attackSpeedText); attackSpeedUpgradeContainer.update = function () { var isMaxLevel = upgrades.attackSpeed >= upgradeMaxLevels.attackSpeed; var canAfford = gold >= upgradeCosts.attackSpeed; attackSpeedUpgradeBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x2E8B57 : 0x8B0000; attackSpeedText.setText("Attack Speed +0.3 (" + upgrades.attackSpeed + "/" + upgradeMaxLevels.attackSpeed + ") - Cost: " + upgradeCosts.attackSpeed + " gold"); }; attackSpeedUpgradeContainer.down = function () { if (upgrades.attackSpeed >= upgradeMaxLevels.attackSpeed) { var notification = game.addChild(new Notification("Attack speed upgrade maxed out!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (gold >= upgradeCosts.attackSpeed) { setGold(gold - upgradeCosts.attackSpeed); upgrades.attackSpeed++; storage.attackSpeedUpgrades = upgrades.attackSpeed; // Apply upgrade to player (reduce cooldown for faster attacks) if (player) { player.attackCooldownMax = Math.max(5, player.attackCooldownMax - 9); // Reduce by 0.15 seconds (9 frames) } var notification = game.addChild(new Notification("Attack speed upgraded! Faster attacks")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; // Stamina upgrade var staminaUpgradeContainer = new Container(); upgradesContainer.addChild(staminaUpgradeContainer); staminaUpgradeContainer.y = 250; var staminaUpgradeBg = staminaUpgradeContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); staminaUpgradeBg.width = 1400; staminaUpgradeBg.height = 120; staminaUpgradeBg.tint = upgrades.stamina >= upgradeMaxLevels.stamina ? 0x555555 : gold >= upgradeCosts.stamina ? 0x2E8B57 : 0x8B0000; staminaUpgradeBg.alpha = 0.9; var staminaText = new Text2("Stamina +20 (" + upgrades.stamina + "/" + upgradeMaxLevels.stamina + ") - Cost: " + upgradeCosts.stamina + " gold", { size: 50, fill: 0xFFFFFF, weight: 800 }); staminaText.anchor.set(0.5, 0.5); staminaUpgradeContainer.addChild(staminaText); staminaUpgradeContainer.update = function () { var isMaxLevel = upgrades.stamina >= upgradeMaxLevels.stamina; var canAfford = gold >= upgradeCosts.stamina; staminaUpgradeBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x2E8B57 : 0x8B0000; staminaText.setText("Stamina +20 (" + upgrades.stamina + "/" + upgradeMaxLevels.stamina + ") - Cost: " + upgradeCosts.stamina + " gold"); }; staminaUpgradeContainer.down = function () { if (upgrades.stamina >= upgradeMaxLevels.stamina) { var notification = game.addChild(new Notification("Stamina upgrade maxed out!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (gold >= upgradeCosts.stamina) { setGold(gold - upgradeCosts.stamina); upgrades.stamina++; storage.staminaUpgrades = upgrades.stamina; // Apply upgrade to player if (player) { player.maxStamina += 20; player.stamina = Math.min(player.stamina + 20, player.maxStamina); // Restore some stamina when upgrading } var notification = game.addChild(new Notification("Stamina upgraded! +" + upgrades.stamina * 20 + " max stamina")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; // Castle Health upgrade var castleHealthUpgradeContainer = new Container(); upgradesContainer.addChild(castleHealthUpgradeContainer); castleHealthUpgradeContainer.y = 400; var castleHealthUpgradeBg = castleHealthUpgradeContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); castleHealthUpgradeBg.width = 1400; castleHealthUpgradeBg.height = 120; castleHealthUpgradeBg.tint = upgrades.castleHealth >= upgradeMaxLevels.castleHealth ? 0x555555 : gold >= upgradeCosts.castleHealth ? 0x2E8B57 : 0x8B0000; castleHealthUpgradeBg.alpha = 0.9; var castleHealthText = new Text2("Castle Health +30 (" + upgrades.castleHealth + "/" + upgradeMaxLevels.castleHealth + ") - Cost: " + upgradeCosts.castleHealth + " gold", { size: 50, fill: 0xFFFFFF, weight: 800 }); castleHealthText.anchor.set(0.5, 0.5); castleHealthUpgradeContainer.addChild(castleHealthText); castleHealthUpgradeContainer.update = function () { var isMaxLevel = upgrades.castleHealth >= upgradeMaxLevels.castleHealth; var canAfford = gold >= upgradeCosts.castleHealth; castleHealthUpgradeBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x2E8B57 : 0x8B0000; castleHealthText.setText("Castle Health +30 (" + upgrades.castleHealth + "/" + upgradeMaxLevels.castleHealth + ") - Cost: " + upgradeCosts.castleHealth + " gold"); }; castleHealthUpgradeContainer.down = function () { if (upgrades.castleHealth >= upgradeMaxLevels.castleHealth) { var notification = game.addChild(new Notification("Castle health upgrade maxed out!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (gold >= upgradeCosts.castleHealth) { setGold(gold - upgradeCosts.castleHealth); upgrades.castleHealth++; storage.castleHealthUpgrades = upgrades.castleHealth; // Apply upgrade to castle if (baseCastle) { baseCastle.maxHealth += 30; baseCastle.health = Math.min(baseCastle.health + 30, baseCastle.maxHealth); // Restore some health when upgrading } var notification = game.addChild(new Notification("Castle health upgraded! +" + upgrades.castleHealth * 30 + " max health")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; // Castle Attack upgrade var castleAttackUpgradeContainer = new Container(); upgradesContainer.addChild(castleAttackUpgradeContainer); castleAttackUpgradeContainer.y = 550; var castleAttackUpgradeBg = castleAttackUpgradeContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); castleAttackUpgradeBg.width = 1400; castleAttackUpgradeBg.height = 120; castleAttackUpgradeBg.tint = upgrades.castleAttack >= upgradeMaxLevels.castleAttack ? 0x555555 : gold >= upgradeCosts.castleAttack ? 0x2E8B57 : 0x8B0000; castleAttackUpgradeBg.alpha = 0.9; var castleAttackText = new Text2("Castle Defense System (" + upgrades.castleAttack + "/" + upgradeMaxLevels.castleAttack + ") - Cost: " + upgradeCosts.castleAttack + " gold", { size: 50, fill: 0xFFFFFF, weight: 800 }); castleAttackText.anchor.set(0.5, 0.5); castleAttackUpgradeContainer.addChild(castleAttackText); castleAttackUpgradeContainer.update = function () { var isMaxLevel = upgrades.castleAttack >= upgradeMaxLevels.castleAttack; var canAfford = gold >= upgradeCosts.castleAttack; castleAttackUpgradeBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x2E8B57 : 0x8B0000; castleAttackText.setText("Castle Defense System (" + upgrades.castleAttack + "/" + upgradeMaxLevels.castleAttack + ") - Cost: " + upgradeCosts.castleAttack + " gold"); }; castleAttackUpgradeContainer.down = function () { if (upgrades.castleAttack >= upgradeMaxLevels.castleAttack) { var notification = game.addChild(new Notification("Castle defense system maxed out!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (gold >= upgradeCosts.castleAttack) { setGold(gold - upgradeCosts.castleAttack); upgrades.castleAttack++; storage.castleAttackUpgrades = upgrades.castleAttack; // Apply upgrade to castle if (baseCastle) { baseCastle.canAttack = true; baseCastle.attackDamage = 20; baseCastle.attackRange = CELL_SIZE * 4; baseCastle.fireRate = 120; // Attack every 2 seconds } var notification = game.addChild(new Notification("Castle defense system activated!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; // Close shop when clicking on background shopBackground.down = function () { shopInterface.destroy(); }; } // Create hero stats display with enhanced aesthetic design and better positioning var heroStatsContainer = new Container(); LK.gui.topRight.addChild(heroStatsContainer); // Position container away from road and with proper spacing heroStatsContainer.x = -40; // Further from right edge to avoid road overlap heroStatsContainer.y = -6; // Moved 0.7cm lower (-33 + 27 = -6) for better positioning // Create main background with better design to avoid road overlap var statsBackground = heroStatsContainer.attachAsset('notification', { anchorX: 1.0, anchorY: 0 }); statsBackground.width = 380; // Slightly narrower to avoid road statsBackground.height = 300; // Optimal height statsBackground.tint = 0x1A252F; // Better contrasting dark background statsBackground.alpha = 0.95; // Create decorative border with better visibility var borderBackground = heroStatsContainer.attachAsset('notification', { anchorX: 1.0, anchorY: 0 }); borderBackground.width = 390; borderBackground.height = 310; borderBackground.tint = 0x52796F; // Darker sage green border borderBackground.alpha = 0.9; borderBackground.x = 5; borderBackground.y = -5; // Create title background with better contrast var titleBackground = heroStatsContainer.attachAsset('notification', { anchorX: 1.0, anchorY: 0 }); titleBackground.width = 360; // Better proportions titleBackground.height = 55; // Optimal title bar height titleBackground.tint = 0x2D3436; // Better contrast dark gray titleBackground.alpha = 0.98; titleBackground.x = -10; titleBackground.y = 10; // Add title with enhanced shadow effect var titleShadow = new Text2('⚔ HERO STATUS ⚔', { // Enhanced title with symbols size: 34, fill: 0x000000, weight: 900 }); titleShadow.anchor.set(1.0, 0.5); titleShadow.x = -25; titleShadow.y = 39; // Centered in title bar heroStatsContainer.addChild(titleShadow); var titleText = new Text2('⚔ HERO STATUS ⚔', { size: 34, fill: 0xFDCB6E, // Better golden color for title weight: 900 }); titleText.anchor.set(1.0, 0.5); titleText.x = -22; titleText.y = 37; // Centered in title bar heroStatsContainer.addChild(titleText); // Create enhanced stat rows with better spacing and design var statRows = []; var statIcons = ['attackst', 'rangest', 'speedst', 'staminast']; var statYPositions = [85, 135, 185, 235]; // Better compact spacing var statLabels = ['DAMAGE', 'RANGE', 'SPEED', 'STAMINA']; // Full descriptive names for (var i = 0; i < 4; i++) { var statRow = new Container(); heroStatsContainer.addChild(statRow); statRow.y = statYPositions[i]; statRows.push(statRow); // Add subtle glow background for each stat row var statGlow = statRow.attachAsset('notification', { anchorX: 1.0, anchorY: 0.5 }); statGlow.width = 355; statGlow.height = 42; statGlow.tint = 0x636E72; // Better neutral glow statGlow.alpha = 0.25; statGlow.x = -12; statGlow.y = 0; // Add main stat background with better design var statBg = statRow.attachAsset('notification', { anchorX: 1.0, anchorY: 0.5 }); statBg.width = 350; statBg.height = 40; statBg.tint = 0x2D3436; // Consistent with title statBg.alpha = 0.9; statBg.x = -15; statBg.y = 0; // Add icon background circle for better contrast var iconBg = statRow.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); iconBg.width = 44; iconBg.height = 44; iconBg.tint = 0x74B9FF; // Vibrant blue background for icons iconBg.alpha = 0.85; iconBg.x = -45; // Better positioning for icon background iconBg.y = 0; // Add stat icon with enhanced positioning and proper coloring var statIcon = statRow.attachAsset(statIcons[i], { anchorX: 0.5, anchorY: 0.5 }); statIcon.width = 36; // Proper icon size statIcon.height = 36; statIcon.x = -45; // Centered on icon background statIcon.y = 0; statIcon.tint = 0xFFFFFF; // Make icons white/visible instead of transparent statIcon.alpha = 1.0; // Full opacity // Add stat label with enhanced shadow var statLabelShadow = new Text2(statLabels[i], { size: 28, fill: 0x000000, weight: 800 }); statLabelShadow.anchor.set(1.0, 0.5); statLabelShadow.x = -250; statLabelShadow.y = 2; statRow.addChild(statLabelShadow); // Add stat label with better styling and colors var statLabel = new Text2(statLabels[i], { size: 28, fill: 0xFFFFFF, // White text for better visibility weight: 800 }); statLabel.anchor.set(1.0, 0.5); statLabel.x = -248; statLabel.y = 0; statRow.addChild(statLabel); // Add stat value shadow with offset var statValueShadow = new Text2('', { size: 32, fill: 0x000000, weight: 900 }); statValueShadow.anchor.set(1.0, 0.5); statValueShadow.x = -82; statValueShadow.y = 2; statRow.addChild(statValueShadow); // Add stat value with color coding var statValue = new Text2('', { size: 32, fill: 0xCCCCCC, // Light gray for stat values weight: 900 }); statValue.anchor.set(1.0, 0.5); statValue.x = -80; statValue.y = 0; statRow.addChild(statValue); } // Update hero stats display with enhanced formatting and effects function updateHeroStats() { if (player && statRows.length >= 4) { // Calculate stats with proper bounds checking and safe conversion var damage = Math.max(0, Math.round(player.attackDamage || 0)); var range = Math.max(0, (player.attackRange || 0) / CELL_SIZE).toFixed(1); var attacksPerSecond = Math.max(0, 60 / Math.max(1, player.attackCooldownMax || 1)).toFixed(1); var stamina = Math.max(0, Math.round(player.stamina || 0)); var maxStamina = Math.max(1, Math.round(player.maxStamina || 1)); var staminaPercent = stamina / maxStamina; // Enhanced stat values with consistent formatting and safe string conversion var statValues = [damage.toString(), range.toString() + ' tiles', attacksPerSecond.toString() + '/sec', stamina.toString() + '/' + maxStamina.toString()]; // Use bright, consistent colors for all stats with better visibility var statColors = [0xFFD700, // Damage - bright gold 0x00BFFF, // Range - bright blue 0x32CD32, // Speed - bright green 0xFF6347 // Stamina - bright orange-red ]; var statLabels = ['DAMAGE', 'RANGE', 'SPEED', 'STAMINA']; for (var i = 0; i < 4; i++) { if (statRows[i] && statRows[i].children && statRows[i].children.length >= 8) { // Ensure all text objects exist before updating var labelShadow = statRows[i].children[4]; var label = statRows[i].children[5]; var valueShadow = statRows[i].children[6]; var value = statRows[i].children[7]; // Update label shadow (child index 4 in new layout) with consistent formatting if (labelShadow && labelShadow.setText && statLabels[i]) { labelShadow.setText(statLabels[i]); labelShadow.tint = 0x000000; labelShadow.alpha = 1.0; } // Update label (child index 5) with bright white text if (label && label.setText && statLabels[i]) { label.setText(statLabels[i]); label.tint = 0xFFFFFF; label.alpha = 1.0; } // Update value shadow (child index 6) with consistent black shadow if (valueShadow && valueShadow.setText && statValues[i]) { valueShadow.setText(statValues[i]); valueShadow.tint = 0x000000; valueShadow.alpha = 1.0; } // Update value with bright color coding (child index 7) if (value && value.setText && statValues[i]) { value.setText(statValues[i]); value.tint = statColors[i]; value.alpha = 1.0; } // Add special visual effects for low stamina if (i === 3 && staminaPercent < 0.3) { // Flash stamina when critically low if (LK.ticks % 60 < 30) { // Flash every second if (value) value.alpha = 0.6; if (valueShadow) valueShadow.alpha = 0.6; } else { if (value) value.alpha = 1.0; if (valueShadow) valueShadow.alpha = 1.0; } } else { if (value) value.alpha = 1.0; if (valueShadow) valueShadow.alpha = 1.0; } } } } } // Hide stats initially heroStatsContainer.visible = false; // Create base health UI display with heart icon var baseHealthContainer = new Container(); LK.gui.topLeft.addChild(baseHealthContainer); // Position container away from the top-left platform menu icon (avoid 100x100 px area) baseHealthContainer.x = 120; // 120px from left edge to avoid platform menu baseHealthContainer.y = 20; // 20px from top edge // Create main background for base health UI var baseHealthBackground = baseHealthContainer.attachAsset('notification', { anchorX: 0, anchorY: 0 }); baseHealthBackground.width = 280; // Compact width baseHealthBackground.height = 80; // Compact height baseHealthBackground.tint = 0x2C3E50; // Dark blue-gray background baseHealthBackground.alpha = 0.9; // Create decorative border var baseHealthBorder = baseHealthContainer.attachAsset('notification', { anchorX: 0, anchorY: 0 }); baseHealthBorder.width = 290; baseHealthBorder.height = 90; baseHealthBorder.tint = 0x8B0000; // Dark red border for castle theme baseHealthBorder.alpha = 0.8; baseHealthBorder.x = -5; baseHealthBorder.y = -5; // Add health UI icon var heartIcon = baseHealthContainer.attachAsset('healthui', { anchorX: 0.5, anchorY: 0.5 }); heartIcon.width = 50; // Appropriate size for compact UI heartIcon.height = 50; heartIcon.tint = 0xFF0000; // Red heart color heartIcon.x = 35; // Position near left side heartIcon.y = 40; // Center vertically // Add castle health text with shadow var baseHealthTextShadow = new Text2('', { size: 32, fill: 0x000000, weight: 800 }); baseHealthTextShadow.anchor.set(0, 0.5); baseHealthTextShadow.x = 72; // Position after heart icon + margin baseHealthTextShadow.y = 42; // Slightly offset for shadow effect baseHealthContainer.addChild(baseHealthTextShadow); // Add main castle health text var baseHealthText = new Text2('', { size: 32, fill: 0xFFFFFF, weight: 800 }); baseHealthText.anchor.set(0, 0.5); baseHealthText.x = 70; // Position after heart icon + margin baseHealthText.y = 40; // Center vertically baseHealthContainer.addChild(baseHealthText); // Function to update base health display function updateBaseHealth() { if (baseCastle && baseHealthText && baseHealthTextShadow) { var healthText = baseCastle.health + '/' + baseCastle.maxHealth; baseHealthText.setText(healthText); baseHealthTextShadow.setText(healthText); // Change heart color based on health percentage var healthPercent = baseCastle.health / baseCastle.maxHealth; if (healthPercent > 0.6) { heartIcon.tint = 0xFF0000; // Red for healthy } else if (healthPercent > 0.3) { heartIcon.tint = 0xFF6600; // Orange for damaged } else { heartIcon.tint = 0x990000; // Dark red for critical } // Change text color based on health if (healthPercent > 0.6) { baseHealthText.tint = 0x00FF00; // Green for healthy } else if (healthPercent > 0.3) { baseHealthText.tint = 0xFFFF00; // Yellow for damaged } else { baseHealthText.tint = 0xFF0000; // Red for critical } } } // Hide base health UI initially baseHealthContainer.visible = false; function initializeGameElements() { // Show previously hidden UI elements // waveIndicator.visible = true; // Removed - keep wave indicator hidden nextWaveButton.visible = true; shopButton.visible = true; // Show shop button when game starts towerLayer.visible = true; enemyLayer.visible = true; goldImage.visible = true; goldText.visible = true; heroStatsContainer.visible = true; // Show hero stats when game starts baseHealthContainer.visible = true; // Show base health when game starts // 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; // Reset player stats to base values only (no upgrades applied) player.attackDamage = 15; // Base damage player.attackRange = CELL_SIZE * 1.0; // Base range player.attackCooldownMax = 30; // Base cooldown // No upgrades applied since they are all reset to 0 } // Show spawn point if (spawnPoint) { spawnPoint.visible = true; } // Show base castle if (baseCastle) { baseCastle.visible = true; } // Initialize hero stats display updateHeroStats(); // Any other game initialization that should happen when starting } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; // Initialize upgrade system with storage - reset everything to 0 var upgrades = { damage: 0, range: 0, attackSpeed: 0, stamina: 0, castleHealth: 0, castleAttack: 0 }; // Clear all stored upgrade values storage.damageUpgrades = 0; storage.rangeUpgrades = 0; storage.attackSpeedUpgrades = 0; storage.staminaUpgrades = 0; storage.castleHealthUpgrades = 0; storage.castleAttackUpgrades = 0; // Function to reset game state for new games function resetGameState() { // Reset upgrades to zero and clear storage upgrades = { damage: 0, range: 0, attackSpeed: 0, stamina: 0, castleHealth: 0, castleAttack: 0 }; // Clear all stored upgrade values storage.damageUpgrades = 0; storage.rangeUpgrades = 0; storage.attackSpeedUpgrades = 0; storage.staminaUpgrades = 0; storage.castleHealthUpgrades = 0; storage.castleAttackUpgrades = 0; // Reset other game variables gameStarted = false; currentWave = 0; // Hide base health UI when resetting if (baseHealthContainer) { baseHealthContainer.visible = false; } waveTimer = 0; waveInProgress = false; waveSpawned = false; enemies = []; towers = []; bullets = []; gold = 60; selectedTower = null; } // Upgrade costs and limits var upgradeCosts = { damage: 40, range: 50, attackSpeed: 40, stamina: 50, castleHealth: 35, castleAttack: 70 }; var upgradeMaxLevels = { damage: 3, range: 3, attackSpeed: 3, stamina: 3, castleHealth: 3, castleAttack: 1 }; 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(); // Initialize base health display updateBaseHealth(); } 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; } } } // Check if any existing tower overlaps with the proposed tower position for (var t = 0; t < towers.length; t++) { var existingTower = towers[t]; var existingGridX = existingTower.gridX; var existingGridY = existingTower.gridY; // Check if the 2x2 footprints overlap var overlapX = !(gridX + 2 <= existingGridX || gridX >= existingGridX + 2); var overlapY = !(gridY + 2 <= existingGridY || gridY >= existingGridY + 2); if (overlapX && overlapY) { // Towers would overlap 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 = 10; switch (towerType) { case 'rapid': cost = 20; break; case 'sniper': cost = 30; break; case 'splash': cost = 40; break; case 'slow': cost = 50; break; case 'poison': cost = 60; 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); // Create shop button 3cm below next wave button var shopContainer = new Container(); var shopButton = new Container(); var shopGraphics = shopButton.attachAsset('shop', { anchorX: 0.5, anchorY: 0.5 }); shopButton.x = 189; // Same X position as next wave button shopButton.y = 378 + 113 + 113; // 6cm total below next wave button (6cm * 37.8 pixels/cm = 226 pixels) shopButton.visible = false; // Hide initially until game starts shopButton.down = function () { if (!gameStarted) { return; } // Show shop interface showShopInterface(); }; shopContainer.addChild(shopButton); game.addChild(shopContainer); // 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(); } // Update hero stats display if (gameStarted && LK.ticks % 30 === 0) { // Update every 0.5 seconds updateHeroStats(); // Update base health display updateBaseHealth(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
===================================================================
--- original.js
+++ change.js
@@ -3102,19 +3102,135 @@
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;
+ showStoryScreen();
};
}
function startGame() {
if (gameStarted) return;
// Show difficulty page instead of starting game directly
showDifficultyPage();
}
+function showStoryScreen() {
+ // Hide main menu
+ if (mainMenuContainer) {
+ mainMenuContainer.visible = false;
+ }
+ // Create story screen container
+ var storyContainer = new Container();
+ game.addChild(storyContainer);
+ // Add black background
+ var blackBackground = storyContainer.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 story content background
+ var storyBackground = storyContainer.attachAsset('notification', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ storyBackground.width = 1800;
+ storyBackground.height = 2200;
+ storyBackground.tint = 0x2C3E50;
+ storyBackground.alpha = 0.95;
+ storyBackground.x = 2048 / 2;
+ storyBackground.y = 2732 / 2;
+ // Add story title
+ var storyTitle = new Text2("STORY", {
+ size: 120,
+ fill: 0xFFD700,
+ weight: 900
+ });
+ storyTitle.anchor.set(0.5, 0.5);
+ storyTitle.x = 2048 / 2;
+ storyTitle.y = 400;
+ storyContainer.addChild(storyTitle);
+ // Add story text with proper line breaks and formatting
+ var storyText = "Humanity is on the brink of extinction.\n\n" + "The Titans have breached the final wall. The last remnants of human\n" + "civilization are under siege — and this time, there is no cavalry coming.\n" + "The cities are burning, the soldiers are scattered, and the people cry\n" + "out in fear.\n\n" + "Only one defense remains: you.\n\n" + "You stand alone on the last line of resistance, entrusted with the power\n" + "to command the greatest warriors humanity has ever known. As the Titans\n" + "march forward in waves of destruction, you must deploy Armin, Sasha,\n" + "Erwin, Levi, Mikasa, and Eren — each with their own unique strengths,\n" + "tactics, and abilities — to push back the horde.\n\n" + "This is not just a battle.\n" + "This is a last stand.\n\n" + "Use the terrain, upgrade your defenses, and make every decision count.\n" + "The fate of humanity lies in your hands.\n\n" + "Hold the line. Command the legends. Stop the Titans.";
+ var storyContentText = new Text2(storyText, {
+ size: 46,
+ fill: 0xFFFFFF,
+ weight: 600
+ });
+ storyContentText.anchor.set(0.5, 0.5);
+ storyContentText.x = 2048 / 2;
+ storyContentText.y = 1400;
+ storyContainer.addChild(storyContentText);
+ // Create back button
+ var backButton = new Container();
+ storyContainer.addChild(backButton);
+ // Add button background
+ var backButtonBg = backButton.attachAsset('notification', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ backButtonBg.width = 400;
+ backButtonBg.height = 120;
+ backButtonBg.tint = 0x8B0000;
+ backButtonBg.alpha = 0.9;
+ var backText = new Text2("BACK", {
+ size: 60,
+ fill: 0xFFFFFF,
+ weight: 800
+ });
+ backText.anchor.set(0.5, 0.5);
+ backButton.addChild(backText);
+ backButton.x = 2048 / 2;
+ backButton.y = 2500;
+ // Add hover effect for back button
+ var backHoverEffect = backButton.attachAsset('menuhover', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ backHoverEffect.visible = false;
+ backHoverEffect.width = 420;
+ backHoverEffect.height = 140;
+ // Back button functionality
+ backButton.down = function () {
+ // Remove story screen
+ storyContainer.destroy();
+ // Show main menu again
+ if (mainMenuContainer) {
+ mainMenuContainer.visible = true;
+ }
+ };
+ // Handle hover effects for back button
+ backButton.move = function (x, y, obj) {
+ var buttonLeft = this.x - 200;
+ var buttonRight = this.x + 200;
+ var buttonTop = this.y - 60;
+ var buttonBottom = this.y + 60;
+ var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
+ backHoverEffect.visible = mouseInButton;
+ if (mouseInButton) {
+ tween.stop(backText, {
+ tint: true
+ });
+ tween(backText, {
+ tint: 0xFF0000
+ }, {
+ duration: 200,
+ easing: tween.easeOut
+ });
+ } else {
+ tween.stop(backText, {
+ tint: true
+ });
+ tween(backText, {
+ tint: 0xFFFFFF
+ }, {
+ duration: 200,
+ easing: tween.easeOut
+ });
+ }
+ };
+}
function showDifficultyPage() {
// Hide main menu
if (mainMenuContainer) {
mainMenuContainer.visible = false;
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