User prompt
Byzantine Kule dayanıklılığı %100 daha fazla olsun. Fakat gelen düşman sayısı 3 daha fazla olsun
User prompt
Kalmar Union Healer rangesi %100 daha çok olsun
User prompt
Scotchta archerın rangesi %25 değil %50 olsun
User prompt
Scotchta kale ücretleri 3 altın daha az olsun. Archerın rangesi %25 daha uzağı görsün
User prompt
englandta mutlaka 5. wavede 1 tane bad enemy gelsin
User prompt
englandta mutlaka 5. wavede 1 tane bad enemy gelsin
User prompt
englandta mutlaka bad enemy gelsin!
User prompt
Englandta normal düşman 3 daha az gelsin ve 100 altınla başlasın
User prompt
Frenchin kule dayanıklılığı %20 daha fazla olsun
User prompt
ottoman 100 ile başlamıyor düzelt
User prompt
Kalmar Union butonunun rengini koyu yeşil yap
User prompt
Austrians: Austrian, Hungarians: Hungarian, Ottomans: Ottoman
User prompt
bu özelliği her imparatorluk için yap. Sadece byzantine için değil
User prompt
info assetini kaldır. İmparatorluk seçilince bir menü daha ekle. O menüde imparatorluğun karakteristik özellikleri yazsın
User prompt
info assetini 2,5 kat büyüt ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
özellikler şu olsun: info imparatorluğun avantajlarını yazsın (mesela Hungarian %20 atım hızı)
User prompt
Austrians seçilirse Kale atım hızı %15 artsın
User prompt
Austrians seçilirse Kale atım hızı %15 artsın
User prompt
info assetine tıklayınca imparatorluğun özellikleri yazsın (az sonra özellik hakkında komut vereceğim)
User prompt
info assetini çok az sağa kaydır
User prompt
info assetini çok az sağa kaydır
User prompt
imparatorluğun yanına info assetini ekle
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'gameSettings.selectedEmpire')' in or related to this line: 'var gold = gameSettings.selectedEmpire === 'Ottomans' ? 100 : 80;' Line Number: 2926
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'gameSettings.selectedEmpire')' in or related to this line: 'var gold = gameSettings.selectedEmpire === 'Ottomans' ? 100 : 80;' Line Number: 2926
User prompt
Austrians: Kule atım hızı %20 arttır Hungarians: Normal ve daha yüksek ayarlarda kule fiyatı 5 altın düşük Ottomans: Başlangıç altını 100, Atım hasarı %20 daha fazla
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ 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 () { // Skip bullet update when game is paused (gameSpeed = 0) if (gameSpeed === 0) { return; } if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy var actualDamage = self.damage; // Artillery towers deal 40% of enemy's current health as damage if (self.type === 'splash') { actualDamage = Math.ceil(self.targetEnemy.health * 0.4); // AntiAir bullets deal half damage to flying enemies if (self.targetEnemy.isFlying) { actualDamage = actualDamage * 0.5; } } // Immune enemies take 10% more damage from all bullets if (self.targetEnemy.isImmune) { actualDamage = actualDamage * 1.1; } // Immune enemies now take damage from all bullet types self.targetEnemy.health -= actualDamage; 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 or 20% of enemy health for Artillery) var splashDamage; if (self.type === 'splash') { splashDamage = Math.ceil(otherEnemy.health * 0.2); // 20% of current health for splash } else { splashDamage = self.damage * 0.5; } // Immune enemies take 10% more splash damage if (otherEnemy.isImmune) { splashDamage = splashDamage * 1.1; } // Immune enemies now take splash damage from all bullet types otherEnemy.health -= splashDamage; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'poison') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual poison effect var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Apply poison effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); numberLabel.visible = false; // Hide numbers in battlefield 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; // Hide numbers in battlefield var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; } else { cellGraphics.tint = 0x88 - tint << 8 | tint; } // Remove arrow rendering - no longer display arrows break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0x008800; numberLabel.visible = false; break; } case 4: { self.removeArrows(); cellGraphics.tint = 0x000000; // Black color for castle walls numberLabel.visible = false; break; } } // numberLabel.setText(Math.floor(data.score / 1000) / 10); // Removed number display }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.speed *= 1.275; // 27.5% faster than normal (reduced by 15% from previous 50%) self.maxHealth = 40; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'angry': self.speed *= 1; // Same speed as normal enemies self.maxHealth = 120; // More health than normal break; case 'bad': self.speed *= 0.5; // 50% slower than normal self.maxHealth = 1000; // 10 times the health of a normal enemy break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type === 'angry') { assetId = 'Angry_Enemy'; } else if (self.type === 'bad') { assetId = 'castlewall'; } else if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { // Skip enemy update when game is paused (gameSpeed = 0) if (gameSpeed === 0) { return; } if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Anti-stuck mechanism: track if enemy hasn't moved significantly if (self.lastPosition === undefined) { self.lastPosition = { x: self.currentCellX, y: self.currentCellY, time: LK.ticks }; self.stuckCounter = 0; } // Check if enemy has been stuck in same position for too long var timeSinceLastCheck = LK.ticks - self.lastPosition.time; if (timeSinceLastCheck > 180) { // Check every 3 seconds var distanceMoved = Math.sqrt((self.currentCellX - self.lastPosition.x) * (self.currentCellX - self.lastPosition.x) + (self.currentCellY - self.lastPosition.y) * (self.currentCellY - self.lastPosition.y)); if (distanceMoved < 0.5) { // If moved less than half a cell self.stuckCounter++; if (self.stuckCounter >= 3) { // If stuck for 3 checks (9 seconds) // Force reset enemy position and target self.currentTarget = null; // Try to find a valid spawn position for (var sx = 9; sx <= 14; sx++) { var testCell = grid.getCell(sx, 5); if (testCell && testCell.pathId === pathId) { self.cellX = sx; self.cellY = 5; self.currentCellX = sx; self.currentCellY = 5; self.stuckCounter = 0; break; } } } } else { self.stuckCounter = 0; // Reset stuck counter if moving } // Update last position self.lastPosition = { x: self.currentCellX, y: self.currentCellY, time: LK.ticks }; } // Immune enemies attack towers in range if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.poisoned = false; self.poisonEffect = false; // Attack towers every 120 frames (2 seconds) if (LK.ticks % Math.max(1, Math.floor(120 / gameSpeed)) === 0) { // Find towers in range and attack them for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.x - self.x; var dy = tower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Attack towers within 2 cell range if (distance <= CELL_SIZE * 2) { // Check if any Healer towers are protecting this tower var isProtectedByHealer = false; for (var h = 0; h < towers.length; h++) { var healerTower = towers[h]; if (healerTower.id === 'poison' && healerTower.isInRange(tower)) { isProtectedByHealer = true; break; } } // Only attack if not protected by a Healer tower if (!isProtectedByHealer) { // Easy mode: towers take no damage if (gameSettings.difficulty !== 'easy') { var damage = Math.floor(tower.maxHealth / 10); // 1/10th of tower's max health // Hardcore mode: enemies deal 2x damage if (gameSettings.difficulty === 'hardcore') { damage *= 2; } tower.health -= damage; if (tower.health <= 0) { tower.health = 0; } // Update tower health bar tower.towerHealthBar.width = tower.health / tower.maxHealth * 70; } } break; // Only attack one tower per update } } } } else if (self.type === 'angry') { // Angry enemies attack towers in range, including healer towers // Attack towers every 120 frames (2 seconds) if (LK.ticks % Math.max(1, Math.floor(120 / gameSpeed)) === 0) { // Find towers in range and attack them for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.x - self.x; var dy = tower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Attack towers within 2 cell range if (distance <= CELL_SIZE * 2) { // Angry enemies can attack ALL towers including healers // Easy mode: towers take no damage if (gameSettings.difficulty !== 'easy') { var damage = Math.floor(tower.maxHealth * 0.25); // 25% of tower's max health // Hardcore mode: enemies deal 2x damage if (gameSettings.difficulty === 'hardcore') { damage *= 2; } tower.health -= damage; if (tower.health <= 0) { tower.health = 0; } // Update tower health bar tower.towerHealthBar.width = tower.health / tower.maxHealth * 70; } break; // Only attack one tower per update } } } } else if (self.type === 'bad') { // Bad enemies attack towers more frequently and deal more damage // Attack towers every 90 frames (1.5 seconds) if (LK.ticks % Math.max(1, Math.floor(90 / gameSpeed)) === 0) { // Find towers in range and attack them for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.x - self.x; var dy = tower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Attack towers within 2.5 cell range (longer range than angry) if (distance <= CELL_SIZE * 2.5) { // Check if any Healer towers are protecting this tower var isProtectedByHealer = false; for (var h = 0; h < towers.length; h++) { var healerTower = towers[h]; if (healerTower.id === 'poison' && healerTower.isInRange(tower)) { isProtectedByHealer = true; break; } } // Only attack if not protected by a Healer tower if (!isProtectedByHealer) { // Easy mode: towers take no damage if (gameSettings.difficulty !== 'easy') { // Bad enemies destroy towers in one hit (or deal massive damage in hardcore) if (gameSettings.difficulty === 'hardcore') { tower.health = 0; // Still one-shot in hardcore } else { tower.health = 0; // One-shot in normal/hard } // Update tower health bar tower.towerHealthBar.width = tower.health / tower.maxHealth * 70; } } break; // Only attack one tower per update } } } } else { // 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), adjusted for game speed if (LK.ticks % Math.max(1, Math.floor(30 / gameSpeed)) === 0) { self.health -= self.poisonDamage * gameSpeed; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration -= gameSpeed; 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) { enemyGraphics.tint = 0x00FFAA; } else if (self.type === 'bad') { // Bad enemies use their asset's natural color - no tint applied } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal 4: Castle wall */ for (var i = 0; i < gridWidth; i++) { var _loop = function _loop() { cell = self.cells[i][j]; cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; // Create castle structure with random labyrinth if (j > 4 && j < gridHeight - 4) { // Simple pseudo-random number generator using seed var seededRandom = function seededRandom() { mazeSeed = (mazeSeed * 9301 + 49297) % 233280; return mazeSeed / 233280; }; // Main castle outer walls // Generate maze seed based on reload counter to ensure different maze each reload // Use reload-based seed as primary factor for maze generation mazeSeed = gameSessionSeed * 7919 + (currentWave + 1) * 1009 + reloadCounter * 4397; if ((i === 2 || i === gridWidth - 3) && j >= 8 && j <= gridHeight - 8) { cellType = 4; // Castle wall } // Castle towers at corners if ((i >= 1 && i <= 4 || i >= gridWidth - 5 && i <= gridWidth - 2) && (j >= 7 && j <= 10 || j >= gridHeight - 11 && j <= gridHeight - 8)) { cellType = 4; } // Generate unique geometric maze patterns based on seed var mazePattern = (mazeSeed * 17 + (currentWave + 1) * 23) % 100; if (j >= 12 && j <= gridHeight - 12) { // Create safe corridors first - ensure at least 3 clear horizontal paths var safePaths = [16, 20, 24]; var isSafePath = false; for (var sp = 0; sp < safePaths.length; sp++) { if (j === safePaths[sp] && i >= 8 && i <= gridWidth - 9) { isSafePath = true; break; } } if (!isSafePath) { // Generate dynamic geometric patterns based on random seed if (mazePattern < 20) { // Concentric geometric shapes var centerX = Math.floor(gridWidth / 2); var centerY = Math.floor((gridHeight - 12 + 12) / 2); var distance = Math.sqrt((i - centerX) * (i - centerX) + (j - centerY) * (j - centerY)); var ringSize = 2 + mazeSeed % 3; if (Math.floor(distance) % ringSize === 1 && distance > 2 && distance < 8 && i >= 6 && i <= gridWidth - 7) { if (seededRandom() > 0.6) { cellType = 4; } } } else if (mazePattern < 40) { // Geometric wave patterns var waveFreq = 0.2 + mazeSeed % 5 * 0.1; var waveHeight = 2 + mazeSeed % 3; var expectedY = Math.floor(Math.sin(i * waveFreq) * waveHeight) + Math.floor((gridHeight - 12 + 12) / 2); if (Math.abs(j - expectedY) < 2 && i >= 6 && i <= gridWidth - 7) { if (seededRandom() > 0.5) { cellType = 4; } } } else if (mazePattern < 60) { // Diamond and rhombus patterns var midX = Math.floor(gridWidth / 2); var midY = Math.floor((gridHeight - 12 + 12) / 2); var manhattanDist = Math.abs(i - midX) + Math.abs(j - midY); var diamondSize = 3 + mazeSeed % 4; if (manhattanDist >= diamondSize && manhattanDist <= diamondSize + 3 && manhattanDist % 2 === 0) { if (seededRandom() > 0.5) { cellType = 4; } } } else if (mazePattern < 80) { // Hexagonal and triangular patterns var hexSize = 4 + mazeSeed % 3; var hexX = Math.floor(i / hexSize); var hexY = Math.floor(j / hexSize); if ((hexX + hexY) % 3 === 0 && (i % hexSize < 2 || j % hexSize < 2) && i >= 6 && i <= gridWidth - 7) { if (seededRandom() > 0.4) { cellType = 4; } } } else { // Spiral and radial patterns var centerX = Math.floor(gridWidth / 2); var centerY = Math.floor((gridHeight - 12 + 12) / 2); var angle = Math.atan2(j - centerY, i - centerX); var spiralArms = 4 + mazeSeed % 4; var armIndex = Math.floor(angle / (Math.PI * 2 / spiralArms)); var distance = Math.sqrt((i - centerX) * (i - centerX) + (j - centerY) * (j - centerY)); if (armIndex % 2 === 0 && distance > 2 && distance < 7 && i >= 6 && i <= gridWidth - 7) { if (seededRandom() > 0.45) { cellType = 4; } } } // Add strategic scattered obstacles with lower density if (i >= 7 && i <= gridWidth - 8 && j >= 14 && j <= gridHeight - 14) { if (seededRandom() > 0.8) { cellType = 4; } } } } // Central castle keep - always present if (i >= 10 && i <= 13 && j >= gridHeight - 16 && j <= gridHeight - 13) { if (!(i === 11 || i === 12) || !(j === gridHeight - 15 || j === gridHeight - 14)) { cellType = 4; } } } // Spawn and goal areas if (i > 11 - 3 && i <= 11 + 3) { if (j === 0) { cellType = 2; self.spawns.push(cell); } else if (j <= 4) { cellType = 0; } else if (j === gridHeight - 1) { cellType = 3; self.goals.push(cell); } else if (j >= gridHeight - 4) { cellType = 0; } } cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } }, cell, cellType, mazeSeed, horizontalWallRows, hr, verticalWallCols, vc, debugCell; for (var j = 0; j < gridHeight; j++) { _loop(); } } 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 && node.type != 4) { 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.up.type != 4 && node.left.type != 1 && node.left.type != 4 && node.upLeft && node.upLeft.type != 1 && node.upLeft.type != 4) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.up.type != 4 && node.right.type != 1 && node.right.type != 4 && node.upRight && node.upRight.type != 1 && node.upRight.type != 4) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.down.type != 4 && node.right.type != 1 && node.right.type != 4 && node.downRight && node.downRight.type != 1 && node.downRight.type != 4) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.down.type != 4 && node.left.type != 1 && node.left.type != 4 && node.downLeft && node.downLeft.type != 1 && node.downLeft.type != 4) { 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); } } // Check if any spawn point has a valid path to any goal var hasValidPath = false; for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId === pathId) { hasValidPath = true; break; } } // Ensure maze is navigable - return false if paths are blocked if (!hasValidPath) { console.log("Maze blocks all paths - this layout will be regenerated"); return true; // Signal that maze needs regeneration } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed * gameSpeed; // 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 * gameSpeed; enemy.currentCellY += Math.sin(angle) * enemy.speed * gameSpeed; 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 || !cell || cell.pathId !== pathId) { // Reset target if we don't have one or if current cell is invalid if (cell && cell.pathId === pathId && cell.targets && cell.targets.length > 0) { enemy.currentTarget = cell.targets[0]; } else { // If current cell is invalid, try to find nearest valid cell var nearestValidCell = null; var minDistance = Infinity; // Search for nearby valid cells for (var searchX = Math.max(0, enemy.cellX - 2); searchX <= Math.min(grid.cells.length - 1, enemy.cellX + 2); searchX++) { for (var searchY = Math.max(0, enemy.cellY - 2); searchY <= Math.min(grid.cells[0].length - 1, enemy.cellY + 2); searchY++) { var searchCell = grid.getCell(searchX, searchY); if (searchCell && searchCell.pathId === pathId && searchCell.targets && searchCell.targets.length > 0) { var dx = searchX - enemy.cellX; var dy = searchY - enemy.cellY; var distance = dx * dx + dy * dy; if (distance < minDistance) { minDistance = distance; nearestValidCell = searchCell; } } } } if (nearestValidCell) { enemy.currentTarget = nearestValidCell; // Snap enemy to the valid cell position enemy.cellX = nearestValidCell.x; enemy.cellY = nearestValidCell.y; enemy.currentCellX = nearestValidCell.x; enemy.currentCellY = nearestValidCell.y; } } } if (enemy.currentTarget) { // Validate that current target is still valid if (enemy.currentTarget.pathId !== pathId) { enemy.currentTarget = null; return; // Will be fixed next frame } // If we have a better path from current cell, use it if (cell && cell.pathId === pathId && 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 * gameSpeed) { // Reached target, snap to grid and clear target enemy.cellX = enemy.currentTarget.x; enemy.cellY = enemy.currentTarget.y; enemy.currentCellX = enemy.currentTarget.x; enemy.currentCellY = enemy.currentTarget.y; enemy.currentTarget = null; // Don't return here, let position update happen } else { // Move toward target var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed * gameSpeed; enemy.currentCellY += Math.sin(angle) * enemy.speed * gameSpeed; } } else { // No valid target - try to get unstuck by moving toward goal var goalFound = false; for (var g = 0; g < self.goals.length; g++) { var goal = self.goals[g]; var gx = goal.x - enemy.currentCellX; var gy = goal.y - enemy.currentCellY; if (Math.abs(gx) > 0.1 || Math.abs(gy) > 0.1) { var gAngle = Math.atan2(gy, gx); enemy.currentCellX += Math.cos(gAngle) * enemy.speed * gameSpeed * 0.5; // Move slower when lost enemy.currentCellY += Math.sin(gAngle) * enemy.speed * gameSpeed * 0.5; goalFound = true; break; } } } // Update visual position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var MainMenu = Container.expand(function () { var self = Container.call(this); // Center the menu container self.x = 2048 / 2; self.y = 2732 / 2; // Background overlay var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 2732; menuBackground.tint = 0x000000; menuBackground.alpha = 0.8; // Current state: 'difficulty' or 'empire' self.currentState = 'difficulty'; self.selectedDifficulty = null; // Difficulty selection elements var difficultyTitle = new Text2("Select Difficulty", { size: 120, fill: 0xFFFFFF, weight: 800 }); difficultyTitle.anchor.set(0.5, 0.5); difficultyTitle.y = -600; self.addChild(difficultyTitle); // Difficulty buttons var difficulties = ['easy', 'normal', 'hard', 'hardcore']; var difficultyButtons = []; var difficultyButtonSpacing = 150; var difficultyStartY = -300; for (var i = 0; i < difficulties.length; i++) { var difficulty = difficulties[i]; var button = new Container(); var buttonBG = button.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBG.width = 600; buttonBG.height = 120; buttonBG.tint = 0x0088FF; var buttonText = new Text2(difficulty.charAt(0).toUpperCase() + difficulty.slice(1), { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); button.addChild(buttonText); button.y = difficultyStartY + i * difficultyButtonSpacing; button.difficulty = difficulty; button.down = function () { self.selectedDifficulty = this.difficulty; self.showEmpireSelection(); }; self.addChild(button); difficultyButtons.push(button); } // Empire selection elements (initially hidden) var empireTitle = new Text2("Choose Your Empire!", { size: 100, fill: 0xFFFFFF, weight: 800 }); empireTitle.anchor.set(0.5, 0.5); empireTitle.y = -700; empireTitle.visible = false; self.addChild(empireTitle); // Empire buttons - 8 different empires var empires = [{ name: 'Austrian', color: 0x00FF00, description: '' }, { name: 'Hungarian', color: 0x0088FF, description: '' }, { name: 'Ottoman', color: 0xFF8800, description: '' }, { name: 'French', color: 0xFF0000, description: '' }, { name: 'England', color: 0x800080, description: '' }, { name: 'Scotch', color: 0x008080, description: '' }, { name: 'Kalmar Union', color: 0x006400, description: '' }, { name: 'Byzantine', color: 0xFF69B4, description: '' }]; var empireButtons = []; var empireButtonSpacing = 200; var empireStartY = -400; for (var i = 0; i < empires.length; i++) { var empire = empires[i]; var button = new Container(); var buttonBG = button.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBG.width = 600; buttonBG.height = 200; buttonBG.tint = empire.color; var buttonText = new Text2(empire.name, { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); buttonText.y = 0; button.addChild(buttonText); var descText = new Text2(empire.description, { size: 40, fill: 0xFFFFFF, weight: 400 }); descText.anchor.set(0.5, 0.5); descText.y = 40; button.addChild(descText); button.y = empireStartY + i * empireButtonSpacing; button.visible = false; // Initially hidden button.down = function (empireData) { return function () { // Show empire characteristics menu instead of starting game directly self.showEmpireCharacteristics(empireData); }; }(empire); self.addChild(button); empireButtons.push(button); } // No exit buttons needed var exitButtons = []; self.showEmpireSelection = function () { self.currentState = 'empire'; // Hide difficulty elements difficultyTitle.visible = false; for (var i = 0; i < difficultyButtons.length; i++) { difficultyButtons[i].visible = false; } // Show empire elements empireTitle.visible = true; for (var i = 0; i < empireButtons.length; i++) { empireButtons[i].visible = true; } // No exit buttons to show }; self.showEmpireCharacteristics = function (empireData) { // Remove any existing characteristics popup if (self.characteristicsPopup) { self.removeChild(self.characteristicsPopup); self.characteristicsPopup.destroy(); } // Create characteristics popup self.characteristicsPopup = new Container(); // Background for characteristics popup var charBG = self.characteristicsPopup.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); charBG.width = 1400; charBG.height = 1000; charBG.tint = 0x222222; charBG.alpha = 0.95; // Empire name title var titleText = new Text2(empireData.name + " Empire", { size: 80, fill: empireData.color, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -400; self.characteristicsPopup.addChild(titleText); // Empire characteristics text var characteristicsText = ""; switch (empireData.name) { case 'Austrian': characteristicsText = "Advantages:\n• +20% tower fire rate\n• All towers attack faster\n• Excellent for rapid defense"; break; case 'Hungarian': characteristicsText = "Advantages:\n• Tower cost -5 gold\n (Normal+ difficulty)\n• Better economy management\n• More affordable towers"; break; case 'Ottoman': characteristicsText = "Advantages:\n• Start with 100 gold\n (instead of 80)\n• +20% damage on all attacks\n• Strong early game"; break; case 'French': characteristicsText = "Advantages:\n• +20% tower durability\n• Towers have more health\n• Better defensive capability"; break; case 'England': characteristicsText = "Advantages:\n• Start with 100 gold\n (instead of 80)\n• 3 fewer enemies per wave\n• Easier defense management"; break; case 'Scotch': characteristicsText = "Advantages:\n• Tower costs -3 gold\n• Archer range +50%\n• Better defense capability"; break; case 'Kalmar Union': characteristicsText = "Advantages:\n• Healer range +100%\n• Better healing coverage\n• Improved tower protection"; break; case 'Byzantine': characteristicsText = "Advantages:\n• Tower durability +100%\n• Towers have double health\nDisadvantages:\n• +3 enemies per wave\n• Higher enemy pressure"; break; default: characteristicsText = "Advantages:\n• Standard empire\n• No special bonuses\n• Balanced gameplay"; } var infoText = new Text2(characteristicsText, { size: 60, fill: 0xFFFFFF, weight: 400 }); infoText.anchor.set(0.5, 0.5); infoText.y = -150; self.characteristicsPopup.addChild(infoText); // Select Empire button var selectButton = new Container(); var selectBG = selectButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); selectBG.width = 400; selectBG.height = 120; selectBG.tint = 0x00AA00; var selectText = new Text2("Select Empire", { size: 60, fill: 0xFFFFFF, weight: 800 }); selectText.anchor.set(0.5, 0.5); selectButton.addChild(selectText); selectButton.y = 200; selectButton.down = function () { // Store selected empire and start game gameSettings.selectedEmpire = empireData.name; storage.selectedEmpire = empireData.name; self.removeChild(self.characteristicsPopup); self.characteristicsPopup.destroy(); self.characteristicsPopup = null; self.startGame(); }; self.characteristicsPopup.addChild(selectButton); // Back button var backButton = new Container(); var backBG = backButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); backBG.width = 200; backBG.height = 100; backBG.tint = 0xAA0000; var backText = new Text2("Back", { size: 50, fill: 0xFFFFFF, weight: 800 }); backText.anchor.set(0.5, 0.5); backButton.addChild(backText); backButton.y = 350; backButton.down = function () { self.removeChild(self.characteristicsPopup); self.characteristicsPopup.destroy(); self.characteristicsPopup = null; }; self.characteristicsPopup.addChild(backButton); self.addChild(self.characteristicsPopup); }; self.startGame = function () { // Apply difficulty settings gameSettings.difficulty = self.selectedDifficulty; storage.selectedDifficulty = self.selectedDifficulty; // Apply empire starting gold bonuses if (gameSettings.selectedEmpire === 'Ottoman' || gameSettings.selectedEmpire === 'England') { setGold(100); } else { setGold(80); } // Enable game interactions gameStartAllowed = true; // Update tower cost displays for new difficulty for (var i = 0; i < sourceTowers.length; i++) { sourceTowers[i].updateCostLabels(); } // Exit menu when empire is selected self.destroy(); }; return self; }); var MusicControl = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 200; buttonBackground.height = 70; buttonBackground.tint = 0xFF8800; var musicText = new Text2("Music1", { size: 50, fill: 0xFFFFFF, weight: 800 }); musicText.anchor.set(0.5, 0.5); self.addChild(musicText); var currentMusicIndex = 1; self.updateDisplay = function () { var musicName = currentMusicIndex === 6 ? "Nomusic" : "Music" + currentMusicIndex; musicText.setText(musicName); // Update button color based on music selection switch (currentMusicIndex) { case 1: buttonBackground.tint = 0xFF8800; // Orange for Music1 break; case 2: buttonBackground.tint = 0x8800FF; // Purple for Music2 break; case 3: buttonBackground.tint = 0x00FF88; // Green for Music3 break; case 4: buttonBackground.tint = 0xFF0088; // Pink for Music4 break; case 5: buttonBackground.tint = 0x0088FF; // Blue for Music5 break; case 6: buttonBackground.tint = 0x444444; // Gray for Nomusic break; } }; self.down = function () { // Block music control if difficulty not selected if (!gameStartAllowed) { return; } // Cycle through music options 1-6 (including Nomusic) currentMusicIndex = currentMusicIndex >= 6 ? 1 : currentMusicIndex + 1; self.updateDisplay(); // Play the corresponding music track or stop music for Nomusic if (currentMusicIndex === 6) { LK.stopMusic(); } else { var musicTrack = "warmusic" + (currentMusicIndex === 1 ? "" : currentMusicIndex); LK.playMusic(musicTrack); } }; // Initialize display self.updateDisplay(); return self; }); 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 () { // Block next wave if difficulty not selected if (!gameStartAllowed) { return; } 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; var notification = game.addChild(new Notification("Wave " + currentWave + " activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Increase size of base for easier touch var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); switch (self.towerType) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'sniper': baseGraphics.tint = 0xFF5500; break; case 'splash': baseGraphics.tint = 0x33CC00; break; case 'slow': baseGraphics.tint = 0x9900FF; break; case 'poison': baseGraphics.tint = 0x00FFAA; break; default: baseGraphics.tint = 0xAAAAAA; } var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var displayNameShadow = self.towerType === 'default' ? 'Infantry' : self.towerType === 'rapid' ? 'Catapult' : self.towerType === 'splash' ? 'Hwacha' : self.towerType === 'sniper' ? 'Archer' : self.towerType === 'slow' ? 'Bullet' : self.towerType === 'poison' ? 'Healer' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1); var typeLabelShadow = new Text2(displayNameShadow, { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); // Add tower type label var displayName = self.towerType === 'default' ? 'Infantry' : self.towerType === 'rapid' ? 'Catapult' : self.towerType === 'splash' ? 'Hwacha' : self.towerType === 'sniper' ? 'Archer' : self.towerType === 'slow' ? 'Bullet' : self.towerType === 'poison' ? 'Healer' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1); var typeLabel = new Text2(displayName, { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -20; // Position above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.updateCostLabels = function () { var newCost = getTowerCost(self.towerType); costLabel.setText(newCost); costLabelShadow.setText(newCost); }; 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 SpeedControl = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 200; buttonBackground.height = 70; buttonBackground.tint = 0x0088FF; var speedText = new Text2("Normal", { size: 50, fill: 0xFFFFFF, weight: 800 }); speedText.anchor.set(0.5, 0.5); self.addChild(speedText); self.updateDisplay = function () { var speedName = gameSpeedOptions[currentSpeedIndex]; speedText.setText(speedName.charAt(0).toUpperCase() + speedName.slice(1)); // Update game speed based on selection switch (speedName) { case 'slow': gameSpeed = 0.5; buttonBackground.tint = 0xFF8800; // Orange for slow break; case 'normal': gameSpeed = 1.0; buttonBackground.tint = 0x0088FF; // Blue for normal break; case 'fast': gameSpeed = 2.0; buttonBackground.tint = 0x00FF00; // Green for fast break; case 'ultra': gameSpeed = 2.5; // 25% faster than fast buttonBackground.tint = 0xFF0000; // Red for ultra break; case 'paused': gameSpeed = 0.0; // Complete pause buttonBackground.tint = 0x666666; // Gray for paused break; } }; self.down = function () { // Block speed control if difficulty not selected if (!gameStartAllowed) { return; } // Cycle through speed options currentSpeedIndex = (currentSpeedIndex + 1) % gameSpeedOptions.length; self.updateDisplay(); }; // Initialize display self.updateDisplay(); 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; // Tower health system self.maxHealth = 100; // Apply French empire bonus: 20% more tower durability if (gameSettings.selectedEmpire === 'French') { self.maxHealth = Math.floor(self.maxHealth * 1.2); // 20% more health } // Apply Byzantine empire bonus: 100% more tower durability if (gameSettings.selectedEmpire === 'Byzantine') { self.maxHealth = Math.floor(self.maxHealth * 2.0); // 100% more health } self.health = self.maxHealth; // 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 var baseRange; if (self.level === self.maxLevel) { baseRange = 12 * CELL_SIZE; // Significantly increased range for max level } else { baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE; } // Apply Scotch empire bonus: 50% more archer range if (gameSettings.selectedEmpire === 'Scotch') { baseRange = baseRange * 1.5; } return baseRange; 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 var baseRange = (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; // Apply Kalmar Union empire bonus: 100% more healer range if (gameSettings.selectedEmpire === 'Kalmar Union') { baseRange = baseRange * 2.0; } return baseRange; default: // Default: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 30; self.damage = 5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'sniper': self.fireRate = 90; self.damage = 25; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'splash': self.fireRate = 52; // 30% faster than 75 (75 * 0.7 = 52.5, rounded to 52) self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; case 'slow': self.fireRate = 50; self.damage = 8; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'poison': self.fireRate = 70; self.damage = 12; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 5; break; } var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); switch (self.id) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'sniper': baseGraphics.tint = 0xFF5500; break; case 'splash': baseGraphics.tint = 0x33CC00; break; case 'slow': baseGraphics.tint = 0x9900FF; break; case 'poison': baseGraphics.tint = 0x00FFAA; break; default: baseGraphics.tint = 0xAAAAAA; } // Add tower health bar var towerHealthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var towerHealthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var towerHealthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); towerHealthBarBG.y = towerHealthBarOutline.y = towerHealthBar.y = baseGraphics.height / 2 + 15; towerHealthBarOutline.x = -towerHealthBarOutline.width / 2; towerHealthBarBG.x = towerHealthBar.x = -towerHealthBar.width / 2 - .5; towerHealthBar.tint = 0x00ff00; towerHealthBarBG.tint = 0xff0000; self.towerHealthBar = towerHealthBar; var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 6; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); var gunGraphics = gunContainer.attachAsset('defense', { anchorX: 0.5, anchorY: 0.5 }); self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { switch (self.id) { case 'rapid': towerLevelIndicator.tint = 0x00AAFF; break; case 'sniper': towerLevelIndicator.tint = 0xFF5500; break; case 'splash': towerLevelIndicator.tint = 0x33CC00; break; case 'slow': towerLevelIndicator.tint = 0x9900FF; break; case 'poison': towerLevelIndicator.tint = 0x00FFAA; break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); 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++; // Increase tower health by 50% on upgrade self.maxHealth = Math.floor(self.maxHealth * 1.5); self.health = self.maxHealth; // Restore to full health on upgrade // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rapid') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect self.damage = 5 + self.level * 10; // double the effect self.bulletSpeed = 7 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades self.damage = 5 + self.level * 3; self.bulletSpeed = 7 + self.level * 0.7; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range if (distance <= self.getRange()) { // Artillery towers can target all enemy types if (self.id === 'splash') { // 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; } } } } else { // Other towers can target both flying and ground enemies // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { // Update tower health bar if (self.health <= 0) { // Healer towers are immune to destruction if (self.id === 'poison') { self.health = self.maxHealth; // Reset to full health self.towerHealthBar.width = 70; // Full health bar } else { self.health = 0; self.towerHealthBar.width = 0; // Destroy tower when health reaches zero // Remove from cells in range 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); } } // Reset grid cells to floor (if not castle walls) for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (cell && cell.type !== 4) { cell.type = 0; } } } // Remove from towers array var towerIndex = towers.indexOf(self); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } // Clear selected tower if this was selected if (selectedTower === self) { selectedTower = null; } // Remove tower from game self.destroy(); // Update pathfinding grid.pathFind(); grid.renderDebug(); return; } } else { self.towerHealthBar.width = self.health / self.maxHealth * 70; } 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); // Only rotate gun container if this is not a Trecher tower or Healer tower if (self.id !== 'slow' && self.id !== 'poison') { gunContainer.rotation = angle; } // Check if this tower is affected by any Trecher towers var effectiveFireRate = self.fireRate; if (self.id !== 'slow') { // Trechers don't boost themselves for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (tower.id === 'slow' && tower.isInRange(self)) { effectiveFireRate = self.fireRate * 0.5; // 50% faster firing (half the fire rate delay) break; } } } // Apply Austrian empire bonus: 20% faster fire rate (20% less delay) if (gameSettings.selectedEmpire === 'Austrian') { effectiveFireRate = effectiveFireRate * 0.8; // 20% faster firing } if (LK.ticks - self.lastFired >= effectiveFireRate / gameSpeed) { 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; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { // Trecher towers don't fire bullets - they only provide support effects if (self.id === 'slow') { return; } // Healer towers don't fire bullets - they only provide healing/protection effects if (self.id === 'poison') { return; } if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bulletDamage = self.damage; // Apply Ottoman empire bonus: 20% more damage if (gameSettings.selectedEmpire === 'Ottoman') { bulletDamage = Math.ceil(bulletDamage * 1.2); // 20% more damage } var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, bulletDamage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0x00AAFF; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'sniper': bullet.children[0].tint = 0xFF5500; bullet.children[0].width = 15; bullet.children[0].height = 15; break; case 'splash': bullet.children[0].tint = 0x33CC00; bullet.children[0].width = 40; bullet.children[0].height = 40; break; case 'poison': bullet.children[0].tint = 0x00FFAA; bullet.children[0].width = 35; bullet.children[0].height = 35; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; } } } self.refreshCellsInRange(); // Completely destroy and recreate tower preview when tower is successfully placed if (towerPreview && towerPreview.parent) { game.removeChild(towerPreview); towerPreview.destroy(); towerPreview = new TowerPreview(); towerPreview.visible = false; } }; 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('rare', { 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; switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; break; case 'sniper': previewGraphics.tint = 0xFF5500; break; case 'splash': previewGraphics.tint = 0x33CC00; break; case 'slow': previewGraphics.tint = 0x9900FF; break; case 'poison': previewGraphics.tint = 0x00FFAA; break; default: previewGraphics.tint = 0xAAAAAA; } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) { validGridPlacement = false; } else { for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (!cell || cell.type !== 0) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) { continue; } // Only check non-flying enemies, flying enemies can pass over towers if (!enemy.isFlying) { if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) { self.blockedByEnemy = true; break; } } } } } self.canPlace = validGridPlacement && !self.blockedByEnemy; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; self.checkPlacement(); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var towerDisplayName = self.tower.id === 'default' ? 'Infantry' : self.tower.id === 'rapid' ? 'Catapult' : self.tower.id === 'splash' ? 'Hwacha' : self.tower.id === 'sniper' ? 'Archer' : self.tower.id === 'slow' ? 'Bullet' : self.tower.id === 'poison' ? 'Healer' : self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1); var towerTypeText = new Text2(towerDisplayName + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { // Only reset to floor if it's not a castle wall if (cell.type !== 4) { cell.type = 0; } var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { // Block game start if difficulty not selected if (!gameStartAllowed) { return; } 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; // All waves are normal enemy waves var waveType = "Dawn"; var enemyType = "normal"; var enemyCount = 10; block.tint = 0xAAAAAA; // 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; } // Skip wave progression when game is paused (gameSpeed = 0) if (gameSpeed === 0) { return; } if (currentWave < totalWaves) { waveTimer += gameSpeed; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; if (currentWave != 1) { var notification = game.addChild(new Notification("Wave " + currentWave + " incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } } }; self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; // Game difficulty settings var gameSettings = { difficulty: storage.selectedDifficulty || 'normal', selectedEmpire: storage.selectedEmpire || 'none' }; var gold = gameSettings.selectedEmpire === 'Ottoman' ? 100 : 80; // Generate a new session seed on each reload/restart var reloadCounter = storage.reloadCounter || 0; reloadCounter++; storage.reloadCounter = reloadCounter; var gameSessionSeed = reloadCounter * 12345 + Math.floor(Date.now() / 1000); // Unique seed per reload // Track if difficulty has been selected and game can start var gameStartAllowed = false; var lives = 10; var currentWave = 0; var totalWaves = 5; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var gameSpeed = 1.0; // 1.0 = normal, 0.5 = slow, 2.0 = fast var gameSpeedOptions = ['slow', 'normal', 'fast', 'ultra', 'paused']; var currentSpeedIndex = 1; // Start with normal (index 1) var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); livesText.x = -spacing; livesText.y = topMargin; goldText.x = spacing; goldText.y = topMargin; function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); } function setGold(value) { gold = value; updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY) { var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 10; switch (towerType) { case 'rapid': cost = 30; break; case 'sniper': cost = 50; break; case 'splash': cost = 50; break; case 'slow': cost = gameSettings.difficulty === 'hardcore' ? 100 : 50; break; case 'poison': cost = gameSettings.difficulty === 'hardcore' ? 100 : 50; break; } // Apply Easy mode discount if (gameSettings.difficulty === 'easy') { cost = Math.floor(cost / 2); } // Apply Hungarian empire bonus: 5 gold less on normal and higher difficulties if (gameSettings.selectedEmpire === 'Hungarian' && gameSettings.difficulty !== 'easy') { cost = Math.max(5, cost - 5); // Minimum cost of 5 gold } // Apply Scotch empire bonus: 3 gold less for all towers if (gameSettings.selectedEmpire === 'Scotch') { cost = Math.max(5, cost - 3); // Minimum cost of 5 gold } 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; } } game.down = function (x, y, obj) { // Block all interactions if difficulty not selected if (!gameStartAllowed) { return; } var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } 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) { // Completely recreate tower preview to ensure clean state if (towerPreview && towerPreview.parent) { game.removeChild(towerPreview); towerPreview.destroy(); } towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); // Apply the same offset as in move handler to ensure consistency when starting drag towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); break; } } }; game.move = function (x, y, obj) { // Block all interactions if difficulty not selected if (!gameStartAllowed) { return; } if (isDragging) { // Shift the y position upward by 1.5 tiles to show preview above finger towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } }; game.up = function (x, y, obj) { // Block all interactions if difficulty not selected if (!gameStartAllowed) { return; } var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; if (towerPreview.canPlace) { if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; // Completely destroy and recreate tower preview to ensure it's completely clean if (towerPreview.parent) { game.removeChild(towerPreview); towerPreview.destroy(); towerPreview = new TowerPreview(); towerPreview.visible = false; } if (isDragging) { var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } } }; var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2; waveIndicator.y = 2732 - 80; game.addChild(waveIndicator); // Always show main menu at game start for difficulty selection var mainMenu = new MainMenu(); game.addChild(mainMenu); var speedControl = new SpeedControl(); speedControl.x = -100; // Position closer to center to be adjacent to music control speedControl.y = topMargin; LK.gui.top.addChild(speedControl); var musicControl = new MusicControl(); musicControl.x = 100; // Position closer to center to be adjacent to speed control musicControl.y = topMargin; LK.gui.top.addChild(musicControl); var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; var sourceTowers = []; var towerSpacing = 300; // Increase spacing for larger towers var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; game.update = function () { if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Just update pathfinding for the existing maze, don't regenerate it grid.pathFind(); grid.renderDebug(); // Every wave spawns 10 enemies on wave 1, then 25% more each wave var enemyCount = Math.floor(10 * Math.pow(1.25, currentWave - 1)); // England empire: 3 fewer normal enemies per wave if (gameSettings.selectedEmpire === 'England') { enemyCount = Math.max(1, enemyCount - 3); // Minimum 1 enemy } // Byzantine empire: 3 more enemies per wave if (gameSettings.selectedEmpire === 'Byzantine') { enemyCount = enemyCount + 3; } // Determine flying enemy count based on wave number var flyingEnemyCount = 0; if (currentWave === 1) { flyingEnemyCount = 0; } else if (currentWave === 2) { flyingEnemyCount = 1; } else if (currentWave === 3 || currentWave === 4) { flyingEnemyCount = 3; } else if (currentWave === 5) { flyingEnemyCount = 5; enemyCount = 9; // Total enemies for wave 5 } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { // Determine enemy type based on specific wave requirements var waveType = 'normal'; // Default to normal var immuneCount = 0; var angryCount = 0; // Set specific immune enemy counts per wave if (currentWave === 2 || currentWave === 3 || currentWave === 4 || currentWave === 5) { immuneCount = 3; } // Hard mode: more immune and angry enemies if (gameSettings.difficulty === 'hard') { // Wave 1: 2 immune enemies if (currentWave === 1) { immuneCount = 2; } // Waves 2,3,4: 3 angry enemies each if (currentWave === 2 || currentWave === 3 || currentWave === 4) { angryCount = 3; immuneCount = 3; } // Wave 5: keep existing counts if (currentWave === 5) { angryCount = 3; immuneCount = 5; } } else { // Set angry enemy spawning: 0 in waves 1-3, 2 in waves 4-5 if (currentWave === 4) { angryCount = 2; } else if (currentWave === 5) { angryCount = 2; } } // Set bad enemy spawning: only in wave 5 var badCount = 0; if (currentWave === 5) { badCount = 1; // 1 bad enemy immuneCount = 5; // 5 immune enemies angryCount = 3; // 3 angry enemies } // Make the first 'angryCount' enemies angry for this wave if (i < angryCount) { waveType = 'angry'; } // Make the next 'badCount' enemies bad for this wave (after angry enemies) else if (i >= angryCount && i < angryCount + badCount) { waveType = 'bad'; } // Make the last 'immuneCount' enemies immune for this wave (but not if already angry or bad) else if (i >= enemyCount - immuneCount) { waveType = 'immune'; } // England empire: guarantee 1 bad enemy in wave 5 if (gameSettings.selectedEmpire === 'England' && currentWave === 5) { // Ensure at least one bad enemy spawns if (i === 0) { waveType = 'bad'; // First enemy in wave 5 is always bad for England } else if (i < angryCount) { waveType = 'angry'; } else if (i >= angryCount && i < angryCount + badCount && badCount > 1) { waveType = 'bad'; } else if (i >= enemyCount - immuneCount) { waveType = 'immune'; } } else if (gameSettings.selectedEmpire === 'England' && currentWave === 5 && i === enemyCount - 1) { waveType = 'bad'; // Last enemy in wave 5 is always bad for England } var enemy = new Enemy(waveType); // All enemies are now ground enemies, add to bottom layer enemyLayerBottom.addChild(enemy); // Scale difficulty with wave number but don't apply to boss // as bosses already have their health multiplier // Use exponential scaling for health var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // All enemy types now spawn in the middle 6 tiles at the top spacing var gridWidth = 24; var midPoint = Math.floor(gridWidth / 2); // 12 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading enemy.cellX = spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; var notification = game.addChild(new Notification("Wave " + currentWave + " completed!")); notification.x = 2048 / 2; notification.y = grid.height - 150; // Automatically advance to next wave if all enemies are gone if (currentWave < totalWaves) { waveTimer = nextWaveTime; // Trigger next wave immediately } } } 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; } // Give gold for every enemy killed - Hard mode gives 5 gold, others give 10 var goldEarned = gameSettings.difficulty === 'hard' ? 5 : 10; 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); lives = Math.max(0, lives - (enemy.type === 'bad' ? 10 : 1)); updateUI(); if (lives <= 0) { LK.showGameOver(); } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } }; // Play war music when game loads LK.playMusic('warmusic');
===================================================================
--- original.js
+++ change.js
@@ -1370,9 +1370,9 @@
case 'Kalmar Union':
characteristicsText = "Advantages:\n• Healer range +100%\n• Better healing coverage\n• Improved tower protection";
break;
case 'Byzantine':
- characteristicsText = "Advantages:\n• Balanced empire\n• No special bonuses\n• Standard gameplay";
+ characteristicsText = "Advantages:\n• Tower durability +100%\n• Towers have double health\nDisadvantages:\n• +3 enemies per wave\n• Higher enemy pressure";
break;
default:
characteristicsText = "Advantages:\n• Standard empire\n• No special bonuses\n• Balanced gameplay";
}
@@ -1748,8 +1748,12 @@
// Apply French empire bonus: 20% more tower durability
if (gameSettings.selectedEmpire === 'French') {
self.maxHealth = Math.floor(self.maxHealth * 1.2); // 20% more health
}
+ // Apply Byzantine empire bonus: 100% more tower durability
+ if (gameSettings.selectedEmpire === 'Byzantine') {
+ self.maxHealth = Math.floor(self.maxHealth * 2.0); // 100% more health
+ }
self.health = self.maxHealth;
// Standardized method to get the current range of the tower
self.getRange = function () {
// Always calculate range based on tower type and level
@@ -3355,8 +3359,12 @@
// England empire: 3 fewer normal enemies per wave
if (gameSettings.selectedEmpire === 'England') {
enemyCount = Math.max(1, enemyCount - 3); // Minimum 1 enemy
}
+ // Byzantine empire: 3 more enemies per wave
+ if (gameSettings.selectedEmpire === 'Byzantine') {
+ enemyCount = enemyCount + 3;
+ }
// Determine flying enemy count based on wave number
var flyingEnemyCount = 0;
if (currentWave === 1) {
flyingEnemyCount = 0;
White circle with two eyes, seen from above.. In-Game asset. 2d. High contrast. No shadows
tower from top. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
tower from top. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
White simple circular enemy seen from above, black outline. Black eyes, with a single Sword in-font of it. Black and white only. Blue background.
White simple circular enemy seen from above, black outline. Black eyes, on a white horse. Black and white only. Blue background.
crown. In-Game asset. 2d. High contrast. No shadows
information İ. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
gece yarısı eski ve gerçekçi bir kale resmi. In-Game asset. 2d. High contrast
fire in a circle. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
money in a circle. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
bu kulenin rasgele noktalarına parlak yeşil ışıklar ekle (duvarlarına sadece!) duvarları mavi yap
kale olmasın. Top down bir orta çağ demirci evi olsun
make it medieval style and not colorful
gold mines. In-Game asset. 2d. High contrast. No shadows
eline parlak yeşil bir kılıç ekle
Butonun yerinde açılmış bir orta çağ stilinde bir yırtık pırtık kağıt olsun
2 boyutlu üstten görülme bir savaş topu. In-Game asset. 2d. High contrast. No shadows
tekerleri mavi olsun ve demir gövdenin üzerinde yeşil yeşil ışık gibi benekler olsun
buna bir savaş topu havası kat
realistic stone wall. In-Game asset. 2d. High contrast. No shadows
karşıdan gözüken bir dağ. In-Game asset. 2d. High contrast. No shadows
ağaç. In-Game asset. 2d. High contrast. No shadows
mükemmel bir kare şeklinde olsun