/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Enemy = Container.expand(function (type, wave, pathIndex, strengthLevel) { var self = Container.call(this); self.type = type; self.wave = wave; self.pathIndex = pathIndex || 1; self.strengthLevel = strengthLevel || 1; self.currentPath = self.pathIndex === 1 ? enemyPath1 : self.pathIndex === 2 ? enemyPath2 : enemyPath3; // Define base stats for each enemy type with exact balancing specifications var baseStats = { // 30 enemies with specific wave appearance, HP, and speed goblin_scout: { speed: 1.2, health: 100, coins: 15, flying: false, waveAppearance: 1 }, mire_crawler: { speed: 1.3, health: 160, coins: 18, flying: false, waveAppearance: 4 }, fang_imp: { speed: 1.4, health: 220, coins: 22, flying: false, waveAppearance: 7 }, crimson_leaper: { speed: 1.5, health: 280, coins: 25, flying: true, waveAppearance: 10 }, rustback_beetle: { speed: 1.6, health: 340, coins: 28, flying: false, waveAppearance: 13 }, gravel_hound: { speed: 1.7, health: 400, coins: 32, flying: false, waveAppearance: 16 }, vine_stalker: { speed: 1.8, health: 460, coins: 35, flying: false, waveAppearance: 19 }, ashborne_wraith: { speed: 1.9, health: 520, coins: 38, flying: true, waveAppearance: 22 }, stonehide_orc: { speed: 2.0, health: 580, coins: 42, flying: false, waveAppearance: 25 }, toxic_maw: { speed: 2.0, health: 640, coins: 45, flying: false, waveAppearance: 28 }, frostback_boar: { speed: 2.1, health: 700, coins: 48, flying: false, waveAppearance: 31 }, ironmaw_troll: { speed: 2.1, health: 760, coins: 52, flying: false, waveAppearance: 34 }, volcanic_strider: { speed: 2.2, health: 820, coins: 55, flying: false, waveAppearance: 37 }, stormblade_knight: { speed: 2.2, health: 880, coins: 58, flying: false, waveAppearance: 40 }, twilight_serpent: { speed: 2.3, health: 940, coins: 62, flying: true, waveAppearance: 43 }, obsidian_colossus: { speed: 2.3, health: 1000, coins: 65, flying: false, waveAppearance: 46 }, spectral_ravager: { speed: 2.4, health: 1060, coins: 68, flying: false, waveAppearance: 49 }, boneclad_warbeast: { speed: 2.4, health: 1120, coins: 72, flying: false, waveAppearance: 52 }, plague_titan: { speed: 2.5, health: 1180, coins: 75, flying: false, waveAppearance: 55 }, abyss_reaper: { speed: 2.5, health: 1240, coins: 78, flying: false, waveAppearance: 58 }, wraith_bringer: { speed: 2.6, health: 1300, coins: 82, flying: true, waveAppearance: 61 }, inferno_crawler: { speed: 2.6, health: 1360, coins: 85, flying: false, waveAppearance: 64 }, thornback_hydra: { speed: 2.7, health: 1420, coins: 88, flying: false, waveAppearance: 67 }, ghostfire_shade: { speed: 2.7, health: 1480, coins: 92, flying: false, waveAppearance: 70 }, storm_herald: { speed: 2.8, health: 1540, coins: 95, flying: true, waveAppearance: 73 }, voidling_runner: { speed: 2.8, health: 1600, coins: 98, flying: false, waveAppearance: 76 }, doom_shell: { speed: 2.9, health: 1660, coins: 102, flying: false, waveAppearance: 79 }, nether_juggernaut: { speed: 2.9, health: 1720, coins: 105, flying: false, waveAppearance: 82 }, abyss_howler: { speed: 3.0, health: 1780, coins: 108, flying: true, waveAppearance: 85 }, void_devourer: { speed: 3.0, health: 1840, coins: 112, flying: false, waveAppearance: 88 }, // Legacy support for existing enemies goblin: { speed: 1.2, health: 100, coins: 15, flying: false, waveAppearance: 1 }, crawler: { speed: 1.3, health: 160, coins: 18, flying: false, waveAppearance: 4 }, bandit: { speed: 1.4, health: 220, coins: 22, flying: false, waveAppearance: 7 }, golem: { speed: 1.5, health: 280, coins: 25, flying: false, waveAppearance: 10 }, imp: { speed: 1.6, health: 340, coins: 28, flying: false, waveAppearance: 13 }, knight: { speed: 1.7, health: 400, coins: 32, flying: false, waveAppearance: 16 }, beast: { speed: 1.8, health: 460, coins: 35, flying: false, waveAppearance: 19 }, mage: { speed: 1.9, health: 520, coins: 38, flying: false, waveAppearance: 22 }, reaver: { speed: 2.0, health: 580, coins: 42, flying: true, waveAppearance: 25 }, doom: { speed: 2.0, health: 640, coins: 45, flying: false, waveAppearance: 28 } }; var stats = baseStats[type] || baseStats.goblin_scout; // Apply endless mode scaling if active var healthMultiplier = 1; var speedMultiplier = 1; if (endlessMode && currentWave > 100) { var endlessWaves = Math.floor((currentWave - 100) / 5); healthMultiplier = 1 + endlessWaves * 0.1; // +10% HP every 5 waves speedMultiplier = 1 + endlessWaves * 0.05; // +5% speed every 5 waves } // Apply scaling to stats self.speed = stats.speed * speedMultiplier; self.maxHealth = Math.floor(stats.health * healthMultiplier); self.health = self.maxHealth; self.pathStep = 0; self.coinValue = stats.coins; self.flying = stats.flying; self.regeneration = type === 'beast' ? 0.5 : type === 'frostback_boar' ? 0.3 : type === 'plague_titan' ? 0.8 : 0; self.poisonResistant = type === 'doom' || type === 'toxic_maw' || type === 'plague_titan' || type === 'void_devourer'; self.slowResistant = type === 'doom' || type === 'volcanic_strider' || type === 'stormblade_knight' || type === 'abyss_reaper'; self.originalSpeed = self.speed; // Store original speed for slowing effect self.slowedByFrost = false; // Track if enemy is slowed by frost tower self.slowTimer = 0; // Timer for slow effect duration self.slowPercentage = 0; // Current slow percentage applied // Map enemy types to valid asset IDs with fallback system var assetMapping = { 'goblin_scout': 'enemy_goblin', 'mire_crawler': 'enemy_mire_crawler', 'fang_imp': 'enemy_fang_imp', 'crimson_leaper': 'enemy_crimson_leaper', 'rustback_beetle': 'enemy_rustback_beetle', 'gravel_hound': 'enemy_gravel_hound', 'vine_stalker': 'enemy_vine_stalker', 'ashborne_wraith': 'enemy_ashborne_wraith', 'stonehide_orc': 'enemy_stonehide_orc', 'toxic_maw': 'enemy_toxic_maw', 'frostback_boar': 'enemy_frostback_boar', 'ironmaw_troll': 'enemy_ironmaw_troll', 'volcanic_strider': 'enemy_volcanic_strider', 'stormblade_knight': 'enemy_stormblade_knight', 'twilight_serpent': 'enemy_twilight_serpent', 'obsidian_colossus': 'enemy_obsidian_colossus', 'spectral_ravager': 'enemy_spectral_ravager', 'boneclad_warbeast': 'enemy_boneclad_warbeast', 'plague_titan': 'enemy_plague_titan', 'abyss_reaper': 'enemy_abyss_reaper', 'wraith_bringer': 'enemy_mage', // fallback to mage 'inferno_crawler': 'enemy_crawler', 'thornback_hydra': 'enemy_beast', // fallback to beast 'ghostfire_shade': 'enemy_mage', // fallback to mage 'storm_herald': 'enemy_knight', // fallback to knight 'voidling_runner': 'enemy_reaver', // fallback to reaver 'doom_shell': 'enemy_doom', 'nether_juggernaut': 'enemy_golem', // fallback to golem 'abyss_howler': 'enemy_beast', // fallback to beast 'void_devourer': 'enemy_void_devourer' }; // Get the correct asset ID or fallback to goblin var assetId = assetMapping[type] || 'enemy_' + type; // Try to get the asset, fallback to goblin if it doesn't exist var graphics; try { graphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); } catch (e) { // Fallback to default goblin sprite if asset doesn't exist graphics = self.attachAsset('enemy_goblin', { anchorX: 0.5, anchorY: 0.5 }); } // Create health bar background using the same asset fallback system try { self.healthBarBg = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.1, y: -25 }); } catch (e) { self.healthBarBg = self.attachAsset('enemy_goblin', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.1, y: -25 }); } self.healthBarBg.tint = 0x333333; // Create health bar fill using the same asset fallback system try { self.healthBarFill = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.1, y: -25 }); } catch (e) { self.healthBarFill = self.attachAsset('enemy_goblin', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.1, y: -25 }); } self.healthBarFill.tint = 0x00FF00; self.x = self.currentPath[0].x; self.y = self.currentPath[0].y; self.takeDamage = function (damage) { self.health -= damage; LK.effects.flashObject(self, 0xFF0000, 200); // Update health bar fill based on remaining health if (self.healthBarFill) { var healthPercent = Math.max(0, self.health / self.maxHealth); self.healthBarFill.scaleX = 0.8 * healthPercent; // Change color from green to red based on health var redComponent = Math.floor(255 * (1 - healthPercent)); var greenComponent = Math.floor(255 * healthPercent); var healthColor = redComponent << 16 | greenComponent << 8 | 0x00; self.healthBarFill.tint = healthColor; } if (self.health <= 0) { if (!audioMuted) { LK.getSound('enemy_hit').play(); LK.getSound('coin_gain').play(); } // Create floating coin gain text with reduced gold reward (35% reduction) var actualCoinValue = Math.floor(self.coinValue * 0.65); var coinText = new Text2('+' + actualCoinValue, { size: 40, fill: 0xFFD700 }); coinText.anchor.set(0.5, 0.5); coinText.x = self.x; coinText.y = self.y - 30; game.addChild(coinText); // Animate coin text floating up and fading out tween(coinText, { y: coinText.y - 80, alpha: 0 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { if (coinText && coinText.destroy) { coinText.destroy(); } } }); // Create death effect at enemy position tween(self, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 400, easing: tween.easeOut }); return true; } return false; }; self.update = function () { // Handle slow timer for frost effect if (self.slowTimer > 0) { self.slowTimer -= gameSpeed; // Apply speed multiplier to timer if (self.slowTimer <= 0) { // Restore original speed when slow effect expires self.speed = self.originalSpeed; self.slowedByFrost = false; self.slowPercentage = 0; } } if (self.pathStep < self.currentPath.length - 1) { var target = self.currentPath[self.pathStep + 1]; var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 5) { self.pathStep++; if (self.pathStep >= self.currentPath.length - 1) { self.reachedCastle = true; } } else { self.x += dx / distance * self.speed * gameSpeed; // Apply speed multiplier self.y += dy / distance * self.speed * gameSpeed; // Apply speed multiplier } } }; self.applyFrostSlow = function (slowPercentage) { // Don't apply slow to slow-resistant enemies if (self.slowResistant) { return; } // Apply the slow effect self.slowedByFrost = true; self.slowPercentage = slowPercentage; self.speed = self.originalSpeed * (1 - slowPercentage / 100); self.slowTimer = 120; // 2 seconds at 60 FPS }; return self; }); var Projectile = Container.expand(function (type, startX, startY, targetEnemy, damage) { var self = Container.call(this); self.type = type; self.damage = damage; self.targetEnemy = targetEnemy; // Store reference to target enemy self.speed = type === 'arrow' ? 16 : type === 'cannonball' ? 12 : 14; var graphics = self.attachAsset('projectile_' + type, { anchorX: 0.5, anchorY: 0.5 }); self.x = startX; self.y = startY; self.update = function () { // Check if target enemy still exists if (!self.targetEnemy || !self.targetEnemy.x) { self.hasReachedTarget = true; return; } // Calculate direction to current enemy position (tracking) var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Move toward enemy at projectile speed if (distance > 0) { self.velocityX = dx / distance * self.speed; self.velocityY = dy / distance * self.speed; self.x += self.velocityX * gameSpeed; // Apply speed multiplier self.y += self.velocityY * gameSpeed; // Apply speed multiplier } // Check if projectile has reached the target (smaller threshold for guaranteed hit) if (distance < 15) { self.hasReachedTarget = true; } }; return self; }); var Tower = Container.expand(function (type) { var self = Container.call(this); self.type = type; self.level = 1; self.lastShotTime = 0; self.maxLevel = type === 'antiair' || type === 'poison' || type === 'tesla' ? 5 : 10; // Initialize tower stats based on balanced specifications if (type === 'infantry') { self.damage = 11.375; self.range = 150; self.attackSpeed = Math.floor(60 / 0.8 * 1.54 * 1.35); // 0.8 shots/sec slowed by 70% = 155 ticks between shots self.cost = 80; self.projectileType = 'arrow'; } else if (type === 'frost') { self.damage = 1.456; self.range = 150; self.attackSpeed = Math.floor(60 * 2 * 1.54 * 1.35); // 1 shot per 2 seconds slowed by 70% = 250 ticks self.cost = 120; self.projectileType = 'magic'; self.slowEffect = true; self.slowPercentage = 27; // Level 1 slow - increased by 30% from 21% self.aoeRadius = 120; } else if (type === 'antiair') { self.damage = 13.65; self.range = 150; self.attackSpeed = Math.floor(60 / 1.5 * 1.54 * 1.35); // 1.5 shots/sec slowed by 70% = 84 ticks self.cost = 100; self.projectileType = 'magic'; self.antiAir = true; } else if (type === 'poison') { self.damage = 4.55; self.range = 150; self.attackSpeed = Math.floor(60 / 1.2 * 1.54 * 1.35); // 1.2 shots/sec slowed by 70% = 104 ticks self.cost = 130; self.projectileType = 'magic'; self.poisonEffect = true; self.poisonDuration = 3; // 3 seconds self.poisonTickInterval = 60; // 1 second in ticks } else if (type === 'tesla') { self.damage = 15.925; self.range = 150; self.attackSpeed = Math.floor(60 / 0.7 * 1.54 * 1.35); // 0.7 shots/sec slowed by 70% = 178 ticks self.cost = 150; self.projectileType = 'magic'; self.chainLightning = true; self.chainTargets = 3; self.chainDamageReduction = 0.1; // 10% reduction per jump } var graphics = self.attachAsset('tower_' + type, { anchorX: 0.5, anchorY: 0.5 }); self.getUpgradeNames = function () { if (self.type === 'infantry') { return ['Rifleman', 'Marksman', 'Sharpshooter', 'Machine Gunner', 'Grenadier', 'Mortar Operator', 'Tank Commander', 'Bazooka Specialist', 'Rocket Launcher', 'Missile Battery']; } else if (self.type === 'frost') { return ['Frost Spire', 'Ice Shard Tower', 'Blizzard Bastion', 'Cryo Cannon', 'Glacier Fortress', 'Snowstorm Citadel', 'Avalanche Keep', 'Frozen Maelstrom', 'Arctic Warden', 'Absolute Zero']; } else if (self.type === 'antiair') { return ['Flak Cannon', 'SAM Launcher', 'Radar Missile Battery', 'Laser Air Defense', 'Plasma Barrier']; } else if (self.type === 'poison') { return ['Venom Sprayer', 'Toxic Spitter', 'Acid Dart Tower', 'Corrosive Spire', 'Plague Altar']; } else if (self.type === 'tesla') { return ['Static Coil', 'Dual Coil', 'Arc Emitter', 'Thunderstorm Core', 'Lightning God']; } }; self.canShoot = function () { return LK.ticks - self.lastShotTime > self.attackSpeed / gameSpeed / (1000 / 60); // Apply speed multiplier to attack speed }; self.findTarget = function () { var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var distance = Math.sqrt((enemy.x - self.x) * (enemy.x - self.x) + (enemy.y - self.y) * (enemy.y - self.y)); if (distance <= self.range && distance < closestDistance) { // Flying enemy rules: only Anti-Air towers can target flying enemies if (enemy.flying && self.type !== 'antiair') { continue; // Skip flying enemies for non-antiair towers } // Non-flying enemies cannot be targeted by Anti-Air towers if (!enemy.flying && self.type === 'antiair') { continue; // Skip non-flying enemies for antiair towers } closestDistance = distance; closestEnemy = enemy; } } return closestEnemy; }; self.shoot = function (target) { if (!self.canShoot()) { return; } self.lastShotTime = LK.ticks; var projectile = new Projectile(self.projectileType, self.x, self.y, target, self.damage); projectile.towerType = self.type; projectile.slowEffect = self.slowEffect; projectile.poisonEffect = self.poisonEffect; projectile.chainLightning = self.chainLightning; projectiles.push(projectile); isoContainer.addChild(projectile); if (!audioMuted) { if (self.type === 'tesla') { LK.getSound('tesla_knock').play(); } else { LK.getSound('shoot_arrow').play(); } } }; self.getUpgradeCost = function () { // Calculate upgrade cost: Level 1 = base price, then +80% each level var upgradeCost = self.cost; for (var i = 2; i <= self.level; i++) { upgradeCost = Math.floor(upgradeCost * 1.8); } return upgradeCost; }; self.upgrade = function () { // Check if upgrade level is unlocked var nextLevel = self.level + 1; if (nextLevel > upgradeUnlockWaves.length || currentWave < upgradeUnlockWaves[nextLevel - 1]) { showUpgradeRestrictedMessage(nextLevel, upgradeUnlockWaves[nextLevel - 1]); return; } if (self.level < self.maxLevel) { self.level++; // Tower-specific damage upgrades if (self.type === 'infantry') { self.damage += 4.55; // +4.55 flat damage per level (30% reduction from 6.5) } else if (self.type === 'frost') { self.damage += 0.437; // +0.437 damage per level (30% reduction from 0.624) // Update slow percentage - increased by 30% if (self.level === 2 || self.level === 3) { self.slowPercentage = 45; } else if (self.level >= 4) { self.slowPercentage = 64; } } else if (self.type === 'antiair') { self.damage += 3.64; // +3.64 damage per level (30% reduction from 5.2) } else if (self.type === 'poison') { self.damage += 2.275; // +2.275 tick damage per level (30% reduction from 3.25) self.poisonDuration += 1; // +1 second duration } else if (self.type === 'tesla') { self.damage += 4.55; // +4.55 base damage per level (30% reduction from 6.5) // Increase chain targets every 2 levels if (self.level % 2 === 0) { self.chainTargets = Math.min(6, self.chainTargets + 1); } } // Calculate range increase with diminishing returns - all towers start at 150 base range var rangeMultiplier = 1.0; if (self.level === 2) { rangeMultiplier = 1.10; // +10% for level 1 upgrade } else if (self.level === 3) { rangeMultiplier = 1.10 * 1.05; // +10% then +5% } else if (self.level === 4) { rangeMultiplier = 1.10 * 1.05 * 1.035; // +10%, +5%, then +3.5% } else if (self.level >= 5) { // +10%, +5%, +3.5%, then +2% for each additional level rangeMultiplier = 1.10 * 1.05 * 1.035; var additionalLevels = self.level - 4; rangeMultiplier *= Math.pow(1.02, additionalLevels); } // Apply range multiplier to base range of 150 for all towers self.range = Math.floor(150 * rangeMultiplier); self.attackSpeed = Math.max(200, Math.floor(self.attackSpeed * 0.95)); // Visual upgrade indicators var tintColors = [0xFFFFFF, 0xFFD700, 0xFF6347, 0xFF4500, 0x8A2BE2, 0x00FF00, 0x00FFFF, 0xFF69B4, 0xFFFF00, 0xFF0000]; graphics.tint = tintColors[self.level - 1] || 0xFF0000; // Show range indicator for upgraded towers if (self.level > 1 && !self.rangeIndicator) { self.rangeIndicator = LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, alpha: 0.1, scaleX: self.range / 40, scaleY: self.range / 40 }); self.rangeIndicator.x = self.x; self.rangeIndicator.y = self.y; self.rangeIndicator.tint = 0x00FF00; isoContainer.addChild(self.rangeIndicator); } else if (self.rangeIndicator) { self.rangeIndicator.scaleX = self.range / 40; self.rangeIndicator.scaleY = self.range / 40; } } }; self.down = function (x, y, obj) { selectedTower = self; // Clear any existing range indicator if (activeRangeIndicator && activeRangeIndicator.destroy) { activeRangeIndicator.destroy(); activeRangeIndicator = null; } // Clear any existing upgrade panel if (upgradePanel && upgradePanel.destroy) { upgradePanel.destroy(); upgradePanel = null; } // Create new range indicator activeRangeIndicator = LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, alpha: 0.2, scaleX: self.range / 27.5, scaleY: self.range / 27.5 }); activeRangeIndicator.x = self.x; activeRangeIndicator.y = self.y; activeRangeIndicator.tint = 0x00FFFF; // Cyan color for range indicator isoContainer.addChild(activeRangeIndicator); // Create larger upgrade panel upgradePanel = new Container(); isoContainer.addChild(upgradePanel); // Smart positioning: determine if panel should open upward or downward var panelHeight = 220; // Approximate panel height (8 * 27.5) var panelY = self.y - 100; // Default upward position var shouldOpenDownward = false; // Check if tower is too close to top edge (within 300 pixels) if (self.y < 300) { shouldOpenDownward = true; panelY = self.y + 100; // Position below tower } // Check if tower is too close to bottom edge else if (self.y > 2732 - 300) { // Keep upward positioning for bottom towers panelY = self.y - 100; } // Check if tower is too close to left edge var panelX = self.x; if (self.x < 350) { panelX = self.x + 50; // Shift right if too close to left edge } // Check if tower is too close to right edge else if (self.x > 2048 - 350) { panelX = self.x - 50; // Shift left if too close to right edge } // Panel background - much larger var panelBg = upgradePanel.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: panelX, y: panelY, scaleX: 12, scaleY: 8 })); panelBg.tint = 0x000000; panelBg.alpha = 0.9; // Calculate total investment for sell value var totalInvestment = self.cost; for (var i = 2; i <= self.level; i++) { var levelCost = self.cost; for (var j = 2; j <= i; j++) { levelCost = Math.floor(levelCost * 1.8); } totalInvestment += levelCost; } var sellValue = Math.floor(totalInvestment * 0.5); if (self.level < self.maxLevel) { var cost = self.getUpgradeCost(); var names = self.getUpgradeNames(); var nextName = names[self.level - 1] || 'Max Level'; var nextLevel = self.level + 1; var isUpgradeUnlocked = nextLevel <= upgradeUnlockWaves.length && currentWave >= upgradeUnlockWaves[nextLevel - 1]; // Upgrade button text - larger size var upgradeText = upgradePanel.addChild(new Text2('Upgrade', { size: 40, fill: isUpgradeUnlocked ? 0xFFD700 : 0x666666 })); upgradeText.anchor.set(0.5, 1); upgradeText.x = panelX; upgradeText.y = shouldOpenDownward ? panelY - 20 : panelY - 20; var costText; if (isUpgradeUnlocked) { costText = upgradePanel.addChild(new Text2('Cost: ' + cost + ' gold', { size: 36, fill: coins >= cost ? 0x00FF00 : 0xFF0000 })); } else { costText = upgradePanel.addChild(new Text2('Unlocks at Wave ' + upgradeUnlockWaves[nextLevel - 1], { size: 32, fill: 0xFFAA00 })); } costText.anchor.set(0.5, 1); costText.x = panelX; costText.y = shouldOpenDownward ? panelY + 20 : panelY + 20; if (coins >= cost && isUpgradeUnlocked) { upgradeText.down = costText.down = panelBg.down = function () { coins -= cost; coinsText.setText('Gold: ' + coins); self.upgrade(); // Play upgrade sound if (!audioMuted) { LK.getSound('upgrade').play(); } if (upgradePanel && upgradePanel.destroy) { upgradePanel.destroy(); upgradePanel = null; } selectedTower = null; if (activeRangeIndicator && activeRangeIndicator.destroy) { activeRangeIndicator.destroy(); activeRangeIndicator = null; } }; } } else { var maxText = upgradePanel.addChild(new Text2('MAX LEVEL', { size: 44, fill: 0xFFFFFF })); maxText.anchor.set(0.5, 1); maxText.x = panelX; maxText.y = shouldOpenDownward ? panelY : panelY; } // Sell button - always visible var sellText = upgradePanel.addChild(new Text2('Sell Tower', { size: 38, fill: 0xFF4444 })); sellText.anchor.set(0.5, 1); sellText.x = panelX; sellText.y = shouldOpenDownward ? panelY + 60 : panelY + 60; var sellValueText = upgradePanel.addChild(new Text2('Refund: ' + sellValue + ' gold', { size: 32, fill: 0xFFD700 })); sellValueText.anchor.set(0.5, 1); sellValueText.x = panelX; sellValueText.y = shouldOpenDownward ? panelY + 95 : panelY + 95; sellText.down = sellValueText.down = function () { // Find and free the build zone for (var i = 0; i < buildZones.length; i++) { var zone = buildZones[i]; var distance = Math.sqrt((zone.x - self.x) * (zone.x - self.x) + (zone.y - self.y) * (zone.y - self.y)); if (distance < 30) { zone.zoneData.occupied = false; break; } } // Add refund money coins += sellValue; coinsText.setText('Gold: ' + coins); // Show refund notification var refundNotification = new Text2('+' + sellValue + ' gold', { size: 50, fill: 0x00FF00 }); refundNotification.anchor.set(0.5, 0.5); refundNotification.x = self.x; refundNotification.y = self.y - 100; isoContainer.addChild(refundNotification); tween(refundNotification, { y: refundNotification.y - 80, alpha: 0 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { if (refundNotification && refundNotification.destroy) { refundNotification.destroy(); } } }); // Remove tower from towers array for (var i = 0; i < towers.length; i++) { if (towers[i] === self) { towers.splice(i, 1); break; } } // Clean up UI elements if (upgradePanel && upgradePanel.destroy) { upgradePanel.destroy(); upgradePanel = null; } if (activeRangeIndicator && activeRangeIndicator.destroy) { activeRangeIndicator.destroy(); activeRangeIndicator = null; } selectedTower = null; // Remove tower self.destroy(); updateBuildZoneVisuals(); }; }; self.update = function () { var target = self.findTarget(); if (target) { self.shoot(target); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x228B22 }); /**** * Game Code ****/ // Use game container directly for top-down view var isoContainer = game; var gameState = 'mainMenu'; var mainMenuContainer = null; var howToPlayContainer = null; var creditsContainer = null; var settingsContainer = null; var storyIntroContainer = null; var settingsIcon = null; var audioMuted = false; var muteIndicator = null; var endlessMode = false; var endlessScore = 0; var gameSpeed = 1; // Current game speed multiplier (1x, 2x, or 3x) var isPaused = false; // Game pause state var lastGameSpeed = 1; // Store last speed before pausing var pauseButton = null; // Pause button reference var pauseButtonText = null; // Pause button text reference var speedButtons = []; // Array to hold speed button references var speedButtonTexts = []; // Array to hold speed button text references var castleType = 'square'; var difficulty = 'easy'; var currentWave = 0; var maxWaves = 100; var waveEnemies = []; var enemies = []; var towers = []; var projectiles = []; var coins = 160; var castleHealth = 100; var waveInProgress = false; var nextWaveTimer = 0; var selectedTowerType = null; var selectedTower = null; var upgradeText = null; var upgradePanel = null; var activeRangeIndicator = null; // Shop button references for visual updates var infantryBtn, frostBtn, antiairBtn, poisonBtn, teslaBtn; var infantryCost, frostCost, antiairCost, poisonCost, teslaCost; var infantryName, frostName, antiairName, poisonName, teslaName; // Tower unlock system - waves when towers become available var towerUnlockWaves = { infantry: 1, frost: 3, antiair: 8, poison: 15, tesla: 25 }; // Upgrade level restrictions based on wave progression var upgradeUnlockWaves = [1, 3, 6, 10, 14, 20, 30, 40, 50, 60]; // Enemy path 1: Detailed horizontal then vertical path var enemyPath1 = [{ x: 0, y: 200 }, { x: 25.6, y: 200 }, { x: 51.2, y: 200 }, { x: 76.8, y: 200 }, { x: 102.4, y: 200 }, { x: 128, y: 200 }, { x: 153.6, y: 200 }, { x: 179.2, y: 200 }, { x: 204.8, y: 200 }, { x: 230.4, y: 200 }, { x: 256, y: 200 }, { x: 281.6, y: 200 }, { x: 307.2, y: 200 }, { x: 332.8, y: 200 }, { x: 358.4, y: 200 }, { x: 384, y: 200 }, { x: 409.6, y: 200 }, { x: 435.2, y: 200 }, { x: 460.8, y: 200 }, { x: 486.4, y: 200 }, { x: 512, y: 200 }, { x: 537.6, y: 200 }, { x: 563.2, y: 200 }, { x: 588.8, y: 200 }, { x: 614.4, y: 200 }, { x: 640, y: 200 }, { x: 665.6, y: 200 }, { x: 691.2, y: 200 }, { x: 716.8, y: 200 }, { x: 742.4, y: 200 }, { x: 768, y: 200 }, { x: 793.6, y: 200 }, { x: 819.2, y: 200 }, { x: 844.8, y: 200 }, { x: 870.4, y: 200 }, { x: 896, y: 200 }, { x: 921.6, y: 200 }, { x: 947.2, y: 200 }, { x: 972.8, y: 200 }, { x: 998.4, y: 200 }, { x: 1024, y: 200 }, { x: 1024, y: 240 }, { x: 1024, y: 280 }, { x: 1024, y: 320 }, { x: 1024, y: 360 }, { x: 1024, y: 400 }, { x: 1024, y: 440 }, { x: 1024, y: 480 }, { x: 1024, y: 520 }, { x: 1024, y: 560 }, { x: 1024, y: 600 }, { x: 998.4, y: 600 }, { x: 972.8, y: 600 }, { x: 947.2, y: 600 }, { x: 921.6, y: 600 }, { x: 896, y: 600 }, { x: 870.4, y: 600 }, { x: 844.8, y: 600 }, { x: 819.2, y: 600 }, { x: 793.6, y: 600 }, { x: 768, y: 600 }, { x: 768, y: 640 }, { x: 768, y: 680 }, { x: 768, y: 720 }, { x: 768, y: 760 }, { x: 768, y: 800 }, { x: 768, y: 840 }, { x: 768, y: 880 }, { x: 768, y: 920 }, { x: 768, y: 960 }, { x: 768, y: 1000 }, { x: 768, y: 1040 }, { x: 768, y: 1080 }, { x: 768, y: 1120 }, { x: 768, y: 1160 }, { x: 768, y: 1200 }, { x: 768, y: 1240 }, { x: 768, y: 1280 }, { x: 768, y: 1320 }, { x: 768, y: 1360 }, { x: 768, y: 1400 }, { x: 768, y: 1440 }, { x: 768, y: 1480 }, { x: 768, y: 1520 }, { x: 768, y: 1560 }, { x: 768, y: 1600 }, { x: 768, y: 1640 }, { x: 768, y: 1680 }, { x: 768, y: 1720 }, { x: 768, y: 1760 }, { x: 768, y: 1800 }, { x: 793.6, y: 1800 }, { x: 819.2, y: 1800 }, { x: 844.8, y: 1800 }, { x: 870.4, y: 1800 }, { x: 896, y: 1800 }, { x: 921.6, y: 1800 }, { x: 947.2, y: 1800 }, { x: 972.8, y: 1800 }, { x: 998.4, y: 1800 }, { x: 1024, y: 1800 }, { x: 1024, y: 1840 }, { x: 1024, y: 1880 }, { x: 1024, y: 1920 }, { x: 1024, y: 1960 }, { x: 1024, y: 2000 }, { x: 1024, y: 2040 }, { x: 1024, y: 2080 }, { x: 1024, y: 2120 }, { x: 1024, y: 2160 }, { x: 1024, y: 2200 }]; // Enemy path 2: Detailed eastern route path var enemyPath2 = [{ x: 2048, y: 800 }, { x: 2022.4, y: 800 }, { x: 1996.8, y: 800 }, { x: 1971.2, y: 800 }, { x: 1945.6, y: 800 }, { x: 1920, y: 800 }, { x: 1894.4, y: 800 }, { x: 1868.8, y: 800 }, { x: 1843.2, y: 800 }, { x: 1817.6, y: 800 }, { x: 1792, y: 800 }, { x: 1766.4, y: 800 }, { x: 1740.8, y: 800 }, { x: 1715.2, y: 800 }, { x: 1689.6, y: 800 }, { x: 1664, y: 800 }, { x: 1638.4, y: 800 }, { x: 1612.8, y: 800 }, { x: 1587.2, y: 800 }, { x: 1561.6, y: 800 }, { x: 1536, y: 800 }, { x: 1510.4, y: 800 }, { x: 1484.8, y: 800 }, { x: 1459.2, y: 800 }, { x: 1433.6, y: 800 }, { x: 1408, y: 800 }, { x: 1382.4, y: 800 }, { x: 1356.8, y: 800 }, { x: 1331.2, y: 800 }, { x: 1305.6, y: 800 }, { x: 1280, y: 800 }, { x: 1254.4, y: 800 }, { x: 1228.8, y: 800 }, { x: 1203.2, y: 800 }, { x: 1177.6, y: 800 }, { x: 1152, y: 800 }, { x: 1126.4, y: 800 }, { x: 1100.8, y: 800 }, { x: 1075.2, y: 800 }, { x: 1049.6, y: 800 }, { x: 1024, y: 800 }, { x: 1024, y: 780 }, { x: 1024, y: 760 }, { x: 1024, y: 740 }, { x: 1024, y: 720 }, { x: 1024, y: 700 }, { x: 1024, y: 680 }, { x: 1024, y: 660 }, { x: 1024, y: 640 }, { x: 1024, y: 620 }, { x: 1024, y: 600 }, { x: 1049.6, y: 600 }, { x: 1075.2, y: 600 }, { x: 1100.8, y: 600 }, { x: 1126.4, y: 600 }, { x: 1152, y: 600 }, { x: 1177.6, y: 600 }, { x: 1203.2, y: 600 }, { x: 1228.8, y: 600 }, { x: 1254.4, y: 600 }, { x: 1280, y: 600 }, { x: 1280, y: 640 }, { x: 1280, y: 680 }, { x: 1280, y: 720 }, { x: 1280, y: 760 }, { x: 1280, y: 800 }, { x: 1280, y: 840 }, { x: 1280, y: 880 }, { x: 1280, y: 920 }, { x: 1280, y: 960 }, { x: 1280, y: 1000 }, { x: 1280, y: 1040 }, { x: 1280, y: 1080 }, { x: 1280, y: 1120 }, { x: 1280, y: 1160 }, { x: 1280, y: 1200 }, { x: 1280, y: 1240 }, { x: 1280, y: 1280 }, { x: 1280, y: 1320 }, { x: 1280, y: 1360 }, { x: 1280, y: 1400 }, { x: 1280, y: 1440 }, { x: 1280, y: 1480 }, { x: 1280, y: 1520 }, { x: 1280, y: 1560 }, { x: 1280, y: 1600 }, { x: 1280, y: 1640 }, { x: 1280, y: 1680 }, { x: 1280, y: 1720 }, { x: 1280, y: 1760 }, { x: 1280, y: 1800 }, { x: 1254.4, y: 1800 }, { x: 1228.8, y: 1800 }, { x: 1203.2, y: 1800 }, { x: 1177.6, y: 1800 }, { x: 1152, y: 1800 }, { x: 1126.4, y: 1800 }, { x: 1100.8, y: 1800 }, { x: 1075.2, y: 1800 }, { x: 1049.6, y: 1800 }, { x: 1024, y: 1800 }, { x: 1024, y: 1840 }, { x: 1024, y: 1880 }, { x: 1024, y: 1920 }, { x: 1024, y: 1960 }, { x: 1024, y: 2000 }, { x: 1024, y: 2040 }, { x: 1024, y: 2080 }, { x: 1024, y: 2120 }, { x: 1024, y: 2160 }, { x: 1024, y: 2200 }]; // Enemy path 3: Detailed western route path var enemyPath3 = [{ x: 2048, y: 50 }, { x: 2022.4, y: 50 }, { x: 1996.8, y: 50 }, { x: 1971.2, y: 50 }, { x: 1945.6, y: 50 }, { x: 1920, y: 50 }, { x: 1894.4, y: 50 }, { x: 1868.8, y: 50 }, { x: 1843.2, y: 50 }, { x: 1817.6, y: 50 }, { x: 1792, y: 50 }, { x: 1766.4, y: 50 }, { x: 1740.8, y: 50 }, { x: 1715.2, y: 50 }, { x: 1689.6, y: 50 }, { x: 1664, y: 50 }, { x: 1638.4, y: 50 }, { x: 1612.8, y: 50 }, { x: 1587.2, y: 50 }, { x: 1561.6, y: 50 }, { x: 1536, y: 50 }, { x: 1510.4, y: 50 }, { x: 1484.8, y: 50 }, { x: 1459.2, y: 50 }, { x: 1433.6, y: 50 }, { x: 1408, y: 50 }, { x: 1382.4, y: 50 }, { x: 1356.8, y: 50 }, { x: 1331.2, y: 50 }, { x: 1305.6, y: 50 }, { x: 1280, y: 50 }, { x: 1280, y: 90 }, { x: 1280, y: 130 }, { x: 1280, y: 170 }, { x: 1280, y: 210 }, { x: 1280, y: 250 }, { x: 1280, y: 290 }, { x: 1280, y: 330 }, { x: 1280, y: 370 }, { x: 1280, y: 410 }, { x: 1280, y: 450 }, { x: 1280, y: 490 }, { x: 1280, y: 530 }, { x: 1280, y: 570 }, { x: 1280, y: 610 }, { x: 1280, y: 650 }, { x: 1280, y: 690 }, { x: 1280, y: 730 }, { x: 1280, y: 770 }, { x: 1280, y: 810 }, { x: 1280, y: 850 }, { x: 1280, y: 890 }, { x: 1280, y: 930 }, { x: 1280, y: 970 }, { x: 1280, y: 1010 }, { x: 1280, y: 1050 }, { x: 1280, y: 1090 }, { x: 1280, y: 1130 }, { x: 1280, y: 1170 }, { x: 1280, y: 1210 }, { x: 1280, y: 1250 }, { x: 1280, y: 1290 }, { x: 1280, y: 1330 }, { x: 1280, y: 1370 }, { x: 1280, y: 1410 }, { x: 1280, y: 1450 }, { x: 1280, y: 1490 }, { x: 1280, y: 1530 }, { x: 1280, y: 1570 }, { x: 1280, y: 1610 }, { x: 1280, y: 1650 }, { x: 1280, y: 1690 }, { x: 1280, y: 1730 }, { x: 1280, y: 1770 }, { x: 1280, y: 1800 }, { x: 1254.4, y: 1800 }, { x: 1228.8, y: 1800 }, { x: 1203.2, y: 1800 }, { x: 1177.6, y: 1800 }, { x: 1152, y: 1800 }, { x: 1126.4, y: 1800 }, { x: 1100.8, y: 1800 }, { x: 1075.2, y: 1800 }, { x: 1049.6, y: 1800 }, { x: 1024, y: 1800 }, { x: 1024, y: 1840 }, { x: 1024, y: 1880 }, { x: 1024, y: 1920 }, { x: 1024, y: 1960 }, { x: 1024, y: 2000 }, { x: 1024, y: 2040 }, { x: 1024, y: 2080 }, { x: 1024, y: 2120 }, { x: 1024, y: 2160 }, { x: 1024, y: 2200 }]; var buildZones = []; var terrainFeatures = []; function createPath() { // Create all paths visible from the start for strategic planning // Create path 1 for (var i = 0; i < enemyPath1.length; i++) { var pathTile = isoContainer.addChild(LK.getAsset('path_tile', { anchorX: 0.5, anchorY: 0.5, x: enemyPath1[i].x, y: enemyPath1[i].y, alpha: 0.7 })); } // Create path 2 - always visible for (var i = 0; i < enemyPath2.length; i++) { var pathTile = isoContainer.addChild(LK.getAsset('path_tile', { anchorX: 0.5, anchorY: 0.5, x: enemyPath2[i].x, y: enemyPath2[i].y, alpha: 0.7 })); } // Create path 3 - always visible for (var i = 0; i < enemyPath3.length; i++) { var pathTile = isoContainer.addChild(LK.getAsset('path_tile', { anchorX: 0.5, anchorY: 0.5, x: enemyPath3[i].x, y: enemyPath3[i].y, alpha: 0.7 })); } // Create build zones around paths createBuildZones(); } function createTerrain() { // Create lake var lake = isoContainer.addChild(LK.getAsset('lake', { anchorX: 0.5, anchorY: 0.5, x: 400, y: 500, alpha: 0.8 })); // Create waterfall (three parallel lines) var waterfall1 = isoContainer.addChild(LK.getAsset('waterfall_line', { anchorX: 0.5, anchorY: 0.5, x: 1550, y: 300 })); var waterfall2 = isoContainer.addChild(LK.getAsset('waterfall_line', { anchorX: 0.5, anchorY: 0.5, x: 1560, y: 300 })); var waterfall3 = isoContainer.addChild(LK.getAsset('waterfall_line', { anchorX: 0.5, anchorY: 0.5, x: 1570, y: 300 })); // Create bottom right waterfall (400 pixels above bottom right corner) var bottomRightWaterfall = isoContainer.addChild(LK.getAsset('waterfall_line', { anchorX: 0.5, anchorY: 0.5, x: 1748, y: 1932 })); // Create lake in the middle of the map var centralLake = isoContainer.addChild(LK.getAsset('lake', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366, alpha: 0.8, scaleX: 1.5, scaleY: 1.5 })); // Create left village (further west of castle) var leftVillagePositions = [{ x: 450, y: 2050 }, { x: 700, y: 2300 }, { x: 300, y: 2350 }, { x: 650, y: 2100 }]; for (var i = 0; i < leftVillagePositions.length; i++) { var pos = leftVillagePositions[i]; // Create house base var house = isoContainer.addChild(LK.getAsset('village_house', { anchorX: 0.5, anchorY: 0.5, x: pos.x, y: pos.y, alpha: 0.9 })); // Roof removed } // Create right village (further east of castle) var rightVillagePositions = [{ x: 1600, y: 2050 }, { x: 1350, y: 2300 }, { x: 1750, y: 2250 }, { x: 1500, y: 2450 }]; for (var i = 0; i < rightVillagePositions.length; i++) { var pos = rightVillagePositions[i]; // Create house base var house = isoContainer.addChild(LK.getAsset('village_house', { anchorX: 0.5, anchorY: 0.5, x: pos.x, y: pos.y, alpha: 0.9 })); // Roof removed } // Create 50 trees scattered across the map avoiding all objects var treePositions = []; // Helper function to check if a position conflicts with existing objects function isPositionSafe(x, y, buffer) { buffer = buffer || 200; // Check boundaries if (x < 50 || x > 1998 || y < 50 || y > 2682) { return false; } // Check castle area if (Math.sqrt((x - 1024) * (x - 1024) + (y - 2200) * (y - 2200)) < 300 + buffer) { return false; } // Check waterfall area if (x >= 1500 - buffer && x <= 1600 + buffer) { return false; } // Check central lake area if (Math.sqrt((x - 1024) * (x - 1024) + (y - 1366) * (y - 1366)) < 200 + buffer) { return false; } // Check original lake area if (Math.sqrt((x - 400) * (x - 400) + (y - 500) * (y - 500)) < 150 + buffer) { return false; } // Check all enemy paths with buffer var allPaths = [enemyPath1, enemyPath2, enemyPath3]; for (var p = 0; p < allPaths.length; p++) { var path = allPaths[p]; for (var i = 0; i < path.length; i++) { if (Math.sqrt((x - path[i].x) * (x - path[i].x) + (y - path[i].y) * (y - path[i].y)) < buffer + 20) { return false; } } } // Check build zones - now requiring 200 pixels distance for (var i = 0; i < buildZones.length; i++) { if (Math.sqrt((x - buildZones[i].x) * (x - buildZones[i].x) + (y - buildZones[i].y) * (y - buildZones[i].y)) < 200) { return false; } } // Check village house areas var allVillagePositions = leftVillagePositions.concat(rightVillagePositions); for (var i = 0; i < allVillagePositions.length; i++) { var house = allVillagePositions[i]; if (Math.sqrt((x - house.x) * (x - house.x) + (y - house.y) * (y - house.y)) < buffer) { return false; } } // Check against other trees for (var i = 0; i < treePositions.length; i++) { var tree = treePositions[i]; if (Math.sqrt((x - tree.x) * (x - tree.x) + (y - tree.y) * (y - tree.y)) < buffer) { return false; } } // Check against rocks for (var i = 0; i < rockPositions.length; i++) { var rock = rockPositions[i]; if (Math.sqrt((x - rock.x) * (x - rock.x) + (y - rock.y) * (y - rock.y)) < buffer) { return false; } } return true; } // Define fixed tree positions far from roads and tower installation points var fixedTreePositions = [{ x: 1700, y: 200 }, { x: 1800, y: 250 }, { x: 1650, y: 350 }, { x: 1750, y: 450 }, { x: 1900, y: 300 }, { x: 1950, y: 200 }, { x: 1600, y: 150 }, { x: 1850, y: 100 }, { x: 1950, y: 150 }, { x: 150, y: 300 }, { x: 250, y: 350 }, { x: 350, y: 400 }, { x: 200, y: 450 }, { x: 100, y: 500 }, { x: 300, y: 300 }, { x: 400, y: 350 }, { x: 500, y: 400 }, { x: 450, y: 300 }, { x: 550, y: 350 }, { x: 100, y: 1000 }, { x: 200, y: 1100 }, { x: 300, y: 1200 }, { x: 150, y: 1300 }, { x: 250, y: 1400 }, { x: 100, y: 1500 }, { x: 350, y: 1000 }, { x: 400, y: 1100 }, { x: 450, y: 1200 }, { x: 500, y: 1300 }, { x: 550, y: 1400 }, { x: 600, y: 1500 }, { x: 1700, y: 1000 }, { x: 1800, y: 1100 }, { x: 1900, y: 1200 }, { x: 1950, y: 1300 }, { x: 1850, y: 1400 }, { x: 1750, y: 1500 }, { x: 1650, y: 1000 }, { x: 1600, y: 1100 }, { x: 1550, y: 1200 }, { x: 1500, y: 1300 }, { x: 1450, y: 1400 }, { x: 1400, y: 1500 }, { x: 100, y: 2100 }, { x: 200, y: 2000 }, { x: 300, y: 1950 }, { x: 400, y: 2000 }, { x: 500, y: 2100 }, { x: 1600, y: 2100 }, { x: 1700, y: 2000 }]; // Define rock positions in empty spaces at least 200 pixels away from any object var rockPositions = [{ x: 200, y: 800 }, { x: 1700, y: 350 }, { x: 250, y: 1700 }, { x: 1800, y: 1500 }, { x: 700, y: 350 }, { x: 1600, y: 100 }, { x: 1900, y: 950 }, { x: 150, y: 1200 }, { x: 1700, y: 1200 }, { x: 650, y: 1750 }]; // Use the first 50 fixed positions (or however many we have) for (var i = 0; i < Math.min(50, fixedTreePositions.length); i++) { treePositions.push(fixedTreePositions[i]); } // Create the trees for (var i = 0; i < treePositions.length; i++) { var pos = treePositions[i]; var tree = isoContainer.addChild(LK.getAsset('tree', { anchorX: 0.5, anchorY: 0.5, x: pos.x, y: pos.y, scaleX: 1.0, scaleY: 1.0, alpha: 0.9 })); } // Create rocks at predefined positions for (var i = 0; i < rockPositions.length; i++) { var pos = rockPositions[i]; var rock = isoContainer.addChild(LK.getAsset('ruin', { anchorX: 0.5, anchorY: 0.5, x: pos.x, y: pos.y, scaleX: 1.2, scaleY: 1.2, alpha: 0.8 })); terrainFeatures.push(rock); } } function createBuildZones() { var zones = [ // enemyPath1: y=200, both sides { x: 900, y: 150 }, { x: 900, y: 250 }, // enemyPath1: Corner (1024, 200, right turn) { x: 974, y: 150 }, { x: 1074, y: 150 }, { x: 974, y: 250 }, { x: 1074, y: 250 }, // enemyPath1: U-shape (x=1024, y=200-600) { x: 974, y: 500 }, { x: 1074, y: 500 }, // enemyPath1: Corner (768, 1800, right turn) { x: 718, y: 1850 }, { x: 818, y: 1850 }, { x: 718, y: 1344 }, { x: 818, y: 1344 }, { x: 718, y: 1244 }, { x: 818, y: 1244 }, { x: 718, y: 1444 }, { x: 818, y: 1444 }, // enemyPath1: Corner (1024, 1800, down, intersection) { x: 974, y: 1750 }, { x: 1074, y: 1750 }, { x: 974, y: 1850 }, { x: 1074, y: 1850 }, // enemyPath2: y=800, both sides { x: 1448, y: 750 }, { x: 1448, y: 850 }, { x: 1148, y: 750 }, { x: 1148, y: 850 }, // enemyPath2: Corner (1024, 800, down) { x: 974, y: 750 }, { x: 1074, y: 750 }, { x: 974, y: 850 }, { x: 1074, y: 850 }, // enemyPath2: Corner (1024, 600, right turn, intersection) { x: 974, y: 550 }, { x: 1074, y: 550 }, { x: 974, y: 650 }, { x: 1074, y: 650 }, // enemyPath2: U-shape (x=1280, y=600-1800) { x: 1230, y: 650 }, { x: 1330, y: 650 }, { x: 1230, y: 750 }, { x: 1330, y: 750 }, { x: 1230, y: 850 }, { x: 1330, y: 850 }, // enemyPath3: Corner (1280, 50, down) { x: 1230, y: 0 }, { x: 1330, y: 0 }, { x: 1230, y: 100 }, { x: 1330, y: 100 }, // enemyPath3: Corner (1280, 1800, left turn) { x: 1230, y: 1850 }, { x: 1330, y: 1850 }, // Additional tower placement spots near paths { x: 1230, y: 1344 }, { x: 1330, y: 1344 }, { x: 1230, y: 1244 }, { x: 1330, y: 1244 }, { x: 1230, y: 1444 }, { x: 1330, y: 1444 }]; for (var i = 0; i < zones.length; i++) { var zone = isoContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: zones[i].x, y: zones[i].y, alpha: 0.3 })); zone.zoneData = { x: zones[i].x, y: zones[i].y, occupied: false, originalAlpha: 0.3 }; buildZones.push(zone); } } function updateBuildZoneVisuals() { for (var i = 0; i < buildZones.length; i++) { var zone = buildZones[i]; if (zone.zoneData.occupied) { // Occupied zones are invisible zone.alpha = 0; } else if (selectedTowerType) { // Available zones are highlighted when a tower is selected zone.alpha = 0.6; zone.tint = 0x00FF00; // Green highlight for available zones } else { // Normal state when no tower is selected zone.alpha = zone.zoneData.originalAlpha; zone.tint = 0xFFFFFF; // Reset tint } } } var castleHealthBarBg = null; var castleHealthBarFill = null; function createCastle() { var castle = isoContainer.addChild(LK.getAsset('castle_' + castleType, { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 2200 })); // Create health bar background positioned under the castle (moved down) castleHealthBarBg = isoContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 2350, scaleX: 4, scaleY: 0.4 })); castleHealthBarBg.tint = 0x333333; // Create health bar fill positioned under the castle (moved down) castleHealthBarFill = isoContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 2350, scaleX: 4, scaleY: 0.4 })); castleHealthBarFill.tint = 0x00FF00; // Store references for updating castle.healthBarBg = castleHealthBarBg; castle.healthBarFill = castleHealthBarFill; } function createMainMenu() { mainMenuContainer = new Container(); isoContainer.addChild(mainMenuContainer); // Main menu background - war scene with towers shooting arrows at goblins var menuBg = mainMenuContainer.addChild(LK.getAsset('war_scene_background', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366 })); // Title var titleText = mainMenuContainer.addChild(new Text2('Goblin Invasion', { size: 120, fill: 0xFFD700 })); titleText.anchor.set(0.5, 0.5); titleText.x = 1024; titleText.y = 800; // New Game Button var newGameBtn = mainMenuContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1200, scaleX: 8, scaleY: 2 })); newGameBtn.tint = 0x00AA00; var newGameText = mainMenuContainer.addChild(new Text2('New Game', { size: 60, fill: 0xFFFFFF })); newGameText.anchor.set(0.5, 0.5); newGameText.x = 1024; newGameText.y = 1200; // How to Play Button var howToPlayBtn = mainMenuContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1400, scaleX: 8, scaleY: 2 })); howToPlayBtn.tint = 0x0066CC; var howToPlayText = mainMenuContainer.addChild(new Text2('How to Play', { size: 60, fill: 0xFFFFFF })); howToPlayText.anchor.set(0.5, 0.5); howToPlayText.x = 1024; howToPlayText.y = 1400; // Credits Button var creditsBtn = mainMenuContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1600, scaleX: 8, scaleY: 2 })); creditsBtn.tint = 0xAA6600; var creditsText = mainMenuContainer.addChild(new Text2('Credits', { size: 60, fill: 0xFFFFFF })); creditsText.anchor.set(0.5, 0.5); creditsText.x = 1024; creditsText.y = 1600; // Button interactions newGameBtn.down = function () { startGame(); }; howToPlayBtn.down = function () { showHowToPlay(); }; creditsBtn.down = function () { showCredits(); }; } function showStoryIntro() { if (mainMenuContainer) { mainMenuContainer.alpha = 0.2; } storyIntroContainer = new Container(); isoContainer.addChild(storyIntroContainer); // Background var bg = storyIntroContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366, scaleX: 45, scaleY: 50 })); bg.tint = 0x000000; bg.alpha = 0.95; // Story text var storyLines = ["The land is on the brink of ruin.", "", "Shadows rise from the east, and only your towers", "stand in their way.", "", "You must defend the kingdom wave after wave,", "until the last breath of hope remains..."]; for (var i = 0; i < storyLines.length; i++) { var storyText = storyIntroContainer.addChild(new Text2(storyLines[i], { size: 50, fill: 0xFFFFFF })); storyText.anchor.set(0.5, 0.5); storyText.x = 1024; storyText.y = 1000 + i * 80; } // Skip instruction var skipText = storyIntroContainer.addChild(new Text2('Click anywhere to begin...', { size: 40, fill: 0xFFD700 })); skipText.anchor.set(0.5, 0.5); skipText.x = 1024; skipText.y = 1700; // Auto-skip after 6 seconds or manual skip var skipTimer = LK.setTimeout(function () { startGameAfterIntro(); }, 6000); // Manual skip bg.down = function () { LK.clearTimeout(skipTimer); startGameAfterIntro(); }; } function startGameAfterIntro() { if (storyIntroContainer) { storyIntroContainer.destroy(); storyIntroContainer = null; } if (mainMenuContainer) { mainMenuContainer.destroy(); mainMenuContainer = null; } gameState = 'playing'; // Play background music when game starts if (!audioMuted) { LK.playMusic('main'); } createSettingsIcon(); createSpeedControls(); // Create speed control buttons createPath(); createTerrain(); createCastle(); createShop(); createWave(); updateShopVisuals(); // Show UI elements with fade-in animation if (castleLivesTextBg) { castleLivesTextBg.visible = true; castleLivesTextBg.alpha = 0; tween(castleLivesTextBg, { alpha: 0.8 }, { duration: 800, easing: tween.easeOut }); } if (castleLivesText) { castleLivesText.visible = true; castleLivesText.alpha = 0; tween(castleLivesText, { alpha: 1 }, { duration: 800, easing: tween.easeOut }); } if (coinsTextBg) { coinsTextBg.visible = true; coinsTextBg.alpha = 0; tween(coinsTextBg, { alpha: 0.8 }, { duration: 800, easing: tween.easeOut }); } if (coinsText) { coinsText.visible = true; coinsText.alpha = 0; tween(coinsText, { alpha: 1 }, { duration: 800, easing: tween.easeOut }); } if (waveText) { waveText.visible = true; waveText.alpha = 0; tween(waveText, { alpha: 1 }, { duration: 800, easing: tween.easeOut }); } } function startGame() { showStoryIntro(); } function showHowToPlay() { if (mainMenuContainer) { mainMenuContainer.alpha = 0.3; } howToPlayContainer = new Container(); isoContainer.addChild(howToPlayContainer); // Background var bg = howToPlayContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366, scaleX: 35, scaleY: 45 })); bg.tint = 0x000000; bg.alpha = 0.9; // Title var title = howToPlayContainer.addChild(new Text2('How to Play', { size: 80, fill: 0xFFD700 })); title.anchor.set(0.5, 0.5); title.x = 1024; title.y = 600; // Instructions var instructions = ['Build towers to defend your castle from waves of enemies', 'Click tower icons at bottom to select, then click build zones', 'Upgrade towers by clicking them when you have enough gold', 'Different towers have different abilities:', '• Infantry: Basic damage, good range', '• Freezer: Slows enemies down', '• Air: Targets flying enemies effectively', '• Poison: Damages over time', '• Tesla: Chain lightning damage', 'Survive 100 waves to win!']; for (var i = 0; i < instructions.length; i++) { var instructText = howToPlayContainer.addChild(new Text2(instructions[i], { size: 35, fill: 0xFFFFFF })); instructText.anchor.set(0.5, 0.5); instructText.x = 1024; instructText.y = 800 + i * 60; } // Back button var backBtn = howToPlayContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 2000, scaleX: 6, scaleY: 2 })); backBtn.tint = 0xAA0000; var backText = howToPlayContainer.addChild(new Text2('Back', { size: 50, fill: 0xFFFFFF })); backText.anchor.set(0.5, 0.5); backText.x = 1024; backText.y = 2000; backBtn.down = function () { howToPlayContainer.destroy(); howToPlayContainer = null; if (mainMenuContainer) { mainMenuContainer.alpha = 1.0; } }; } function showCredits() { if (mainMenuContainer) { mainMenuContainer.alpha = 0.3; } creditsContainer = new Container(); isoContainer.addChild(creditsContainer); // Background var bg = creditsContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366, scaleX: 25, scaleY: 30 })); bg.tint = 0x000000; bg.alpha = 0.9; // Title var title = creditsContainer.addChild(new Text2('Credits', { size: 80, fill: 0xFFD700 })); title.anchor.set(0.5, 0.5); title.x = 1024; title.y = 1000; // Credits info var gameDesignText = creditsContainer.addChild(new Text2('Game Design: ilker.mez', { size: 50, fill: 0xFFFFFF })); gameDesignText.anchor.set(0.5, 0.5); gameDesignText.x = 1024; gameDesignText.y = 1300; // Back button var backBtn = creditsContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1600, scaleX: 6, scaleY: 2 })); backBtn.tint = 0xAA0000; var backText = creditsContainer.addChild(new Text2('Back', { size: 50, fill: 0xFFFFFF })); backText.anchor.set(0.5, 0.5); backText.x = 1024; backText.y = 1600; backBtn.down = function () { creditsContainer.destroy(); creditsContainer = null; if (mainMenuContainer) { mainMenuContainer.alpha = 1.0; } }; } function createSettingsIcon() { settingsIcon = LK.gui.topLeft.addChild(LK.getAsset('build_zone', { anchorX: 0, anchorY: 0, x: 20, y: 400, scaleX: 2, scaleY: 2 })); settingsIcon.tint = 0x666666; var settingsText = LK.gui.topLeft.addChild(new Text2('⚙', { size: 60, fill: 0xFFFFFF })); settingsText.anchor.set(0, 0); settingsText.x = 20; settingsText.y = 400; settingsIcon.down = function () { showSettings(); }; } function createSpeedControls() { // Clear existing speed buttons for (var i = 0; i < speedButtons.length; i++) { if (speedButtons[i] && speedButtons[i].destroy) { speedButtons[i].destroy(); } } for (var i = 0; i < speedButtonTexts.length; i++) { if (speedButtonTexts[i] && speedButtonTexts[i].destroy) { speedButtonTexts[i].destroy(); } } // Clear pause button if (pauseButton && pauseButton.destroy) { pauseButton.destroy(); } if (pauseButtonText && pauseButtonText.destroy) { pauseButtonText.destroy(); } speedButtons = []; speedButtonTexts = []; // Create pause button pauseButton = LK.gui.topLeft.addChild(LK.getAsset('build_zone', { anchorX: 0, anchorY: 0, x: 220, y: 0, scaleX: 2, scaleY: 1.5 })); pauseButton.tint = isPaused ? 0xFF4444 : 0x666666; pauseButton.alpha = isPaused ? 1.0 : 0.7; pauseButtonText = LK.gui.topLeft.addChild(new Text2('⏸', { size: 40, fill: isPaused ? 0xFF4444 : 0xFFFFFF })); pauseButtonText.anchor.set(0, 0); pauseButtonText.x = 230; pauseButtonText.y = 10; pauseButton.down = function () { togglePause(); }; // Create speed control buttons (X1, X2, X3) var speeds = [1, 2, 3]; var speedLabels = ['X1', 'X2', 'X3']; for (var i = 0; i < speeds.length; i++) { var speedBtn = LK.gui.topLeft.addChild(LK.getAsset('build_zone', { anchorX: 0, anchorY: 0, x: 320 + i * 80, y: 0, scaleX: 2, scaleY: 1.5 })); speedBtn.tint = gameSpeed === speeds[i] && !isPaused ? 0x00FF00 : 0x666666; speedBtn.alpha = gameSpeed === speeds[i] && !isPaused ? 1.0 : 0.7; speedButtons.push(speedBtn); var speedText = LK.gui.topLeft.addChild(new Text2(speedLabels[i], { size: 40, fill: gameSpeed === speeds[i] && !isPaused ? 0x00FF00 : 0xFFFFFF })); speedText.anchor.set(0, 0); speedText.x = 330 + i * 80; speedText.y = 10; speedButtonTexts.push(speedText); // Create closure to capture the speed value (function (speed) { speedBtn.down = function () { setGameSpeed(speed); }; })(speeds[i]); } } function setGameSpeed(newSpeed) { if (isPaused) { // If paused, store the selected speed but don't apply it yet lastGameSpeed = newSpeed; } else { gameSpeed = newSpeed; } // Update button visuals for (var i = 0; i < speedButtons.length; i++) { var isActive = (isPaused ? lastGameSpeed : gameSpeed) === i + 1; speedButtons[i].tint = isActive && !isPaused ? 0x00FF00 : 0x666666; speedButtons[i].alpha = isActive && !isPaused ? 1.0 : 0.7; speedButtonTexts[i].fill = isActive && !isPaused ? 0x00FF00 : 0xFFFFFF; speedButtonTexts[i].tint = isActive && !isPaused ? 0x00FF00 : 0xFFFFFF; } } function togglePause() { isPaused = !isPaused; if (isPaused) { // Store current speed and pause the game lastGameSpeed = gameSpeed; gameSpeed = 0; } else { // Resume with the last selected speed gameSpeed = lastGameSpeed; } // Update pause button visuals if (pauseButton) { pauseButton.tint = isPaused ? 0xFF4444 : 0x666666; pauseButton.alpha = isPaused ? 1.0 : 0.7; } if (pauseButtonText) { pauseButtonText.fill = isPaused ? 0xFF4444 : 0xFFFFFF; pauseButtonText.tint = isPaused ? 0xFF4444 : 0xFFFFFF; } // Update speed button visuals to reflect pause state for (var i = 0; i < speedButtons.length; i++) { var isActive = lastGameSpeed === i + 1; speedButtons[i].tint = isActive && !isPaused ? 0x00FF00 : 0x666666; speedButtons[i].alpha = isActive && !isPaused ? 1.0 : 0.7; speedButtonTexts[i].fill = isActive && !isPaused ? 0x00FF00 : 0xFFFFFF; speedButtonTexts[i].tint = isActive && !isPaused ? 0x00FF00 : 0xFFFFFF; } } function showSettings() { if (settingsContainer) { return; } settingsContainer = new Container(); isoContainer.addChild(settingsContainer); // Background var bg = settingsContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366, scaleX: 20, scaleY: 25 })); bg.tint = 0x000000; bg.alpha = 0.9; // Title var title = settingsContainer.addChild(new Text2('Settings', { size: 80, fill: 0xFFD700 })); title.anchor.set(0.5, 0.5); title.x = 1024; title.y = 1000; // Return to Main Menu button var mainMenuBtn = settingsContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1200, scaleX: 10, scaleY: 2 })); mainMenuBtn.tint = 0x0066CC; var mainMenuText = settingsContainer.addChild(new Text2('Return to Main Menu', { size: 50, fill: 0xFFFFFF })); mainMenuText.anchor.set(0.5, 0.5); mainMenuText.x = 1024; mainMenuText.y = 1200; // Mute/Unmute button var muteBtn = settingsContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1400, scaleX: 8, scaleY: 2 })); muteBtn.tint = 0x00AA00; var muteText = settingsContainer.addChild(new Text2('Mute Audio', { size: 50, fill: 0xFFFFFF })); muteText.anchor.set(0.5, 0.5); muteText.x = 1024; muteText.y = 1400; // Close button var closeBtn = settingsContainer.addChild(LK.getAsset('build_zone', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1600, scaleX: 6, scaleY: 2 })); closeBtn.tint = 0xAA0000; var closeText = settingsContainer.addChild(new Text2('Close', { size: 50, fill: 0xFFFFFF })); closeText.anchor.set(0.5, 0.5); closeText.x = 1024; closeText.y = 1600; // Button interactions mainMenuBtn.down = function () { // Stop background music when returning to main menu LK.stopMusic(); // Reset game state gameState = 'mainMenu'; // Clear all game objects for (var i = enemies.length - 1; i >= 0; i--) { enemies[i].destroy(); } enemies = []; for (var i = towers.length - 1; i >= 0; i--) { towers[i].destroy(); } towers = []; for (var i = projectiles.length - 1; i >= 0; i--) { projectiles[i].destroy(); } projectiles = []; // Reset game variables currentWave = 0; coins = 160; castleHealth = 100; waveInProgress = false; // Hide UI elements when returning to main menu if (castleLivesTextBg) { castleLivesTextBg.visible = false; } if (castleLivesText) { castleLivesText.visible = false; } if (coinsTextBg) { coinsTextBg.visible = false; } if (coinsText) { coinsText.visible = false; } if (waveText) { waveText.visible = false; } // Clear UI elements if (settingsIcon) { settingsIcon.destroy(); settingsIcon = null; } if (muteIndicator) { muteIndicator.destroy(); muteIndicator = null; } // Clean up speed controls for (var i = 0; i < speedButtons.length; i++) { if (speedButtons[i] && speedButtons[i].destroy) { speedButtons[i].destroy(); } } for (var i = 0; i < speedButtonTexts.length; i++) { if (speedButtonTexts[i] && speedButtonTexts[i].destroy) { speedButtonTexts[i].destroy(); } } // Clean up pause controls if (pauseButton && pauseButton.destroy) { pauseButton.destroy(); pauseButton = null; } if (pauseButtonText && pauseButtonText.destroy) { pauseButtonText.destroy(); pauseButtonText = null; } speedButtons = []; speedButtonTexts = []; gameSpeed = 1; // Reset game speed isPaused = false; // Reset pause state lastGameSpeed = 1; // Reset last speed // Close settings and show main menu settingsContainer.destroy(); settingsContainer = null; createMainMenu(); }; muteBtn.down = function () { // Toggle audio mute state audioMuted = !audioMuted; // Update button text and color if (audioMuted) { muteText.setText('Unmute Audio'); muteBtn.tint = 0xAA0000; // Stop music when muting LK.stopMusic(); // Create mute indicator overlay on settings icon if (settingsIcon && !muteIndicator) { muteIndicator = LK.gui.topLeft.addChild(new Text2('✕', { size: 40, fill: 0xFF0000 })); muteIndicator.anchor.set(0, 0); muteIndicator.x = 30; muteIndicator.y = 410; // Animate the mute indicator tween(muteIndicator, { scaleX: 1.3, scaleY: 1.3, alpha: 0.8 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { // Auto-hide the X indicator after 2 seconds LK.setTimeout(function () { if (muteIndicator && audioMuted) { tween(muteIndicator, { alpha: 0 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { if (muteIndicator && muteIndicator.destroy) { muteIndicator.destroy(); muteIndicator = null; } } }); } }, 2000); } }); } } else { muteText.setText('Mute Audio'); muteBtn.tint = 0x00AA00; // Resume music when unmuting (only if in game state) if (gameState === 'playing') { LK.playMusic('main'); } // Remove mute indicator immediately when unmuting if (muteIndicator && muteIndicator.destroy) { muteIndicator.destroy(); muteIndicator = null; } } }; closeBtn.down = function () { settingsContainer.destroy(); settingsContainer = null; }; } function showUpgradeRestrictedMessage(level, requiredWave) { // Create temporary message var message = new Text2('Upgrade level ' + level + ' is locked until Wave ' + requiredWave + '.', { size: 45, fill: 0xFFAA00 }); message.anchor.set(0.5, 0.5); message.x = 1024; message.y = 1100; isoContainer.addChild(message); // Animate and remove message tween(message, { y: message.y - 50, alpha: 0 }, { duration: 2500, easing: tween.easeOut, onFinish: function onFinish() { if (message && message.destroy) { message.destroy(); } } }); } function showUnlockMessage(towerType, unlockWave) { // Create temporary message var message = new Text2('This tower will unlock at Wave ' + unlockWave + '.', { size: 50, fill: 0xFFFF00 }); message.anchor.set(0.5, 0.5); message.x = 1024; message.y = 1200; isoContainer.addChild(message); // Animate and remove message tween(message, { y: message.y - 50, alpha: 0 }, { duration: 2000, easing: tween.easeOut, onFinish: function onFinish() { if (message && message.destroy) { message.destroy(); } } }); } function updateShopVisuals() { // Update tower button visuals based on unlock status var towers = [{ btn: infantryBtn, cost: infantryCost, name: infantryName, type: 'infantry', price: 80 }, { btn: frostBtn, cost: frostCost, name: frostName, type: 'frost', price: 120 }, { btn: antiairBtn, cost: antiairCost, name: antiairName, type: 'antiair', price: 100 }, { btn: poisonBtn, cost: poisonCost, name: poisonName, type: 'poison', price: 130 }, { btn: teslaBtn, cost: teslaCost, name: teslaName, type: 'tesla', price: 150 }]; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var isUnlocked = currentWave >= towerUnlockWaves[tower.type]; var canAfford = coins >= tower.price; if (!isUnlocked) { // Greyed out for locked towers tower.btn.tint = 0x666666; tower.btn.alpha = 0.5; tower.cost.tint = 0x666666; tower.name.tint = 0x666666; } else if (!canAfford) { // Red tint for unaffordable towers tower.btn.tint = 0xFFFFFF; tower.btn.alpha = 1.0; tower.cost.tint = 0xFF0000; tower.name.tint = 0xFFFFFF; } else { // Normal appearance for available towers tower.btn.tint = 0xFFFFFF; tower.btn.alpha = 1.0; tower.cost.tint = 0xFFD700; tower.name.tint = 0xFFFFFF; } } } function createShop() { var shopBg = LK.gui.bottom.addChild(LK.getAsset('shop_bg', { anchorX: 0.5, anchorY: 1, alpha: 1.0 })); shopBg.tint = 0x000000; infantryBtn = LK.gui.bottom.addChild(LK.getAsset('tower_infantry', { anchorX: 0.5, anchorY: 1, x: -400, y: -60 })); frostBtn = LK.gui.bottom.addChild(LK.getAsset('tower_frost', { anchorX: 0.5, anchorY: 1, x: -200, y: -60 })); antiairBtn = LK.gui.bottom.addChild(LK.getAsset('tower_antiair', { anchorX: 0.5, anchorY: 1, x: 0, y: -60 })); poisonBtn = LK.gui.bottom.addChild(LK.getAsset('tower_poison', { anchorX: 0.5, anchorY: 1, x: 200, y: -60 })); teslaBtn = LK.gui.bottom.addChild(LK.getAsset('tower_tesla', { anchorX: 0.5, anchorY: 1, x: 400, y: -60 })); // Add cost labels with larger, more distinct text - updated balanced costs infantryCost = LK.gui.bottom.addChild(new Text2('80g', { size: 32, fill: 0xFFD700 })); infantryCost.anchor.set(0.5, 1); infantryCost.x = -400; infantryCost.y = -130; frostCost = LK.gui.bottom.addChild(new Text2('120g', { size: 32, fill: 0xFFD700 })); frostCost.anchor.set(0.5, 1); frostCost.x = -200; frostCost.y = -130; antiairCost = LK.gui.bottom.addChild(new Text2('100g', { size: 32, fill: 0xFFD700 })); antiairCost.anchor.set(0.5, 1); antiairCost.x = 0; antiairCost.y = -130; poisonCost = LK.gui.bottom.addChild(new Text2('130g', { size: 32, fill: 0xFFD700 })); poisonCost.anchor.set(0.5, 1); poisonCost.x = 200; poisonCost.y = -130; teslaCost = LK.gui.bottom.addChild(new Text2('150g', { size: 32, fill: 0xFFD700 })); teslaCost.anchor.set(0.5, 1); teslaCost.x = 400; teslaCost.y = -130; // Add tower name labels below the cost text infantryName = LK.gui.bottom.addChild(new Text2('Infantry', { size: 28, fill: 0xFFFFFF })); infantryName.anchor.set(0.5, 1); infantryName.x = -400; infantryName.y = -160; frostName = LK.gui.bottom.addChild(new Text2('Freezer', { size: 28, fill: 0xFFFFFF })); frostName.anchor.set(0.5, 1); frostName.x = -200; frostName.y = -160; antiairName = LK.gui.bottom.addChild(new Text2('Air', { size: 28, fill: 0xFFFFFF })); antiairName.anchor.set(0.5, 1); antiairName.x = 0; antiairName.y = -160; poisonName = LK.gui.bottom.addChild(new Text2('Poison', { size: 28, fill: 0xFFFFFF })); poisonName.anchor.set(0.5, 1); poisonName.x = 200; poisonName.y = -160; teslaName = LK.gui.bottom.addChild(new Text2('Tesla', { size: 28, fill: 0xFFFFFF })); teslaName.anchor.set(0.5, 1); teslaName.x = 400; teslaName.y = -160; infantryBtn.down = function () { if (currentWave < towerUnlockWaves.infantry) { showUnlockMessage('infantry', towerUnlockWaves.infantry); return; } if (coins >= 80) { selectedTowerType = 'infantry'; updateBuildZoneVisuals(); } }; frostBtn.down = function () { if (currentWave < towerUnlockWaves.frost) { showUnlockMessage('frost', towerUnlockWaves.frost); return; } if (coins >= 120) { selectedTowerType = 'frost'; updateBuildZoneVisuals(); } }; antiairBtn.down = function () { if (currentWave < towerUnlockWaves.antiair) { showUnlockMessage('antiair', towerUnlockWaves.antiair); return; } if (coins >= 100) { selectedTowerType = 'antiair'; updateBuildZoneVisuals(); } }; poisonBtn.down = function () { if (currentWave < towerUnlockWaves.poison) { showUnlockMessage('poison', towerUnlockWaves.poison); return; } if (coins >= 130) { selectedTowerType = 'poison'; updateBuildZoneVisuals(); } }; teslaBtn.down = function () { if (currentWave < towerUnlockWaves.tesla) { showUnlockMessage('tesla', towerUnlockWaves.tesla); return; } if (coins >= 150) { selectedTowerType = 'tesla'; updateBuildZoneVisuals(); } }; } function createWave() { if (currentWave >= maxWaves && !endlessMode) { // Enable endless mode endlessMode = true; endlessScore = 0; // Show endless mode notification var endlessNotification = new Text2('♾️ ENDLESS MODE UNLOCKED! ♾️', { size: 80, fill: 0xFFD700 }); endlessNotification.anchor.set(0.5, 0.5); endlessNotification.x = 1024; endlessNotification.y = 1000; isoContainer.addChild(endlessNotification); tween(endlessNotification, { scaleX: 1.2, scaleY: 1.2, alpha: 0 }, { duration: 3000, easing: tween.easeOut, onFinish: function onFinish() { if (endlessNotification && endlessNotification.destroy) { endlessNotification.destroy(); } } }); } currentWave++; if (endlessMode) { endlessScore++; } waveEnemies = []; // Scale enemy count based on wave number with new progression var enemyCount; if (currentWave <= 5) { // Waves 1-5: 15 to 20 enemies enemyCount = 15 + Math.floor(Math.random() * 6); } else if (currentWave <= 15) { // Waves 6-15: 20 to 25 enemies enemyCount = 20 + Math.floor(Math.random() * 6); } else if (currentWave <= 30) { // Waves 16-30: 25 to 35 enemies enemyCount = 25 + Math.floor(Math.random() * 11); } else if (currentWave <= 60) { // Waves 31-60: 35 to 45 enemies enemyCount = 35 + Math.floor(Math.random() * 11); } else if (currentWave <= 97) { // Waves 61-97: 45 to 55 enemies enemyCount = 45 + Math.floor(Math.random() * 11); } else { // Waves 98-100: 60 to 75 enemies enemyCount = 60 + Math.floor(Math.random() * 16); } // Calculate available enemy strength levels based on wave var maxStrengthLevel = Math.min(10, Math.floor((currentWave + 1) / 2)); var minStrengthLevel = Math.max(1, maxStrengthLevel - 2); for (var i = 0; i < enemyCount; i++) { // Define which enemies can appear based on wave number - exact wave appearances as specified var availableEnemies = []; // 30 balanced enemies with exact wave appearance and phase-out logic if (currentWave >= 1 && currentWave <= 5) { availableEnemies.push('goblin_scout'); } if (currentWave >= 4 && currentWave <= 8) { availableEnemies.push('mire_crawler'); } if (currentWave >= 7 && currentWave <= 12) { availableEnemies.push('fang_imp'); } if (currentWave >= 10 && currentWave <= 16) { availableEnemies.push('crimson_leaper'); } if (currentWave >= 13 && currentWave <= 20) { availableEnemies.push('rustback_beetle'); } if (currentWave >= 16 && currentWave <= 24) { availableEnemies.push('gravel_hound'); } if (currentWave >= 19 && currentWave <= 28) { availableEnemies.push('vine_stalker'); } if (currentWave >= 22 && currentWave <= 32) { availableEnemies.push('ashborne_wraith'); } if (currentWave >= 25 && currentWave <= 36) { availableEnemies.push('stonehide_orc'); } if (currentWave >= 28 && currentWave <= 40) { availableEnemies.push('toxic_maw'); } if (currentWave >= 31 && currentWave <= 43) { availableEnemies.push('frostback_boar'); } if (currentWave >= 34 && currentWave <= 46) { availableEnemies.push('ironmaw_troll'); } if (currentWave >= 37 && currentWave <= 49) { availableEnemies.push('volcanic_strider'); } if (currentWave >= 40 && currentWave <= 52) { availableEnemies.push('stormblade_knight'); } if (currentWave >= 43 && currentWave <= 55) { availableEnemies.push('twilight_serpent'); } if (currentWave >= 46 && currentWave <= 58) { availableEnemies.push('obsidian_colossus'); } if (currentWave >= 49 && currentWave <= 61) { availableEnemies.push('spectral_ravager'); } if (currentWave >= 52 && currentWave <= 64) { availableEnemies.push('boneclad_warbeast'); } if (currentWave >= 55 && currentWave <= 67) { availableEnemies.push('plague_titan'); } if (currentWave >= 58 && currentWave <= 70) { availableEnemies.push('abyss_reaper'); } if (currentWave >= 61 && currentWave <= 73) { availableEnemies.push('wraith_bringer'); } if (currentWave >= 64 && currentWave <= 76) { availableEnemies.push('inferno_crawler'); } if (currentWave >= 67 && currentWave <= 79) { availableEnemies.push('thornback_hydra'); } if (currentWave >= 70 && currentWave <= 82) { availableEnemies.push('ghostfire_shade'); } if (currentWave >= 73 && currentWave <= 85) { availableEnemies.push('storm_herald'); } if (currentWave >= 76 && currentWave <= 88) { availableEnemies.push('voidling_runner'); } if (currentWave >= 79 && currentWave <= 91) { availableEnemies.push('doom_shell'); } if (currentWave >= 82 && currentWave <= 94) { availableEnemies.push('nether_juggernaut'); } if (currentWave >= 85) { availableEnemies.push('abyss_howler'); } if (currentWave >= 88) { availableEnemies.push('void_devourer'); } // Legacy enemy support for existing assets if (currentWave >= 1) { availableEnemies.push('goblin'); } if (currentWave >= 4) { availableEnemies.push('crawler'); } if (currentWave >= 7) { availableEnemies.push('bandit'); } if (currentWave >= 10) { availableEnemies.push('golem'); } if (currentWave >= 13) { availableEnemies.push('imp'); } if (currentWave >= 16) { availableEnemies.push('knight'); } if (currentWave >= 19) { availableEnemies.push('beast'); } if (currentWave >= 22) { availableEnemies.push('mage'); } if (currentWave >= 25) { availableEnemies.push('reaver'); } if (currentWave >= 28) { availableEnemies.push('doom'); } // Select random enemy from available types var enemyType = availableEnemies[Math.floor(Math.random() * availableEnemies.length)]; // Assign strength level within available range var strengthLevel = minStrengthLevel + Math.floor(Math.random() * (maxStrengthLevel - minStrengthLevel + 1)); var pathChoice = 1; if (currentWave >= 6) { pathChoice = Math.floor(Math.random() * 3) + 1; } else if (currentWave >= 3) { pathChoice = Math.floor(Math.random() * 2) + 1; } waveEnemies.push({ type: enemyType, pathIndex: pathChoice, strengthLevel: strengthLevel, spawnTime: LK.ticks + Math.floor(i * 90 / gameSpeed) // Apply speed multiplier to spawn timing }); } waveInProgress = true; if (!audioMuted) { LK.getSound('wave_start').play(); } // Show round start announcement if (roundStartText && roundStartText.destroy) { roundStartText.destroy(); } roundStartText = new Text2('Wave ' + currentWave + ' / 100 Starting...', { size: 80, fill: 0xFFFFFF }); roundStartText.anchor.set(0.5, 0.5); roundStartText.x = 1024; roundStartText.y = 1000; isoContainer.addChild(roundStartText); // Animate the announcement text tween(roundStartText, { scaleX: 1.2, scaleY: 1.2 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { tween(roundStartText, { alpha: 0 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { if (roundStartText && roundStartText.destroy) { roundStartText.destroy(); roundStartText = null; } } }); } }); } // Create background panel for castle lives text var castleLivesTextBg = isoContainer.addChild(LK.getAsset('build_zone', { anchorX: 0, anchorY: 0.5, x: 30, y: 1900, scaleX: 8, scaleY: 1.8 })); castleLivesTextBg.tint = 0x000000; castleLivesTextBg.alpha = 0.8; castleLivesTextBg.visible = false; // Hidden on main menu var castleLivesText = new Text2('Castle Health: 100', { size: 70, fill: 0xFF4444, font: "'Arial Black', Impact, 'Arial Bold'" }); castleLivesText.anchor.set(0, 0.5); castleLivesText.x = 50; castleLivesText.y = 1900; castleLivesText.visible = false; // Hidden on main menu isoContainer.addChild(castleLivesText); // Create background panel for gold text var coinsTextBg = isoContainer.addChild(LK.getAsset('build_zone', { anchorX: 0, anchorY: 0.5, x: 30, y: 2180, scaleX: 6, scaleY: 1.8 })); coinsTextBg.tint = 0x000000; coinsTextBg.alpha = 0.8; coinsTextBg.visible = false; // Hidden on main menu var coinsText = new Text2('Gold: 160', { size: 70, fill: 0xFFD700, font: "'Arial Black', Impact, 'Arial Bold'" }); coinsText.anchor.set(0, 0.5); coinsText.x = 50; coinsText.y = 2180; coinsText.visible = false; // Hidden on main menu isoContainer.addChild(coinsText); var waveText = new Text2('Wave: 0/100', { size: 40, fill: 0xFFFFFF }); waveText.anchor.set(0.5, 0); waveText.visible = false; // Hidden on main menu LK.gui.top.addChild(waveText); var roundStartText = null; if (gameState === 'mainMenu') { createMainMenu(); } else { createPath(); createTerrain(); createCastle(); createShop(); } game.down = function (x, y, obj) { // Clear any existing upgrade panel and range indicator when clicking elsewhere if (upgradePanel && upgradePanel.destroy && selectedTower && obj && !selectedTower.intersects(obj)) { upgradePanel.destroy(); upgradePanel = null; selectedTower = null; // Clear range indicator if (activeRangeIndicator && activeRangeIndicator.destroy) { activeRangeIndicator.destroy(); activeRangeIndicator = null; } } // Fallback for old upgradeText system if (upgradeText && upgradeText.destroy && selectedTower && obj && !selectedTower.intersects(obj)) { upgradeText.destroy(); upgradeText = null; selectedTower = null; } // Use screen coordinates directly for top-down view var isoPos = { x: x, y: y }; if (selectedTowerType && gameState === 'playing') { // Check if clicking on a valid, unoccupied build zone var validBuildZone = null; for (var i = 0; i < buildZones.length; i++) { if (!buildZones[i].zoneData.occupied) { var distance = Math.sqrt((buildZones[i].x - isoPos.x) * (buildZones[i].x - isoPos.x) + (buildZones[i].y - isoPos.y) * (buildZones[i].y - isoPos.y)); if (distance < 40) { validBuildZone = buildZones[i]; break; } } } // Only place tower if clicking on a valid build zone and player has enough coins if (validBuildZone) { var tower = new Tower(selectedTowerType); if (coins >= tower.cost) { // Place tower exactly at build zone center tower.x = validBuildZone.x; tower.y = validBuildZone.y; towers.push(tower); isoContainer.addChild(tower); coins -= tower.cost; coinsText.setText('Gold: ' + coins); selectedTowerType = null; // Mark build zone as occupied validBuildZone.zoneData.occupied = true; updateBuildZoneVisuals(); // Play placement sound if (!audioMuted) { LK.getSound('placement').play(); } } } else { // Clear selection if clicking outside build zones selectedTowerType = null; updateBuildZoneVisuals(); } } }; game.update = function () { if (gameState === 'mainMenu' || gameState === 'setup') { return; } if (gameState !== 'playing') { return; } if (waveInProgress) { for (var i = waveEnemies.length - 1; i >= 0; i--) { var enemyData = waveEnemies[i]; if (LK.ticks >= enemyData.spawnTime) { var enemy = new Enemy(enemyData.type, currentWave, enemyData.pathIndex, enemyData.strengthLevel); enemies.push(enemy); isoContainer.addChild(enemy); waveEnemies.splice(i, 1); } } if (waveEnemies.length === 0 && enemies.length === 0) { waveInProgress = false; nextWaveTimer = LK.ticks + Math.floor(180 / gameSpeed); // Apply speed multiplier to wave delay } } if (!waveInProgress && LK.ticks >= nextWaveTimer && enemies.length === 0) { createWave(); } for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; if (enemy.reachedCastle) { castleHealth -= 15; // Update castle lives text castleLivesText.setText('Castle Health: ' + castleHealth); // Update health bar with smooth animation var healthPercent = castleHealth / 100; if (castleHealthBarFill) { // Animate health bar scale change smoothly tween(castleHealthBarFill, { scaleX: 4 * healthPercent }, { duration: 300 }); // Update color based on health percentage - green to red gradient var redComponent = Math.floor(255 * (1 - healthPercent)); var greenComponent = Math.floor(255 * healthPercent); var healthColor = redComponent << 16 | greenComponent << 8 | 0x00; castleHealthBarFill.tint = healthColor; // Flash effect when taking damage LK.effects.flashObject(castleHealthBarFill, 0xFF0000, 200); } enemy.destroy(); enemies.splice(i, 1); if (castleHealth <= 0) { LK.showGameOver(); return; } } } for (var i = projectiles.length - 1; i >= 0; i--) { var projectile = projectiles[i]; if (projectile.hasReachedTarget) { // Target tracking ensures 100% hit rate - damage the intended target if (projectile.targetEnemy && projectile.targetEnemy.x) { var targetEnemy = projectile.targetEnemy; // Apply tower-specific effects if (projectile.towerType === 'frost') { // Find the tower that shot this projectile to get its slow percentage var frostTower = null; for (var k = 0; k < towers.length; k++) { if (towers[k].type === 'frost') { var towerDistance = Math.sqrt((towers[k].x - projectile.x) * (towers[k].x - projectile.x) + (towers[k].y - projectile.y) * (towers[k].y - projectile.y)); if (towerDistance <= towers[k].range + 100) { // Some tolerance for projectile travel frostTower = towers[k]; break; } } } if (frostTower) { targetEnemy.applyFrostSlow(frostTower.slowPercentage); } } // Handle area damage for cannonball projectiles if (projectile.type === 'cannonball') { // Deal damage to target and nearby enemies for (var j = 0; j < enemies.length; j++) { var enemy = enemies[j]; var distance = Math.sqrt((enemy.x - projectile.x) * (enemy.x - projectile.x) + (enemy.y - projectile.y) * (enemy.y - projectile.y)); if (distance <= 50) { if (enemy.takeDamage(projectile.damage)) { coins += Math.floor(enemy.coinValue * 0.65 * 0.7); coinsText.setText('Gold: ' + coins); // Create death effect at enemy position tween(enemy, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 400, easing: tween.easeOut }); enemy.destroy(); enemies.splice(j, 1); j--; } } } } else { // Single target damage - guaranteed hit on intended target if (targetEnemy.takeDamage(projectile.damage)) { coins += Math.floor(targetEnemy.coinValue * 0.65 * 0.7); coinsText.setText('Gold: ' + coins); // Create death effect at enemy position tween(targetEnemy, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 400, easing: tween.easeOut }); // Remove enemy from array for (var j = 0; j < enemies.length; j++) { if (enemies[j] === targetEnemy) { enemies.splice(j, 1); break; } } targetEnemy.destroy(); } } } projectile.destroy(); projectiles.splice(i, 1); } else if (projectile.x < -50 || projectile.x > 2098 || projectile.y < -50 || projectile.y > 2782) { projectile.destroy(); projectiles.splice(i, 1); } } if (endlessMode) { waveText.setText('Endless Mode - Survived: ' + endlessScore + ' waves'); } else { waveText.setText('Wave: ' + currentWave + '/' + maxWaves); } updateShopVisuals(); };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Enemy = Container.expand(function (type, wave, pathIndex, strengthLevel) {
var self = Container.call(this);
self.type = type;
self.wave = wave;
self.pathIndex = pathIndex || 1;
self.strengthLevel = strengthLevel || 1;
self.currentPath = self.pathIndex === 1 ? enemyPath1 : self.pathIndex === 2 ? enemyPath2 : enemyPath3;
// Define base stats for each enemy type with exact balancing specifications
var baseStats = {
// 30 enemies with specific wave appearance, HP, and speed
goblin_scout: {
speed: 1.2,
health: 100,
coins: 15,
flying: false,
waveAppearance: 1
},
mire_crawler: {
speed: 1.3,
health: 160,
coins: 18,
flying: false,
waveAppearance: 4
},
fang_imp: {
speed: 1.4,
health: 220,
coins: 22,
flying: false,
waveAppearance: 7
},
crimson_leaper: {
speed: 1.5,
health: 280,
coins: 25,
flying: true,
waveAppearance: 10
},
rustback_beetle: {
speed: 1.6,
health: 340,
coins: 28,
flying: false,
waveAppearance: 13
},
gravel_hound: {
speed: 1.7,
health: 400,
coins: 32,
flying: false,
waveAppearance: 16
},
vine_stalker: {
speed: 1.8,
health: 460,
coins: 35,
flying: false,
waveAppearance: 19
},
ashborne_wraith: {
speed: 1.9,
health: 520,
coins: 38,
flying: true,
waveAppearance: 22
},
stonehide_orc: {
speed: 2.0,
health: 580,
coins: 42,
flying: false,
waveAppearance: 25
},
toxic_maw: {
speed: 2.0,
health: 640,
coins: 45,
flying: false,
waveAppearance: 28
},
frostback_boar: {
speed: 2.1,
health: 700,
coins: 48,
flying: false,
waveAppearance: 31
},
ironmaw_troll: {
speed: 2.1,
health: 760,
coins: 52,
flying: false,
waveAppearance: 34
},
volcanic_strider: {
speed: 2.2,
health: 820,
coins: 55,
flying: false,
waveAppearance: 37
},
stormblade_knight: {
speed: 2.2,
health: 880,
coins: 58,
flying: false,
waveAppearance: 40
},
twilight_serpent: {
speed: 2.3,
health: 940,
coins: 62,
flying: true,
waveAppearance: 43
},
obsidian_colossus: {
speed: 2.3,
health: 1000,
coins: 65,
flying: false,
waveAppearance: 46
},
spectral_ravager: {
speed: 2.4,
health: 1060,
coins: 68,
flying: false,
waveAppearance: 49
},
boneclad_warbeast: {
speed: 2.4,
health: 1120,
coins: 72,
flying: false,
waveAppearance: 52
},
plague_titan: {
speed: 2.5,
health: 1180,
coins: 75,
flying: false,
waveAppearance: 55
},
abyss_reaper: {
speed: 2.5,
health: 1240,
coins: 78,
flying: false,
waveAppearance: 58
},
wraith_bringer: {
speed: 2.6,
health: 1300,
coins: 82,
flying: true,
waveAppearance: 61
},
inferno_crawler: {
speed: 2.6,
health: 1360,
coins: 85,
flying: false,
waveAppearance: 64
},
thornback_hydra: {
speed: 2.7,
health: 1420,
coins: 88,
flying: false,
waveAppearance: 67
},
ghostfire_shade: {
speed: 2.7,
health: 1480,
coins: 92,
flying: false,
waveAppearance: 70
},
storm_herald: {
speed: 2.8,
health: 1540,
coins: 95,
flying: true,
waveAppearance: 73
},
voidling_runner: {
speed: 2.8,
health: 1600,
coins: 98,
flying: false,
waveAppearance: 76
},
doom_shell: {
speed: 2.9,
health: 1660,
coins: 102,
flying: false,
waveAppearance: 79
},
nether_juggernaut: {
speed: 2.9,
health: 1720,
coins: 105,
flying: false,
waveAppearance: 82
},
abyss_howler: {
speed: 3.0,
health: 1780,
coins: 108,
flying: true,
waveAppearance: 85
},
void_devourer: {
speed: 3.0,
health: 1840,
coins: 112,
flying: false,
waveAppearance: 88
},
// Legacy support for existing enemies
goblin: {
speed: 1.2,
health: 100,
coins: 15,
flying: false,
waveAppearance: 1
},
crawler: {
speed: 1.3,
health: 160,
coins: 18,
flying: false,
waveAppearance: 4
},
bandit: {
speed: 1.4,
health: 220,
coins: 22,
flying: false,
waveAppearance: 7
},
golem: {
speed: 1.5,
health: 280,
coins: 25,
flying: false,
waveAppearance: 10
},
imp: {
speed: 1.6,
health: 340,
coins: 28,
flying: false,
waveAppearance: 13
},
knight: {
speed: 1.7,
health: 400,
coins: 32,
flying: false,
waveAppearance: 16
},
beast: {
speed: 1.8,
health: 460,
coins: 35,
flying: false,
waveAppearance: 19
},
mage: {
speed: 1.9,
health: 520,
coins: 38,
flying: false,
waveAppearance: 22
},
reaver: {
speed: 2.0,
health: 580,
coins: 42,
flying: true,
waveAppearance: 25
},
doom: {
speed: 2.0,
health: 640,
coins: 45,
flying: false,
waveAppearance: 28
}
};
var stats = baseStats[type] || baseStats.goblin_scout;
// Apply endless mode scaling if active
var healthMultiplier = 1;
var speedMultiplier = 1;
if (endlessMode && currentWave > 100) {
var endlessWaves = Math.floor((currentWave - 100) / 5);
healthMultiplier = 1 + endlessWaves * 0.1; // +10% HP every 5 waves
speedMultiplier = 1 + endlessWaves * 0.05; // +5% speed every 5 waves
}
// Apply scaling to stats
self.speed = stats.speed * speedMultiplier;
self.maxHealth = Math.floor(stats.health * healthMultiplier);
self.health = self.maxHealth;
self.pathStep = 0;
self.coinValue = stats.coins;
self.flying = stats.flying;
self.regeneration = type === 'beast' ? 0.5 : type === 'frostback_boar' ? 0.3 : type === 'plague_titan' ? 0.8 : 0;
self.poisonResistant = type === 'doom' || type === 'toxic_maw' || type === 'plague_titan' || type === 'void_devourer';
self.slowResistant = type === 'doom' || type === 'volcanic_strider' || type === 'stormblade_knight' || type === 'abyss_reaper';
self.originalSpeed = self.speed; // Store original speed for slowing effect
self.slowedByFrost = false; // Track if enemy is slowed by frost tower
self.slowTimer = 0; // Timer for slow effect duration
self.slowPercentage = 0; // Current slow percentage applied
// Map enemy types to valid asset IDs with fallback system
var assetMapping = {
'goblin_scout': 'enemy_goblin',
'mire_crawler': 'enemy_mire_crawler',
'fang_imp': 'enemy_fang_imp',
'crimson_leaper': 'enemy_crimson_leaper',
'rustback_beetle': 'enemy_rustback_beetle',
'gravel_hound': 'enemy_gravel_hound',
'vine_stalker': 'enemy_vine_stalker',
'ashborne_wraith': 'enemy_ashborne_wraith',
'stonehide_orc': 'enemy_stonehide_orc',
'toxic_maw': 'enemy_toxic_maw',
'frostback_boar': 'enemy_frostback_boar',
'ironmaw_troll': 'enemy_ironmaw_troll',
'volcanic_strider': 'enemy_volcanic_strider',
'stormblade_knight': 'enemy_stormblade_knight',
'twilight_serpent': 'enemy_twilight_serpent',
'obsidian_colossus': 'enemy_obsidian_colossus',
'spectral_ravager': 'enemy_spectral_ravager',
'boneclad_warbeast': 'enemy_boneclad_warbeast',
'plague_titan': 'enemy_plague_titan',
'abyss_reaper': 'enemy_abyss_reaper',
'wraith_bringer': 'enemy_mage',
// fallback to mage
'inferno_crawler': 'enemy_crawler',
'thornback_hydra': 'enemy_beast',
// fallback to beast
'ghostfire_shade': 'enemy_mage',
// fallback to mage
'storm_herald': 'enemy_knight',
// fallback to knight
'voidling_runner': 'enemy_reaver',
// fallback to reaver
'doom_shell': 'enemy_doom',
'nether_juggernaut': 'enemy_golem',
// fallback to golem
'abyss_howler': 'enemy_beast',
// fallback to beast
'void_devourer': 'enemy_void_devourer'
};
// Get the correct asset ID or fallback to goblin
var assetId = assetMapping[type] || 'enemy_' + type;
// Try to get the asset, fallback to goblin if it doesn't exist
var graphics;
try {
graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
} catch (e) {
// Fallback to default goblin sprite if asset doesn't exist
graphics = self.attachAsset('enemy_goblin', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Create health bar background using the same asset fallback system
try {
self.healthBarBg = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.1,
y: -25
});
} catch (e) {
self.healthBarBg = self.attachAsset('enemy_goblin', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.1,
y: -25
});
}
self.healthBarBg.tint = 0x333333;
// Create health bar fill using the same asset fallback system
try {
self.healthBarFill = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.1,
y: -25
});
} catch (e) {
self.healthBarFill = self.attachAsset('enemy_goblin', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.1,
y: -25
});
}
self.healthBarFill.tint = 0x00FF00;
self.x = self.currentPath[0].x;
self.y = self.currentPath[0].y;
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFF0000, 200);
// Update health bar fill based on remaining health
if (self.healthBarFill) {
var healthPercent = Math.max(0, self.health / self.maxHealth);
self.healthBarFill.scaleX = 0.8 * healthPercent;
// Change color from green to red based on health
var redComponent = Math.floor(255 * (1 - healthPercent));
var greenComponent = Math.floor(255 * healthPercent);
var healthColor = redComponent << 16 | greenComponent << 8 | 0x00;
self.healthBarFill.tint = healthColor;
}
if (self.health <= 0) {
if (!audioMuted) {
LK.getSound('enemy_hit').play();
LK.getSound('coin_gain').play();
}
// Create floating coin gain text with reduced gold reward (35% reduction)
var actualCoinValue = Math.floor(self.coinValue * 0.65);
var coinText = new Text2('+' + actualCoinValue, {
size: 40,
fill: 0xFFD700
});
coinText.anchor.set(0.5, 0.5);
coinText.x = self.x;
coinText.y = self.y - 30;
game.addChild(coinText);
// Animate coin text floating up and fading out
tween(coinText, {
y: coinText.y - 80,
alpha: 0
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (coinText && coinText.destroy) {
coinText.destroy();
}
}
});
// Create death effect at enemy position
tween(self, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut
});
return true;
}
return false;
};
self.update = function () {
// Handle slow timer for frost effect
if (self.slowTimer > 0) {
self.slowTimer -= gameSpeed; // Apply speed multiplier to timer
if (self.slowTimer <= 0) {
// Restore original speed when slow effect expires
self.speed = self.originalSpeed;
self.slowedByFrost = false;
self.slowPercentage = 0;
}
}
if (self.pathStep < self.currentPath.length - 1) {
var target = self.currentPath[self.pathStep + 1];
var dx = target.x - self.x;
var dy = target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 5) {
self.pathStep++;
if (self.pathStep >= self.currentPath.length - 1) {
self.reachedCastle = true;
}
} else {
self.x += dx / distance * self.speed * gameSpeed; // Apply speed multiplier
self.y += dy / distance * self.speed * gameSpeed; // Apply speed multiplier
}
}
};
self.applyFrostSlow = function (slowPercentage) {
// Don't apply slow to slow-resistant enemies
if (self.slowResistant) {
return;
}
// Apply the slow effect
self.slowedByFrost = true;
self.slowPercentage = slowPercentage;
self.speed = self.originalSpeed * (1 - slowPercentage / 100);
self.slowTimer = 120; // 2 seconds at 60 FPS
};
return self;
});
var Projectile = Container.expand(function (type, startX, startY, targetEnemy, damage) {
var self = Container.call(this);
self.type = type;
self.damage = damage;
self.targetEnemy = targetEnemy; // Store reference to target enemy
self.speed = type === 'arrow' ? 16 : type === 'cannonball' ? 12 : 14;
var graphics = self.attachAsset('projectile_' + type, {
anchorX: 0.5,
anchorY: 0.5
});
self.x = startX;
self.y = startY;
self.update = function () {
// Check if target enemy still exists
if (!self.targetEnemy || !self.targetEnemy.x) {
self.hasReachedTarget = true;
return;
}
// Calculate direction to current enemy position (tracking)
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Move toward enemy at projectile speed
if (distance > 0) {
self.velocityX = dx / distance * self.speed;
self.velocityY = dy / distance * self.speed;
self.x += self.velocityX * gameSpeed; // Apply speed multiplier
self.y += self.velocityY * gameSpeed; // Apply speed multiplier
}
// Check if projectile has reached the target (smaller threshold for guaranteed hit)
if (distance < 15) {
self.hasReachedTarget = true;
}
};
return self;
});
var Tower = Container.expand(function (type) {
var self = Container.call(this);
self.type = type;
self.level = 1;
self.lastShotTime = 0;
self.maxLevel = type === 'antiair' || type === 'poison' || type === 'tesla' ? 5 : 10;
// Initialize tower stats based on balanced specifications
if (type === 'infantry') {
self.damage = 11.375;
self.range = 150;
self.attackSpeed = Math.floor(60 / 0.8 * 1.54 * 1.35); // 0.8 shots/sec slowed by 70% = 155 ticks between shots
self.cost = 80;
self.projectileType = 'arrow';
} else if (type === 'frost') {
self.damage = 1.456;
self.range = 150;
self.attackSpeed = Math.floor(60 * 2 * 1.54 * 1.35); // 1 shot per 2 seconds slowed by 70% = 250 ticks
self.cost = 120;
self.projectileType = 'magic';
self.slowEffect = true;
self.slowPercentage = 27; // Level 1 slow - increased by 30% from 21%
self.aoeRadius = 120;
} else if (type === 'antiair') {
self.damage = 13.65;
self.range = 150;
self.attackSpeed = Math.floor(60 / 1.5 * 1.54 * 1.35); // 1.5 shots/sec slowed by 70% = 84 ticks
self.cost = 100;
self.projectileType = 'magic';
self.antiAir = true;
} else if (type === 'poison') {
self.damage = 4.55;
self.range = 150;
self.attackSpeed = Math.floor(60 / 1.2 * 1.54 * 1.35); // 1.2 shots/sec slowed by 70% = 104 ticks
self.cost = 130;
self.projectileType = 'magic';
self.poisonEffect = true;
self.poisonDuration = 3; // 3 seconds
self.poisonTickInterval = 60; // 1 second in ticks
} else if (type === 'tesla') {
self.damage = 15.925;
self.range = 150;
self.attackSpeed = Math.floor(60 / 0.7 * 1.54 * 1.35); // 0.7 shots/sec slowed by 70% = 178 ticks
self.cost = 150;
self.projectileType = 'magic';
self.chainLightning = true;
self.chainTargets = 3;
self.chainDamageReduction = 0.1; // 10% reduction per jump
}
var graphics = self.attachAsset('tower_' + type, {
anchorX: 0.5,
anchorY: 0.5
});
self.getUpgradeNames = function () {
if (self.type === 'infantry') {
return ['Rifleman', 'Marksman', 'Sharpshooter', 'Machine Gunner', 'Grenadier', 'Mortar Operator', 'Tank Commander', 'Bazooka Specialist', 'Rocket Launcher', 'Missile Battery'];
} else if (self.type === 'frost') {
return ['Frost Spire', 'Ice Shard Tower', 'Blizzard Bastion', 'Cryo Cannon', 'Glacier Fortress', 'Snowstorm Citadel', 'Avalanche Keep', 'Frozen Maelstrom', 'Arctic Warden', 'Absolute Zero'];
} else if (self.type === 'antiair') {
return ['Flak Cannon', 'SAM Launcher', 'Radar Missile Battery', 'Laser Air Defense', 'Plasma Barrier'];
} else if (self.type === 'poison') {
return ['Venom Sprayer', 'Toxic Spitter', 'Acid Dart Tower', 'Corrosive Spire', 'Plague Altar'];
} else if (self.type === 'tesla') {
return ['Static Coil', 'Dual Coil', 'Arc Emitter', 'Thunderstorm Core', 'Lightning God'];
}
};
self.canShoot = function () {
return LK.ticks - self.lastShotTime > self.attackSpeed / gameSpeed / (1000 / 60); // Apply speed multiplier to attack speed
};
self.findTarget = function () {
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var distance = Math.sqrt((enemy.x - self.x) * (enemy.x - self.x) + (enemy.y - self.y) * (enemy.y - self.y));
if (distance <= self.range && distance < closestDistance) {
// Flying enemy rules: only Anti-Air towers can target flying enemies
if (enemy.flying && self.type !== 'antiair') {
continue; // Skip flying enemies for non-antiair towers
}
// Non-flying enemies cannot be targeted by Anti-Air towers
if (!enemy.flying && self.type === 'antiair') {
continue; // Skip non-flying enemies for antiair towers
}
closestDistance = distance;
closestEnemy = enemy;
}
}
return closestEnemy;
};
self.shoot = function (target) {
if (!self.canShoot()) {
return;
}
self.lastShotTime = LK.ticks;
var projectile = new Projectile(self.projectileType, self.x, self.y, target, self.damage);
projectile.towerType = self.type;
projectile.slowEffect = self.slowEffect;
projectile.poisonEffect = self.poisonEffect;
projectile.chainLightning = self.chainLightning;
projectiles.push(projectile);
isoContainer.addChild(projectile);
if (!audioMuted) {
if (self.type === 'tesla') {
LK.getSound('tesla_knock').play();
} else {
LK.getSound('shoot_arrow').play();
}
}
};
self.getUpgradeCost = function () {
// Calculate upgrade cost: Level 1 = base price, then +80% each level
var upgradeCost = self.cost;
for (var i = 2; i <= self.level; i++) {
upgradeCost = Math.floor(upgradeCost * 1.8);
}
return upgradeCost;
};
self.upgrade = function () {
// Check if upgrade level is unlocked
var nextLevel = self.level + 1;
if (nextLevel > upgradeUnlockWaves.length || currentWave < upgradeUnlockWaves[nextLevel - 1]) {
showUpgradeRestrictedMessage(nextLevel, upgradeUnlockWaves[nextLevel - 1]);
return;
}
if (self.level < self.maxLevel) {
self.level++;
// Tower-specific damage upgrades
if (self.type === 'infantry') {
self.damage += 4.55; // +4.55 flat damage per level (30% reduction from 6.5)
} else if (self.type === 'frost') {
self.damage += 0.437; // +0.437 damage per level (30% reduction from 0.624)
// Update slow percentage - increased by 30%
if (self.level === 2 || self.level === 3) {
self.slowPercentage = 45;
} else if (self.level >= 4) {
self.slowPercentage = 64;
}
} else if (self.type === 'antiair') {
self.damage += 3.64; // +3.64 damage per level (30% reduction from 5.2)
} else if (self.type === 'poison') {
self.damage += 2.275; // +2.275 tick damage per level (30% reduction from 3.25)
self.poisonDuration += 1; // +1 second duration
} else if (self.type === 'tesla') {
self.damage += 4.55; // +4.55 base damage per level (30% reduction from 6.5)
// Increase chain targets every 2 levels
if (self.level % 2 === 0) {
self.chainTargets = Math.min(6, self.chainTargets + 1);
}
}
// Calculate range increase with diminishing returns - all towers start at 150 base range
var rangeMultiplier = 1.0;
if (self.level === 2) {
rangeMultiplier = 1.10; // +10% for level 1 upgrade
} else if (self.level === 3) {
rangeMultiplier = 1.10 * 1.05; // +10% then +5%
} else if (self.level === 4) {
rangeMultiplier = 1.10 * 1.05 * 1.035; // +10%, +5%, then +3.5%
} else if (self.level >= 5) {
// +10%, +5%, +3.5%, then +2% for each additional level
rangeMultiplier = 1.10 * 1.05 * 1.035;
var additionalLevels = self.level - 4;
rangeMultiplier *= Math.pow(1.02, additionalLevels);
}
// Apply range multiplier to base range of 150 for all towers
self.range = Math.floor(150 * rangeMultiplier);
self.attackSpeed = Math.max(200, Math.floor(self.attackSpeed * 0.95));
// Visual upgrade indicators
var tintColors = [0xFFFFFF, 0xFFD700, 0xFF6347, 0xFF4500, 0x8A2BE2, 0x00FF00, 0x00FFFF, 0xFF69B4, 0xFFFF00, 0xFF0000];
graphics.tint = tintColors[self.level - 1] || 0xFF0000;
// Show range indicator for upgraded towers
if (self.level > 1 && !self.rangeIndicator) {
self.rangeIndicator = LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.1,
scaleX: self.range / 40,
scaleY: self.range / 40
});
self.rangeIndicator.x = self.x;
self.rangeIndicator.y = self.y;
self.rangeIndicator.tint = 0x00FF00;
isoContainer.addChild(self.rangeIndicator);
} else if (self.rangeIndicator) {
self.rangeIndicator.scaleX = self.range / 40;
self.rangeIndicator.scaleY = self.range / 40;
}
}
};
self.down = function (x, y, obj) {
selectedTower = self;
// Clear any existing range indicator
if (activeRangeIndicator && activeRangeIndicator.destroy) {
activeRangeIndicator.destroy();
activeRangeIndicator = null;
}
// Clear any existing upgrade panel
if (upgradePanel && upgradePanel.destroy) {
upgradePanel.destroy();
upgradePanel = null;
}
// Create new range indicator
activeRangeIndicator = LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.2,
scaleX: self.range / 27.5,
scaleY: self.range / 27.5
});
activeRangeIndicator.x = self.x;
activeRangeIndicator.y = self.y;
activeRangeIndicator.tint = 0x00FFFF; // Cyan color for range indicator
isoContainer.addChild(activeRangeIndicator);
// Create larger upgrade panel
upgradePanel = new Container();
isoContainer.addChild(upgradePanel);
// Smart positioning: determine if panel should open upward or downward
var panelHeight = 220; // Approximate panel height (8 * 27.5)
var panelY = self.y - 100; // Default upward position
var shouldOpenDownward = false;
// Check if tower is too close to top edge (within 300 pixels)
if (self.y < 300) {
shouldOpenDownward = true;
panelY = self.y + 100; // Position below tower
}
// Check if tower is too close to bottom edge
else if (self.y > 2732 - 300) {
// Keep upward positioning for bottom towers
panelY = self.y - 100;
}
// Check if tower is too close to left edge
var panelX = self.x;
if (self.x < 350) {
panelX = self.x + 50; // Shift right if too close to left edge
}
// Check if tower is too close to right edge
else if (self.x > 2048 - 350) {
panelX = self.x - 50; // Shift left if too close to right edge
}
// Panel background - much larger
var panelBg = upgradePanel.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: panelX,
y: panelY,
scaleX: 12,
scaleY: 8
}));
panelBg.tint = 0x000000;
panelBg.alpha = 0.9;
// Calculate total investment for sell value
var totalInvestment = self.cost;
for (var i = 2; i <= self.level; i++) {
var levelCost = self.cost;
for (var j = 2; j <= i; j++) {
levelCost = Math.floor(levelCost * 1.8);
}
totalInvestment += levelCost;
}
var sellValue = Math.floor(totalInvestment * 0.5);
if (self.level < self.maxLevel) {
var cost = self.getUpgradeCost();
var names = self.getUpgradeNames();
var nextName = names[self.level - 1] || 'Max Level';
var nextLevel = self.level + 1;
var isUpgradeUnlocked = nextLevel <= upgradeUnlockWaves.length && currentWave >= upgradeUnlockWaves[nextLevel - 1];
// Upgrade button text - larger size
var upgradeText = upgradePanel.addChild(new Text2('Upgrade', {
size: 40,
fill: isUpgradeUnlocked ? 0xFFD700 : 0x666666
}));
upgradeText.anchor.set(0.5, 1);
upgradeText.x = panelX;
upgradeText.y = shouldOpenDownward ? panelY - 20 : panelY - 20;
var costText;
if (isUpgradeUnlocked) {
costText = upgradePanel.addChild(new Text2('Cost: ' + cost + ' gold', {
size: 36,
fill: coins >= cost ? 0x00FF00 : 0xFF0000
}));
} else {
costText = upgradePanel.addChild(new Text2('Unlocks at Wave ' + upgradeUnlockWaves[nextLevel - 1], {
size: 32,
fill: 0xFFAA00
}));
}
costText.anchor.set(0.5, 1);
costText.x = panelX;
costText.y = shouldOpenDownward ? panelY + 20 : panelY + 20;
if (coins >= cost && isUpgradeUnlocked) {
upgradeText.down = costText.down = panelBg.down = function () {
coins -= cost;
coinsText.setText('Gold: ' + coins);
self.upgrade();
// Play upgrade sound
if (!audioMuted) {
LK.getSound('upgrade').play();
}
if (upgradePanel && upgradePanel.destroy) {
upgradePanel.destroy();
upgradePanel = null;
}
selectedTower = null;
if (activeRangeIndicator && activeRangeIndicator.destroy) {
activeRangeIndicator.destroy();
activeRangeIndicator = null;
}
};
}
} else {
var maxText = upgradePanel.addChild(new Text2('MAX LEVEL', {
size: 44,
fill: 0xFFFFFF
}));
maxText.anchor.set(0.5, 1);
maxText.x = panelX;
maxText.y = shouldOpenDownward ? panelY : panelY;
}
// Sell button - always visible
var sellText = upgradePanel.addChild(new Text2('Sell Tower', {
size: 38,
fill: 0xFF4444
}));
sellText.anchor.set(0.5, 1);
sellText.x = panelX;
sellText.y = shouldOpenDownward ? panelY + 60 : panelY + 60;
var sellValueText = upgradePanel.addChild(new Text2('Refund: ' + sellValue + ' gold', {
size: 32,
fill: 0xFFD700
}));
sellValueText.anchor.set(0.5, 1);
sellValueText.x = panelX;
sellValueText.y = shouldOpenDownward ? panelY + 95 : panelY + 95;
sellText.down = sellValueText.down = function () {
// Find and free the build zone
for (var i = 0; i < buildZones.length; i++) {
var zone = buildZones[i];
var distance = Math.sqrt((zone.x - self.x) * (zone.x - self.x) + (zone.y - self.y) * (zone.y - self.y));
if (distance < 30) {
zone.zoneData.occupied = false;
break;
}
}
// Add refund money
coins += sellValue;
coinsText.setText('Gold: ' + coins);
// Show refund notification
var refundNotification = new Text2('+' + sellValue + ' gold', {
size: 50,
fill: 0x00FF00
});
refundNotification.anchor.set(0.5, 0.5);
refundNotification.x = self.x;
refundNotification.y = self.y - 100;
isoContainer.addChild(refundNotification);
tween(refundNotification, {
y: refundNotification.y - 80,
alpha: 0
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (refundNotification && refundNotification.destroy) {
refundNotification.destroy();
}
}
});
// Remove tower from towers array
for (var i = 0; i < towers.length; i++) {
if (towers[i] === self) {
towers.splice(i, 1);
break;
}
}
// Clean up UI elements
if (upgradePanel && upgradePanel.destroy) {
upgradePanel.destroy();
upgradePanel = null;
}
if (activeRangeIndicator && activeRangeIndicator.destroy) {
activeRangeIndicator.destroy();
activeRangeIndicator = null;
}
selectedTower = null;
// Remove tower
self.destroy();
updateBuildZoneVisuals();
};
};
self.update = function () {
var target = self.findTarget();
if (target) {
self.shoot(target);
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x228B22
});
/****
* Game Code
****/
// Use game container directly for top-down view
var isoContainer = game;
var gameState = 'mainMenu';
var mainMenuContainer = null;
var howToPlayContainer = null;
var creditsContainer = null;
var settingsContainer = null;
var storyIntroContainer = null;
var settingsIcon = null;
var audioMuted = false;
var muteIndicator = null;
var endlessMode = false;
var endlessScore = 0;
var gameSpeed = 1; // Current game speed multiplier (1x, 2x, or 3x)
var isPaused = false; // Game pause state
var lastGameSpeed = 1; // Store last speed before pausing
var pauseButton = null; // Pause button reference
var pauseButtonText = null; // Pause button text reference
var speedButtons = []; // Array to hold speed button references
var speedButtonTexts = []; // Array to hold speed button text references
var castleType = 'square';
var difficulty = 'easy';
var currentWave = 0;
var maxWaves = 100;
var waveEnemies = [];
var enemies = [];
var towers = [];
var projectiles = [];
var coins = 160;
var castleHealth = 100;
var waveInProgress = false;
var nextWaveTimer = 0;
var selectedTowerType = null;
var selectedTower = null;
var upgradeText = null;
var upgradePanel = null;
var activeRangeIndicator = null;
// Shop button references for visual updates
var infantryBtn, frostBtn, antiairBtn, poisonBtn, teslaBtn;
var infantryCost, frostCost, antiairCost, poisonCost, teslaCost;
var infantryName, frostName, antiairName, poisonName, teslaName;
// Tower unlock system - waves when towers become available
var towerUnlockWaves = {
infantry: 1,
frost: 3,
antiair: 8,
poison: 15,
tesla: 25
};
// Upgrade level restrictions based on wave progression
var upgradeUnlockWaves = [1, 3, 6, 10, 14, 20, 30, 40, 50, 60];
// Enemy path 1: Detailed horizontal then vertical path
var enemyPath1 = [{
x: 0,
y: 200
}, {
x: 25.6,
y: 200
}, {
x: 51.2,
y: 200
}, {
x: 76.8,
y: 200
}, {
x: 102.4,
y: 200
}, {
x: 128,
y: 200
}, {
x: 153.6,
y: 200
}, {
x: 179.2,
y: 200
}, {
x: 204.8,
y: 200
}, {
x: 230.4,
y: 200
}, {
x: 256,
y: 200
}, {
x: 281.6,
y: 200
}, {
x: 307.2,
y: 200
}, {
x: 332.8,
y: 200
}, {
x: 358.4,
y: 200
}, {
x: 384,
y: 200
}, {
x: 409.6,
y: 200
}, {
x: 435.2,
y: 200
}, {
x: 460.8,
y: 200
}, {
x: 486.4,
y: 200
}, {
x: 512,
y: 200
}, {
x: 537.6,
y: 200
}, {
x: 563.2,
y: 200
}, {
x: 588.8,
y: 200
}, {
x: 614.4,
y: 200
}, {
x: 640,
y: 200
}, {
x: 665.6,
y: 200
}, {
x: 691.2,
y: 200
}, {
x: 716.8,
y: 200
}, {
x: 742.4,
y: 200
}, {
x: 768,
y: 200
}, {
x: 793.6,
y: 200
}, {
x: 819.2,
y: 200
}, {
x: 844.8,
y: 200
}, {
x: 870.4,
y: 200
}, {
x: 896,
y: 200
}, {
x: 921.6,
y: 200
}, {
x: 947.2,
y: 200
}, {
x: 972.8,
y: 200
}, {
x: 998.4,
y: 200
}, {
x: 1024,
y: 200
}, {
x: 1024,
y: 240
}, {
x: 1024,
y: 280
}, {
x: 1024,
y: 320
}, {
x: 1024,
y: 360
}, {
x: 1024,
y: 400
}, {
x: 1024,
y: 440
}, {
x: 1024,
y: 480
}, {
x: 1024,
y: 520
}, {
x: 1024,
y: 560
}, {
x: 1024,
y: 600
}, {
x: 998.4,
y: 600
}, {
x: 972.8,
y: 600
}, {
x: 947.2,
y: 600
}, {
x: 921.6,
y: 600
}, {
x: 896,
y: 600
}, {
x: 870.4,
y: 600
}, {
x: 844.8,
y: 600
}, {
x: 819.2,
y: 600
}, {
x: 793.6,
y: 600
}, {
x: 768,
y: 600
}, {
x: 768,
y: 640
}, {
x: 768,
y: 680
}, {
x: 768,
y: 720
}, {
x: 768,
y: 760
}, {
x: 768,
y: 800
}, {
x: 768,
y: 840
}, {
x: 768,
y: 880
}, {
x: 768,
y: 920
}, {
x: 768,
y: 960
}, {
x: 768,
y: 1000
}, {
x: 768,
y: 1040
}, {
x: 768,
y: 1080
}, {
x: 768,
y: 1120
}, {
x: 768,
y: 1160
}, {
x: 768,
y: 1200
}, {
x: 768,
y: 1240
}, {
x: 768,
y: 1280
}, {
x: 768,
y: 1320
}, {
x: 768,
y: 1360
}, {
x: 768,
y: 1400
}, {
x: 768,
y: 1440
}, {
x: 768,
y: 1480
}, {
x: 768,
y: 1520
}, {
x: 768,
y: 1560
}, {
x: 768,
y: 1600
}, {
x: 768,
y: 1640
}, {
x: 768,
y: 1680
}, {
x: 768,
y: 1720
}, {
x: 768,
y: 1760
}, {
x: 768,
y: 1800
}, {
x: 793.6,
y: 1800
}, {
x: 819.2,
y: 1800
}, {
x: 844.8,
y: 1800
}, {
x: 870.4,
y: 1800
}, {
x: 896,
y: 1800
}, {
x: 921.6,
y: 1800
}, {
x: 947.2,
y: 1800
}, {
x: 972.8,
y: 1800
}, {
x: 998.4,
y: 1800
}, {
x: 1024,
y: 1800
}, {
x: 1024,
y: 1840
}, {
x: 1024,
y: 1880
}, {
x: 1024,
y: 1920
}, {
x: 1024,
y: 1960
}, {
x: 1024,
y: 2000
}, {
x: 1024,
y: 2040
}, {
x: 1024,
y: 2080
}, {
x: 1024,
y: 2120
}, {
x: 1024,
y: 2160
}, {
x: 1024,
y: 2200
}];
// Enemy path 2: Detailed eastern route path
var enemyPath2 = [{
x: 2048,
y: 800
}, {
x: 2022.4,
y: 800
}, {
x: 1996.8,
y: 800
}, {
x: 1971.2,
y: 800
}, {
x: 1945.6,
y: 800
}, {
x: 1920,
y: 800
}, {
x: 1894.4,
y: 800
}, {
x: 1868.8,
y: 800
}, {
x: 1843.2,
y: 800
}, {
x: 1817.6,
y: 800
}, {
x: 1792,
y: 800
}, {
x: 1766.4,
y: 800
}, {
x: 1740.8,
y: 800
}, {
x: 1715.2,
y: 800
}, {
x: 1689.6,
y: 800
}, {
x: 1664,
y: 800
}, {
x: 1638.4,
y: 800
}, {
x: 1612.8,
y: 800
}, {
x: 1587.2,
y: 800
}, {
x: 1561.6,
y: 800
}, {
x: 1536,
y: 800
}, {
x: 1510.4,
y: 800
}, {
x: 1484.8,
y: 800
}, {
x: 1459.2,
y: 800
}, {
x: 1433.6,
y: 800
}, {
x: 1408,
y: 800
}, {
x: 1382.4,
y: 800
}, {
x: 1356.8,
y: 800
}, {
x: 1331.2,
y: 800
}, {
x: 1305.6,
y: 800
}, {
x: 1280,
y: 800
}, {
x: 1254.4,
y: 800
}, {
x: 1228.8,
y: 800
}, {
x: 1203.2,
y: 800
}, {
x: 1177.6,
y: 800
}, {
x: 1152,
y: 800
}, {
x: 1126.4,
y: 800
}, {
x: 1100.8,
y: 800
}, {
x: 1075.2,
y: 800
}, {
x: 1049.6,
y: 800
}, {
x: 1024,
y: 800
}, {
x: 1024,
y: 780
}, {
x: 1024,
y: 760
}, {
x: 1024,
y: 740
}, {
x: 1024,
y: 720
}, {
x: 1024,
y: 700
}, {
x: 1024,
y: 680
}, {
x: 1024,
y: 660
}, {
x: 1024,
y: 640
}, {
x: 1024,
y: 620
}, {
x: 1024,
y: 600
}, {
x: 1049.6,
y: 600
}, {
x: 1075.2,
y: 600
}, {
x: 1100.8,
y: 600
}, {
x: 1126.4,
y: 600
}, {
x: 1152,
y: 600
}, {
x: 1177.6,
y: 600
}, {
x: 1203.2,
y: 600
}, {
x: 1228.8,
y: 600
}, {
x: 1254.4,
y: 600
}, {
x: 1280,
y: 600
}, {
x: 1280,
y: 640
}, {
x: 1280,
y: 680
}, {
x: 1280,
y: 720
}, {
x: 1280,
y: 760
}, {
x: 1280,
y: 800
}, {
x: 1280,
y: 840
}, {
x: 1280,
y: 880
}, {
x: 1280,
y: 920
}, {
x: 1280,
y: 960
}, {
x: 1280,
y: 1000
}, {
x: 1280,
y: 1040
}, {
x: 1280,
y: 1080
}, {
x: 1280,
y: 1120
}, {
x: 1280,
y: 1160
}, {
x: 1280,
y: 1200
}, {
x: 1280,
y: 1240
}, {
x: 1280,
y: 1280
}, {
x: 1280,
y: 1320
}, {
x: 1280,
y: 1360
}, {
x: 1280,
y: 1400
}, {
x: 1280,
y: 1440
}, {
x: 1280,
y: 1480
}, {
x: 1280,
y: 1520
}, {
x: 1280,
y: 1560
}, {
x: 1280,
y: 1600
}, {
x: 1280,
y: 1640
}, {
x: 1280,
y: 1680
}, {
x: 1280,
y: 1720
}, {
x: 1280,
y: 1760
}, {
x: 1280,
y: 1800
}, {
x: 1254.4,
y: 1800
}, {
x: 1228.8,
y: 1800
}, {
x: 1203.2,
y: 1800
}, {
x: 1177.6,
y: 1800
}, {
x: 1152,
y: 1800
}, {
x: 1126.4,
y: 1800
}, {
x: 1100.8,
y: 1800
}, {
x: 1075.2,
y: 1800
}, {
x: 1049.6,
y: 1800
}, {
x: 1024,
y: 1800
}, {
x: 1024,
y: 1840
}, {
x: 1024,
y: 1880
}, {
x: 1024,
y: 1920
}, {
x: 1024,
y: 1960
}, {
x: 1024,
y: 2000
}, {
x: 1024,
y: 2040
}, {
x: 1024,
y: 2080
}, {
x: 1024,
y: 2120
}, {
x: 1024,
y: 2160
}, {
x: 1024,
y: 2200
}];
// Enemy path 3: Detailed western route path
var enemyPath3 = [{
x: 2048,
y: 50
}, {
x: 2022.4,
y: 50
}, {
x: 1996.8,
y: 50
}, {
x: 1971.2,
y: 50
}, {
x: 1945.6,
y: 50
}, {
x: 1920,
y: 50
}, {
x: 1894.4,
y: 50
}, {
x: 1868.8,
y: 50
}, {
x: 1843.2,
y: 50
}, {
x: 1817.6,
y: 50
}, {
x: 1792,
y: 50
}, {
x: 1766.4,
y: 50
}, {
x: 1740.8,
y: 50
}, {
x: 1715.2,
y: 50
}, {
x: 1689.6,
y: 50
}, {
x: 1664,
y: 50
}, {
x: 1638.4,
y: 50
}, {
x: 1612.8,
y: 50
}, {
x: 1587.2,
y: 50
}, {
x: 1561.6,
y: 50
}, {
x: 1536,
y: 50
}, {
x: 1510.4,
y: 50
}, {
x: 1484.8,
y: 50
}, {
x: 1459.2,
y: 50
}, {
x: 1433.6,
y: 50
}, {
x: 1408,
y: 50
}, {
x: 1382.4,
y: 50
}, {
x: 1356.8,
y: 50
}, {
x: 1331.2,
y: 50
}, {
x: 1305.6,
y: 50
}, {
x: 1280,
y: 50
}, {
x: 1280,
y: 90
}, {
x: 1280,
y: 130
}, {
x: 1280,
y: 170
}, {
x: 1280,
y: 210
}, {
x: 1280,
y: 250
}, {
x: 1280,
y: 290
}, {
x: 1280,
y: 330
}, {
x: 1280,
y: 370
}, {
x: 1280,
y: 410
}, {
x: 1280,
y: 450
}, {
x: 1280,
y: 490
}, {
x: 1280,
y: 530
}, {
x: 1280,
y: 570
}, {
x: 1280,
y: 610
}, {
x: 1280,
y: 650
}, {
x: 1280,
y: 690
}, {
x: 1280,
y: 730
}, {
x: 1280,
y: 770
}, {
x: 1280,
y: 810
}, {
x: 1280,
y: 850
}, {
x: 1280,
y: 890
}, {
x: 1280,
y: 930
}, {
x: 1280,
y: 970
}, {
x: 1280,
y: 1010
}, {
x: 1280,
y: 1050
}, {
x: 1280,
y: 1090
}, {
x: 1280,
y: 1130
}, {
x: 1280,
y: 1170
}, {
x: 1280,
y: 1210
}, {
x: 1280,
y: 1250
}, {
x: 1280,
y: 1290
}, {
x: 1280,
y: 1330
}, {
x: 1280,
y: 1370
}, {
x: 1280,
y: 1410
}, {
x: 1280,
y: 1450
}, {
x: 1280,
y: 1490
}, {
x: 1280,
y: 1530
}, {
x: 1280,
y: 1570
}, {
x: 1280,
y: 1610
}, {
x: 1280,
y: 1650
}, {
x: 1280,
y: 1690
}, {
x: 1280,
y: 1730
}, {
x: 1280,
y: 1770
}, {
x: 1280,
y: 1800
}, {
x: 1254.4,
y: 1800
}, {
x: 1228.8,
y: 1800
}, {
x: 1203.2,
y: 1800
}, {
x: 1177.6,
y: 1800
}, {
x: 1152,
y: 1800
}, {
x: 1126.4,
y: 1800
}, {
x: 1100.8,
y: 1800
}, {
x: 1075.2,
y: 1800
}, {
x: 1049.6,
y: 1800
}, {
x: 1024,
y: 1800
}, {
x: 1024,
y: 1840
}, {
x: 1024,
y: 1880
}, {
x: 1024,
y: 1920
}, {
x: 1024,
y: 1960
}, {
x: 1024,
y: 2000
}, {
x: 1024,
y: 2040
}, {
x: 1024,
y: 2080
}, {
x: 1024,
y: 2120
}, {
x: 1024,
y: 2160
}, {
x: 1024,
y: 2200
}];
var buildZones = [];
var terrainFeatures = [];
function createPath() {
// Create all paths visible from the start for strategic planning
// Create path 1
for (var i = 0; i < enemyPath1.length; i++) {
var pathTile = isoContainer.addChild(LK.getAsset('path_tile', {
anchorX: 0.5,
anchorY: 0.5,
x: enemyPath1[i].x,
y: enemyPath1[i].y,
alpha: 0.7
}));
}
// Create path 2 - always visible
for (var i = 0; i < enemyPath2.length; i++) {
var pathTile = isoContainer.addChild(LK.getAsset('path_tile', {
anchorX: 0.5,
anchorY: 0.5,
x: enemyPath2[i].x,
y: enemyPath2[i].y,
alpha: 0.7
}));
}
// Create path 3 - always visible
for (var i = 0; i < enemyPath3.length; i++) {
var pathTile = isoContainer.addChild(LK.getAsset('path_tile', {
anchorX: 0.5,
anchorY: 0.5,
x: enemyPath3[i].x,
y: enemyPath3[i].y,
alpha: 0.7
}));
}
// Create build zones around paths
createBuildZones();
}
function createTerrain() {
// Create lake
var lake = isoContainer.addChild(LK.getAsset('lake', {
anchorX: 0.5,
anchorY: 0.5,
x: 400,
y: 500,
alpha: 0.8
}));
// Create waterfall (three parallel lines)
var waterfall1 = isoContainer.addChild(LK.getAsset('waterfall_line', {
anchorX: 0.5,
anchorY: 0.5,
x: 1550,
y: 300
}));
var waterfall2 = isoContainer.addChild(LK.getAsset('waterfall_line', {
anchorX: 0.5,
anchorY: 0.5,
x: 1560,
y: 300
}));
var waterfall3 = isoContainer.addChild(LK.getAsset('waterfall_line', {
anchorX: 0.5,
anchorY: 0.5,
x: 1570,
y: 300
}));
// Create bottom right waterfall (400 pixels above bottom right corner)
var bottomRightWaterfall = isoContainer.addChild(LK.getAsset('waterfall_line', {
anchorX: 0.5,
anchorY: 0.5,
x: 1748,
y: 1932
}));
// Create lake in the middle of the map
var centralLake = isoContainer.addChild(LK.getAsset('lake', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
}));
// Create left village (further west of castle)
var leftVillagePositions = [{
x: 450,
y: 2050
}, {
x: 700,
y: 2300
}, {
x: 300,
y: 2350
}, {
x: 650,
y: 2100
}];
for (var i = 0; i < leftVillagePositions.length; i++) {
var pos = leftVillagePositions[i];
// Create house base
var house = isoContainer.addChild(LK.getAsset('village_house', {
anchorX: 0.5,
anchorY: 0.5,
x: pos.x,
y: pos.y,
alpha: 0.9
}));
// Roof removed
}
// Create right village (further east of castle)
var rightVillagePositions = [{
x: 1600,
y: 2050
}, {
x: 1350,
y: 2300
}, {
x: 1750,
y: 2250
}, {
x: 1500,
y: 2450
}];
for (var i = 0; i < rightVillagePositions.length; i++) {
var pos = rightVillagePositions[i];
// Create house base
var house = isoContainer.addChild(LK.getAsset('village_house', {
anchorX: 0.5,
anchorY: 0.5,
x: pos.x,
y: pos.y,
alpha: 0.9
}));
// Roof removed
}
// Create 50 trees scattered across the map avoiding all objects
var treePositions = [];
// Helper function to check if a position conflicts with existing objects
function isPositionSafe(x, y, buffer) {
buffer = buffer || 200;
// Check boundaries
if (x < 50 || x > 1998 || y < 50 || y > 2682) {
return false;
}
// Check castle area
if (Math.sqrt((x - 1024) * (x - 1024) + (y - 2200) * (y - 2200)) < 300 + buffer) {
return false;
}
// Check waterfall area
if (x >= 1500 - buffer && x <= 1600 + buffer) {
return false;
}
// Check central lake area
if (Math.sqrt((x - 1024) * (x - 1024) + (y - 1366) * (y - 1366)) < 200 + buffer) {
return false;
}
// Check original lake area
if (Math.sqrt((x - 400) * (x - 400) + (y - 500) * (y - 500)) < 150 + buffer) {
return false;
}
// Check all enemy paths with buffer
var allPaths = [enemyPath1, enemyPath2, enemyPath3];
for (var p = 0; p < allPaths.length; p++) {
var path = allPaths[p];
for (var i = 0; i < path.length; i++) {
if (Math.sqrt((x - path[i].x) * (x - path[i].x) + (y - path[i].y) * (y - path[i].y)) < buffer + 20) {
return false;
}
}
}
// Check build zones - now requiring 200 pixels distance
for (var i = 0; i < buildZones.length; i++) {
if (Math.sqrt((x - buildZones[i].x) * (x - buildZones[i].x) + (y - buildZones[i].y) * (y - buildZones[i].y)) < 200) {
return false;
}
}
// Check village house areas
var allVillagePositions = leftVillagePositions.concat(rightVillagePositions);
for (var i = 0; i < allVillagePositions.length; i++) {
var house = allVillagePositions[i];
if (Math.sqrt((x - house.x) * (x - house.x) + (y - house.y) * (y - house.y)) < buffer) {
return false;
}
}
// Check against other trees
for (var i = 0; i < treePositions.length; i++) {
var tree = treePositions[i];
if (Math.sqrt((x - tree.x) * (x - tree.x) + (y - tree.y) * (y - tree.y)) < buffer) {
return false;
}
}
// Check against rocks
for (var i = 0; i < rockPositions.length; i++) {
var rock = rockPositions[i];
if (Math.sqrt((x - rock.x) * (x - rock.x) + (y - rock.y) * (y - rock.y)) < buffer) {
return false;
}
}
return true;
}
// Define fixed tree positions far from roads and tower installation points
var fixedTreePositions = [{
x: 1700,
y: 200
}, {
x: 1800,
y: 250
}, {
x: 1650,
y: 350
}, {
x: 1750,
y: 450
}, {
x: 1900,
y: 300
}, {
x: 1950,
y: 200
}, {
x: 1600,
y: 150
}, {
x: 1850,
y: 100
}, {
x: 1950,
y: 150
}, {
x: 150,
y: 300
}, {
x: 250,
y: 350
}, {
x: 350,
y: 400
}, {
x: 200,
y: 450
}, {
x: 100,
y: 500
}, {
x: 300,
y: 300
}, {
x: 400,
y: 350
}, {
x: 500,
y: 400
}, {
x: 450,
y: 300
}, {
x: 550,
y: 350
}, {
x: 100,
y: 1000
}, {
x: 200,
y: 1100
}, {
x: 300,
y: 1200
}, {
x: 150,
y: 1300
}, {
x: 250,
y: 1400
}, {
x: 100,
y: 1500
}, {
x: 350,
y: 1000
}, {
x: 400,
y: 1100
}, {
x: 450,
y: 1200
}, {
x: 500,
y: 1300
}, {
x: 550,
y: 1400
}, {
x: 600,
y: 1500
}, {
x: 1700,
y: 1000
}, {
x: 1800,
y: 1100
}, {
x: 1900,
y: 1200
}, {
x: 1950,
y: 1300
}, {
x: 1850,
y: 1400
}, {
x: 1750,
y: 1500
}, {
x: 1650,
y: 1000
}, {
x: 1600,
y: 1100
}, {
x: 1550,
y: 1200
}, {
x: 1500,
y: 1300
}, {
x: 1450,
y: 1400
}, {
x: 1400,
y: 1500
}, {
x: 100,
y: 2100
}, {
x: 200,
y: 2000
}, {
x: 300,
y: 1950
}, {
x: 400,
y: 2000
}, {
x: 500,
y: 2100
}, {
x: 1600,
y: 2100
}, {
x: 1700,
y: 2000
}];
// Define rock positions in empty spaces at least 200 pixels away from any object
var rockPositions = [{
x: 200,
y: 800
}, {
x: 1700,
y: 350
}, {
x: 250,
y: 1700
}, {
x: 1800,
y: 1500
}, {
x: 700,
y: 350
}, {
x: 1600,
y: 100
}, {
x: 1900,
y: 950
}, {
x: 150,
y: 1200
}, {
x: 1700,
y: 1200
}, {
x: 650,
y: 1750
}];
// Use the first 50 fixed positions (or however many we have)
for (var i = 0; i < Math.min(50, fixedTreePositions.length); i++) {
treePositions.push(fixedTreePositions[i]);
}
// Create the trees
for (var i = 0; i < treePositions.length; i++) {
var pos = treePositions[i];
var tree = isoContainer.addChild(LK.getAsset('tree', {
anchorX: 0.5,
anchorY: 0.5,
x: pos.x,
y: pos.y,
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.9
}));
}
// Create rocks at predefined positions
for (var i = 0; i < rockPositions.length; i++) {
var pos = rockPositions[i];
var rock = isoContainer.addChild(LK.getAsset('ruin', {
anchorX: 0.5,
anchorY: 0.5,
x: pos.x,
y: pos.y,
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}));
terrainFeatures.push(rock);
}
}
function createBuildZones() {
var zones = [
// enemyPath1: y=200, both sides
{
x: 900,
y: 150
}, {
x: 900,
y: 250
},
// enemyPath1: Corner (1024, 200, right turn)
{
x: 974,
y: 150
}, {
x: 1074,
y: 150
}, {
x: 974,
y: 250
}, {
x: 1074,
y: 250
},
// enemyPath1: U-shape (x=1024, y=200-600)
{
x: 974,
y: 500
}, {
x: 1074,
y: 500
},
// enemyPath1: Corner (768, 1800, right turn)
{
x: 718,
y: 1850
}, {
x: 818,
y: 1850
}, {
x: 718,
y: 1344
}, {
x: 818,
y: 1344
}, {
x: 718,
y: 1244
}, {
x: 818,
y: 1244
}, {
x: 718,
y: 1444
}, {
x: 818,
y: 1444
},
// enemyPath1: Corner (1024, 1800, down, intersection)
{
x: 974,
y: 1750
}, {
x: 1074,
y: 1750
}, {
x: 974,
y: 1850
}, {
x: 1074,
y: 1850
},
// enemyPath2: y=800, both sides
{
x: 1448,
y: 750
}, {
x: 1448,
y: 850
}, {
x: 1148,
y: 750
}, {
x: 1148,
y: 850
},
// enemyPath2: Corner (1024, 800, down)
{
x: 974,
y: 750
}, {
x: 1074,
y: 750
}, {
x: 974,
y: 850
}, {
x: 1074,
y: 850
},
// enemyPath2: Corner (1024, 600, right turn, intersection)
{
x: 974,
y: 550
}, {
x: 1074,
y: 550
}, {
x: 974,
y: 650
}, {
x: 1074,
y: 650
},
// enemyPath2: U-shape (x=1280, y=600-1800)
{
x: 1230,
y: 650
}, {
x: 1330,
y: 650
}, {
x: 1230,
y: 750
}, {
x: 1330,
y: 750
}, {
x: 1230,
y: 850
}, {
x: 1330,
y: 850
},
// enemyPath3: Corner (1280, 50, down)
{
x: 1230,
y: 0
}, {
x: 1330,
y: 0
}, {
x: 1230,
y: 100
}, {
x: 1330,
y: 100
},
// enemyPath3: Corner (1280, 1800, left turn)
{
x: 1230,
y: 1850
}, {
x: 1330,
y: 1850
},
// Additional tower placement spots near paths
{
x: 1230,
y: 1344
}, {
x: 1330,
y: 1344
}, {
x: 1230,
y: 1244
}, {
x: 1330,
y: 1244
}, {
x: 1230,
y: 1444
}, {
x: 1330,
y: 1444
}];
for (var i = 0; i < zones.length; i++) {
var zone = isoContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: zones[i].x,
y: zones[i].y,
alpha: 0.3
}));
zone.zoneData = {
x: zones[i].x,
y: zones[i].y,
occupied: false,
originalAlpha: 0.3
};
buildZones.push(zone);
}
}
function updateBuildZoneVisuals() {
for (var i = 0; i < buildZones.length; i++) {
var zone = buildZones[i];
if (zone.zoneData.occupied) {
// Occupied zones are invisible
zone.alpha = 0;
} else if (selectedTowerType) {
// Available zones are highlighted when a tower is selected
zone.alpha = 0.6;
zone.tint = 0x00FF00; // Green highlight for available zones
} else {
// Normal state when no tower is selected
zone.alpha = zone.zoneData.originalAlpha;
zone.tint = 0xFFFFFF; // Reset tint
}
}
}
var castleHealthBarBg = null;
var castleHealthBarFill = null;
function createCastle() {
var castle = isoContainer.addChild(LK.getAsset('castle_' + castleType, {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 2200
}));
// Create health bar background positioned under the castle (moved down)
castleHealthBarBg = isoContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 2350,
scaleX: 4,
scaleY: 0.4
}));
castleHealthBarBg.tint = 0x333333;
// Create health bar fill positioned under the castle (moved down)
castleHealthBarFill = isoContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 2350,
scaleX: 4,
scaleY: 0.4
}));
castleHealthBarFill.tint = 0x00FF00;
// Store references for updating
castle.healthBarBg = castleHealthBarBg;
castle.healthBarFill = castleHealthBarFill;
}
function createMainMenu() {
mainMenuContainer = new Container();
isoContainer.addChild(mainMenuContainer);
// Main menu background - war scene with towers shooting arrows at goblins
var menuBg = mainMenuContainer.addChild(LK.getAsset('war_scene_background', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
// Title
var titleText = mainMenuContainer.addChild(new Text2('Goblin Invasion', {
size: 120,
fill: 0xFFD700
}));
titleText.anchor.set(0.5, 0.5);
titleText.x = 1024;
titleText.y = 800;
// New Game Button
var newGameBtn = mainMenuContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1200,
scaleX: 8,
scaleY: 2
}));
newGameBtn.tint = 0x00AA00;
var newGameText = mainMenuContainer.addChild(new Text2('New Game', {
size: 60,
fill: 0xFFFFFF
}));
newGameText.anchor.set(0.5, 0.5);
newGameText.x = 1024;
newGameText.y = 1200;
// How to Play Button
var howToPlayBtn = mainMenuContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1400,
scaleX: 8,
scaleY: 2
}));
howToPlayBtn.tint = 0x0066CC;
var howToPlayText = mainMenuContainer.addChild(new Text2('How to Play', {
size: 60,
fill: 0xFFFFFF
}));
howToPlayText.anchor.set(0.5, 0.5);
howToPlayText.x = 1024;
howToPlayText.y = 1400;
// Credits Button
var creditsBtn = mainMenuContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1600,
scaleX: 8,
scaleY: 2
}));
creditsBtn.tint = 0xAA6600;
var creditsText = mainMenuContainer.addChild(new Text2('Credits', {
size: 60,
fill: 0xFFFFFF
}));
creditsText.anchor.set(0.5, 0.5);
creditsText.x = 1024;
creditsText.y = 1600;
// Button interactions
newGameBtn.down = function () {
startGame();
};
howToPlayBtn.down = function () {
showHowToPlay();
};
creditsBtn.down = function () {
showCredits();
};
}
function showStoryIntro() {
if (mainMenuContainer) {
mainMenuContainer.alpha = 0.2;
}
storyIntroContainer = new Container();
isoContainer.addChild(storyIntroContainer);
// Background
var bg = storyIntroContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
scaleX: 45,
scaleY: 50
}));
bg.tint = 0x000000;
bg.alpha = 0.95;
// Story text
var storyLines = ["The land is on the brink of ruin.", "", "Shadows rise from the east, and only your towers", "stand in their way.", "", "You must defend the kingdom wave after wave,", "until the last breath of hope remains..."];
for (var i = 0; i < storyLines.length; i++) {
var storyText = storyIntroContainer.addChild(new Text2(storyLines[i], {
size: 50,
fill: 0xFFFFFF
}));
storyText.anchor.set(0.5, 0.5);
storyText.x = 1024;
storyText.y = 1000 + i * 80;
}
// Skip instruction
var skipText = storyIntroContainer.addChild(new Text2('Click anywhere to begin...', {
size: 40,
fill: 0xFFD700
}));
skipText.anchor.set(0.5, 0.5);
skipText.x = 1024;
skipText.y = 1700;
// Auto-skip after 6 seconds or manual skip
var skipTimer = LK.setTimeout(function () {
startGameAfterIntro();
}, 6000);
// Manual skip
bg.down = function () {
LK.clearTimeout(skipTimer);
startGameAfterIntro();
};
}
function startGameAfterIntro() {
if (storyIntroContainer) {
storyIntroContainer.destroy();
storyIntroContainer = null;
}
if (mainMenuContainer) {
mainMenuContainer.destroy();
mainMenuContainer = null;
}
gameState = 'playing';
// Play background music when game starts
if (!audioMuted) {
LK.playMusic('main');
}
createSettingsIcon();
createSpeedControls(); // Create speed control buttons
createPath();
createTerrain();
createCastle();
createShop();
createWave();
updateShopVisuals();
// Show UI elements with fade-in animation
if (castleLivesTextBg) {
castleLivesTextBg.visible = true;
castleLivesTextBg.alpha = 0;
tween(castleLivesTextBg, {
alpha: 0.8
}, {
duration: 800,
easing: tween.easeOut
});
}
if (castleLivesText) {
castleLivesText.visible = true;
castleLivesText.alpha = 0;
tween(castleLivesText, {
alpha: 1
}, {
duration: 800,
easing: tween.easeOut
});
}
if (coinsTextBg) {
coinsTextBg.visible = true;
coinsTextBg.alpha = 0;
tween(coinsTextBg, {
alpha: 0.8
}, {
duration: 800,
easing: tween.easeOut
});
}
if (coinsText) {
coinsText.visible = true;
coinsText.alpha = 0;
tween(coinsText, {
alpha: 1
}, {
duration: 800,
easing: tween.easeOut
});
}
if (waveText) {
waveText.visible = true;
waveText.alpha = 0;
tween(waveText, {
alpha: 1
}, {
duration: 800,
easing: tween.easeOut
});
}
}
function startGame() {
showStoryIntro();
}
function showHowToPlay() {
if (mainMenuContainer) {
mainMenuContainer.alpha = 0.3;
}
howToPlayContainer = new Container();
isoContainer.addChild(howToPlayContainer);
// Background
var bg = howToPlayContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
scaleX: 35,
scaleY: 45
}));
bg.tint = 0x000000;
bg.alpha = 0.9;
// Title
var title = howToPlayContainer.addChild(new Text2('How to Play', {
size: 80,
fill: 0xFFD700
}));
title.anchor.set(0.5, 0.5);
title.x = 1024;
title.y = 600;
// Instructions
var instructions = ['Build towers to defend your castle from waves of enemies', 'Click tower icons at bottom to select, then click build zones', 'Upgrade towers by clicking them when you have enough gold', 'Different towers have different abilities:', '• Infantry: Basic damage, good range', '• Freezer: Slows enemies down', '• Air: Targets flying enemies effectively', '• Poison: Damages over time', '• Tesla: Chain lightning damage', 'Survive 100 waves to win!'];
for (var i = 0; i < instructions.length; i++) {
var instructText = howToPlayContainer.addChild(new Text2(instructions[i], {
size: 35,
fill: 0xFFFFFF
}));
instructText.anchor.set(0.5, 0.5);
instructText.x = 1024;
instructText.y = 800 + i * 60;
}
// Back button
var backBtn = howToPlayContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 2000,
scaleX: 6,
scaleY: 2
}));
backBtn.tint = 0xAA0000;
var backText = howToPlayContainer.addChild(new Text2('Back', {
size: 50,
fill: 0xFFFFFF
}));
backText.anchor.set(0.5, 0.5);
backText.x = 1024;
backText.y = 2000;
backBtn.down = function () {
howToPlayContainer.destroy();
howToPlayContainer = null;
if (mainMenuContainer) {
mainMenuContainer.alpha = 1.0;
}
};
}
function showCredits() {
if (mainMenuContainer) {
mainMenuContainer.alpha = 0.3;
}
creditsContainer = new Container();
isoContainer.addChild(creditsContainer);
// Background
var bg = creditsContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
scaleX: 25,
scaleY: 30
}));
bg.tint = 0x000000;
bg.alpha = 0.9;
// Title
var title = creditsContainer.addChild(new Text2('Credits', {
size: 80,
fill: 0xFFD700
}));
title.anchor.set(0.5, 0.5);
title.x = 1024;
title.y = 1000;
// Credits info
var gameDesignText = creditsContainer.addChild(new Text2('Game Design: ilker.mez', {
size: 50,
fill: 0xFFFFFF
}));
gameDesignText.anchor.set(0.5, 0.5);
gameDesignText.x = 1024;
gameDesignText.y = 1300;
// Back button
var backBtn = creditsContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1600,
scaleX: 6,
scaleY: 2
}));
backBtn.tint = 0xAA0000;
var backText = creditsContainer.addChild(new Text2('Back', {
size: 50,
fill: 0xFFFFFF
}));
backText.anchor.set(0.5, 0.5);
backText.x = 1024;
backText.y = 1600;
backBtn.down = function () {
creditsContainer.destroy();
creditsContainer = null;
if (mainMenuContainer) {
mainMenuContainer.alpha = 1.0;
}
};
}
function createSettingsIcon() {
settingsIcon = LK.gui.topLeft.addChild(LK.getAsset('build_zone', {
anchorX: 0,
anchorY: 0,
x: 20,
y: 400,
scaleX: 2,
scaleY: 2
}));
settingsIcon.tint = 0x666666;
var settingsText = LK.gui.topLeft.addChild(new Text2('⚙', {
size: 60,
fill: 0xFFFFFF
}));
settingsText.anchor.set(0, 0);
settingsText.x = 20;
settingsText.y = 400;
settingsIcon.down = function () {
showSettings();
};
}
function createSpeedControls() {
// Clear existing speed buttons
for (var i = 0; i < speedButtons.length; i++) {
if (speedButtons[i] && speedButtons[i].destroy) {
speedButtons[i].destroy();
}
}
for (var i = 0; i < speedButtonTexts.length; i++) {
if (speedButtonTexts[i] && speedButtonTexts[i].destroy) {
speedButtonTexts[i].destroy();
}
}
// Clear pause button
if (pauseButton && pauseButton.destroy) {
pauseButton.destroy();
}
if (pauseButtonText && pauseButtonText.destroy) {
pauseButtonText.destroy();
}
speedButtons = [];
speedButtonTexts = [];
// Create pause button
pauseButton = LK.gui.topLeft.addChild(LK.getAsset('build_zone', {
anchorX: 0,
anchorY: 0,
x: 220,
y: 0,
scaleX: 2,
scaleY: 1.5
}));
pauseButton.tint = isPaused ? 0xFF4444 : 0x666666;
pauseButton.alpha = isPaused ? 1.0 : 0.7;
pauseButtonText = LK.gui.topLeft.addChild(new Text2('⏸', {
size: 40,
fill: isPaused ? 0xFF4444 : 0xFFFFFF
}));
pauseButtonText.anchor.set(0, 0);
pauseButtonText.x = 230;
pauseButtonText.y = 10;
pauseButton.down = function () {
togglePause();
};
// Create speed control buttons (X1, X2, X3)
var speeds = [1, 2, 3];
var speedLabels = ['X1', 'X2', 'X3'];
for (var i = 0; i < speeds.length; i++) {
var speedBtn = LK.gui.topLeft.addChild(LK.getAsset('build_zone', {
anchorX: 0,
anchorY: 0,
x: 320 + i * 80,
y: 0,
scaleX: 2,
scaleY: 1.5
}));
speedBtn.tint = gameSpeed === speeds[i] && !isPaused ? 0x00FF00 : 0x666666;
speedBtn.alpha = gameSpeed === speeds[i] && !isPaused ? 1.0 : 0.7;
speedButtons.push(speedBtn);
var speedText = LK.gui.topLeft.addChild(new Text2(speedLabels[i], {
size: 40,
fill: gameSpeed === speeds[i] && !isPaused ? 0x00FF00 : 0xFFFFFF
}));
speedText.anchor.set(0, 0);
speedText.x = 330 + i * 80;
speedText.y = 10;
speedButtonTexts.push(speedText);
// Create closure to capture the speed value
(function (speed) {
speedBtn.down = function () {
setGameSpeed(speed);
};
})(speeds[i]);
}
}
function setGameSpeed(newSpeed) {
if (isPaused) {
// If paused, store the selected speed but don't apply it yet
lastGameSpeed = newSpeed;
} else {
gameSpeed = newSpeed;
}
// Update button visuals
for (var i = 0; i < speedButtons.length; i++) {
var isActive = (isPaused ? lastGameSpeed : gameSpeed) === i + 1;
speedButtons[i].tint = isActive && !isPaused ? 0x00FF00 : 0x666666;
speedButtons[i].alpha = isActive && !isPaused ? 1.0 : 0.7;
speedButtonTexts[i].fill = isActive && !isPaused ? 0x00FF00 : 0xFFFFFF;
speedButtonTexts[i].tint = isActive && !isPaused ? 0x00FF00 : 0xFFFFFF;
}
}
function togglePause() {
isPaused = !isPaused;
if (isPaused) {
// Store current speed and pause the game
lastGameSpeed = gameSpeed;
gameSpeed = 0;
} else {
// Resume with the last selected speed
gameSpeed = lastGameSpeed;
}
// Update pause button visuals
if (pauseButton) {
pauseButton.tint = isPaused ? 0xFF4444 : 0x666666;
pauseButton.alpha = isPaused ? 1.0 : 0.7;
}
if (pauseButtonText) {
pauseButtonText.fill = isPaused ? 0xFF4444 : 0xFFFFFF;
pauseButtonText.tint = isPaused ? 0xFF4444 : 0xFFFFFF;
}
// Update speed button visuals to reflect pause state
for (var i = 0; i < speedButtons.length; i++) {
var isActive = lastGameSpeed === i + 1;
speedButtons[i].tint = isActive && !isPaused ? 0x00FF00 : 0x666666;
speedButtons[i].alpha = isActive && !isPaused ? 1.0 : 0.7;
speedButtonTexts[i].fill = isActive && !isPaused ? 0x00FF00 : 0xFFFFFF;
speedButtonTexts[i].tint = isActive && !isPaused ? 0x00FF00 : 0xFFFFFF;
}
}
function showSettings() {
if (settingsContainer) {
return;
}
settingsContainer = new Container();
isoContainer.addChild(settingsContainer);
// Background
var bg = settingsContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
scaleX: 20,
scaleY: 25
}));
bg.tint = 0x000000;
bg.alpha = 0.9;
// Title
var title = settingsContainer.addChild(new Text2('Settings', {
size: 80,
fill: 0xFFD700
}));
title.anchor.set(0.5, 0.5);
title.x = 1024;
title.y = 1000;
// Return to Main Menu button
var mainMenuBtn = settingsContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1200,
scaleX: 10,
scaleY: 2
}));
mainMenuBtn.tint = 0x0066CC;
var mainMenuText = settingsContainer.addChild(new Text2('Return to Main Menu', {
size: 50,
fill: 0xFFFFFF
}));
mainMenuText.anchor.set(0.5, 0.5);
mainMenuText.x = 1024;
mainMenuText.y = 1200;
// Mute/Unmute button
var muteBtn = settingsContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1400,
scaleX: 8,
scaleY: 2
}));
muteBtn.tint = 0x00AA00;
var muteText = settingsContainer.addChild(new Text2('Mute Audio', {
size: 50,
fill: 0xFFFFFF
}));
muteText.anchor.set(0.5, 0.5);
muteText.x = 1024;
muteText.y = 1400;
// Close button
var closeBtn = settingsContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1600,
scaleX: 6,
scaleY: 2
}));
closeBtn.tint = 0xAA0000;
var closeText = settingsContainer.addChild(new Text2('Close', {
size: 50,
fill: 0xFFFFFF
}));
closeText.anchor.set(0.5, 0.5);
closeText.x = 1024;
closeText.y = 1600;
// Button interactions
mainMenuBtn.down = function () {
// Stop background music when returning to main menu
LK.stopMusic();
// Reset game state
gameState = 'mainMenu';
// Clear all game objects
for (var i = enemies.length - 1; i >= 0; i--) {
enemies[i].destroy();
}
enemies = [];
for (var i = towers.length - 1; i >= 0; i--) {
towers[i].destroy();
}
towers = [];
for (var i = projectiles.length - 1; i >= 0; i--) {
projectiles[i].destroy();
}
projectiles = [];
// Reset game variables
currentWave = 0;
coins = 160;
castleHealth = 100;
waveInProgress = false;
// Hide UI elements when returning to main menu
if (castleLivesTextBg) {
castleLivesTextBg.visible = false;
}
if (castleLivesText) {
castleLivesText.visible = false;
}
if (coinsTextBg) {
coinsTextBg.visible = false;
}
if (coinsText) {
coinsText.visible = false;
}
if (waveText) {
waveText.visible = false;
}
// Clear UI elements
if (settingsIcon) {
settingsIcon.destroy();
settingsIcon = null;
}
if (muteIndicator) {
muteIndicator.destroy();
muteIndicator = null;
}
// Clean up speed controls
for (var i = 0; i < speedButtons.length; i++) {
if (speedButtons[i] && speedButtons[i].destroy) {
speedButtons[i].destroy();
}
}
for (var i = 0; i < speedButtonTexts.length; i++) {
if (speedButtonTexts[i] && speedButtonTexts[i].destroy) {
speedButtonTexts[i].destroy();
}
}
// Clean up pause controls
if (pauseButton && pauseButton.destroy) {
pauseButton.destroy();
pauseButton = null;
}
if (pauseButtonText && pauseButtonText.destroy) {
pauseButtonText.destroy();
pauseButtonText = null;
}
speedButtons = [];
speedButtonTexts = [];
gameSpeed = 1; // Reset game speed
isPaused = false; // Reset pause state
lastGameSpeed = 1; // Reset last speed
// Close settings and show main menu
settingsContainer.destroy();
settingsContainer = null;
createMainMenu();
};
muteBtn.down = function () {
// Toggle audio mute state
audioMuted = !audioMuted;
// Update button text and color
if (audioMuted) {
muteText.setText('Unmute Audio');
muteBtn.tint = 0xAA0000;
// Stop music when muting
LK.stopMusic();
// Create mute indicator overlay on settings icon
if (settingsIcon && !muteIndicator) {
muteIndicator = LK.gui.topLeft.addChild(new Text2('✕', {
size: 40,
fill: 0xFF0000
}));
muteIndicator.anchor.set(0, 0);
muteIndicator.x = 30;
muteIndicator.y = 410;
// Animate the mute indicator
tween(muteIndicator, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.8
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
// Auto-hide the X indicator after 2 seconds
LK.setTimeout(function () {
if (muteIndicator && audioMuted) {
tween(muteIndicator, {
alpha: 0
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (muteIndicator && muteIndicator.destroy) {
muteIndicator.destroy();
muteIndicator = null;
}
}
});
}
}, 2000);
}
});
}
} else {
muteText.setText('Mute Audio');
muteBtn.tint = 0x00AA00;
// Resume music when unmuting (only if in game state)
if (gameState === 'playing') {
LK.playMusic('main');
}
// Remove mute indicator immediately when unmuting
if (muteIndicator && muteIndicator.destroy) {
muteIndicator.destroy();
muteIndicator = null;
}
}
};
closeBtn.down = function () {
settingsContainer.destroy();
settingsContainer = null;
};
}
function showUpgradeRestrictedMessage(level, requiredWave) {
// Create temporary message
var message = new Text2('Upgrade level ' + level + ' is locked until Wave ' + requiredWave + '.', {
size: 45,
fill: 0xFFAA00
});
message.anchor.set(0.5, 0.5);
message.x = 1024;
message.y = 1100;
isoContainer.addChild(message);
// Animate and remove message
tween(message, {
y: message.y - 50,
alpha: 0
}, {
duration: 2500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (message && message.destroy) {
message.destroy();
}
}
});
}
function showUnlockMessage(towerType, unlockWave) {
// Create temporary message
var message = new Text2('This tower will unlock at Wave ' + unlockWave + '.', {
size: 50,
fill: 0xFFFF00
});
message.anchor.set(0.5, 0.5);
message.x = 1024;
message.y = 1200;
isoContainer.addChild(message);
// Animate and remove message
tween(message, {
y: message.y - 50,
alpha: 0
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (message && message.destroy) {
message.destroy();
}
}
});
}
function updateShopVisuals() {
// Update tower button visuals based on unlock status
var towers = [{
btn: infantryBtn,
cost: infantryCost,
name: infantryName,
type: 'infantry',
price: 80
}, {
btn: frostBtn,
cost: frostCost,
name: frostName,
type: 'frost',
price: 120
}, {
btn: antiairBtn,
cost: antiairCost,
name: antiairName,
type: 'antiair',
price: 100
}, {
btn: poisonBtn,
cost: poisonCost,
name: poisonName,
type: 'poison',
price: 130
}, {
btn: teslaBtn,
cost: teslaCost,
name: teslaName,
type: 'tesla',
price: 150
}];
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var isUnlocked = currentWave >= towerUnlockWaves[tower.type];
var canAfford = coins >= tower.price;
if (!isUnlocked) {
// Greyed out for locked towers
tower.btn.tint = 0x666666;
tower.btn.alpha = 0.5;
tower.cost.tint = 0x666666;
tower.name.tint = 0x666666;
} else if (!canAfford) {
// Red tint for unaffordable towers
tower.btn.tint = 0xFFFFFF;
tower.btn.alpha = 1.0;
tower.cost.tint = 0xFF0000;
tower.name.tint = 0xFFFFFF;
} else {
// Normal appearance for available towers
tower.btn.tint = 0xFFFFFF;
tower.btn.alpha = 1.0;
tower.cost.tint = 0xFFD700;
tower.name.tint = 0xFFFFFF;
}
}
}
function createShop() {
var shopBg = LK.gui.bottom.addChild(LK.getAsset('shop_bg', {
anchorX: 0.5,
anchorY: 1,
alpha: 1.0
}));
shopBg.tint = 0x000000;
infantryBtn = LK.gui.bottom.addChild(LK.getAsset('tower_infantry', {
anchorX: 0.5,
anchorY: 1,
x: -400,
y: -60
}));
frostBtn = LK.gui.bottom.addChild(LK.getAsset('tower_frost', {
anchorX: 0.5,
anchorY: 1,
x: -200,
y: -60
}));
antiairBtn = LK.gui.bottom.addChild(LK.getAsset('tower_antiair', {
anchorX: 0.5,
anchorY: 1,
x: 0,
y: -60
}));
poisonBtn = LK.gui.bottom.addChild(LK.getAsset('tower_poison', {
anchorX: 0.5,
anchorY: 1,
x: 200,
y: -60
}));
teslaBtn = LK.gui.bottom.addChild(LK.getAsset('tower_tesla', {
anchorX: 0.5,
anchorY: 1,
x: 400,
y: -60
}));
// Add cost labels with larger, more distinct text - updated balanced costs
infantryCost = LK.gui.bottom.addChild(new Text2('80g', {
size: 32,
fill: 0xFFD700
}));
infantryCost.anchor.set(0.5, 1);
infantryCost.x = -400;
infantryCost.y = -130;
frostCost = LK.gui.bottom.addChild(new Text2('120g', {
size: 32,
fill: 0xFFD700
}));
frostCost.anchor.set(0.5, 1);
frostCost.x = -200;
frostCost.y = -130;
antiairCost = LK.gui.bottom.addChild(new Text2('100g', {
size: 32,
fill: 0xFFD700
}));
antiairCost.anchor.set(0.5, 1);
antiairCost.x = 0;
antiairCost.y = -130;
poisonCost = LK.gui.bottom.addChild(new Text2('130g', {
size: 32,
fill: 0xFFD700
}));
poisonCost.anchor.set(0.5, 1);
poisonCost.x = 200;
poisonCost.y = -130;
teslaCost = LK.gui.bottom.addChild(new Text2('150g', {
size: 32,
fill: 0xFFD700
}));
teslaCost.anchor.set(0.5, 1);
teslaCost.x = 400;
teslaCost.y = -130;
// Add tower name labels below the cost text
infantryName = LK.gui.bottom.addChild(new Text2('Infantry', {
size: 28,
fill: 0xFFFFFF
}));
infantryName.anchor.set(0.5, 1);
infantryName.x = -400;
infantryName.y = -160;
frostName = LK.gui.bottom.addChild(new Text2('Freezer', {
size: 28,
fill: 0xFFFFFF
}));
frostName.anchor.set(0.5, 1);
frostName.x = -200;
frostName.y = -160;
antiairName = LK.gui.bottom.addChild(new Text2('Air', {
size: 28,
fill: 0xFFFFFF
}));
antiairName.anchor.set(0.5, 1);
antiairName.x = 0;
antiairName.y = -160;
poisonName = LK.gui.bottom.addChild(new Text2('Poison', {
size: 28,
fill: 0xFFFFFF
}));
poisonName.anchor.set(0.5, 1);
poisonName.x = 200;
poisonName.y = -160;
teslaName = LK.gui.bottom.addChild(new Text2('Tesla', {
size: 28,
fill: 0xFFFFFF
}));
teslaName.anchor.set(0.5, 1);
teslaName.x = 400;
teslaName.y = -160;
infantryBtn.down = function () {
if (currentWave < towerUnlockWaves.infantry) {
showUnlockMessage('infantry', towerUnlockWaves.infantry);
return;
}
if (coins >= 80) {
selectedTowerType = 'infantry';
updateBuildZoneVisuals();
}
};
frostBtn.down = function () {
if (currentWave < towerUnlockWaves.frost) {
showUnlockMessage('frost', towerUnlockWaves.frost);
return;
}
if (coins >= 120) {
selectedTowerType = 'frost';
updateBuildZoneVisuals();
}
};
antiairBtn.down = function () {
if (currentWave < towerUnlockWaves.antiair) {
showUnlockMessage('antiair', towerUnlockWaves.antiair);
return;
}
if (coins >= 100) {
selectedTowerType = 'antiair';
updateBuildZoneVisuals();
}
};
poisonBtn.down = function () {
if (currentWave < towerUnlockWaves.poison) {
showUnlockMessage('poison', towerUnlockWaves.poison);
return;
}
if (coins >= 130) {
selectedTowerType = 'poison';
updateBuildZoneVisuals();
}
};
teslaBtn.down = function () {
if (currentWave < towerUnlockWaves.tesla) {
showUnlockMessage('tesla', towerUnlockWaves.tesla);
return;
}
if (coins >= 150) {
selectedTowerType = 'tesla';
updateBuildZoneVisuals();
}
};
}
function createWave() {
if (currentWave >= maxWaves && !endlessMode) {
// Enable endless mode
endlessMode = true;
endlessScore = 0;
// Show endless mode notification
var endlessNotification = new Text2('♾️ ENDLESS MODE UNLOCKED! ♾️', {
size: 80,
fill: 0xFFD700
});
endlessNotification.anchor.set(0.5, 0.5);
endlessNotification.x = 1024;
endlessNotification.y = 1000;
isoContainer.addChild(endlessNotification);
tween(endlessNotification, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0
}, {
duration: 3000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (endlessNotification && endlessNotification.destroy) {
endlessNotification.destroy();
}
}
});
}
currentWave++;
if (endlessMode) {
endlessScore++;
}
waveEnemies = [];
// Scale enemy count based on wave number with new progression
var enemyCount;
if (currentWave <= 5) {
// Waves 1-5: 15 to 20 enemies
enemyCount = 15 + Math.floor(Math.random() * 6);
} else if (currentWave <= 15) {
// Waves 6-15: 20 to 25 enemies
enemyCount = 20 + Math.floor(Math.random() * 6);
} else if (currentWave <= 30) {
// Waves 16-30: 25 to 35 enemies
enemyCount = 25 + Math.floor(Math.random() * 11);
} else if (currentWave <= 60) {
// Waves 31-60: 35 to 45 enemies
enemyCount = 35 + Math.floor(Math.random() * 11);
} else if (currentWave <= 97) {
// Waves 61-97: 45 to 55 enemies
enemyCount = 45 + Math.floor(Math.random() * 11);
} else {
// Waves 98-100: 60 to 75 enemies
enemyCount = 60 + Math.floor(Math.random() * 16);
}
// Calculate available enemy strength levels based on wave
var maxStrengthLevel = Math.min(10, Math.floor((currentWave + 1) / 2));
var minStrengthLevel = Math.max(1, maxStrengthLevel - 2);
for (var i = 0; i < enemyCount; i++) {
// Define which enemies can appear based on wave number - exact wave appearances as specified
var availableEnemies = [];
// 30 balanced enemies with exact wave appearance and phase-out logic
if (currentWave >= 1 && currentWave <= 5) {
availableEnemies.push('goblin_scout');
}
if (currentWave >= 4 && currentWave <= 8) {
availableEnemies.push('mire_crawler');
}
if (currentWave >= 7 && currentWave <= 12) {
availableEnemies.push('fang_imp');
}
if (currentWave >= 10 && currentWave <= 16) {
availableEnemies.push('crimson_leaper');
}
if (currentWave >= 13 && currentWave <= 20) {
availableEnemies.push('rustback_beetle');
}
if (currentWave >= 16 && currentWave <= 24) {
availableEnemies.push('gravel_hound');
}
if (currentWave >= 19 && currentWave <= 28) {
availableEnemies.push('vine_stalker');
}
if (currentWave >= 22 && currentWave <= 32) {
availableEnemies.push('ashborne_wraith');
}
if (currentWave >= 25 && currentWave <= 36) {
availableEnemies.push('stonehide_orc');
}
if (currentWave >= 28 && currentWave <= 40) {
availableEnemies.push('toxic_maw');
}
if (currentWave >= 31 && currentWave <= 43) {
availableEnemies.push('frostback_boar');
}
if (currentWave >= 34 && currentWave <= 46) {
availableEnemies.push('ironmaw_troll');
}
if (currentWave >= 37 && currentWave <= 49) {
availableEnemies.push('volcanic_strider');
}
if (currentWave >= 40 && currentWave <= 52) {
availableEnemies.push('stormblade_knight');
}
if (currentWave >= 43 && currentWave <= 55) {
availableEnemies.push('twilight_serpent');
}
if (currentWave >= 46 && currentWave <= 58) {
availableEnemies.push('obsidian_colossus');
}
if (currentWave >= 49 && currentWave <= 61) {
availableEnemies.push('spectral_ravager');
}
if (currentWave >= 52 && currentWave <= 64) {
availableEnemies.push('boneclad_warbeast');
}
if (currentWave >= 55 && currentWave <= 67) {
availableEnemies.push('plague_titan');
}
if (currentWave >= 58 && currentWave <= 70) {
availableEnemies.push('abyss_reaper');
}
if (currentWave >= 61 && currentWave <= 73) {
availableEnemies.push('wraith_bringer');
}
if (currentWave >= 64 && currentWave <= 76) {
availableEnemies.push('inferno_crawler');
}
if (currentWave >= 67 && currentWave <= 79) {
availableEnemies.push('thornback_hydra');
}
if (currentWave >= 70 && currentWave <= 82) {
availableEnemies.push('ghostfire_shade');
}
if (currentWave >= 73 && currentWave <= 85) {
availableEnemies.push('storm_herald');
}
if (currentWave >= 76 && currentWave <= 88) {
availableEnemies.push('voidling_runner');
}
if (currentWave >= 79 && currentWave <= 91) {
availableEnemies.push('doom_shell');
}
if (currentWave >= 82 && currentWave <= 94) {
availableEnemies.push('nether_juggernaut');
}
if (currentWave >= 85) {
availableEnemies.push('abyss_howler');
}
if (currentWave >= 88) {
availableEnemies.push('void_devourer');
}
// Legacy enemy support for existing assets
if (currentWave >= 1) {
availableEnemies.push('goblin');
}
if (currentWave >= 4) {
availableEnemies.push('crawler');
}
if (currentWave >= 7) {
availableEnemies.push('bandit');
}
if (currentWave >= 10) {
availableEnemies.push('golem');
}
if (currentWave >= 13) {
availableEnemies.push('imp');
}
if (currentWave >= 16) {
availableEnemies.push('knight');
}
if (currentWave >= 19) {
availableEnemies.push('beast');
}
if (currentWave >= 22) {
availableEnemies.push('mage');
}
if (currentWave >= 25) {
availableEnemies.push('reaver');
}
if (currentWave >= 28) {
availableEnemies.push('doom');
}
// Select random enemy from available types
var enemyType = availableEnemies[Math.floor(Math.random() * availableEnemies.length)];
// Assign strength level within available range
var strengthLevel = minStrengthLevel + Math.floor(Math.random() * (maxStrengthLevel - minStrengthLevel + 1));
var pathChoice = 1;
if (currentWave >= 6) {
pathChoice = Math.floor(Math.random() * 3) + 1;
} else if (currentWave >= 3) {
pathChoice = Math.floor(Math.random() * 2) + 1;
}
waveEnemies.push({
type: enemyType,
pathIndex: pathChoice,
strengthLevel: strengthLevel,
spawnTime: LK.ticks + Math.floor(i * 90 / gameSpeed) // Apply speed multiplier to spawn timing
});
}
waveInProgress = true;
if (!audioMuted) {
LK.getSound('wave_start').play();
}
// Show round start announcement
if (roundStartText && roundStartText.destroy) {
roundStartText.destroy();
}
roundStartText = new Text2('Wave ' + currentWave + ' / 100 Starting...', {
size: 80,
fill: 0xFFFFFF
});
roundStartText.anchor.set(0.5, 0.5);
roundStartText.x = 1024;
roundStartText.y = 1000;
isoContainer.addChild(roundStartText);
// Animate the announcement text
tween(roundStartText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(roundStartText, {
alpha: 0
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (roundStartText && roundStartText.destroy) {
roundStartText.destroy();
roundStartText = null;
}
}
});
}
});
}
// Create background panel for castle lives text
var castleLivesTextBg = isoContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0,
anchorY: 0.5,
x: 30,
y: 1900,
scaleX: 8,
scaleY: 1.8
}));
castleLivesTextBg.tint = 0x000000;
castleLivesTextBg.alpha = 0.8;
castleLivesTextBg.visible = false; // Hidden on main menu
var castleLivesText = new Text2('Castle Health: 100', {
size: 70,
fill: 0xFF4444,
font: "'Arial Black', Impact, 'Arial Bold'"
});
castleLivesText.anchor.set(0, 0.5);
castleLivesText.x = 50;
castleLivesText.y = 1900;
castleLivesText.visible = false; // Hidden on main menu
isoContainer.addChild(castleLivesText);
// Create background panel for gold text
var coinsTextBg = isoContainer.addChild(LK.getAsset('build_zone', {
anchorX: 0,
anchorY: 0.5,
x: 30,
y: 2180,
scaleX: 6,
scaleY: 1.8
}));
coinsTextBg.tint = 0x000000;
coinsTextBg.alpha = 0.8;
coinsTextBg.visible = false; // Hidden on main menu
var coinsText = new Text2('Gold: 160', {
size: 70,
fill: 0xFFD700,
font: "'Arial Black', Impact, 'Arial Bold'"
});
coinsText.anchor.set(0, 0.5);
coinsText.x = 50;
coinsText.y = 2180;
coinsText.visible = false; // Hidden on main menu
isoContainer.addChild(coinsText);
var waveText = new Text2('Wave: 0/100', {
size: 40,
fill: 0xFFFFFF
});
waveText.anchor.set(0.5, 0);
waveText.visible = false; // Hidden on main menu
LK.gui.top.addChild(waveText);
var roundStartText = null;
if (gameState === 'mainMenu') {
createMainMenu();
} else {
createPath();
createTerrain();
createCastle();
createShop();
}
game.down = function (x, y, obj) {
// Clear any existing upgrade panel and range indicator when clicking elsewhere
if (upgradePanel && upgradePanel.destroy && selectedTower && obj && !selectedTower.intersects(obj)) {
upgradePanel.destroy();
upgradePanel = null;
selectedTower = null;
// Clear range indicator
if (activeRangeIndicator && activeRangeIndicator.destroy) {
activeRangeIndicator.destroy();
activeRangeIndicator = null;
}
}
// Fallback for old upgradeText system
if (upgradeText && upgradeText.destroy && selectedTower && obj && !selectedTower.intersects(obj)) {
upgradeText.destroy();
upgradeText = null;
selectedTower = null;
}
// Use screen coordinates directly for top-down view
var isoPos = {
x: x,
y: y
};
if (selectedTowerType && gameState === 'playing') {
// Check if clicking on a valid, unoccupied build zone
var validBuildZone = null;
for (var i = 0; i < buildZones.length; i++) {
if (!buildZones[i].zoneData.occupied) {
var distance = Math.sqrt((buildZones[i].x - isoPos.x) * (buildZones[i].x - isoPos.x) + (buildZones[i].y - isoPos.y) * (buildZones[i].y - isoPos.y));
if (distance < 40) {
validBuildZone = buildZones[i];
break;
}
}
}
// Only place tower if clicking on a valid build zone and player has enough coins
if (validBuildZone) {
var tower = new Tower(selectedTowerType);
if (coins >= tower.cost) {
// Place tower exactly at build zone center
tower.x = validBuildZone.x;
tower.y = validBuildZone.y;
towers.push(tower);
isoContainer.addChild(tower);
coins -= tower.cost;
coinsText.setText('Gold: ' + coins);
selectedTowerType = null;
// Mark build zone as occupied
validBuildZone.zoneData.occupied = true;
updateBuildZoneVisuals();
// Play placement sound
if (!audioMuted) {
LK.getSound('placement').play();
}
}
} else {
// Clear selection if clicking outside build zones
selectedTowerType = null;
updateBuildZoneVisuals();
}
}
};
game.update = function () {
if (gameState === 'mainMenu' || gameState === 'setup') {
return;
}
if (gameState !== 'playing') {
return;
}
if (waveInProgress) {
for (var i = waveEnemies.length - 1; i >= 0; i--) {
var enemyData = waveEnemies[i];
if (LK.ticks >= enemyData.spawnTime) {
var enemy = new Enemy(enemyData.type, currentWave, enemyData.pathIndex, enemyData.strengthLevel);
enemies.push(enemy);
isoContainer.addChild(enemy);
waveEnemies.splice(i, 1);
}
}
if (waveEnemies.length === 0 && enemies.length === 0) {
waveInProgress = false;
nextWaveTimer = LK.ticks + Math.floor(180 / gameSpeed); // Apply speed multiplier to wave delay
}
}
if (!waveInProgress && LK.ticks >= nextWaveTimer && enemies.length === 0) {
createWave();
}
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.reachedCastle) {
castleHealth -= 15;
// Update castle lives text
castleLivesText.setText('Castle Health: ' + castleHealth);
// Update health bar with smooth animation
var healthPercent = castleHealth / 100;
if (castleHealthBarFill) {
// Animate health bar scale change smoothly
tween(castleHealthBarFill, {
scaleX: 4 * healthPercent
}, {
duration: 300
});
// Update color based on health percentage - green to red gradient
var redComponent = Math.floor(255 * (1 - healthPercent));
var greenComponent = Math.floor(255 * healthPercent);
var healthColor = redComponent << 16 | greenComponent << 8 | 0x00;
castleHealthBarFill.tint = healthColor;
// Flash effect when taking damage
LK.effects.flashObject(castleHealthBarFill, 0xFF0000, 200);
}
enemy.destroy();
enemies.splice(i, 1);
if (castleHealth <= 0) {
LK.showGameOver();
return;
}
}
}
for (var i = projectiles.length - 1; i >= 0; i--) {
var projectile = projectiles[i];
if (projectile.hasReachedTarget) {
// Target tracking ensures 100% hit rate - damage the intended target
if (projectile.targetEnemy && projectile.targetEnemy.x) {
var targetEnemy = projectile.targetEnemy;
// Apply tower-specific effects
if (projectile.towerType === 'frost') {
// Find the tower that shot this projectile to get its slow percentage
var frostTower = null;
for (var k = 0; k < towers.length; k++) {
if (towers[k].type === 'frost') {
var towerDistance = Math.sqrt((towers[k].x - projectile.x) * (towers[k].x - projectile.x) + (towers[k].y - projectile.y) * (towers[k].y - projectile.y));
if (towerDistance <= towers[k].range + 100) {
// Some tolerance for projectile travel
frostTower = towers[k];
break;
}
}
}
if (frostTower) {
targetEnemy.applyFrostSlow(frostTower.slowPercentage);
}
}
// Handle area damage for cannonball projectiles
if (projectile.type === 'cannonball') {
// Deal damage to target and nearby enemies
for (var j = 0; j < enemies.length; j++) {
var enemy = enemies[j];
var distance = Math.sqrt((enemy.x - projectile.x) * (enemy.x - projectile.x) + (enemy.y - projectile.y) * (enemy.y - projectile.y));
if (distance <= 50) {
if (enemy.takeDamage(projectile.damage)) {
coins += Math.floor(enemy.coinValue * 0.65 * 0.7);
coinsText.setText('Gold: ' + coins);
// Create death effect at enemy position
tween(enemy, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut
});
enemy.destroy();
enemies.splice(j, 1);
j--;
}
}
}
} else {
// Single target damage - guaranteed hit on intended target
if (targetEnemy.takeDamage(projectile.damage)) {
coins += Math.floor(targetEnemy.coinValue * 0.65 * 0.7);
coinsText.setText('Gold: ' + coins);
// Create death effect at enemy position
tween(targetEnemy, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut
});
// Remove enemy from array
for (var j = 0; j < enemies.length; j++) {
if (enemies[j] === targetEnemy) {
enemies.splice(j, 1);
break;
}
}
targetEnemy.destroy();
}
}
}
projectile.destroy();
projectiles.splice(i, 1);
} else if (projectile.x < -50 || projectile.x > 2098 || projectile.y < -50 || projectile.y > 2782) {
projectile.destroy();
projectiles.splice(i, 1);
}
}
if (endlessMode) {
waveText.setText('Endless Mode - Survived: ' + endlessScore + ' waves');
} else {
waveText.setText('Wave: ' + currentWave + '/' + maxWaves);
}
updateShopVisuals();
};
A lake with a 3d view. In-Game asset. 2d. High contrast. No shadows
tree. In-Game asset. 2d. High contrast. No shadows
house. In-Game asset. 2d. High contrast. No shadows
magic ball. In-Game asset. 2d. High contrast. No shadows
arrow bullet. In-Game asset. 2d. High contrast. No shadows
realistic majestic castle. In-Game asset. 2d. High contrast. No shadows
dirt road. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
This is a button, the outer part is red, the middle circle is red, the 2nd circle is dark gray. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
village house. In-Game asset. 2d. High contrast. No shadows
poisonous tower realistic. In-Game asset. 2d. High contrast. No shadows
freezing tower realistic. In-Game asset. 2d. High contrast. No shadows
goblin. In-Game asset. 2d. High contrast. No shadows
goblin emperor. In-Game asset. 2d. High contrast. No shadows
haydut goblin. In-Game asset. 2d. High contrast. No shadows
titan goblin. In-Game asset. 2d. High contrast. No shadows
goblin tazısı. In-Game asset. 2d. High contrast. No shadows
gezgin goblin. In-Game asset. 2d. High contrast. No shadows
kıyamet canavarı goblin. In-Game asset. 2d. High contrast. No shadows
kemikli iskelet goblin. In-Game asset. 2d. High contrast. No shadows
buzul goblin. In-Game asset. 2d. High contrast. No shadows
golem goblin. In-Game asset. 2d. High contrast. No shadows
kadın goblin imparatoru. In-Game asset. 2d. High contrast. No shadows
demir goblin. In-Game asset. 2d. High contrast. No shadows
şövalye goblin. In-Game asset. 2d. High contrast. No shadows
obsidyen dev goblin. In-Game asset. 2d. High contrast. No shadows
üzerinde okçu olan, ahşap, okçu kulesi. In-Game asset. 2d. High contrast. No shadows
ahşap hava savunma kulesi, üstünde arbalet olsun. In-Game asset. 2d. High contrast. No shadows
antik elektrik atıcı kule. In-Game asset. 2d. High contrast. No shadows
goblin canavar. In-Game asset. 2d. High contrast. No shadows
volkanik gezgini goblin. In-Game asset. 2d. High contrast. No shadows
kahverengi gezgin eli bıçaklı goblin. In-Game asset. 2d. High contrast. No shadows
büyücü goblin. In-Game asset. 2d. High contrast. No shadows
paslı böcek goblin. In-Game asset. 2d. High contrast. No shadows
spektral yıkıcı goblin. In-Game asset. 2d. High contrast. No shadows
taş goblin. In-Game asset. 2d. High contrast. No shadows
kimyasalcı goblin, boydan. In-Game asset. 2d. High contrast. No shadows
yuvarlak havan topu mermisi. In-Game asset. 2d. High contrast. No shadows
karanlık büyücü goblin. In-Game asset. 2d. High contrast. No shadows
yeşil yapraklı dövüşçü goblin. In-Game asset. 2d. High contrast. No shadows
kayalık. In-Game asset. 2d. High contrast. No shadows
uçan küllü goblin. In-Game asset. 2d. High contrast. No shadows
uçan goblin yılan. In-Game asset. 2d. High contrast. No shadows
kırmızı kaslı uçan goblin. In-Game asset. 2d. High contrast. No shadows
uçan karanlık goblin. In-Game asset. 2d. High contrast. No shadows
uçan goblin yağmacı. In-Game asset. 2d. High contrast. No shadows
uçan fırtına golem. In-Game asset. 2d. High contrast. No shadows