/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { unlockedWorlds: 1, completedWaves: 0, digiviceC: false, digiviceB: false, digiviceA: false, worldLevels: {}, language: "en", securityPoints: 0 }); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Play splash attack sound LK.getSound('splashAttack').play(); // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Play freeze effect sound for slow attacks LK.getSound('freezeEffect').play(); // Apply slow effect // Make slow percentage scale with tower level (default 50%, up to 80% at max level) var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6 var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; // Slow by X% self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { self.targetEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'poison') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual poison effect var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Play poison effect sound LK.getSound('poisonEffect').play(); // Apply poison effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } // Special Digimon evolutionary line effects if (self.type === 'agumon' && self.burnChance) { // Agumon line: Fire/burn effects if (!self.targetEnemy.isImmune && Math.random() < self.burnChance) { self.targetEnemy.burning = true; self.targetEnemy.burnDamage = self.damage * 0.15; self.targetEnemy.burnDuration = 240; // 4 seconds var burnEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'burn'); game.addChild(burnEffect); // Play burn effect sound LK.getSound('burnEffect').play(); } } else if (self.type === 'gabumon' && self.freezeChance) { // Gabumon line: Ice/freeze effects if (!self.targetEnemy.isImmune && Math.random() < self.freezeChance) { self.targetEnemy.frozen = true; self.targetEnemy.frozenDuration = 120; // 2 seconds if (!self.targetEnemy.originalSpeed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; } self.targetEnemy.speed = 0; // Completely frozen var freezeEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'freeze'); game.addChild(freezeEffect); // Play freeze effect sound LK.getSound('freezeEffect').play(); } } else if (self.type === 'tentomon' && self.paralyzeChance) { // Tentomon line: Electric paralysis affecting multiple enemies if (!self.targetEnemy.isImmune && Math.random() < self.paralyzeChance) { // Paralyze main target self.targetEnemy.paralyzed = true; self.targetEnemy.paralyzeDuration = 180; // 3 seconds if (!self.targetEnemy.originalSpeed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; } self.targetEnemy.speed *= 0.1; // Nearly stopped var paralyzeEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'paralyze'); game.addChild(paralyzeEffect); // Spread paralysis to nearby enemies for (var i = 0; i < enemies.length; i++) { var nearbyEnemy = enemies[i]; if (nearbyEnemy !== self.targetEnemy && !nearbyEnemy.isImmune) { var dx = nearbyEnemy.x - self.targetEnemy.x; var dy = nearbyEnemy.y - self.targetEnemy.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.paralyzeArea) { nearbyEnemy.paralyzed = true; nearbyEnemy.paralyzeDuration = 120; // Shorter for spread effect if (!nearbyEnemy.originalSpeed) { nearbyEnemy.originalSpeed = nearbyEnemy.speed; } nearbyEnemy.speed *= 0.3; var spreadEffect = new EffectIndicator(nearbyEnemy.x, nearbyEnemy.y, 'paralyze'); game.addChild(spreadEffect); } } } } } else if (self.type === 'palmon' && self.poisonSpread) { // Palmon line: Spreading poison effects if (!self.targetEnemy.isImmune) { // Apply poison to main target self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.25; self.targetEnemy.poisonDuration = 360; // 6 seconds var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Spread poison to nearby enemies with a chance for (var i = 0; i < enemies.length; i++) { var nearbyEnemy = enemies[i]; if (nearbyEnemy !== self.targetEnemy && !nearbyEnemy.isImmune) { var dx = nearbyEnemy.x - self.targetEnemy.x; var dy = nearbyEnemy.y - self.targetEnemy.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.poisonRadius) { // Use poisonSpreadChance if defined, otherwise default to 30% var spreadChance = typeof self.poisonSpreadChance === "number" ? self.poisonSpreadChance : 0.3; if (Math.random() < spreadChance) { nearbyEnemy.poisoned = true; nearbyEnemy.poisonDamage = self.damage * 0.15; // Weaker spread poison nearbyEnemy.poisonDuration = 240; var spreadPoisonEffect = new EffectIndicator(nearbyEnemy.x, nearbyEnemy.y, 'poison'); game.addChild(spreadPoisonEffect); } } } } } } else if (self.type === 'gomamon' && self.moistureEffect) { // Gomamon line: Moisture effects that affect fire/ice interactions if (!self.targetEnemy.isImmune) { self.targetEnemy.moist = true; self.targetEnemy.moistDuration = self.moistureDuration; self.targetEnemy.fireResistance = 0.5; // Reduced fire damage self.targetEnemy.freezeVulnerability = 1.5; // Increased freeze chance var moistEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'moist'); game.addChild(moistEffect); } } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var Coin = Container.expand(function (x, y, value) { var self = Container.call(this); self.value = value || 5; self.x = x; self.y = y; self.collected = false; self.walkSpeed = 0.5; self.direction = Math.random() * Math.PI * 2; self.changeDirectionTimer = 0; var coinGraphics = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); coinGraphics.width = 30; coinGraphics.height = 30; coinGraphics.tint = 0xFFD700; // Add coin value text var valueText = new Text2(self.value.toString(), { size: 20, fill: 0x000000, weight: 800 }); valueText.anchor.set(0.5, 0.5); self.addChild(valueText); self.update = function () { if (self.collected) return; // Change direction occasionally self.changeDirectionTimer++; if (self.changeDirectionTimer > 120) { // Change direction every 2 seconds self.direction = Math.random() * Math.PI * 2; self.changeDirectionTimer = 0; } // Move in current direction var newX = self.x + Math.cos(self.direction) * self.walkSpeed; var newY = self.y + Math.sin(self.direction) * self.walkSpeed; // Keep within playable bounds var minX = grid.x + CELL_SIZE; var maxX = grid.x + grid.cells.length * CELL_SIZE - CELL_SIZE; var minY = grid.y + CELL_SIZE; var maxY = grid.y + grid.cells[0].length * CELL_SIZE - CELL_SIZE; if (newX < minX || newX > maxX) { self.direction = Math.PI - self.direction; // Bounce horizontally } else { self.x = newX; } if (newY < minY || newY > maxY) { self.direction = -self.direction; // Bounce vertically } else { self.y = newY; } // Check if player clicks on coin }; self.down = function () { if (!self.collected) { self.collected = true; setGold(gold + self.value); // Add 10 security score points score += 10; // Save security points to storage storage.securityPoints = score; updateUI(); // Play coin collect sound LK.getSound('coinCollect').play(); var goldIndicator = new GoldIndicator(self.value, self.x, self.y); game.addChild(goldIndicator); // Remove coin from coins array var coinIndex = coins.indexOf(self); if (coinIndex !== -1) { coins.splice(coinIndex, 1); } self.destroy(); } }; return self; }); var ComicPanel = Container.expand(function (imagePath, text) { var self = Container.call(this); // Panel background - larger panel var panelBg = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); panelBg.width = 700; panelBg.height = 500; panelBg.tint = 0x222222; panelBg.alpha = 0.9; // Add border effect var border = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); border.width = 710; border.height = 510; border.tint = 0x000000; border.alpha = 0.8; self.addChildAt(border, 0); // Text area - larger and repositioned var textBg = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); textBg.width = 660; textBg.height = 160; textBg.tint = 0xffffff; textBg.alpha = 0.95; textBg.y = 170; self.addChild(textBg); // Character image if provided - repositioned if (imagePath) { var characterImage = self.attachAsset(imagePath, { anchorX: 0.5, anchorY: 0.5 }); characterImage.width = 200; characterImage.height = 200; characterImage.y = -80; self.addChild(characterImage); } // Story text - improved sizing and positioning var storyText = new Text2(text, { size: 28, fill: 0x000000, weight: 600 }); storyText.anchor.set(0.5, 0.5); storyText.y = 170; storyText.wordWrap = true; storyText.wordWrapWidth = 600; storyText.maxWidth = 600; self.addChild(storyText); // Scale animation entrance self.scaleX = 0.3; self.scaleY = 0.3; self.alpha = 0; self.show = function () { tween(self, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 400, easing: tween.backOut }); }; self.hide = function (callback) { tween(self, { scaleX: 0.8, scaleY: 0.8, alpha: 0 }, { duration: 300, easing: tween.easeIn, onFinish: callback }); }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; // Removed number label to improve performance // Numbers were causing lag due to text rendering overhead self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { // Renderizado visual desactivado para evitar interferencia con tiles del mundo // Solo mantiene la lógica de pathfinding sin elementos visuales // Los tiles del mundo se renderizan a través de WorldRenderer // Actualizar las flechas solo si hay una torre seleccionada para debug if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { // Mostrar flechas solo para torres seleccionadas while (debugArrows.length > data.targets.length) { self.removeChild(debugArrows.pop()); } for (var a = 0; a < data.targets.length; a++) { var destination = data.targets[a]; var ox = destination.x - data.x; var oy = destination.y - data.y; var angle = Math.atan2(oy, ox); if (!debugArrows[a]) { debugArrows[a] = LK.getAsset('arrow', { anchorX: -.5, anchorY: 0.5 }); debugArrows[a].alpha = .5; self.addChildAt(debugArrows[a], 1); } debugArrows[a].rotation = angle; } } else { // Remover flechas si no hay torre seleccionada self.removeArrows(); } // Hacer el gráfico de celda invisible para no interferir con tiles cellGraphics.alpha = 0; }; }); var DigimonShop = Container.expand(function () { var self = Container.call(this); self.visible = false; self.y = 2732; var shopBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); shopBackground.width = 2048; shopBackground.height = 600; shopBackground.tint = 0x222222; shopBackground.alpha = 0.95; var titleText = new Text2('Digimon Firewall Shop', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -250; self.addChild(titleText); // Create shop items container var itemsContainer = new Container(); itemsContainer.y = -50; self.addChild(itemsContainer); // Digivice items data var digiviceItems = [{ id: 'digiviceC', name: 'Digivice C', cost: 500, description: 'Unlocks Champion Level' }, { id: 'digiviceB', name: 'Digivice B', cost: 2000, description: 'Unlocks Ultimate Level' }, { id: 'digiviceA', name: 'Digivice A', cost: 8000, description: 'Unlocks Mega Level' }]; // Create shop item buttons for (var i = 0; i < digiviceItems.length; i++) { var item = digiviceItems[i]; var itemButton = new Container(); itemButton.x = -600 + i * 400; itemButton.y = 0; itemsContainer.addChild(itemButton); var itemBg = itemButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); itemBg.width = 350; itemBg.height = 200; var itemNameText = new Text2(item.name, { size: 50, fill: 0xFFFFFF, weight: 800 }); itemNameText.anchor.set(0.5, 0.5); itemNameText.y = -50; itemButton.addChild(itemNameText); var itemDescText = new Text2(item.description, { size: 35, fill: 0xCCCCCC, weight: 400 }); itemDescText.anchor.set(0.5, 0.5); itemDescText.y = -10; itemButton.addChild(itemDescText); var itemCostText = new Text2(item.cost + ' points', { size: 45, fill: 0xFFD700, weight: 800 }); itemCostText.anchor.set(0.5, 0.5); itemCostText.y = 40; itemButton.addChild(itemCostText); // Create purchase functionality (function (itemData, button, background, costText) { button.update = function () { var owned = storage[itemData.id] || false; var canAfford = score >= itemData.cost; if (owned) { background.tint = 0x00AA00; costText.setText('OWNED'); button.alpha = 0.7; } else if (canAfford) { background.tint = 0x4444FF; costText.setText(itemData.cost + ' points'); button.alpha = 1.0; } else { background.tint = 0x666666; costText.setText(itemData.cost + ' points'); button.alpha = 0.5; } }; button.down = function () { var owned = storage[itemData.id] || false; var canAfford = score >= itemData.cost; if (owned) { var notification = game.addChild(new Notification("You already own " + itemData.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (canAfford) { score -= itemData.cost; updateUI(); storage[itemData.id] = true; var notification = game.addChild(new Notification("Purchased " + itemData.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Not enough security points for " + itemData.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; })(item, itemButton, itemBg, itemCostText); } var closeButton = new Container(); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = shopBackground.width / 2 - 57; closeButton.y = -shopBackground.height / 2 + 57; self.addChild(closeButton); closeButton.down = function () { self.hide(); }; self.show = function () { self.visible = true; tween(self, { y: 2732 - 300 }, { duration: 300, easing: tween.backOut }); }; self.hide = function () { tween(self, { y: 2732 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { self.visible = false; } }); }; return self; }); var DigimonSummonMenu = Container.expand(function () { var self = Container.call(this); self.visible = false; self.y = 2732; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 600; menuBackground.tint = 0x222222; menuBackground.alpha = 0.95; var titleText = new Text2('Summon Digimon Allies', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -250; self.addChild(titleText); // Create summon items container var itemsContainer = new Container(); itemsContainer.y = -50; self.addChild(itemsContainer); // Available Digimon based on unlocked Digivices function getAvailableDigimon() { // All Digimon that can be summoned, matching those available by microphone var available = [{ id: 'koromon', name: 'Koromon', cost: 50, description: 'Rookie Melee Fighter', reqDigivice: null }, { id: 'tsunomon', name: 'Tsunomon', cost: 60, description: 'Rookie Ranged Fighter', reqDigivice: null }, // Champion level (Digivice C required) { id: 'greymon', name: 'Greymon', cost: 150, description: 'Champion Fire Dragon', reqDigivice: 'digiviceC' }, { id: 'garurumon', name: 'Garurumon', cost: 180, description: 'Champion Ice Wolf', reqDigivice: 'digiviceC' }, { id: 'kabuterimon', name: 'Kabuterimon', cost: 160, description: 'Champion Electric Insect', reqDigivice: 'digiviceC' }, // Ultimate level (Digivice B required) { id: 'metalgreymon', name: 'MetalGreymon', cost: 400, description: 'Ultimate Cyborg Dragon', reqDigivice: 'digiviceB' }, { id: 'weregarurumon', name: 'WereGarurumon', cost: 450, description: 'Ultimate Beast Warrior', reqDigivice: 'digiviceB' }, { id: 'megakabuterimon', name: 'MegaKabuterimon', cost: 420, description: 'Ultimate Giant Insect', reqDigivice: 'digiviceB' }, // Mega level (Digivice A required) { id: 'wargreymon', name: 'WarGreymon', cost: 800, description: 'Mega Dragon Warrior', reqDigivice: 'digiviceA' }]; return available; } function updateDigimonButtons() { // Clear existing buttons while (itemsContainer.children.length > 0) { itemsContainer.removeChild(itemsContainer.children[0]); } // Only show Digimon that can actually be summoned (requirements met) var availableDigimon = getAvailableDigimon().filter(function (d) { // Check digivice requirement if (d.reqDigivice && !storage[d.reqDigivice]) return false; // Check cost if (score < d.cost) return false; // Check maxAlliedUnits if (alliedUnits.length >= maxAlliedUnits) return false; return true; }); // If a filterLevel is set (from C, B, or A button), filter Digimon by evolution level var filterLevel = self._filterLevel; if (filterLevel) { // Map Digimon id to evolution level var digimonLevelMap = { koromon: "rookie", tsunomon: "rookie", greymon: "champion", garurumon: "champion", kabuterimon: "champion", metalgreymon: "ultimate", weregarurumon: "ultimate", megakabuterimon: "ultimate", wargreymon: "mega" }; availableDigimon = availableDigimon.filter(function (d) { return (digimonLevelMap[d.id] || "").toLowerCase() === filterLevel.toLowerCase(); }); } // Layout: 3 per row, center horizontally var buttonsPerRow = Math.min(3, availableDigimon.length); var buttonSpacing = 400; var startX = -((buttonsPerRow - 1) * buttonSpacing) / 2; for (var i = 0; i < availableDigimon.length; i++) { var digimon = availableDigimon[i]; var row = Math.floor(i / buttonsPerRow); var col = i % buttonsPerRow; var digimonButton = new Container(); digimonButton.x = startX + col * buttonSpacing; digimonButton.y = row * 180; itemsContainer.addChild(digimonButton); // Use correct asset for Digimon var digimonAssetMap = { koromon: 'agumon', tsunomon: 'gabumon', greymon: 'greymon', garurumon: 'garurumon', kabuterimon: 'kabuterimon', metalgreymon: 'metalgreymon', weregarurumon: 'weregarurumon', megakabuterimon: 'megakabuterimon', wargreymon: 'wargreymon' }; var assetId = digimonAssetMap[digimon.id] || 'agumon'; var digimonArt = digimonButton.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); digimonArt.width = 90; digimonArt.height = 90; digimonArt.y = -60; var buttonBg = digimonButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBg.width = 350; buttonBg.height = 160; var nameText = new Text2(digimon.name, { size: 45, fill: 0xFFFFFF, weight: 800 }); nameText.anchor.set(0.5, 0.5); nameText.y = -40; digimonButton.addChild(nameText); var descText = new Text2(digimon.description, { size: 28, fill: 0xCCCCCC, weight: 400 }); descText.anchor.set(0.5, 0.5); descText.y = -10; digimonButton.addChild(descText); var costText = new Text2(digimon.cost + ' points', { size: 35, fill: 0xFFD700, weight: 800 }); costText.anchor.set(0.5, 0.5); costText.y = 30; digimonButton.addChild(costText); // Create functionality for each button (function (digimonData, button, background, costText) { button.update = function () { var canAfford = score >= digimonData.cost; var hasSpace = alliedUnits.length < maxAlliedUnits; var isAvailable = !summonCooldown || LK.ticks - lastSummonTime > summonCooldown; if (canAfford && hasSpace && isAvailable) { background.tint = 0x4444FF; button.alpha = 1.0; } else { background.tint = 0x666666; button.alpha = 0.7; } }; button.down = function () { var canAfford = score >= digimonData.cost; var hasSpace = alliedUnits.length < maxAlliedUnits; var isAvailable = !summonCooldown || LK.ticks - lastSummonTime > summonCooldown; if (!canAfford) { var notification = game.addChild(new Notification("Not enough security points for " + digimonData.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (!hasSpace) { var notification = game.addChild(new Notification("¡Límite de aliados alcanzado!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (!isAvailable) { var remainingCooldown = Math.ceil((summonCooldown - (LK.ticks - lastSummonTime)) / 60); var notification = game.addChild(new Notification("Summon cooldown: " + remainingCooldown + "s")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { // Summon the Digimon score -= digimonData.cost; updateUI(); var newUnit = new DigimonUnit(digimonData.id, 1); enemyLayerTop.addChild(newUnit); // Add to top layer so they appear above enemies alliedUnits.push(newUnit); lastSummonTime = LK.ticks; // Play summon sound for all Digimon LK.getSound('voiceSummon').play(); var notification = game.addChild(new Notification(digimonData.name + " summoned!")); notification.x = 2048 / 2; notification.y = grid.height - 50; self.hide(); } }; })(digimon, digimonButton, buttonBg, costText); } } // Close button var closeButton = new Container(); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; self.addChild(closeButton); closeButton.down = function () { self.hide(); }; self.show = function (filterLevel) { // If a filterLevel is provided, filter Digimon by evolution level self._filterLevel = filterLevel || null; updateDigimonButtons(); self.visible = true; tween(self, { y: 2732 - 300 }, { duration: 300, easing: tween.backOut }); }; self.hide = function () { self._filterLevel = null; // Reset filter when closing tween(self, { y: 2732 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { self.visible = false; } }); }; self.update = function () { // Update all button states for (var i = 0; i < itemsContainer.children.length; i++) { var button = itemsContainer.children[i]; if (button.update) button.update(); } }; return self; }); var DigimonUnit = Container.expand(function (type, level) { var self = Container.call(this); self.type = type || 'koromon'; self.level = level || 1; self.health = 100 + (self.level - 1) * 50; self.maxHealth = self.health; self.damage = 20 + (self.level - 1) * 10; self.speed = 0.8; self.range = CELL_SIZE * 1.5; self.attackType = 'melee'; // 'melee', 'ranged', 'area' self.lastAttacked = 0; self.attackRate = 90; // Frames between attacks self.targetEnemy = null; self.isDead = false; // --- Digimon UI contextual elements --- var digimonNames = { koromon: "Koromon", tsunomon: "Tsunomon", greymon: "Greymon", garurumon: "Garurumon", kabuterimon: "Kabuterimon", metalgreymon: "MetalGreymon", weregarurumon: "WereGarurumon", megakabuterimon: "MegaKabuterimon", wargreymon: "WarGreymon" }; var digimonLevels = { koromon: "Rookie", tsunomon: "Rookie", greymon: "Champion", garurumon: "Champion", kabuterimon: "Champion", metalgreymon: "Ultimate", weregarurumon: "Ultimate", megakabuterimon: "Ultimate", wargreymon: "Mega" }; var digimonDescriptions = { koromon: "Rookie Melee Fighter", tsunomon: "Rookie Ranged Fighter", greymon: "Champion Fire Dragon", garurumon: "Champion Ice Wolf", kabuterimon: "Champion Electric Insect", metalgreymon: "Ultimate Cyborg Dragon", weregarurumon: "Ultimate Beast Warrior", megakabuterimon: "Ultimate Giant Insect", wargreymon: "Mega Dragon Warrior" }; var digimonColor = { koromon: 0xFFAAAA, tsunomon: 0xAAAAFF, greymon: 0xFF6600, garurumon: 0x0066FF, kabuterimon: 0x33CC00, metalgreymon: 0xFF3300, weregarurumon: 0x0066CC, megakabuterimon: 0x228B22, wargreymon: 0xFFCC00 }; // Set properties based on Digimon type and level switch (self.type) { case 'koromon': self.attackType = 'melee'; self.damage = 15 + (self.level - 1) * 8; break; case 'tsunomon': self.attackType = 'ranged'; self.damage = 12 + (self.level - 1) * 6; self.range = CELL_SIZE * 2; break; case 'greymon': self.attackType = 'area'; self.damage = 25 + (self.level - 1) * 15; self.health = 150 + (self.level - 1) * 75; break; case 'garurumon': self.attackType = 'ranged'; self.damage = 20 + (self.level - 1) * 12; self.range = CELL_SIZE * 2.5; self.speed = 1.2; break; case 'kabuterimon': self.attackType = 'area'; self.damage = 22 + (self.level - 1) * 13; self.health = 140 + (self.level - 1) * 60; self.range = CELL_SIZE * 2; break; case 'metalgreymon': self.attackType = 'area'; self.damage = 40 + (self.level - 1) * 25; self.health = 250 + (self.level - 1) * 100; self.range = CELL_SIZE * 2; break; case 'weregarurumon': self.attackType = 'ranged'; self.damage = 38 + (self.level - 1) * 20; self.health = 220 + (self.level - 1) * 90; self.range = CELL_SIZE * 3; self.speed = 1.5; break; case 'megakabuterimon': self.attackType = 'area'; self.damage = 36 + (self.level - 1) * 18; self.health = 230 + (self.level - 1) * 90; self.range = CELL_SIZE * 2.2; break; case 'wargreymon': self.attackType = 'area'; self.damage = 60 + (self.level - 1) * 40; self.health = 400 + (self.level - 1) * 150; self.range = CELL_SIZE * 2.5; self.speed = 1.0; break; } self.maxHealth = self.health; // Use correct asset for all allied units var digimonAssetMap = { koromon: 'agumon', tsunomon: 'gabumon', greymon: 'greymon', garurumon: 'garurumon', kabuterimon: 'kabuterimon', metalgreymon: 'metalgreymon', weregarurumon: 'weregarurumon', megakabuterimon: 'megakabuterimon', wargreymon: 'wargreymon' }; var assetId = digimonAssetMap[self.type] || 'agumon'; var unitGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Tint and scale based on type switch (self.type) { case 'koromon': unitGraphics.tint = 0xFFAAAA; break; case 'tsunomon': unitGraphics.tint = 0xAAAAFF; break; case 'greymon': unitGraphics.tint = 0xFF6600; unitGraphics.scaleX = 1.3; unitGraphics.scaleY = 1.3; break; case 'garurumon': unitGraphics.tint = 0x0066FF; unitGraphics.scaleX = 1.3; unitGraphics.scaleY = 1.3; break; case 'kabuterimon': unitGraphics.tint = 0x33CC00; unitGraphics.scaleX = 1.2; unitGraphics.scaleY = 1.2; break; case 'metalgreymon': unitGraphics.tint = 0xFF3300; unitGraphics.scaleX = 1.6; unitGraphics.scaleY = 1.6; break; case 'weregarurumon': unitGraphics.tint = 0x0066CC; unitGraphics.scaleX = 1.6; unitGraphics.scaleY = 1.6; break; case 'megakabuterimon': unitGraphics.tint = 0x228B22; unitGraphics.scaleX = 1.6; unitGraphics.scaleY = 1.6; break; case 'wargreymon': unitGraphics.tint = 0xFFCC00; unitGraphics.scaleX = 2.0; unitGraphics.scaleY = 2.0; break; } // --- Level badge (top left of unit) --- var badgeColors = { Rookie: 0x00BFFF, Champion: 0x32CD32, Ultimate: 0xFFD700, Mega: 0xFF4500 }; var levelName = digimonLevels[self.type] || "Rookie"; var badgeColor = badgeColors[levelName] || 0x00BFFF; var badge = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); badge.width = 44; badge.height = 44; badge.x = -unitGraphics.width / 2 + 22; badge.y = -unitGraphics.height / 2 + 22; badge.tint = badgeColor; var badgeText = new Text2(levelName.charAt(0), { size: 32, fill: 0xffffff, weight: 800 }); badgeText.anchor.set(0.5, 0.5); badgeText.x = badge.x; badgeText.y = badge.y; self.addChild(badge); self.addChild(badgeText); // --- Name popup (on summon, above unit) --- var namePopup = new Text2(digimonNames[self.type] || self.type, { size: 60, fill: 0xffffff, weight: 800 }); namePopup.anchor.set(0.5, 1.0); namePopup.x = 0; namePopup.y = -unitGraphics.height / 2 - 30; namePopup.alpha = 0; self.addChild(namePopup); // Animate name popup on spawn tween(namePopup, { alpha: 1, y: namePopup.y - 30 }, { duration: 350, easing: tween.easeOut, onFinish: function onFinish() { tween(namePopup, { alpha: 0, y: namePopup.y - 60 }, { duration: 700, delay: 700, easing: tween.easeIn, onFinish: function onFinish() { namePopup.visible = false; } }); } }); // Play summon sound and effect for all Digimon if (typeof LK !== "undefined" && LK.getSound) { LK.getSound('voiceSummon').play(); } LK.effects.flashObject(self, 0x00FF00, 400); // --- Tooltip on touch (shows name, level, description) --- var tooltip = new Container(); tooltip.visible = false; var tooltipBg = tooltip.attachAsset('notification', { anchorX: 0.5, anchorY: 1.0 }); tooltipBg.width = 340; tooltipBg.height = 120; tooltipBg.tint = 0x222222; tooltipBg.alpha = 0.95; var tooltipName = new Text2(digimonNames[self.type] || self.type, { size: 38, fill: 0xffffff, weight: 800 }); tooltipName.anchor.set(0.5, 0); tooltipName.y = -50; var tooltipLevel = new Text2(levelName, { size: 28, fill: badgeColor, weight: 800 }); tooltipLevel.anchor.set(0.5, 0); tooltipLevel.y = -15; var tooltipDesc = new Text2(digimonDescriptions[self.type] || "", { size: 24, fill: 0xcccccc, weight: 400 }); tooltipDesc.anchor.set(0.5, 0); tooltipDesc.y = 15; tooltip.addChild(tooltipBg); tooltip.addChild(tooltipName); tooltip.addChild(tooltipLevel); tooltip.addChild(tooltipDesc); tooltip.x = 0; tooltip.y = -unitGraphics.height / 2 - 10; self.addChild(tooltip); // Health bar var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -unitGraphics.height / 2 - 15; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - 0.5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; // Movement properties self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = null; self.pathTargets = []; // Initialize at goal positions (moving towards spawns) if (grid.goals && grid.goals.length > 0) { var startGoal = grid.goals[Math.floor(Math.random() * grid.goals.length)]; self.cellX = startGoal.x; self.cellY = startGoal.y; self.currentCellX = startGoal.x; self.currentCellY = startGoal.y; self.x = grid.x + self.currentCellX * CELL_SIZE; self.y = grid.y + self.currentCellY * CELL_SIZE; } // --- Touch interaction for tooltip --- self.down = function () { tooltip.visible = true; tooltip.alpha = 0; tween(tooltip, { alpha: 1 }, { duration: 120, easing: tween.easeOut }); // Hide after 2 seconds LK.setTimeout(function () { tween(tooltip, { alpha: 0 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { tooltip.visible = false; } }); }, 2000); }; self.findNearbyEnemies = function () { var nearbyEnemies = []; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.range) { nearbyEnemies.push(enemy); } } return nearbyEnemies; }; self.attack = function (targetEnemy) { if (LK.ticks - self.lastAttacked < self.attackRate) return; self.lastAttacked = LK.ticks; switch (self.attackType) { case 'melee': // Direct damage targetEnemy.health -= self.damage; if (targetEnemy.health <= 0) targetEnemy.health = 0;else targetEnemy.healthBar.width = targetEnemy.health / targetEnemy.maxHealth * 70; // Visual effect var effect = new EffectIndicator(targetEnemy.x, targetEnemy.y, 'sniper'); game.addChild(effect); break; case 'ranged': // Create projectile var bullet = new Bullet(self.x, self.y, targetEnemy, self.damage, 8); bullet.type = 'sniper'; bullet.children[0].tint = 0x00AAFF; game.addChild(bullet); bullets.push(bullet); targetEnemy.bulletsTargetingThis.push(bullet); break; case 'area': // Area damage to all enemies in range var nearbyEnemies = self.findNearbyEnemies(); for (var i = 0; i < nearbyEnemies.length; i++) { var enemy = nearbyEnemies[i]; enemy.health -= self.damage; if (enemy.health <= 0) enemy.health = 0;else enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } // Visual splash effect var splashEffect = new EffectIndicator(self.x, self.y, 'splash'); game.addChild(splashEffect); break; } }; self.update = function () { if (self.isDead) return; // --- Visual feedback for status effects (UX/UI improvement) --- if (self.burning && LK.ticks % 45 === 0) { var burnFx = new EffectIndicator(self.x, self.y, 'burn'); if (game && game.addChild) game.addChild(burnFx); } if (self.frozen && LK.ticks % 30 === 0) { var freezeFx = new EffectIndicator(self.x, self.y, 'freeze'); if (game && game.addChild) game.addChild(freezeFx); } if (self.poisoned && LK.ticks % 30 === 0) { var poisonFx = new EffectIndicator(self.x, self.y, 'poison'); if (game && game.addChild) game.addChild(poisonFx); } if (self.paralyzed && LK.ticks % 30 === 0) { var paralyzeFx = new EffectIndicator(self.x, self.y, 'paralyze'); if (game && game.addChild) game.addChild(paralyzeFx); } if (self.moist && LK.ticks % 60 === 0) { var moistFx = new EffectIndicator(self.x, self.y, 'moist'); if (game && game.addChild) game.addChild(moistFx); } if (self.healing && LK.ticks % 30 === 0) { var healFx = new EffectIndicator(self.x, self.y, 'heal'); if (game && game.addChild) game.addChild(healFx); } // Update health bar if (self.health <= 0) { self.isDead = true; self.healthBar.width = 0; // Show disappearance animation (fade out and scale up) tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 600, easing: tween.easeIn, onFinish: function onFinish() { // Remove from allied units array and destroy after delay var unitIndex = alliedUnits.indexOf(self); if (unitIndex !== -1) alliedUnits.splice(unitIndex, 1); self.destroy(); } }); // Show notification of defeat var defeatNote = game.addChild(new Notification((digimonNames[self.type] || self.type) + " defeated!")); defeatNote.x = 2048 / 2; defeatNote.y = grid.height - 120; return; } self.healthBar.width = self.health / self.maxHealth * 70; // Find and attack nearby enemies var nearbyEnemies = self.findNearbyEnemies(); if (nearbyEnemies.length > 0) { // Stop and attack closest enemy var closestEnemy = nearbyEnemies[0]; var closestDistance = Infinity; for (var i = 0; i < nearbyEnemies.length; i++) { var enemy = nearbyEnemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } self.targetEnemy = closestEnemy; self.attack(closestEnemy); // Face the enemy var dx = closestEnemy.x - self.x; var dy = closestEnemy.y - self.y; var angle = Math.atan2(dy, dx); unitGraphics.rotation = angle; return; // Don't move while attacking } self.targetEnemy = null; // --- Movement along the path toward enemies (from goal to spawn) --- if (!self.currentTarget) { var cell = grid.getCell(Math.round(self.currentCellX), Math.round(self.currentCellY)); if (cell && cell.targets && cell.targets.length > 0) { var bestTarget = null; var highestScore = -1; for (var i = 0; i < cell.targets.length; i++) { var target = cell.targets[i]; if (target.score > highestScore) { highestScore = target.score; bestTarget = target; } } self.currentTarget = bestTarget; } } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < self.speed * gameSpeed) { self.currentCellX = self.currentTarget.x; self.currentCellY = self.currentTarget.y; self.cellX = Math.round(self.currentCellX); self.cellY = Math.round(self.currentCellY); self.currentTarget = null; return; } var angle = Math.atan2(oy, ox); self.currentCellX += Math.cos(angle) * self.speed * gameSpeed; self.currentCellY += Math.sin(angle) * self.speed * gameSpeed; // Face movement direction unitGraphics.rotation = angle; } // Update position self.x = grid.x + self.currentCellX * CELL_SIZE; self.y = grid.y + self.currentCellY * CELL_SIZE; // Check if reached spawn area (remove unit) if (self.currentCellY <= 3) { var unitIndex = alliedUnits.indexOf(self); if (unitIndex !== -1) alliedUnits.splice(unitIndex, 1); self.destroy(); } }; return self; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'burn': effectGraphics.tint = 0xFF4400; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.2; break; case 'freeze': effectGraphics.tint = 0x66CCFF; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.3; break; case 'paralyze': effectGraphics.tint = 0xFFFF00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.1; break; case 'moist': effectGraphics.tint = 0x0099CC; effectGraphics.width = effectGraphics.height = CELL_SIZE * 0.9; break; case 'heal': effectGraphics.tint = 0x00FF88; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.4; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; self.worldNumber = currentWorld; // Track which world this enemy belongs to // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'normal': default: // Normal enemy uses default values break; } // --- ENEMY ATTACK TOWER LOGIC --- // Enemies will attack towers if they are adjacent or on the same cell self.attackTowerCooldown = 0; self.attackTowerRate = 60; // Try to attack a tower every 1 second self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; var slowFx = new EffectIndicator(self.x, self.y, 'slow'); if (game && game.addChild) game.addChild(slowFx); } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; var poisonFx = new EffectIndicator(self.x, self.y, 'poison'); if (game && game.addChild) game.addChild(poisonFx); } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle burn effect (Agumon line) if (self.burning) { if (LK.ticks % 45 === 0) { // Every 0.75 seconds var burnDamage = self.burnDamage; // Reduce burn damage if enemy is moist if (self.moist && self.fireResistance) { burnDamage *= self.fireResistance; } self.health -= burnDamage; if (self.health <= 0) self.health = 0;else self.healthBar.width = self.health / self.maxHealth * 70; var burnFx = new EffectIndicator(self.x, self.y, 'burn'); if (game && game.addChild) game.addChild(burnFx); } self.burnDuration--; if (self.burnDuration <= 0) { self.burning = false; } } // Handle freeze effect (Gabumon line) if (self.frozen) { if (LK.ticks % 30 === 0) { var freezeFx = new EffectIndicator(self.x, self.y, 'freeze'); if (game && game.addChild) game.addChild(freezeFx); } self.frozenDuration--; if (self.frozenDuration <= 0) { self.frozen = false; if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } } // Handle paralysis effect (Tentomon line) if (self.paralyzed) { if (LK.ticks % 30 === 0) { var paralyzeFx = new EffectIndicator(self.x, self.y, 'paralyze'); if (game && game.addChild) game.addChild(paralyzeFx); } self.paralyzeDuration--; if (self.paralyzeDuration <= 0) { self.paralyzed = false; if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } } // Handle moisture effect (Gomamon line) if (self.moist) { if (LK.ticks % 60 === 0) { var moistFx = new EffectIndicator(self.x, self.y, 'moist'); if (game && game.addChild) game.addChild(moistFx); } self.moistDuration--; if (self.moistDuration <= 0) { self.moist = false; self.fireResistance = 1.0; self.freezeVulnerability = 1.0; } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Combine poison (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } // --- ENEMY ATTACK TOWER LOGIC --- // Enemies will attack towers if they are adjacent or on the same cell if (!self.isFlying && !self.isBoss) { self.attackTowerCooldown--; if (self.attackTowerCooldown <= 0) { // Check for towers in adjacent cells (including current cell) var foundTower = null; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; // Check if tower is in a 2x2 area adjacent to enemy's cell var dx = Math.abs(tower.gridX + 0.5 - self.cellX); var dy = Math.abs(tower.gridY + 0.5 - self.cellY); if (dx <= 1.5 && dy <= 1.5) { foundTower = tower; break; } } if (foundTower) { // Deal damage to the tower var baseDamage = 3; // Increase damage with world and wave var worldMultiplier = 1 + (self.worldNumber - 1) * 0.2; var waveMultiplier = 1 + currentWave * 0.03; var totalDamage = Math.ceil(baseDamage * worldMultiplier * waveMultiplier); foundTower.health = Math.max(0, foundTower.health - totalDamage); foundTower.towerHealthBar.width = foundTower.health / foundTower.maxHealth * 76; // Play system damage sound LK.getSound('systemDamage').play(); // Show damage indicator var damageIndicator = new Notification("-" + totalDamage + " Tower Health!"); damageIndicator.x = foundTower.x; damageIndicator.y = foundTower.y - 80; if (game && game.addChild) game.addChild(damageIndicator); // If tower destroyed, remove from game if (foundTower.health <= 0) { var idx = towers.indexOf(foundTower); if (idx !== -1) towers.splice(idx, 1); if (foundTower.parent) foundTower.parent.removeChild(foundTower); } self.attackTowerCooldown = Math.max(30, self.attackTowerRate - Math.floor(currentWave * 0.5)); // Faster attacks as game progresses } else { self.attackTowerCooldown = 15; // Check again soon } } } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; // Apply world-based scaling to all enemy types self.applyWorldScaling = function () { var worldMultiplier = 1; var speedMultiplier = 1; // Scale stats based on world progression switch (self.worldNumber) { case 1: // Forest - Base stats worldMultiplier = 1; speedMultiplier = 1; break; case 2: // Desert - 25% more health, 10% faster worldMultiplier = 1.25; speedMultiplier = 1.1; break; case 3: // Glacier - 50% more health, slightly slower worldMultiplier = 1.5; speedMultiplier = 0.95; break; case 4: // Village - 75% more health, 15% faster worldMultiplier = 1.75; speedMultiplier = 1.15; break; case 5: // Tech Lab - Double health, 20% faster worldMultiplier = 2.0; speedMultiplier = 1.2; break; case 6: // Inferno - Triple health, 25% faster worldMultiplier = 3.0; speedMultiplier = 1.25; break; default: worldMultiplier = 1; speedMultiplier = 1; } // Apply scaling self.maxHealth = Math.floor(self.maxHealth * worldMultiplier); self.speed *= speedMultiplier; }; // Apply world scaling self.applyWorldScaling(); if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this virus type var assetId = 'virus'; if (self.isBoss) { // Use world-specific boss assets switch (self.worldNumber) { case 1: assetId = 'boss_snorlax'; break; case 2: assetId = 'boss_rhydon'; break; case 3: assetId = 'boss_articuno'; break; case 4: assetId = 'boss_machamp'; break; case 5: assetId = 'boss_groudon'; break; case 6: assetId = 'boss_mewtwo'; break; default: assetId = 'virus'; } } else if (self.type !== 'normal') { assetId = 'virus_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies (but less since they already have larger base assets) if (self.isBoss) { enemyGraphics.scaleX = 1.2; enemyGraphics.scaleY = 1.2; } // Apply world-based color tinting system self.getWorldTint = function () { var baseTint = 0xFFFFFF; // Default white tint // Get base color based on world switch (self.worldNumber) { case 1: // Forest - Green tints baseTint = 0x90EE90; break; case 2: // Desert - Sand/yellow tints baseTint = 0xF4A460; break; case 3: // Glacier - Blue/ice tints baseTint = 0xADD8E6; break; case 4: // Village - Brown/earth tints baseTint = 0xD2B48C; break; case 5: // Tech Lab - Metallic/silver tints baseTint = 0xC0C0C0; break; case 6: // Inferno - Red/fire tints baseTint = 0xFF6B6B; break; default: baseTint = 0xFFFFFF; } // Modify base tint slightly based on enemy type while keeping world theme switch (self.type) { case 'fast': // Make it slightly more blue-ish while keeping world color baseTint = tween.linear(baseTint, 0x0080FF, 0.3); break; case 'immune': // Make it slightly more red-ish while keeping world color baseTint = tween.linear(baseTint, 0xFF4444, 0.3); break; case 'flying': // Make it brighter while keeping world color baseTint = Math.min(0xFFFFFF, baseTint + 0x202020); break; case 'swarm': // Make it slightly darker while keeping world color baseTint = Math.max(0x404040, baseTint - 0x202020); break; } return baseTint; }; // Apply world-based tinting var worldTint = self.getWorldTint(); enemyGraphics.tint = worldTint; // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle burn effect (Agumon line) if (self.burning) { if (LK.ticks % 45 === 0) { // Every 0.75 seconds var burnDamage = self.burnDamage; // Reduce burn damage if enemy is moist if (self.moist && self.fireResistance) { burnDamage *= self.fireResistance; } self.health -= burnDamage; if (self.health <= 0) self.health = 0;else self.healthBar.width = self.health / self.maxHealth * 70; } self.burnDuration--; if (self.burnDuration <= 0) { self.burning = false; } } // Handle freeze effect (Gabumon line) if (self.frozen) { self.frozenDuration--; if (self.frozenDuration <= 0) { self.frozen = false; if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } } // Handle paralysis effect (Tentomon line) if (self.paralyzed) { self.paralyzeDuration--; if (self.paralyzeDuration <= 0) { self.paralyzed = false; if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } } // Handle moisture effect (Gomamon line) if (self.moist) { self.moistDuration--; if (self.moistDuration <= 0) { self.moist = false; self.fireResistance = 1.0; self.freezeVulnerability = 1.0; } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Combine poison (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ // Create world-based maze layout self.generateMazeForWorld = function (worldNumber) { // Clear existing maze for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { self.cells[i][j].type = 1; // Default to wall } } self.spawns = []; self.goals = []; // Always create entry area (top 5 rows) with 3-block wide path for (var i = 0; i < gridWidth; i++) { for (var j = 0; j <= 4; j++) { if (i >= 11 && i <= 13) { self.cells[i][j].type = 0; // Open path in center (3 blocks wide) if (j === 0) { self.cells[i][j].type = 2; // Spawn points self.spawns.push(self.cells[i][j]); } } else { self.cells[i][j].type = 1; // Walls on sides } } } // Always create exit area (bottom 5 rows) with 3-block wide path for (var i = 0; i < gridWidth; i++) { for (var j = gridHeight - 5; j < gridHeight; j++) { if (i >= 11 && i <= 13) { self.cells[i][j].type = 0; // Open path in center (3 blocks wide) if (j === gridHeight - 1) { self.cells[i][j].type = 3; // Goal points self.goals.push(self.cells[i][j]); } } else { self.cells[i][j].type = 1; // Walls on sides } } } // Create classic square-cornered labyrinth with generous tower placement areas // Simple pattern: Create a winding path with 90-degree turns and wide wall corridors // Path layout: Start center-top, go down, turn right, down, turn left, repeat creating a serpentine pattern // Main path coordinates (center of 3-block wide paths) // Reduced to only 2 curves for a simpler path var pathCenterX = 12; // Center column var pathPattern = [ // Start: go down from entry { x: pathCenterX, startY: 5, endY: 12, type: 'vertical' }, // First curve: turn left { y: 12, startX: pathCenterX, endX: 4, type: 'horizontal' }, // Go down on left side { x: 4, startY: 12, endY: 20, type: 'vertical' }, // Second curve: turn right to center { y: 20, startX: 4, endX: pathCenterX, type: 'horizontal' }, // Final path to exit { x: pathCenterX, startY: 20, endY: gridHeight - 5, type: 'vertical' }]; // Crear los segmentos del camino con 3 bloques de grosor (camino) y dejar espacio extra de 1 bloque a cada lado para asegurar zonas de 2x2 para torretas for (var p = 0; p < pathPattern.length; p++) { var segment = pathPattern[p]; if (segment.type === 'vertical') { var startY = Math.min(segment.startY, segment.endY); var endY = Math.max(segment.startY, segment.endY); for (var y = startY; y <= endY; y++) { // Camino de 3 bloques de ancho for (var offset = -1; offset <= 1; offset++) { var pathX = segment.x + offset; if (pathX >= 0 && pathX < gridWidth && y >= 0 && y < gridHeight) { self.cells[pathX][y].type = 0; } } // Dejar espacio extra de 1 bloque a cada lado del camino para permitir torretas 2x2 for (var extra = -2; extra <= 2; extra += 4) { var sideX = segment.x + extra; if (sideX >= 0 && sideX < gridWidth && y >= 0 && y < gridHeight) { // Solo marcar como espacio de torre si no es camino ni spawn/goal if (self.cells[sideX][y].type === 1) { // No cambiar si ya es camino/spawn/goal self.cells[sideX][y].type = 1; // Mantener como muro, pero dejarlo para posible torre } } } // Asegurar espacio de 2x2 para torretas a la izquierda y derecha del camino for (var offsetY = 0; offsetY <= 1; offsetY++) { for (var extra = -2; extra <= 2; extra += 4) { var baseX = segment.x + extra; var baseY = y + offsetY; if (baseX >= 0 && baseX + 1 < gridWidth && baseY >= 0 && baseY + 1 < gridHeight) { // Solo marcar como espacio de torre si ambos son muro if (self.cells[baseX][baseY].type === 1 && self.cells[baseX + 1][baseY].type === 1 && self.cells[baseX][baseY + 1].type === 1 && self.cells[baseX + 1][baseY + 1].type === 1) { // Marcar como espacio de torre (type 1) para permitir torretas 2x2 self.cells[baseX][baseY].type = 1; self.cells[baseX + 1][baseY].type = 1; self.cells[baseX][baseY + 1].type = 1; self.cells[baseX + 1][baseY + 1].type = 1; } } } } } } else if (segment.type === 'horizontal') { var startX = Math.min(segment.startX, segment.endX); var endX = Math.max(segment.startX, segment.endX); for (var x = startX; x <= endX; x++) { // Camino de 3 bloques de alto for (var offset = -1; offset <= 1; offset++) { var pathY = segment.y + offset; if (x >= 0 && x < gridWidth && pathY >= 0 && pathY < gridHeight) { self.cells[x][pathY].type = 0; } } // Dejar espacio extra de 1 bloque arriba y abajo del camino para permitir torretas 2x2 for (var extra = -2; extra <= 2; extra += 4) { var sideY = segment.y + extra; if (x >= 0 && x < gridWidth && sideY >= 0 && sideY < gridHeight) { if (self.cells[x][sideY].type === 1) { self.cells[x][sideY].type = 1; } } } // Asegurar espacio de 2x2 para torretas arriba y abajo del camino for (var offsetX = 0; offsetX <= 1; offsetX++) { for (var extra = -2; extra <= 2; extra += 4) { var baseX = x + offsetX; var baseY = segment.y + extra; if (baseX >= 0 && baseX + 1 < gridWidth && baseY >= 0 && baseY + 1 < gridHeight) { if (self.cells[baseX][baseY].type === 1 && self.cells[baseX + 1][baseY].type === 1 && self.cells[baseX][baseY + 1].type === 1 && self.cells[baseX + 1][baseY + 1].type === 1) { // Marcar como espacio de torre (type 1) para permitir torretas 2x2 self.cells[baseX][baseY].type = 1; self.cells[baseX + 1][baseY].type = 1; self.cells[baseX][baseY + 1].type = 1; self.cells[baseX + 1][baseY + 1].type = 1; } } } } } } } // Asegurar conexiones suaves en las intersecciones y dejar espacio de 2x2 para torret }; // Generate maze for current world var world = Math.ceil(currentWave / 9); if (world < 1) world = 1; if (world > 6) world = 6; self.generateMazeForWorld(world); // Apply the maze layout to cells for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = cell.type; // Use the type set by generateMazeForWorld cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; // Only create debug cells for visible areas and reduce frequency if (j > 3 && j <= gridHeight - 4 && (i + j) % 2 === 0) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; // Simplified pathfinding - only check cardinal directions for better performance var targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward with speed multiplier enemy.currentCellY += enemy.speed * gameSpeed; // Ensure enemy moves towards the center of the 3-block wide path (x=12) var pathCenterX = 12; if (enemy.currentCellX !== pathCenterX) { var xDiff = pathCenterX - enemy.currentCellX; var moveSpeed = Math.min(Math.abs(xDiff), enemy.speed * 0.5 * gameSpeed); enemy.currentCellX += xDiff > 0 ? moveSpeed : -moveSpeed; } // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) var angle = Math.PI / 2; if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update enemy's position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // If enemy has now reached the entry area, update cell coordinates if (enemy.currentCellY >= 4) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed * gameSpeed; enemy.currentCellY += Math.sin(angle) * enemy.speed * gameSpeed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } // Initialize stuck tracking for enemy if (enemy.lastPosition === undefined) { enemy.lastPosition = { x: enemy.currentCellX, y: enemy.currentCellY }; enemy.stuckCounter = 0; enemy.lastMovementTime = LK.ticks; } // Check if enemy is stuck (hasn't moved significantly in a while) var currentPos = { x: enemy.currentCellX, y: enemy.currentCellY }; var distanceMoved = Math.sqrt(Math.pow(currentPos.x - enemy.lastPosition.x, 2) + Math.pow(currentPos.y - enemy.lastPosition.y, 2)); // If enemy hasn't moved much in the last 60 ticks (1 second), consider it stuck if (distanceMoved < 0.1 && LK.ticks - enemy.lastMovementTime > 60) { enemy.stuckCounter++; enemy.lastMovementTime = LK.ticks; // If stuck for too long, try to find alternative path if (enemy.stuckCounter > 3) { // Reset stuck counter to prevent infinite loops enemy.stuckCounter = 0; // Try to find alternative targets from current cell if (cell.targets && cell.targets.length > 1) { // Find a different target than the current one for (var i = 0; i < cell.targets.length; i++) { var alternativeTarget = cell.targets[i]; if (alternativeTarget !== enemy.currentTarget) { enemy.currentTarget = alternativeTarget; break; } } } else { // If no alternative targets, try neighboring cells var neighbors = [cell.up, cell.right, cell.down, cell.left]; var validNeighbors = []; for (var i = 0; i < neighbors.length; i++) { var neighbor = neighbors[i]; if (neighbor && neighbor.type !== 1 && neighbor.pathId === pathId && neighbor.targets && neighbor.targets.length > 0) { validNeighbors.push(neighbor); } } if (validNeighbors.length > 0) { // Choose a random valid neighbor and use its target var randomNeighbor = validNeighbors[Math.floor(Math.random() * validNeighbors.length)]; enemy.currentTarget = randomNeighbor.targets[0]; // Move slightly towards the chosen neighbor to unstuck var neighborX = randomNeighbor.x; var neighborY = randomNeighbor.y; var unstuckAngle = Math.atan2(neighborY - enemy.currentCellY, neighborX - enemy.currentCellX); enemy.currentCellX += Math.cos(unstuckAngle) * enemy.speed * 0.5; enemy.currentCellY += Math.sin(unstuckAngle) * enemy.speed * 0.5; } } } } else if (distanceMoved >= 0.1) { // Enemy is moving, reset stuck tracking enemy.stuckCounter = 0; enemy.lastMovementTime = LK.ticks; enemy.lastPosition = { x: currentPos.x, y: currentPos.y }; } if (enemy.currentTarget) { if (cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; // Reset position tracking when reaching target enemy.lastPosition = { x: enemy.currentCellX, y: enemy.currentCellY }; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed * gameSpeed; enemy.currentCellY += Math.sin(angle) * enemy.speed * gameSpeed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var LevelSelectionMenu = Container.expand(function (worldNumber) { var self = Container.call(this); self.worldNumber = worldNumber; // Position the menu at center of screen self.x = 2048 / 2; self.y = 2732 / 2; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 1800; menuBackground.height = 1200; menuBackground.tint = 0x333333; menuBackground.alpha = 0.9; var worldNames = ["", "Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"]; var titleText = new Text2('Select Level - ' + worldNames[worldNumber], { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -400; self.addChild(titleText); var worldLevels = storage.worldLevels || { 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1 }; var unlockedLevel = worldLevels[worldNumber] || 1; // Create level buttons in a grid var buttonsPerRow = 5; var buttonWidth = 120; var buttonHeight = 80; var buttonSpacing = 160; var startX = -((buttonsPerRow - 1) * buttonSpacing) / 2; var startY = -200; for (var i = 1; i <= 10; i++) { var levelButton = new Container(); var levelButtonBg = levelButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); levelButtonBg.width = buttonWidth; levelButtonBg.height = buttonHeight; var isUnlocked = i <= unlockedLevel; levelButtonBg.tint = isUnlocked ? 0x4444FF : 0x666666; var levelButtonText = new Text2(isUnlocked ? i.toString() : "🔒", { size: 40, fill: isUnlocked ? 0xFFFFFF : 0x999999, weight: 800 }); levelButtonText.anchor.set(0.5, 0.5); levelButton.addChild(levelButtonText); // Position buttons in grid var row = Math.floor((i - 1) / buttonsPerRow); var col = (i - 1) % buttonsPerRow; levelButton.x = startX + col * buttonSpacing; levelButton.y = startY + row * 120; self.addChild(levelButton); (function (levelIndex, unlocked) { levelButton.down = function () { if (unlocked) { self.destroy(); game.startWorldLevel(self.worldNumber, levelIndex); } else { var notification = game.addChild(new Notification("Complete previous level to unlock!")); notification.x = 2048 / 2; notification.y = 2732 / 2 + 200; } }; })(i, isUnlocked); } // Back button var backButton = new Container(); var backButtonBg = backButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); backButtonBg.width = 200; backButtonBg.height = 80; backButtonBg.tint = 0x666666; var backButtonText = new Text2('Back', { size: 40, fill: 0xFFFFFF, weight: 800 }); backButtonText.anchor.set(0.5, 0.5); backButton.addChild(backButtonText); backButton.x = 0; backButton.y = 400; self.addChild(backButton); backButton.down = function () { self.destroy(); var worldSelectionMenu = new WorldSelectionMenu(); game.addChild(worldSelectionMenu); }; return self; }); var MainMenu = Container.expand(function () { var self = Container.call(this); // Position the menu at center of screen self.x = 2048 / 2; self.y = 2732 / 2; // Stop any currently playing music first LK.stopMusic(); // Start main menu music LK.playMusic('mainMenuMusic', { fade: { start: 0, end: 0.8, duration: 1500 } }); var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 1800; menuBackground.height = 1200; menuBackground.tint = 0x333333; menuBackground.alpha = 0.9; var titleText = new Text2(getText('firewallDefensors'), { size: 100, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -300; self.addChild(titleText); // Add version number below title var versionText = new Text2('v0.1', { size: 40, fill: 0xCCCCCC, weight: 400 }); versionText.anchor.set(0.5, 0.5); versionText.y = -220; self.addChild(versionText); var startButton = new Container(); var startButtonBg = startButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startButtonBg.width = 400; startButtonBg.height = 120; startButtonBg.tint = 0x00AA00; var startButtonText = new Text2(getText('startGame'), { size: 50, fill: 0xFFFFFF, weight: 800 }); startButtonText.anchor.set(0.5, 0.5); startButton.addChild(startButtonText); startButton.y = 50; self.addChild(startButton); startButton.down = function () { self.destroy(); game.startGame(); }; // Add tutorial button var tutorialButton = new Container(); var tutorialButtonBg = tutorialButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); tutorialButtonBg.width = 400; tutorialButtonBg.height = 120; tutorialButtonBg.tint = 0x00AAAA; var tutorialButtonText = new Text2(getText('tutorial'), { size: 50, fill: 0xFFFFFF, weight: 800 }); tutorialButtonText.anchor.set(0.5, 0.5); tutorialButton.addChild(tutorialButtonText); tutorialButton.y = 180; self.addChild(tutorialButton); tutorialButton.down = function () { self.destroy(); game.startTutorial(); }; // Add leaderboard button var leaderboardButton = new Container(); var leaderboardButtonBg = leaderboardButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); leaderboardButtonBg.width = 400; leaderboardButtonBg.height = 120; leaderboardButtonBg.tint = 0x4444FF; var leaderboardButtonText = new Text2('Leaderboard', { size: 50, fill: 0xFFFFFF, weight: 800 }); leaderboardButtonText.anchor.set(0.5, 0.5); leaderboardButton.addChild(leaderboardButtonText); leaderboardButton.y = 310 + 130; self.addChild(leaderboardButton); leaderboardButton.down = function () { LK.showLeaderboard(); }; // Add share achievement button var shareButton = new Container(); var shareButtonBg = shareButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); shareButtonBg.width = 400; shareButtonBg.height = 120; shareButtonBg.tint = 0x00C0C0; var shareButtonText = new Text2('Share Achievement', { size: 50, fill: 0xFFFFFF, weight: 800 }); shareButtonText.anchor.set(0.5, 0.5); shareButton.addChild(shareButtonText); shareButton.y = 310 + 260; self.addChild(shareButton); shareButton.down = function () { var shareMsg = "I just scored " + (storage.securityPoints || 0) + " points in Firewall Defensors! Can you beat my score?"; LK.shareAchievement && LK.shareAchievement(shareMsg); }; // Add language button var languageButton = new Container(); var languageButtonBg = languageButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); languageButtonBg.width = 400; languageButtonBg.height = 120; languageButtonBg.tint = 0xFF6600; var languageButtonText = new Text2(getText('language'), { size: 50, fill: 0xFFFFFF, weight: 800 }); languageButtonText.anchor.set(0.5, 0.5); languageButton.addChild(languageButtonText); languageButton.y = 310; self.addChild(languageButton); languageButton.down = function () { // Toggle language between English and Spanish var newLang = currentLanguage === 'en' ? 'es' : 'en'; setLanguage(newLang); // Recreate main menu with new language self.destroy(); var newMainMenu = new MainMenu(); game.addChild(newMainMenu); }; return self; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { // Check if we can start next wave (10 waves max per world) var worldWave = (currentWave - 1) % 10 + 1; if (currentWave === 0) worldWave = 0; // Show button during tutorial or when can start next wave var showForTutorial = waveIndicator && waveIndicator.gameStarted && currentWave === 0; var showForNextWave = waveIndicator && waveIndicator.gameStarted && worldWave < 10 && !waveInProgress && enemies.length === 0; if (showForTutorial || showForNextWave) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves && !waveInProgress && enemies.length === 0) { currentWave++; // Increment to the next wave directly // Calculate current world and level for 10-wave system currentWorld = Math.ceil(currentWave / 10); currentLevel = (currentWave - 1) % 10 + 1; waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentLevel); var enemyCount = waveIndicator.getEnemyCount(currentLevel); // Update wave counter display updateWaveCounter(); var notification = game.addChild(new Notification("Wave " + currentLevel + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; // Play wave start sound LK.getSound('waveStart').play(); } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Increase size of base for easier touch var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); switch (self.towerType) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'sniper': baseGraphics.tint = 0xFF5500; break; case 'splash': baseGraphics.tint = 0x33CC00; break; case 'slow': baseGraphics.tint = 0x9900FF; break; case 'poison': baseGraphics.tint = 0x00FFAA; break; default: baseGraphics.tint = 0xAAAAAA; } var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 60, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -25; // Position above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost + ' bits', { size: 60, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 25 + 12; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var StorySequence = Container.expand(function (worldNumber) { var self = Container.call(this); self.worldNumber = worldNumber; self.currentPanel = 0; self.panels = []; self.onComplete = null; // Position at center of screen self.x = 2048 / 2; self.y = 2732 / 2; // Semi-transparent background overlay var overlay = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); overlay.width = 2048; overlay.height = 2732; overlay.tint = 0x000000; overlay.alpha = 0.7; self.getWorldStory = function (worldNumber) { switch (worldNumber) { case 1: return [{ text: getText('story1_1'), image: 'agumon' }, { text: getText('story1_2'), image: 'gabumon' }, { text: getText('story1_3'), image: 'tentomon' }]; case 2: return [{ text: getText('story2_1'), image: 'palmon' }, { text: getText('story2_2'), image: 'gomamon' }, { text: getText('story2_3'), image: 'patamon' }]; case 3: return [{ text: getText('story3_1'), image: null }, { text: getText('story3_2'), image: null }, { text: getText('story3_3'), image: null }]; case 4: return [{ text: getText('story4_1'), image: null }, { text: getText('story4_2'), image: null }, { text: getText('story4_3'), image: null }]; case 5: return [{ text: getText('story5_1'), image: null }, { text: getText('story5_2'), image: null }, { text: getText('story5_3'), image: null }]; case 6: return [{ text: getText('story6_1'), image: null }, { text: getText('story6_2'), image: null }, { text: getText('story6_3'), image: null }]; default: return [{ text: getText('storyDefault_1'), image: null }, { text: getText('storyDefault_2'), image: null }, { text: getText('storyDefault_3'), image: null }]; } }; // World-specific story content var storyData = self.getWorldStory(worldNumber); // Create panels for (var i = 0; i < storyData.length; i++) { var panel = new ComicPanel(storyData[i].image, storyData[i].text); panel.x = (i - 1) * 650; // Position panels side by side self.addChild(panel); self.panels.push(panel); } // Navigation indicators var indicatorContainer = new Container(); indicatorContainer.y = 250; self.addChild(indicatorContainer); for (var i = 0; i < self.panels.length; i++) { var indicator = indicatorContainer.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = 20; indicator.height = 20; indicator.tint = i === 0 ? 0xffffff : 0x666666; indicator.x = (i - (self.panels.length - 1) / 2) * 40; } // Skip button var skipButton = new Container(); var skipBg = skipButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); skipBg.width = 150; skipBg.height = 60; skipBg.tint = 0x666666; var skipText = new Text2('Skip', { size: 40, fill: 0xffffff, weight: 800 }); skipText.anchor.set(0.5, 0.5); skipButton.addChild(skipText); skipButton.x = 400; skipButton.y = -300; self.addChild(skipButton); skipButton.down = function () { self.complete(); }; self.showPanel = function (index) { if (index < 0 || index >= self.panels.length) return; // Hide all panels for (var i = 0; i < self.panels.length; i++) { self.panels[i].alpha = 0; indicatorContainer.children[i].tint = 0x666666; } // Show current panel self.panels[index].show(); indicatorContainer.children[index].tint = 0xffffff; self.currentPanel = index; }; self.nextPanel = function () { if (self.currentPanel < self.panels.length - 1) { self.showPanel(self.currentPanel + 1); } else { self.complete(); } }; self.complete = function () { if (self.onComplete) { self.onComplete(); } self.destroy(); }; // Show first panel self.showPanel(0); // Auto-advance after 4 seconds or click to advance var autoAdvanceTimer = LK.setTimeout(function () { self.nextPanel(); }, 4000); self.down = function () { LK.clearTimeout(autoAdvanceTimer); autoAdvanceTimer = LK.setTimeout(function () { self.nextPanel(); }, 4000); self.nextPanel(); }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // Tower health system self.maxHealth = 100; self.health = self.maxHealth; // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case 'sniper': // Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost if (self.level === self.maxLevel) { return 12 * CELL_SIZE; // Significantly increased range for max level } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case 'splash': // Splash: base 2, +0.2 per level (max ~4 blocks at max level) return (2 + (self.level - 1) * 0.2) * CELL_SIZE; case 'rapid': // Rapid: base 2.5, +0.5 per level return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'slow': // Slow: base 3.5, +0.5 per level return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'poison': // Poison: base 3.2, +0.5 per level return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; default: // Default: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'agumon': // Fire-based attacks, burns enemies self.fireRate = 60; self.damage = 12; self.range = 3 * CELL_SIZE; self.bulletSpeed = 6; self.maxHealth = 120; // Tanky fire dragon break; case 'gabumon': // Ice-based attacks, freezes enemies self.fireRate = 55; self.damage = 10; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 6; self.maxHealth = 110; // Ice wolf durability break; case 'tentomon': // Electric paralysis attacks self.fireRate = 70; self.damage = 14; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 8; self.maxHealth = 90; // Electric insect, moderate health break; case 'palmon': // Poison attacks that spread self.fireRate = 65; self.damage = 11; self.range = 3 * CELL_SIZE; self.bulletSpeed = 5; self.maxHealth = 100; // Plant creature, balanced break; case 'gomamon': // Water/moisture attacks, weakens fire resistance self.fireRate = 60; self.damage = 9; self.range = 3.3 * CELL_SIZE; self.bulletSpeed = 5; self.maxHealth = 105; // Aquatic mammal, decent health break; case 'patamon': // Healing and support abilities self.fireRate = 80; self.damage = 8; self.range = 4 * CELL_SIZE; // Larger healing range self.bulletSpeed = 7; self.maxHealth = 85; // Angel creature, less physical but supportive break; } self.health = self.maxHealth; var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); switch (self.id) { case 'gabumon': //{93} // Blue colors for Gabumon baseGraphics.tint = 0x00AAFF; break; case 'tentomon': //{96} // Red colors for Tentomon baseGraphics.tint = 0xFF5500; break; case 'palmon': //{99} // Green colors for Palmon baseGraphics.tint = 0x33CC00; break; case 'gomamon': //{9c} // Purple colors for Gomamon baseGraphics.tint = 0x9900FF; break; case 'patamon': //{9f} // Cyan colors for Patamon baseGraphics.tint = 0x00FFAA; break; default: //{9i} // Agumon default baseGraphics.tint = 0xAAAAAA; } // Tower health bar var towerHealthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0.5, anchorY: 0.5 }); var towerHealthBar = self.attachAsset('healthBar', { anchorX: 0.5, anchorY: 0.5 }); towerHealthBarOutline.width = 80; towerHealthBarOutline.height = 8; towerHealthBarOutline.tint = 0x000000; towerHealthBar.width = 76; towerHealthBar.height = 6; towerHealthBar.tint = 0x00ff00; towerHealthBarOutline.y = towerHealthBar.y = -CELL_SIZE - 15; self.towerHealthBar = towerHealthBar; var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 6; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); var gunGraphics = gunContainer.attachAsset(self.id, { anchorX: 0.5, anchorY: 0.5 }); self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { switch (self.id) { case 'rapid': towerLevelIndicator.tint = 0x00AAFF; break; case 'sniper': towerLevelIndicator.tint = 0xFF5500; break; case 'splash': towerLevelIndicator.tint = 0x33CC00; break; case 'slow': towerLevelIndicator.tint = 0x9900FF; break; case 'poison': towerLevelIndicator.tint = 0x00FFAA; break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // Play tower upgrade sound LK.getSound('towerUpgrade').play(); // --- HEALTH UPGRADE LOGIC --- // Increase maxHealth and heal tower on upgrade var prevMaxHealth = self.maxHealth; // Health scaling per tower type switch (self.id) { case 'agumon': self.maxHealth = 120 + (self.level - 1) * 20; break; case 'gabumon': self.maxHealth = 110 + (self.level - 1) * 18; break; case 'tentomon': self.maxHealth = 90 + (self.level - 1) * 15; break; case 'palmon': self.maxHealth = 100 + (self.level - 1) * 18; break; case 'gomamon': self.maxHealth = 105 + (self.level - 1) * 17; break; case 'patamon': self.maxHealth = 85 + (self.level - 1) * 14; break; default: self.maxHealth = 100 + (self.level - 1) * 15; break; } // Heal tower to new max health on upgrade self.health = self.maxHealth; // --- DAMAGE/FIRERATE UPGRADE LOGIC --- // Tweak last upgrade so it's not overpowered if (self.id === 'rapid') { if (self.level === self.maxLevel) { // Last upgrade: only a moderate boost, not double self.fireRate = Math.max(8, 30 - self.level * 6); // was 4, now 8 min self.damage = 5 + self.level * 5; // was 10, now 5 per level self.bulletSpeed = 7 + self.level * 1.2; // was 2.4, now 1.2 per level } else { self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades self.damage = 5 + self.level * 3; self.bulletSpeed = 7 + self.level * 0.7; } } else { if (self.level === self.maxLevel) { // Last upgrade: only a moderate boost, not double self.fireRate = Math.max(10, 60 - self.level * 12); // was 5, now 10 min self.damage = 10 + self.level * 10; // was 20, now 10 per level self.bulletSpeed = 5 + self.level * 1.2; // was 2.4, now 1.2 per level } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } // Check if tower reached maximum level and award bonus if (self.level === self.maxLevel) { // Award 300 bits and security points for reaching max level setGold(gold + 300); score += 300; // Save security points to storage storage.securityPoints = score; updateUI(); var notification = game.addChild(new Notification("Max level reached! +300 bits & security points!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } return true; } else { var notification = game.addChild(new Notification("Not enough bits to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range if (distance <= self.getRange()) { // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { // Update tower health bar self.towerHealthBar.width = self.health / self.maxHealth * 76; if (self.health / self.maxHealth > 0.6) { self.towerHealthBar.tint = 0x00ff00; // Green } else if (self.health / self.maxHealth > 0.3) { self.towerHealthBar.tint = 0xffff00; // Yellow } else { self.towerHealthBar.tint = 0xff0000; // Red } self.targetEnemy = self.findTarget(); if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); gunContainer.rotation = angle; var effectiveFireRate = gameSpeed > 1 ? Math.max(1, Math.floor(self.fireRate / gameSpeed)) : self.fireRate; if (LK.ticks - self.lastFired >= effectiveFireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; // Add evolution arrow if digivice is available and tower level is appropriate function canDigivolve() { if (self.level < 2) return false; // Need at least level 2 var hasDigivice = false; if (self.level >= 2 && self.level <= 3 && storage.digiviceC) hasDigivice = true; if (self.level >= 4 && self.level <= 5 && storage.digiviceB) hasDigivice = true; if (self.level >= 6 && storage.digiviceA) hasDigivice = true; return hasDigivice; } if (canDigivolve()) { var evolutionArrow = rangeIndicator.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5 }); evolutionArrow.width = 60; evolutionArrow.height = 60; evolutionArrow.tint = 0xFF6600; evolutionArrow.x = self.getRange() + 40; evolutionArrow.y = -40; evolutionArrow.rotation = -Math.PI / 4; // Point diagonally up-right // Add glow effect evolutionArrow.alpha = 0.8; var _glowTween = function glowTween() { tween(evolutionArrow, { alpha: 1.0, scaleX: 1.2, scaleY: 1.2 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { tween(evolutionArrow, { alpha: 0.8, scaleX: 1.0, scaleY: 1.0 }, { duration: 500, easing: tween.easeInOut, onFinish: _glowTween }); } }); }; _glowTween(); } // Check if clicked on evolution arrow var clickedOnEvolutionArrow = false; if (rangeIndicator.children.length > 1) { // Has evolution arrow var evolutionArrow = rangeIndicator.children[1]; var arrowGlobalPos = rangeIndicator.toGlobal(evolutionArrow.position); var arrowX = arrowGlobalPos.x; var arrowY = arrowGlobalPos.y; if (Math.abs(x - arrowX) < 40 && Math.abs(y - arrowY) < 40) { clickedOnEvolutionArrow = true; // Trigger digivolution var evolutionCost = getTowerCost(self.id) * 2; if (gold >= evolutionCost) { setGold(gold - evolutionCost); // Apply evolution effects self.damage *= 1.5; self.fireRate = Math.max(5, Math.floor(self.fireRate * 0.8)); var notification = game.addChild(new Notification(self.id.toUpperCase() + " DIGIVOLVED!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Visual evolution effect LK.effects.flashObject(self, 0xFF6600, 1000); // Remove evolution arrow after use rangeIndicator.removeChild(evolutionArrow); } else { var notification = game.addChild(new Notification("Not enough bits to digivolve!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } } if (!clickedOnEvolutionArrow) { var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); } grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { // Patamon healing ability - heal nearby towers instead of attacking if (self.id === 'patamon' && LK.ticks % 120 === 0) { // Every 2 seconds var healingRange = self.getRange(); var healAmount = 5 + self.level * 2; var towersHealed = 0; for (var i = 0; i < towers.length; i++) { var otherTower = towers[i]; if (otherTower !== self && otherTower.health < otherTower.maxHealth) { var dx = otherTower.x - self.x; var dy = otherTower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= healingRange) { otherTower.health = Math.min(otherTower.maxHealth, otherTower.health + healAmount); otherTower.towerHealthBar.width = otherTower.health / otherTower.maxHealth * 76; towersHealed++; // Visual healing effect var healEffect = new EffectIndicator(otherTower.x, otherTower.y, 'heal'); game.addChild(healEffect); } } } if (towersHealed > 0) { // Show healing indicator on Patamon var healSelfEffect = new EffectIndicator(self.x, self.y, 'heal'); game.addChild(healSelfEffect); } } if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type and add special effects bullet.type = self.id; bullet.sourceTowerLevel = self.level; // Special attack patterns for different evolutionary lines switch (self.id) { case 'agumon': // Fire attacks that can burn enemies // Burn chance: 5% at level 1, up to 50% at max level (linear) bullet.burnChance = Math.min(0.05 + (self.level - 1) * 0.09, 0.5); break; case 'gabumon': // Ice attacks that can freeze enemies // Freeze chance: 5% at level 1, up to 50% at max level (linear) bullet.freezeChance = Math.min(0.05 + (self.level - 1) * 0.09, 0.5); break; case 'tentomon': // Electric attacks that can paralyze multiple enemies // Paralyze chance: 5% at level 1, up to 55% at max level (linear) bullet.paralyzeChance = Math.min(0.05 + (self.level - 1) * 0.10, 0.55); bullet.paralyzeArea = CELL_SIZE * (1 + self.level * 0.2); break; case 'palmon': // Poison attacks that spread to nearby enemies // Poison spread chance: 5% at level 1, up to 50% at max level (linear) bullet.poisonSpread = true; bullet.poisonSpreadChance = Math.min(0.05 + (self.level - 1) * 0.09, 0.5); bullet.poisonRadius = CELL_SIZE * (0.8 + self.level * 0.15); break; case 'gomamon': // Water attacks that make enemies wet (vulnerable to freeze, resistant to burn) bullet.moistureEffect = true; bullet.moistureDuration = 180 + self.level * 30; break; } // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0x00AAFF; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'sniper': bullet.children[0].tint = 0xFF5500; bullet.children[0].width = 15; bullet.children[0].height = 15; break; case 'splash': bullet.children[0].tint = 0x33CC00; bullet.children[0].width = 40; bullet.children[0].height = 40; break; case 'slow': bullet.children[0].tint = 0x9900FF; bullet.children[0].width = 35; bullet.children[0].height = 35; break; case 'poison': bullet.children[0].tint = 0x00FFAA; bullet.children[0].width = 35; bullet.children[0].height = 35; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; // Mark cells as occupied by tower (type 1 = wall/occupied) for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; } } } self.refreshCellsInRange(); }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; self.hasEnoughGold = true; var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; break; case 'sniper': previewGraphics.tint = 0xFF5500; break; case 'splash': previewGraphics.tint = 0x33CC00; break; case 'slow': previewGraphics.tint = 0x9900FF; break; case 'poison': previewGraphics.tint = 0x00FFAA; break; default: previewGraphics.tint = 0xAAAAAA; } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; // Check if tower would be placed within valid grid bounds if (self.gridX < 0 || self.gridY < 0 || self.gridX + 1 >= grid.cells.length || self.gridY + 1 >= grid.cells[0].length) { validGridPlacement = false; } else { // Check if all 4 cells for the 2x2 tower are available (not on enemy path) for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); // Only allow placement on wall cells (type 1) - not on path, spawn, or goal if (!cell || cell.type !== 1) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; // Remove enemy blocking detection since towers can only be placed on wall tiles // which enemies cannot occupy anyway self.blockedByEnemy = false; self.canPlace = validGridPlacement; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; self.checkPlacement(); }; return self; }); var TutorialSequence = Container.expand(function () { var self = Container.call(this); console.log("Starting interactive tutorial sequence"); self.currentStep = 0; self.onComplete = null; self.tutorialActive = true; self.waitingForUserAction = false; self.stepCompleted = false; // Tutorial steps data self.tutorialSteps = [{ textKey: 'tutorialWelcome', action: 'intro' }, { textKey: 'tutorialStep1', action: 'placeTower', completeTextKey: 'tutorialStep1Complete' }, { textKey: 'tutorialStep2', action: 'startWave', completeTextKey: 'tutorialStep2Complete' }, { textKey: 'tutorialStep3', action: 'upgradeTower', completeTextKey: 'tutorialStep3Complete' }, { textKey: 'tutorialCompleted', action: 'finish' }]; // Semi-transparent background overlay var overlay = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); overlay.width = 2048; overlay.height = 2732; overlay.tint = 0x000000; overlay.alpha = 0.4; // Instruction panel var instructionBg = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); instructionBg.width = 1600; instructionBg.height = 400; instructionBg.tint = 0x222222; instructionBg.alpha = 0.95; instructionBg.x = 2048 / 2; instructionBg.y = 400; var instructionText = new Text2(getText(self.tutorialSteps[0].textKey), { size: 50, fill: 0xFFFFFF, weight: 600 }); instructionText.anchor.set(0.5, 0.5); instructionText.wordWrap = true; instructionText.wordWrapWidth = 1500; instructionText.x = 2048 / 2; instructionText.y = 400; self.addChild(instructionText); // Next button var nextButton = new Container(); var nextBg = nextButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); nextBg.width = 250; nextBg.height = 80; nextBg.tint = 0x00AA00; var nextText = new Text2(getText('nextStep'), { size: 45, fill: 0xFFFFFF, weight: 800 }); nextText.anchor.set(0.5, 0.5); nextButton.addChild(nextText); nextButton.x = 2048 / 2; nextButton.y = 580; self.addChild(nextButton); // Skip button var skipButton = new Container(); var skipBg = skipButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); skipBg.width = 200; skipBg.height = 80; skipBg.tint = 0x666666; var skipText = new Text2(getText('skipTutorial'), { size: 35, fill: 0xffffff, weight: 800 }); skipText.anchor.set(0.5, 0.5); skipButton.addChild(skipText); skipButton.x = 2048 - 150; skipButton.y = 150; self.addChild(skipButton); // Button handlers nextButton.down = function () { self.nextStep(); }; skipButton.down = function () { self.complete(); }; // Update button state self.updateButtonState = function () { if (self.waitingForUserAction && !self.stepCompleted) { nextBg.tint = 0x666666; nextButton.alpha = 0.5; nextText.setText('Complete Action'); } else { nextBg.tint = 0x00AA00; nextButton.alpha = 1.0; nextText.setText(getText('nextStep')); } }; self.nextStep = function () { // Don't advance if waiting for user action and step not completed if (self.waitingForUserAction && !self.stepCompleted) { return; } self.currentStep++; if (self.currentStep >= self.tutorialSteps.length) { self.complete(); return; } self.waitingForUserAction = false; self.stepCompleted = false; var step = self.tutorialSteps[self.currentStep]; instructionText.setText(getText(step.textKey)); // Handle specific step actions switch (step.action) { case 'placeTower': self.waitingForUserAction = true; // Give enough gold for tutorial setGold(200); updateUI(); break; case 'startWave': self.waitingForUserAction = true; waveIndicator.gameStarted = true; break; case 'upgradeTower': self.waitingForUserAction = true; // Give extra gold for upgrades setGold(gold + 200); updateUI(); break; } self.updateButtonState(); }; // Check for step completion self.update = function () { if (!self.waitingForUserAction || self.stepCompleted) { return; } var step = self.tutorialSteps[self.currentStep]; switch (step.action) { case 'placeTower': if (towers.length > 0) { self.stepCompleted = true; if (step.completeTextKey) { instructionText.setText(getText(step.completeTextKey)); } self.updateButtonState(); } break; case 'startWave': if (waveInProgress || currentWave > 0) { self.stepCompleted = true; if (step.completeTextKey) { instructionText.setText(getText(step.completeTextKey)); } self.updateButtonState(); } break; case 'upgradeTower': // Check if any tower has been upgraded or if upgrade menu was closed var hasUpgradedTower = false; for (var i = 0; i < towers.length; i++) { if (towers[i].level > 1) { hasUpgradedTower = true; break; } } // Also check if upgrade menu was opened and closed var upgradeMenuExists = false; for (var i = 0; i < game.children.length; i++) { if (game.children[i] instanceof UpgradeMenu) { upgradeMenuExists = true; break; } } // If tower was upgraded OR if menu was opened and then closed if (hasUpgradedTower || !upgradeMenuExists && self.menuWasOpened) { self.stepCompleted = true; if (step.completeTextKey) { instructionText.setText(getText(step.completeTextKey)); } self.updateButtonState(); } // Track if menu was opened if (upgradeMenuExists) { self.menuWasOpened = true; } break; } }; // Initialize first step self.updateButtonState(); self.complete = function () { self.tutorialActive = false; // Mark tutorial as completed globally tutorialCompleted = true; tutorialInProgress = false; // Stop all music first to prevent mixing LK.stopMusic(); // Clear tutorial game state currentWave = 0; currentWorld = 1; currentLevel = 1; waveInProgress = false; waveSpawned = false; waveIndicator.gameStarted = false; gold = 80; lives = 20; score = 0; enemiesKilledInWave = 0; // Clear all game entities while (enemies.length > 0) { var enemy = enemies.pop(); if (enemy.parent) { enemy.parent.removeChild(enemy); } if (enemy.shadow && enemy.shadow.parent) { enemy.shadow.parent.removeChild(enemy.shadow); } } while (towers.length > 0) { var tower = towers.pop(); if (tower.parent) { tower.parent.removeChild(tower); } } while (bullets.length > 0) { var bullet = bullets.pop(); if (bullet.parent) { bullet.parent.removeChild(bullet); } } while (alliedUnits.length > 0) { var unit = alliedUnits.pop(); if (unit.parent) { unit.parent.removeChild(unit); } } // Clear grid state for (var i = 0; i < 24; i++) { for (var j = 0; j < 35; j++) { if (grid.cells[i] && grid.cells[i][j]) { grid.cells[i][j].towersInRange = []; } } } // Reset UI updateUI(); updateWaveCounter(); if (self.onComplete) { self.onComplete(); } self.destroy(); // Wait before creating main menu to ensure clean transition LK.setTimeout(function () { // Return to main menu after tutorial var mainMenu = new MainMenu(); game.addChild(mainMenu); }, 500); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 600; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nHealth: ' + self.tower.health + '/' + self.tower.maxHealth + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 60, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' bits', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' bits', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -75; sellButton.y = 75; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' bits'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' bits'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " bits!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); // Sincronizar el mapa de visualización después de vender torre syncVisualizationMap(); worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' bits'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0xFFFF00; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Boss Normal"; break; case 'fast': block.tint = 0x00AAFF; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xAA0000; waveType = "Boss Immune"; break; case 'flying': block.tint = 0xFFFF00; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the color of the wave type switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; break; case 'fast': block.tint = 0x00AAFF; break; case 'immune': block.tint = 0xAA0000; break; case 'flying': block.tint = 0xFFFF00; break; default: block.tint = 0xFF0000; break; } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a crown or some indicator to the wave marker for boss waves var bossIndicator = marker.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); bossIndicator.width = 30; bossIndicator.height = 30; bossIndicator.tint = 0xFFD700; // Gold color bossIndicator.y = -block.height / 2 - 15; // Change the wave type text to indicate boss waveType = "BOSS"; } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2(waveType, { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (every 10th wave) if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; // Fix positioning to properly show first wave when currentWave is 0 var displayWave = Math.max(0, currentWave); var moveAmount = (progress + displayWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; for (var i = 0; i < totalWaves + 1; i++) { var marker = self.waveMarkers[i]; if (i === 0) { continue; } var block = marker.children[0]; // Fix comparison to properly handle wave 0 and 1 if (i - 1 < displayWave) { block.alpha = .5; } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } // Waves no longer advance automatically - they must be triggered manually via NextWaveButton // This function now only handles the initial game start }; self.handleWaveProgression(); }; return self; }); var WorldRenderer = Container.expand(function () { var self = Container.call(this); self.currentWorld = 1; self.backgroundTiles = []; self.pathTiles = []; self.wallTiles = []; self.sceneryElements = []; self.getWorldAssets = function (worldNumber) { switch (worldNumber) { case 1: return { background: 'forestBg', path: 'forestPath', wall: 'forestWall', scenery: 'forestScenery', ambient: 0x90ee90 }; case 2: return { background: 'desertBg', path: 'desertPath', wall: 'desertWall', scenery: 'desertScenery', ambient: 0xffd700 }; case 3: return { background: 'glacierBg', path: 'glacierPath', wall: 'glacierWall', scenery: 'glacierScenery', ambient: 0xe6f3ff }; case 4: return { background: 'villageBg', path: 'villagePath', wall: 'villageWall', scenery: 'villageScenery', ambient: 0xf0e68c }; case 5: return { background: 'techLabBg', path: 'techLabPath', wall: 'techLabWall', scenery: 'techLabScenery', ambient: 0x87ceeb }; case 6: return { background: 'infernoBg', path: 'infernoPath', wall: 'infernoWall', scenery: 'infernoScenery', ambient: 0xff6347 }; default: return { background: 'forestBg', path: 'forestPath', wall: 'forestWall', scenery: 'forestScenery', ambient: 0x90ee90 }; } }; self.updateWorldGraphics = function (worldNumber, gridInstance) { // Forzar renderizado siempre, incluso si el mundo no cambia self.currentWorld = worldNumber; var worldAssets = self.getWorldAssets(worldNumber); // Clear existing tiles while (self.backgroundTiles.length) { self.removeChild(self.backgroundTiles.pop()); } while (self.pathTiles.length) { self.removeChild(self.pathTiles.pop()); } while (self.wallTiles.length) { self.removeChild(self.wallTiles.pop()); } while (self.sceneryElements.length) { self.removeChild(self.sceneryElements.pop()); } // Create new tiles based on current world using the correct assets var gridWidth = 24; var gridHeight = 35; for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { // Create background tile using world-specific assets var bgTile = self.attachAsset(worldAssets.background, { anchorX: 0, anchorY: 0 }); bgTile.x = i * CELL_SIZE; bgTile.y = j * CELL_SIZE; self.backgroundTiles.push(bgTile); // Add world-specific scenery elements randomly if (Math.random() < 0.15) { // 15% chance for scenery - use world-specific scenery assets var scenery = self.attachAsset(worldAssets.scenery, { anchorX: 0.5, anchorY: 0.5 }); scenery.x = i * CELL_SIZE + CELL_SIZE / 2; scenery.y = j * CELL_SIZE + CELL_SIZE / 2; // World-specific scenery shapes switch (worldNumber) { case 1: // Forest - trees/bushes (ellipses) scenery.scaleY = 1.2 + Math.random() * 0.8; break; case 2: // Desert - cacti/rocks (tall thin or wide) if (Math.random() < 0.5) { scenery.scaleX = 0.6; scenery.scaleY = 1.8; // Tall cactus } else { scenery.scaleX = 1.4; scenery.scaleY = 0.7; // Wide rock } break; case 3: // Glacier - ice crystals scenery.scaleX = 0.8 + Math.random() * 0.4; scenery.scaleY = 0.8 + Math.random() * 0.4; scenery.alpha = 0.7; break; case 4: // Village - small structures scenery.scaleX = 1.2; scenery.scaleY = 1.0; break; case 5: // Technology - machinery scenery.scaleX = 0.9; scenery.scaleY = 0.9; scenery.alpha = 0.8; break; case 6: // Hell - lava bubbles/flames scenery.scaleX = 1.1 + Math.random() * 0.6; scenery.scaleY = 1.1 + Math.random() * 0.6; scenery.alpha = 0.9; break; } self.sceneryElements.push(scenery); } } } // Overlay path and wall tiles on top of scenery if (gridInstance && gridInstance.cells) { for (var i = 0; i < Math.min(gridWidth, gridInstance.cells.length); i++) { for (var j = 0; j < Math.min(gridHeight, gridInstance.cells[i].length); j++) { var cell = gridInstance.cells[i][j]; if (cell.type === 0 || cell.type === 2 || cell.type === 3) { // Path, spawn, or goal - use world-specific path assets var pathTile = self.attachAsset(worldAssets.path, { anchorX: 0, anchorY: 0 }); pathTile.x = i * CELL_SIZE; pathTile.y = j * CELL_SIZE; pathTile.alpha = 0.95; self.pathTiles.push(pathTile); } else if (cell.type === 1) { // Wall - use appropriate wall tile based on world var wallTile = self.attachAsset(worldAssets.wall, { anchorX: 0, anchorY: 0 }); wallTile.x = i * CELL_SIZE; wallTile.y = j * CELL_SIZE; wallTile.alpha = 0.98; self.wallTiles.push(wallTile); } } } } // Asegurar que WorldRenderer esté en el fondo, debajo de todo if (self.parent) { self.parent.addChildAt(self, 0); } }; return self; }); var WorldSelectionMenu = Container.expand(function () { var self = Container.call(this); // Position the menu at center of screen self.x = 2048 / 2; self.y = 2732 / 2; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 1800; menuBackground.height = 1200; menuBackground.tint = 0x333333; menuBackground.alpha = 0.9; var titleText = new Text2('Select World', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -400; self.addChild(titleText); var worldNames = ["Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"]; var unlockedWorlds = storage.unlockedWorlds || 1; for (var i = 0; i < worldNames.length; i++) { var worldButton = new Container(); var worldButtonBg = worldButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); worldButtonBg.width = 350; worldButtonBg.height = 80; var isUnlocked = i + 1 <= unlockedWorlds; worldButtonBg.tint = isUnlocked ? 0x4444FF : 0x666666; var worldButtonText = new Text2(isUnlocked ? worldNames[i] : "Locked", { size: 40, fill: isUnlocked ? 0xFFFFFF : 0x999999, weight: 800 }); worldButtonText.anchor.set(0.5, 0.5); worldButton.addChild(worldButtonText); worldButton.y = -200 + i * 100; self.addChild(worldButton); (function (worldIndex, unlocked) { worldButton.down = function () { if (unlocked) { self.destroy(); var levelSelectionMenu = new LevelSelectionMenu(worldIndex + 1); game.addChild(levelSelectionMenu); } else { var notification = game.addChild(new Notification("Complete the previous world to unlock!")); notification.x = 2048 / 2; notification.y = 2732 / 2 + 200; } }; })(i, isUnlocked); } return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ // Try to import facekit but make it optional to prevent loading issues // Patamon Evolution Line (Sacred/Angel) // Gomamon Evolution Line (Water) // Palmon Evolution Line (Plant/Poison) // Tentomon Evolution Line (Electric/Insect) // Gabumon Evolution Line (Ice/Wolf) // Agumon Evolution Line (Fire/Dragon) // Digimon Evolution Assets for Voice Summoning // Notify user about microphone usage for voice commands // Music system for world progression // Combat & Action Sounds // Special Effects // UI & Feedback // Boss & Special Events // Voice & Summoning // We never call any camera or face detection methods, so the browser will only request microphone access. // NOTE: We only use facekit for microphone/voice features (facekit.volume). // Only use microphone features from facekit. Do NOT call any camera-related methods. // This ensures the browser only requests microphone access, not camera. var facekit = null; try { facekit = LK.import("@upit/facekit.v1"); } catch (e) { console.log("Facekit not available - voice features disabled"); // Create a mock facekit object to prevent errors facekit = { volume: 0 }; } console.log("This game uses microphone for voice summoning commands, not camera."); function playWorldMusic(worldNumber, isBoss, isFinalBoss) { // Stop any currently playing music first to prevent mixing LK.stopMusic(); // Play world-specific music with fade in effect var musicId; // Check for boss music first if (isFinalBoss) { musicId = 'finalBossMusic'; } else if (isBoss) { musicId = 'bossMusic'; } else { switch (worldNumber) { case 1: musicId = 'forestMusic'; break; case 2: musicId = 'desertMusic'; break; case 3: musicId = 'glacierMusic'; break; case 4: musicId = 'villageMusic'; break; case 5: musicId = 'techLabMusic'; break; case 6: musicId = 'infernoMusic'; break; default: musicId = 'forestMusic'; } } console.log("Playing music for world " + worldNumber + ": " + musicId); LK.playMusic(musicId, { fade: { start: 0, end: isBoss || isFinalBoss ? 1.0 : 0.8, duration: 1500 } }); } // Language system var currentLanguage = storage.language || 'en'; var translations = { en: { firewallDefensors: 'Firewall Defensors', startGame: 'Start Game', tutorial: 'Tutorial', language: 'Language', selectWorld: 'Select World', selectLevel: 'Select Level', back: 'Back', locked: 'Locked', completeLevel: 'Complete previous level to unlock!', completeWorld: 'Complete the previous world to unlock!', nextWave: 'Next Wave', shop: 'Shop', bits: 'Bits', systemHealth: 'System Health', securityScore: 'Security Score', wave: 'Wave', upgrade: 'Upgrade', sell: 'Sell', maxLevel: 'Max Level', digivolve: 'Digivolve', owned: 'OWNED', notEnoughBits: 'Not enough bits!', cannotBuild: 'Cannot build here!', pathBlocked: 'Tower would block the path!', tutorialWelcome: 'Welcome to the Tutorial!\n\nThis game uses microphone for voice commands (not camera).\nLearn to play step by step...', tutorialStep1: 'Step 1: Tower Placement\n\nDrag a Digimon tower from the bottom of the screen to a valid position on the battlefield.', tutorialStep2: 'Step 2: Starting Waves\n\nClick the "Next Wave" button to start sending enemies. Towers will attack automatically.', tutorialStep3: 'Step 3: Upgrading Towers\n\nClick on a placed tower and then press the "Upgrade" button to increase its power. Close the menu when done!', tutorialCompleted: 'Tutorial completed!\n\nNow you know how to:\n• Place towers\n• Start waves\n• Upgrade towers\n• Shout Digimon names to summon allies (requires microphone)\n\nDefend the digital world!', tutorialStep1Complete: 'Excellent! You placed your first tower.', tutorialStep2Complete: 'Well done! The wave has started.', tutorialStep3Complete: 'Perfect! You upgraded the tower.', nextStep: 'Next', skipTutorial: 'Skip Tutorial', // Story translations (EN) story1_1: "ALERT! Pokémon infiltrators have breached\nthe Digital Forest servers! They're attempting\nto steal Digimon data files!", story1_2: "These Pokémon spies are using advanced\nstealth protocols to access our core database.\nDeploy Digimon guardians immediately!", story1_3: "The future of the Digimon franchise\ndepends on you! Stop Pokémon from\ncorrupting our digital ecosystem!", story2_1: "Pokémon agents have infiltrated the Desert\nData Center! They're planting malicious code\nto corrupt our systems!", story2_2: "Fire-type Pokémon are overheating our\nservers while others steal precious\nDigimon evolution data!", story2_3: "Their coordinated attack is more sophisticated\nthan before. Pokémon want to monopolize\nthe children's entertainment industry!", story3_1: "Ice-type Pokémon have frozen our Glacier\nservers to slow down our defenses\nwhile they extract data!", story3_2: "Flying Pokémon are bypassing our security\nwalls! They're trying to reach the core\nDigimon genetic database!", story3_3: "Critical system temperatures detected!\nPokémon are trying to cause a complete\nserver meltdown!", story4_1: "Pokémon sleeper agents hidden in the Village\nNetwork have activated! They've been\ngathering intelligence for months!", story4_2: "Multiple Pokémon strike teams are attacking\nsimultaneously, trying to overwhelm\nour Digimon defenders!", story4_3: "This is corporate espionage on a massive\nscale! Pokémon Company wants to steal\nour digital creature technology!", story5_1: "MAXIMUM THREAT LEVEL! Elite Pokémon\nhackers have breached our most secure\nTechnology Labs!", story5_2: "They're using legendary Pokémon abilities\nto bypass our quantum encryption!\nOur most sensitive data is at risk!", story5_3: "Deploy our strongest Mega-level Digimon!\nOnly they can stop this corporate\ncyber warfare!", story6_1: "FINAL ASSAULT! Pokémon's master plan\nis revealed - they want to delete ALL\nDigimon data permanently!", story6_2: "Legendary Pokémon themselves are leading\nthis final attack on our core servers!\nThis is the ultimate battle for supremacy!", story6_3: "The children's hearts are at stake!\nDefeat Pokémon's invasion and save\nthe future of digital monsters forever!", storyDefault_1: "Pokémon infiltrators detected! Protect the Digimon database!", storyDefault_2: "Deploy your Digimon to stop the corporate espionage!", storyDefault_3: "Save the Digital World from Pokémon's takeover!" }, es: { firewallDefensors: 'Defensores del Firewall', startGame: 'Iniciar Juego', tutorial: 'Tutorial', language: 'Idioma', selectWorld: 'Seleccionar Mundo', selectLevel: 'Seleccionar Nivel', back: 'Atrás', locked: 'Bloqueado', completeLevel: '¡Completa el nivel anterior para desbloquear!', completeWorld: '¡Completa el mundo anterior para desbloquear!', nextWave: 'Siguiente Oleada', shop: 'Tienda', bits: 'Bits', systemHealth: 'Salud del Sistema', securityScore: 'Puntuación de Seguridad', wave: 'Oleada', upgrade: 'Mejorar', sell: 'Vender', maxLevel: 'Nivel Máximo', digivolve: 'Digievolucionar', owned: 'POSEÍDO', notEnoughBits: '¡No tienes suficientes bits!', cannotBuild: '¡No se puede construir aquí!', pathBlocked: '¡La torreta bloquearía el camino!', tutorialWelcome: '¡Bienvenido al Tutorial!\n\nEste juego usa el micrófono para comandos de voz (no la cámara).\nAprende a jugar paso a paso...', tutorialStep1: 'Paso 1: Colocación de torretas\n\nArrastra una torreta Digimon desde la parte inferior de la pantalla hasta una posición válida en el campo de batalla.', tutorialStep2: 'Paso 2: Iniciar oleadas\n\nHaz clic en el botón "Siguiente Oleada" para comenzar a enviar enemigos. Las torretas atacarán automáticamente.', tutorialStep3: 'Paso 3: Mejorar torretas\n\nHaz clic en una torreta colocada y luego presiona el botón "Mejorar" para aumentar su poder. ¡Cierra el menú cuando termines!', tutorialCompleted: '¡Tutorial completado!\n\nAhora ya sabes:\n• Colocar torretas\n• Iniciar oleadas\n• Mejorar torretas\n• Gritar nombres de Digimon para invocar aliados (requiere micrófono)\n\n¡Defiende el mundo digital!', tutorialStep1Complete: 'Excelente! Has colocado tu primera torreta.', tutorialStep2Complete: 'Bien hecho! La oleada ha comenzado.', tutorialStep3Complete: 'Perfecto! Has mejorado la torreta.', nextStep: 'Siguiente', skipTutorial: 'Saltar Tutorial', // Story translations (ES) story1_1: "¡ALERTA! ¡Infiltradores Pokémon han violado los servidores del Bosque Digital! ¡Intentan robar archivos de datos Digimon!", story1_2: "¡Estos espías Pokémon usan protocolos avanzados de sigilo para acceder a nuestra base de datos central! ¡Despliega guardianes Digimon de inmediato!", story1_3: "¡El futuro de la franquicia Digimon depende de ti! ¡Detén a Pokémon antes de que corrompan nuestro ecosistema digital!", story2_1: "¡Agentes Pokémon han infiltrado el Centro de Datos del Desierto! ¡Están plantando código malicioso para corromper nuestros sistemas!", story2_2: "¡Pokémon de tipo fuego están sobrecalentando nuestros servidores mientras otros roban valiosos datos de evolución Digimon!", story2_3: "¡Su ataque coordinado es más sofisticado que antes! ¡Pokémon quiere monopolizar la industria del entretenimiento infantil!", story3_1: "¡Pokémon de tipo hielo han congelado nuestros servidores Glaciar para ralentizar nuestras defensas mientras extraen datos!", story3_2: "¡Pokémon voladores están evadiendo nuestros muros de seguridad! ¡Intentan llegar a la base genética central de Digimon!", story3_3: "¡Temperaturas críticas detectadas! ¡Pokémon intenta provocar un colapso total del servidor!", story4_1: "¡Agentes durmientes Pokémon ocultos en la Red de la Aldea se han activado! ¡Han estado recolectando inteligencia durante meses!", story4_2: "¡Múltiples equipos de ataque Pokémon atacan simultáneamente, intentando sobrepasar a nuestros defensores Digimon!", story4_3: "¡Esto es espionaje corporativo a gran escala! ¡La Compañía Pokémon quiere robar nuestra tecnología de criaturas digitales!", story5_1: "¡NIVEL DE AMENAZA MÁXIMO! ¡Hackers Pokémon de élite han violado nuestros Laboratorios de Tecnología más seguros!", story5_2: "¡Usan habilidades legendarias Pokémon para evadir nuestro cifrado cuántico! ¡Nuestros datos más sensibles están en riesgo!", story5_3: "¡Despliega nuestros Digimon Mega más fuertes! ¡Solo ellos pueden detener esta guerra cibernética corporativa!", story6_1: "¡ASALTO FINAL! ¡El plan maestro de Pokémon se revela: quieren borrar TODOS los datos Digimon permanentemente!", story6_2: "¡Pokémon legendarios lideran este ataque final a nuestros servidores centrales! ¡Es la batalla definitiva por la supremacía!", story6_3: "¡El corazón de los niños está en juego! ¡Derrota la invasión Pokémon y salva el futuro de los monstruos digitales para siempre!", storyDefault_1: "¡Infiltradores Pokémon detectados! ¡Protege la base de datos Digimon!", storyDefault_2: "¡Despliega tus Digimon para detener el espionaje corporativo!", storyDefault_3: "¡Salva el Mundo Digital del dominio de Pokémon!" } }; function getText(key) { return translations[currentLanguage][key] || translations.en[key] || key; } function setLanguage(lang) { currentLanguage = lang; storage.language = lang; } // Assets adicionales para tiles específicos por mundo var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var alliedUnits = []; // Array to track summoned allied Digimon units var selectedTower = null; var gold = 80; var lives = 20; var score = storage.securityPoints || 0; var currentWave = 0; var totalWaves = 10; // 10 waves per world var currentWorld = 1; var currentLevel = 1; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave var enemiesKilledInWave = 0; var coins = []; // Allied units management var maxAlliedUnits = 5; // Maximum number of allied units at once var summonCooldown = 300; // 5 seconds at 60 FPS var lastSummonTime = 0; // Voice summoning system var voiceSummonCooldown = {}; var voiceDetectionActive = false; var lastVoiceDetectionTime = 0; var voiceDetectionCooldown = 60; // 1 second between voice detections var coinSpawnTimer = 0; var goldText = new Text2(getText('bits') + ': ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); // Create health bar container var healthBarContainer = new Container(); var healthBarBg = healthBarContainer.attachAsset('healthBarOutline', { anchorX: 0.5, anchorY: 0.5 }); healthBarBg.width = 300; healthBarBg.height = 20; healthBarBg.tint = 0x000000; var healthBarFill = healthBarContainer.attachAsset('healthBar', { anchorX: 0.5, anchorY: 0.5 }); healthBarFill.width = 296; healthBarFill.height = 16; healthBarFill.tint = 0x00FF00; // Add health text label var livesText = new Text2(getText('systemHealth'), { size: 45, fill: 0xFFFFFF, weight: 800 }); livesText.anchor.set(0.5, 0.5); livesText.y = -35; healthBarContainer.addChild(livesText); var scoreText = new Text2(getText('securityScore') + ': ' + score, { size: currentLanguage === 'es' ? 45 : 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; // Create speed control button var speedButton = new Container(); var speedButtonBg = speedButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); speedButtonBg.width = 150; speedButtonBg.height = 80; speedButtonBg.tint = 0x00AA00; var speedButtonText = new Text2('1x', { size: 50, fill: 0xFFFFFF, weight: 800 }); speedButtonText.anchor.set(0.5, 0.5); speedButton.addChild(speedButtonText); var gameSpeed = 1; var speedLevels = [1, 2]; var currentSpeedIndex = 0; speedButton.down = function () { currentSpeedIndex = (currentSpeedIndex + 1) % speedLevels.length; gameSpeed = speedLevels[currentSpeedIndex]; speedButtonText.setText(gameSpeed + 'x'); // Update button color based on speed if (gameSpeed === 1) { speedButtonBg.tint = 0x00AA00; // Green for normal speed } else { speedButtonBg.tint = 0xFFAA00; // Orange for 2x speed } }; LK.gui.top.addChild(goldText); LK.gui.bottom.addChild(healthBarContainer); LK.gui.top.addChild(scoreText); LK.gui.top.addChild(speedButton); healthBarContainer.x = 0; healthBarContainer.y = -300; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; speedButton.x = 0; speedButton.y = topMargin; function updateUI() { goldText.setText(getText('bits') + ': ' + gold); scoreText.setText(getText('securityScore') + ': ' + score); // Save security points to storage storage.securityPoints = score; // Update health bar var healthPercent = lives / 20; // Assuming max lives is 20 healthBarFill.width = 296 * healthPercent; // Change color based on health if (healthPercent > 0.6) { healthBarFill.tint = 0x00FF00; // Green } else if (healthPercent > 0.3) { healthBarFill.tint = 0xFFFF00; // Yellow } else { healthBarFill.tint = 0xFF0000; // Red } } function setGold(value) { gold = value; updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); // Add world renderer for background graphics var worldRenderer = new WorldRenderer(); var backgroundLayer = new Container(); backgroundLayer.addChild(worldRenderer); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; worldRenderer.x = grid.x; worldRenderer.y = grid.y; grid.pathFind(); // Crear una segunda copia del mapa específicamente para visualización var mapVisualization = new Grid(24, 29 + 6); mapVisualization.x = 150; mapVisualization.y = 200 - CELL_SIZE * 4; // Sincronizar el mapa de visualización con el mapa principal function syncVisualizationMap() { for (var i = 0; i < 24; i++) { for (var j = 0; j < 35; j++) { if (grid.cells[i] && grid.cells[i][j] && mapVisualization.cells[i] && mapVisualization.cells[i][j]) { mapVisualization.cells[i][j].type = grid.cells[i][j].type; } } } } // Forzar actualización visual del mapa cada vez que se sincroniza worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); // Sincronizar inicialmente syncVisualizationMap(); // Render initial world graphics worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); // Only render debug on game start, not every frame if (LK.ticks === 0) { grid.renderDebug(); } debugLayer.addChild(grid); game.addChildAt(backgroundLayer, 0); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY) { var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); // Sincronizar el mapa de visualización después de las verificaciones syncVisualizationMap(); return blocked; } function getTowerCost(towerType) { var cost = 5; switch (towerType) { case 'gabumon': //{ju} // Rapid fire Digimon cost = 15; break; case 'tentomon': //{jw} // Long range Digimon cost = 25; break; case 'palmon': //{jy} // Area damage Digimon cost = 35; break; case 'gomamon': //{jA} // Slowing Digimon cost = 45; break; case 'patamon': //{jC} // Poison/status Digimon cost = 55; break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); // Play tower placement sound LK.getSound('towerPlace').play(); grid.pathFind(); grid.renderDebug(); // Sincronizar el mapa de visualización después de colocar torre syncVisualizationMap(); worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); return true; } else { var notification = game.addChild(new Notification("Not enough bits!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Play purchase fail sound LK.getSound('purchaseFail').play(); return false; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); var shopVisible = digimonShop && digimonShop.visible; var summonMenuVisible = digimonSummonMenu && digimonSummonMenu.visible; if (upgradeMenuVisible || shopVisible || summonMenuVisible) { return; } for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); // Apply the same offset as in move handler to ensure consistency when starting drag towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); break; } } }; game.move = function (x, y, obj) { var shopVisible = digimonShop && digimonShop.visible; var summonMenuVisible = digimonSummonMenu && digimonSummonMenu.visible; if (shopVisible || summonMenuVisible) { return; } if (isDragging) { // Shift the y position upward by 1.5 tiles to show preview above finger towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } }; game.up = function (x, y, obj) { var shopVisible = digimonShop && digimonShop.visible; var summonMenuVisible = digimonSummonMenu && digimonSummonMenu.visible; if (shopVisible || summonMenuVisible) { // Still allow dragging to be reset even when menus are open if (isDragging) { isDragging = false; towerPreview.visible = false; } return; } var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; if (towerPreview.canPlace) { if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; if (isDragging) { var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } } }; // Simple wave counter instead of full wave indicator var waveCounterText = new Text2('Wave: 0/9', { size: 60, fill: 0xFFFFFF, weight: 800 }); waveCounterText.anchor.set(0.5, 0.5); waveCounterText.x = 2048 / 2; waveCounterText.y = 2732 - 80; game.addChild(waveCounterText); // Create a simple wave counter object to replace waveIndicator var waveIndicator = { gameStarted: false, getWaveType: function getWaveType(waveNumber) { if (waveNumber < 1 || waveNumber > 10) return "normal"; // Simple wave type logic for 10 waves var waveTypes = ['normal', 'fast', 'immune', 'flying', 'swarm', 'normal', 'fast', 'immune', 'flying', 'boss']; return waveTypes[waveNumber - 1]; }, getEnemyCount: function getEnemyCount(waveNumber) { if (waveNumber < 1 || waveNumber > 10) return 10; // Wave 10 is boss with 1 giant enemy, others have 10-30 enemies if (waveNumber === 10) return 1; if (waveNumber === 5) return 30; // Swarm wave return 10; }, getWaveTypeName: function getWaveTypeName(waveNumber) { var type = this.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); if (waveNumber === 10) typeName = "BOSS"; return typeName; } }; // Function to update wave counter display function updateWaveCounter() { var currentWorldWave; if (currentWave === 0) { currentWorldWave = 0; } else { // Calculate the wave within the current world (1-10) currentWorldWave = (currentWave - 1) % 10 + 1; } waveCounterText.setText(getText('wave') + ': ' + currentWorldWave + '/10'); } var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); // Add shop button var shopButton = new Container(); var shopButtonBg = shopButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); shopButtonBg.width = 300; shopButtonBg.height = 100; shopButtonBg.tint = 0x4444FF; var shopButtonText = new Text2('Shop', { size: 50, fill: 0xFFFFFF, weight: 800 }); shopButtonText.anchor.set(0.5, 0.5); shopButton.addChild(shopButtonText); shopButton.x = 200; shopButton.y = 2732 - 100 + 20; game.addChild(shopButton); var digimonShop = new DigimonShop(); digimonShop.x = 2048 / 2; game.addChild(digimonShop); shopButton.down = function () { digimonShop.show(); }; // --- Add C, B, and A buttons for Digimon summon filtering --- var summonButtonSpacing = 120; var summonButtonStartX = shopButton.x + 220; // Place to the right of shop button function createSummonLevelButton(label, color, filterLevel, offset) { var btn = new Container(); var btnBg = btn.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); btnBg.width = 100; btnBg.height = 100; btnBg.tint = color; var btnText = new Text2(label, { size: 60, fill: 0xFFFFFF, weight: 800 }); btnText.anchor.set(0.5, 0.5); btn.addChild(btnText); btn.x = summonButtonStartX + offset * summonButtonSpacing; btn.y = shopButton.y; btn.down = function () { // Show summon menu filtered by evolution level if (digimonSummonMenu && digimonSummonMenu.show) { digimonSummonMenu.show(filterLevel); } }; return btn; } // C = Champion, B = Ultimate, A = Mega var buttonC = createSummonLevelButton('C', 0x32CD32, 'champion', 0); var buttonB = createSummonLevelButton('B', 0xFFD700, 'ultimate', 1); var buttonA = createSummonLevelButton('A', 0xFF4500, 'mega', 2); game.addChild(buttonC); game.addChild(buttonB); game.addChild(buttonA); var towerTypes = ['agumon', 'gabumon', 'tentomon', 'palmon', 'gomamon', 'patamon']; var sourceTowers = []; var towerSpacing = 320; // Increase spacing for larger towers var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } // Add summon arrow button at the end of source towers var summonArrowButton = new Container(); var arrowBg = summonArrowButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); arrowBg.width = 200; arrowBg.height = 200; arrowBg.tint = 0xFF6600; var arrowGraphics = summonArrowButton.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5 }); arrowGraphics.width = 80; arrowGraphics.height = 80; arrowGraphics.tint = 0xFFFFFF; arrowGraphics.rotation = Math.PI / 2; // Point upward var summonText = new Text2('SUMMON', { size: 35, fill: 0xFFFFFF, weight: 800 }); summonText.anchor.set(0.5, 0.5); summonText.y = 50; summonArrowButton.addChild(summonText); var summonSubText = new Text2('(No Mic)', { size: 25, fill: 0xCCCCCC, weight: 600 }); summonSubText.anchor.set(0.5, 0.5); summonSubText.y = 75; summonArrowButton.addChild(summonSubText); summonArrowButton.x = startX + towerTypes.length * towerSpacing; summonArrowButton.y = towerY; towerLayer.addChild(summonArrowButton); // Create summon menu var digimonSummonMenu = new DigimonSummonMenu(); digimonSummonMenu.x = 2048 / 2; game.addChild(digimonSummonMenu); summonArrowButton.down = function () { digimonSummonMenu.show(); }; sourceTower = null; enemiesToSpawn = 10; game.update = function () { // Update background color based on current world var worldAssets = worldRenderer.getWorldAssets(currentWorld); if (worldAssets) { game.setBackgroundColor(worldAssets.ambient); } // Apply speed multiplier to frame-dependent updates var effectiveSpeed = gameSpeed; // Note: Wave progression is now manual only via NextWaveButton, no automatic timer if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; enemiesKilledInWave = 0; // Reset kill counter for new wave // Calculate correct world and level for this wave var levelsPerWorld = 10; var newWorld = Math.ceil(currentWave / levelsPerWorld); var newLevel = (currentWave - 1) % levelsPerWorld + 1; var previousWorld = currentWorld; currentWorld = newWorld; currentLevel = newLevel; if (currentWorld > 6) currentWorld = 6; // Regenerate maze and world graphics if world changed or first wave if (currentWorld !== previousWorld || currentWave === 1) { grid.generateMazeForWorld(currentWorld); mapVisualization.generateMazeForWorld(currentWorld); // Regenerar también el mapa de visualización grid.pathFind(); grid.renderDebug(); // Sincronizar mapas después de regenerar syncVisualizationMap(); // Update world graphics usando el mapa de visualización worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); // Change music when entering new world playWorldMusic(currentWorld); // World-specific notification messages var worldNames = ["", "Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"]; var worldNotification = game.addChild(new Notification("Welcome to Digital World " + currentWorld + ": " + worldNames[currentWorld] + "!")); worldNotification.x = 2048 / 2; worldNotification.y = grid.height - 100; } // Get wave type and enemy count from the wave indicator (based on current world/level) var worldWave = currentLevel; var waveType = waveIndicator.getWaveType(worldWave); var enemyCount = waveIndicator.getEnemyCount(worldWave); // Update wave counter display updateWaveCounter(); // Check if this is a boss wave (wave 10 of each world) var isBossWave = worldWave === 10; if (isBossWave) { // Boss waves have just 1 giant enemy enemyCount = 1; // Get boss name based on current world var bossNames = ["", "SNORLAX", "RHYDON", "ARTICUNO", "MACHAMP", "GROUDON", "MEWTWO"]; var bossName = bossNames[currentWorld] || "BOSS"; // Check if this is the final boss (Mewtwo in world 6) var isFinalBoss = currentWorld === 6; // Play boss music playWorldMusic(currentWorld, true, isFinalBoss); // Show boss announcement with specific name var notification = game.addChild(new Notification("⚠️ " + bossName + " APPEARS! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy = new Enemy(waveType); // Make wave 10 boss giant with much more health if (isBossWave) { enemy.isBoss = true; enemy.maxHealth *= 50; // 50x more health for boss enemy.health = enemy.maxHealth; // Make boss visually larger if (enemy.children[0]) { enemy.children[0].scaleX = 3.0; enemy.children[0].scaleY = 3.0; } // Make boss slower but more intimidating enemy.speed *= 0.5; } // Add enemy to the appropriate layer based on type if (enemy.isFlying) { // Add flying enemy to the top layer enemyLayerTop.addChild(enemy); // If it's a flying enemy, add its shadow to the middle layer if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else { // Add normal/ground enemies to the bottom layer enemyLayerBottom.addChild(enemy); } // Scale difficulty with wave number but don't apply to boss // as bosses already have their health multiplier // Use exponential scaling for health var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // All enemy types now spawn in the middle 6 tiles at the top spacing var gridWidth = 24; var midPoint = Math.floor(gridWidth / 2); // 12 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading enemy.cellX = spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; } } // Apply speed multiplier to enemy updates for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; // Update enemy with speed multiplier applied if (enemy.update) { for (var speedTick = 0; speedTick < effectiveSpeed; speedTick++) { enemy.update(); } } if (enemy.health <= 0) { // Play enemy death sound if (enemy.isBoss) { LK.getSound('bossDeath').play(); } else { LK.getSound('enemyDestroyed').play(); } for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Track enemy kills for wave progression if (enemy.waveNumber === currentWave) { enemiesKilledInWave++; // Give bonus gold every 5 enemies killed if (enemiesKilledInWave % 5 === 0) { var bonusGold = enemy.isBoss ? 15 : 10; var bonusIndicator = new GoldIndicator(bonusGold, enemy.x, enemy.y - 50); game.addChild(bonusIndicator); setGold(gold + bonusGold); var notification = game.addChild(new Notification("Kill streak bonus! +" + bonusGold + " bits!")); notification.x = 2048 / 2; notification.y = grid.height - 200; } } // Boss enemies give more gold and score var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Save security points to storage storage.securityPoints = score; // Add a notification for boss defeat if (enemy.isBoss) { // Get boss name based on world var bossNames = ["", "Snorlax", "Rhydon", "Articuno", "Machamp", "Groudon", "Mewtwo"]; var bossName = bossNames[enemy.worldNumber] || "Boss"; var notification = game.addChild(new Notification(bossName + " defeated! +" + goldEarned + " bits!")); notification.x = 2048 / 2; notification.y = grid.height - 150; // Return to normal world music after boss defeat LK.setTimeout(function () { LK.stopMusic(); LK.setTimeout(function () { playWorldMusic(enemy.worldNumber, false, false); }, 500); // Add delay to ensure clean music transition }, 2000); // Wait 2 seconds before switching back to normal music } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); // Calculate damage based on enemy world and type var damageDealt = 1; // Base damage var worldDamageMultiplier = 1; // Scale damage based on world switch (enemy.worldNumber) { case 1: worldDamageMultiplier = 1; break; case 2: worldDamageMultiplier = 1.2; break; case 3: worldDamageMultiplier = 1.4; break; case 4: worldDamageMultiplier = 1.6; break; case 5: worldDamageMultiplier = 1.8; break; case 6: worldDamageMultiplier = 2.0; break; } // Scale damage based on enemy type switch (enemy.type) { case 'normal': damageDealt = 1; break; case 'fast': damageDealt = 1; break; // Fast but same damage case 'immune': damageDealt = 2; break; // Tanky and hits hard case 'flying': damageDealt = 1.5; break; // Aerial advantage case 'swarm': damageDealt = 1; break; // Weak individual damage } // Boss enemies deal significantly more damage if (enemy.isBoss) { damageDealt *= 5; } // Apply world multiplier and round damageDealt = Math.ceil(damageDealt * worldDamageMultiplier); lives = Math.max(0, lives - damageDealt); // Play system damage sound LK.getSound('systemDamage').play(); // Check for critical health warning if (lives <= 5 && lives > 0) { LK.getSound('criticalHealth').play(); } updateUI(); // Show damage indicator var damageIndicator = new Notification("-" + damageDealt + " System Health!"); damageIndicator.x = 2048 / 2; damageIndicator.y = 2732 - 200; game.addChild(damageIndicator); if (lives <= 0) { LK.showGameOver(); } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } // Check for level completion and world progression var levelsPerWorld = 10; var completedWaves = storage.completedWaves || 0; if (currentWave > completedWaves) { storage.completedWaves = currentWave; // Calculate current world and level var currentWorldNum = Math.ceil(currentWave / levelsPerWorld); var currentLevelNum = (currentWave - 1) % levelsPerWorld + 1; // Update world levels in storage var worldLevels = storage.worldLevels || { 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1 }; if (currentWorldNum >= 1 && currentWorldNum <= 6) { worldLevels[currentWorldNum] = Math.max(worldLevels[currentWorldNum], currentLevelNum); storage.worldLevels = worldLevels; } // Check if we completed a world (every 10 waves) var worldsCompleted = Math.floor(currentWave / levelsPerWorld); var unlockedWorlds = storage.unlockedWorlds || 1; if (worldsCompleted + 1 > unlockedWorlds && worldsCompleted + 1 <= 6) { storage.unlockedWorlds = worldsCompleted + 1; var notification = game.addChild(new Notification("New world unlocked!")); notification.x = 2048 / 2; notification.y = grid.height - 100; } } // Spawn coins randomly on the field coinSpawnTimer++; if (coinSpawnTimer > 10800 && coins.length < 3) { // Spawn every 3 minutes (180 seconds * 60 FPS = 10800 ticks), max 3 coins coinSpawnTimer = 0; // Find a random walkable position var attempts = 0; var spawnX, spawnY; do { var gridX = Math.floor(Math.random() * 24); var gridY = Math.floor(Math.random() * 35); var cell = grid.getCell(gridX, gridY); if (cell && cell.type === 0) { // Spawn on path tiles spawnX = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; spawnY = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; break; } attempts++; } while (attempts < 50); if (attempts < 50) { var coinValue = 5; // Fixed 5 bits for security store currency var coin = new Coin(spawnX, spawnY, coinValue); game.addChild(coin); coins.push(coin); } } // Update existing coins for (var i = coins.length - 1; i >= 0; i--) { var coin = coins[i]; if (coin.update) coin.update(); if (coin.collected) { coins.splice(i, 1); } } // Update allied units with speed multiplier for (var i = alliedUnits.length - 1; i >= 0; i--) { var unit = alliedUnits[i]; if (unit.update) { for (var speedTick = 0; speedTick < effectiveSpeed; speedTick++) { unit.update(); } } // Remove dead or destroyed units if (unit.isDead || !unit.parent) { alliedUnits.splice(i, 1); } } // Voice summoning system handleVoiceSummoning(); // Check for completion of current world (10 waves) or all worlds var worldWave = (currentWave - 1) % 10 + 1; if (worldWave >= 10 && enemies.length === 0 && !waveInProgress) { // Play level complete sound LK.getSound('levelComplete').play(); // Stop all music first to prevent mixing LK.stopMusic(); // Clear all timeouts to prevent conflicts // (No direct timeout clearing method available, but stopping music helps) // Show win screen instead of immediately returning to main menu LK.showYouWin(); // This will handle the game state reset and return to main menu properly } }; // Show microphone usage notification var micNotification = game.addChild(new Notification("Microphone access may be requested for voice summoning features")); micNotification.x = 2048 / 2; micNotification.y = 200; var mainMenu = new MainMenu(); game.addChild(mainMenu); // Execute the game console.log("Game initialized and running!"); game.startGame = function () { var worldSelectionMenu = new WorldSelectionMenu(); game.addChild(worldSelectionMenu); }; game.startWorld = function (worldNumber) { currentWorld = worldNumber; grid.generateMazeForWorld(currentWorld); mapVisualization.generateMazeForWorld(currentWorld); // Regenerar también el mapa de visualización grid.pathFind(); grid.renderDebug(); // Sincronizar mapas syncVisualizationMap(); worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); // Change to world-specific music playWorldMusic(worldNumber); // Show story sequence before starting the world var storySequence = new StorySequence(worldNumber); game.addChild(storySequence); storySequence.onComplete = function () { waveIndicator.gameStarted = true; currentWave = (currentWorld - 1) * 10; waveTimer = nextWaveTime; }; }; game.startWorldLevel = function (worldNumber, levelNumber) { currentWorld = worldNumber; currentLevel = levelNumber; // Set currentWave to the correct absolute wave number for this world/level // Each world has 10 levels, so world 1: 1-10, world 2: 11-20, etc. currentWave = (worldNumber - 1) * 10 + levelNumber; grid.generateMazeForWorld(currentWorld); mapVisualization.generateMazeForWorld(currentWorld); grid.pathFind(); grid.renderDebug(); // Sincronizar mapas syncVisualizationMap(); worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); // Change to world-specific music playWorldMusic(worldNumber); // Show story sequence before starting the level var storySequence = new StorySequence(worldNumber); game.addChild(storySequence); storySequence.onComplete = function () { waveIndicator.gameStarted = true; // Set waveTimer so the first wave button click starts at the selected level waveTimer = nextWaveTime; }; }; // Add tutorial state tracking var tutorialCompleted = false; var tutorialInProgress = false; // Voice summoning system handler // This function only uses facekit.volume (microphone input). // No camera or face detection features are used, so only microphone permission is requested. function handleVoiceSummoning() { // Only process voice commands during active gameplay if (!waveIndicator.gameStarted || LK.ticks - lastVoiceDetectionTime < voiceDetectionCooldown) { return; } // Check if facekit is available and for loud voice input (shouting level) if (facekit && facekit.volume > 0.7) { lastVoiceDetectionTime = LK.ticks; // Define Digimon voice commands and their requirements var digimonVoiceCommands = { // Champion level (requires Digivice C and base tower) 'greymon': { baseTower: 'agumon', requiredDigivice: 'digiviceC', level: 'champion', cost: 150, description: 'Champion Fire Dragon' }, 'garurumon': { baseTower: 'gabumon', requiredDigivice: 'digiviceC', level: 'champion', cost: 180, description: 'Champion Ice Wolf' }, 'kabuterimon': { baseTower: 'tentomon', requiredDigivice: 'digiviceC', level: 'champion', cost: 160, description: 'Champion Electric Insect' }, // Ultimate level (requires Digivice B and base tower) 'metalgreymon': { baseTower: 'agumon', requiredDigivice: 'digiviceB', level: 'ultimate', cost: 400, description: 'Ultimate Cyborg Dragon' }, 'weregarurumon': { baseTower: 'gabumon', requiredDigivice: 'digiviceB', level: 'ultimate', cost: 450, description: 'Ultimate Beast Warrior' }, 'megakabuterimon': { baseTower: 'tentomon', requiredDigivice: 'digiviceB', level: 'ultimate', cost: 420, description: 'Ultimate Giant Insect' }, // Mega level (requires Digivice A and base tower) 'wargreymon': { baseTower: 'agumon', requiredDigivice: 'digiviceA', level: 'mega', cost: 800, description: 'Mega Dragon Warrior' } }; // Check each voice command for (var digimonName in digimonVoiceCommands) { var command = digimonVoiceCommands[digimonName]; // Check if player has the required Digivice if (!storage[command.requiredDigivice]) { continue; } // Count base towers of the required type var baseTowerCount = 0; for (var i = 0; i < towers.length; i++) { if (towers[i].id === command.baseTower) { baseTowerCount++; } } // Need at least one base tower to summon evolution if (baseTowerCount === 0) { continue; } // Calculate cooldown based on number of base towers var cooldownReduction = Math.max(1, baseTowerCount); var effectiveCooldown = Math.floor(summonCooldown / cooldownReduction); // Check if this Digimon is off cooldown var lastSummonTime = voiceSummonCooldown[digimonName] || 0; if (LK.ticks - lastSummonTime < effectiveCooldown) { continue; } // Check if player can afford the summon if (score < command.cost) { continue; } // Check if there's space for more allied units if (alliedUnits.length >= maxAlliedUnits) { continue; } // Voice detection successful - summon the Digimon! score -= command.cost; updateUI(); // Create a more powerful allied unit based on evolution level var summonedUnit = new DigimonUnit(digimonName, getEvolutionLevel(command.level)); // Apply evolution bonuses switch (command.level) { case 'champion': summonedUnit.damage *= 2; summonedUnit.health *= 2; summonedUnit.maxHealth = summonedUnit.health; break; case 'ultimate': summonedUnit.damage *= 3; summonedUnit.health *= 3; summonedUnit.maxHealth = summonedUnit.health; summonedUnit.range *= 1.5; break; case 'mega': summonedUnit.damage *= 5; summonedUnit.health *= 5; summonedUnit.maxHealth = summonedUnit.health; summonedUnit.range *= 2; summonedUnit.attackRate = Math.floor(summonedUnit.attackRate * 0.7); break; } // Add to game world enemyLayerTop.addChild(summonedUnit); alliedUnits.push(summonedUnit); // Set cooldown for this specific Digimon voiceSummonCooldown[digimonName] = LK.ticks; // Show success notification var notification = game.addChild(new Notification(digimonName.toUpperCase() + " summoned by voice!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Show microphone icon animation above the summoned Digimon var micIcon = summonedUnit.attachAsset('notification', { anchorX: 0.5, anchorY: 1.0 }); micIcon.width = 60; micIcon.height = 60; micIcon.x = 0; micIcon.y = -summonedUnit.children[0].height / 2 - 10; micIcon.tint = 0x00AAFF; micIcon.alpha = 0; tween(micIcon, { alpha: 1, y: micIcon.y - 20 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(micIcon, { alpha: 0, y: micIcon.y - 40 }, { duration: 600, delay: 600, easing: tween.easeIn, onFinish: function onFinish() { micIcon.visible = false; } }); } }); // Visual and audio feedback LK.effects.flashScreen(0x00FF00, 500); // Play voice summon and digivolve sounds LK.getSound('voiceSummon').play(); LK.setTimeout(function () { LK.getSound('digivolveSound').play(); }, 200); // Only summon one Digimon per voice command break; } } } // Helper function to convert evolution level to numeric level function getEvolutionLevel(levelName) { switch (levelName) { case 'champion': return 3; case 'ultimate': return 5; case 'mega': return 6; default: return 1; } } game.startTutorial = function () { // Clear any existing game state first while (enemies.length > 0) { var enemy = enemies.pop(); if (enemy.parent) { enemy.parent.removeChild(enemy); } if (enemy.shadow && enemy.shadow.parent) { enemy.shadow.parent.removeChild(enemy.shadow); } } while (towers.length > 0) { var tower = towers.pop(); if (tower.parent) { tower.parent.removeChild(tower); } } while (bullets.length > 0) { var bullet = bullets.pop(); if (bullet.parent) { bullet.parent.removeChild(bullet); } } // IMPORTANT: Reset tutorial state BEFORE creating the sequence tutorialCompleted = false; tutorialInProgress = false; // Reset game state for tutorial currentWorld = 1; currentLevel = 1; currentWave = 0; // Reset to 0 so tutorial starts at wave 0/9 waveInProgress = false; waveSpawned = false; gold = 80; lives = 20; score = 0; enemiesKilledInWave = 0; // Update wave counter for tutorial - explicitly set to show 0/10 waveCounterText.setText('Wave: 0/10'); updateUI(); // Generate tutorial world grid.generateMazeForWorld(currentWorld); mapVisualization.generateMazeForWorld(currentWorld); grid.pathFind(); grid.renderDebug(); syncVisualizationMap(); worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); // Start tutorial with forest world music playWorldMusic(1); // Create tutorial sequence immediately after state reset var tutorialSequence = new TutorialSequence(); game.addChild(tutorialSequence); // Set tutorial in progress AFTER creation tutorialInProgress = true; tutorialSequence.onComplete = function () { // Mark tutorial as completed tutorialCompleted = true; tutorialInProgress = false; // After tutorial, ensure game is ready to start from wave 1 waveIndicator.gameStarted = true; // Tutorial complete - show next wave button var notification = game.addChild(new Notification("Tutorial complete! Use Next Wave button to start your first wave!")); notification.x = 2048 / 2; notification.y = grid.height - 150; }; };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
unlockedWorlds: 1,
completedWaves: 0,
digiviceC: false,
digiviceB: false,
digiviceA: false,
worldLevels: {},
language: "en",
securityPoints: 0
});
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Apply special effects based on bullet type
if (self.type === 'splash') {
// Create visual splash effect
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
// Play splash attack sound
LK.getSound('splashAttack').play();
// Splash damage to nearby enemies
var splashRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var splashDx = otherEnemy.x - self.targetEnemy.x;
var splashDy = otherEnemy.y - self.targetEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
// Apply splash damage (50% of original damage)
otherEnemy.health -= self.damage * 0.5;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
// Play freeze effect sound for slow attacks
LK.getSound('freezeEffect').play();
// Apply slow effect
// Make slow percentage scale with tower level (default 50%, up to 80% at max level)
var slowPct = 0.5;
if (self.sourceTowerLevel !== undefined) {
// Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
self.targetEnemy.slowDuration = 180; // Reset duration
}
}
} else if (self.type === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Play poison effect sound
LK.getSound('poisonEffect').play();
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
}
// Special Digimon evolutionary line effects
if (self.type === 'agumon' && self.burnChance) {
// Agumon line: Fire/burn effects
if (!self.targetEnemy.isImmune && Math.random() < self.burnChance) {
self.targetEnemy.burning = true;
self.targetEnemy.burnDamage = self.damage * 0.15;
self.targetEnemy.burnDuration = 240; // 4 seconds
var burnEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'burn');
game.addChild(burnEffect);
// Play burn effect sound
LK.getSound('burnEffect').play();
}
} else if (self.type === 'gabumon' && self.freezeChance) {
// Gabumon line: Ice/freeze effects
if (!self.targetEnemy.isImmune && Math.random() < self.freezeChance) {
self.targetEnemy.frozen = true;
self.targetEnemy.frozenDuration = 120; // 2 seconds
if (!self.targetEnemy.originalSpeed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
}
self.targetEnemy.speed = 0; // Completely frozen
var freezeEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'freeze');
game.addChild(freezeEffect);
// Play freeze effect sound
LK.getSound('freezeEffect').play();
}
} else if (self.type === 'tentomon' && self.paralyzeChance) {
// Tentomon line: Electric paralysis affecting multiple enemies
if (!self.targetEnemy.isImmune && Math.random() < self.paralyzeChance) {
// Paralyze main target
self.targetEnemy.paralyzed = true;
self.targetEnemy.paralyzeDuration = 180; // 3 seconds
if (!self.targetEnemy.originalSpeed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
}
self.targetEnemy.speed *= 0.1; // Nearly stopped
var paralyzeEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'paralyze');
game.addChild(paralyzeEffect);
// Spread paralysis to nearby enemies
for (var i = 0; i < enemies.length; i++) {
var nearbyEnemy = enemies[i];
if (nearbyEnemy !== self.targetEnemy && !nearbyEnemy.isImmune) {
var dx = nearbyEnemy.x - self.targetEnemy.x;
var dy = nearbyEnemy.y - self.targetEnemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.paralyzeArea) {
nearbyEnemy.paralyzed = true;
nearbyEnemy.paralyzeDuration = 120; // Shorter for spread effect
if (!nearbyEnemy.originalSpeed) {
nearbyEnemy.originalSpeed = nearbyEnemy.speed;
}
nearbyEnemy.speed *= 0.3;
var spreadEffect = new EffectIndicator(nearbyEnemy.x, nearbyEnemy.y, 'paralyze');
game.addChild(spreadEffect);
}
}
}
}
} else if (self.type === 'palmon' && self.poisonSpread) {
// Palmon line: Spreading poison effects
if (!self.targetEnemy.isImmune) {
// Apply poison to main target
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.25;
self.targetEnemy.poisonDuration = 360; // 6 seconds
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Spread poison to nearby enemies with a chance
for (var i = 0; i < enemies.length; i++) {
var nearbyEnemy = enemies[i];
if (nearbyEnemy !== self.targetEnemy && !nearbyEnemy.isImmune) {
var dx = nearbyEnemy.x - self.targetEnemy.x;
var dy = nearbyEnemy.y - self.targetEnemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.poisonRadius) {
// Use poisonSpreadChance if defined, otherwise default to 30%
var spreadChance = typeof self.poisonSpreadChance === "number" ? self.poisonSpreadChance : 0.3;
if (Math.random() < spreadChance) {
nearbyEnemy.poisoned = true;
nearbyEnemy.poisonDamage = self.damage * 0.15; // Weaker spread poison
nearbyEnemy.poisonDuration = 240;
var spreadPoisonEffect = new EffectIndicator(nearbyEnemy.x, nearbyEnemy.y, 'poison');
game.addChild(spreadPoisonEffect);
}
}
}
}
}
} else if (self.type === 'gomamon' && self.moistureEffect) {
// Gomamon line: Moisture effects that affect fire/ice interactions
if (!self.targetEnemy.isImmune) {
self.targetEnemy.moist = true;
self.targetEnemy.moistDuration = self.moistureDuration;
self.targetEnemy.fireResistance = 0.5; // Reduced fire damage
self.targetEnemy.freezeVulnerability = 1.5; // Increased freeze chance
var moistEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'moist');
game.addChild(moistEffect);
}
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
var Coin = Container.expand(function (x, y, value) {
var self = Container.call(this);
self.value = value || 5;
self.x = x;
self.y = y;
self.collected = false;
self.walkSpeed = 0.5;
self.direction = Math.random() * Math.PI * 2;
self.changeDirectionTimer = 0;
var coinGraphics = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
coinGraphics.width = 30;
coinGraphics.height = 30;
coinGraphics.tint = 0xFFD700;
// Add coin value text
var valueText = new Text2(self.value.toString(), {
size: 20,
fill: 0x000000,
weight: 800
});
valueText.anchor.set(0.5, 0.5);
self.addChild(valueText);
self.update = function () {
if (self.collected) return;
// Change direction occasionally
self.changeDirectionTimer++;
if (self.changeDirectionTimer > 120) {
// Change direction every 2 seconds
self.direction = Math.random() * Math.PI * 2;
self.changeDirectionTimer = 0;
}
// Move in current direction
var newX = self.x + Math.cos(self.direction) * self.walkSpeed;
var newY = self.y + Math.sin(self.direction) * self.walkSpeed;
// Keep within playable bounds
var minX = grid.x + CELL_SIZE;
var maxX = grid.x + grid.cells.length * CELL_SIZE - CELL_SIZE;
var minY = grid.y + CELL_SIZE;
var maxY = grid.y + grid.cells[0].length * CELL_SIZE - CELL_SIZE;
if (newX < minX || newX > maxX) {
self.direction = Math.PI - self.direction; // Bounce horizontally
} else {
self.x = newX;
}
if (newY < minY || newY > maxY) {
self.direction = -self.direction; // Bounce vertically
} else {
self.y = newY;
}
// Check if player clicks on coin
};
self.down = function () {
if (!self.collected) {
self.collected = true;
setGold(gold + self.value);
// Add 10 security score points
score += 10;
// Save security points to storage
storage.securityPoints = score;
updateUI();
// Play coin collect sound
LK.getSound('coinCollect').play();
var goldIndicator = new GoldIndicator(self.value, self.x, self.y);
game.addChild(goldIndicator);
// Remove coin from coins array
var coinIndex = coins.indexOf(self);
if (coinIndex !== -1) {
coins.splice(coinIndex, 1);
}
self.destroy();
}
};
return self;
});
var ComicPanel = Container.expand(function (imagePath, text) {
var self = Container.call(this);
// Panel background - larger panel
var panelBg = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
panelBg.width = 700;
panelBg.height = 500;
panelBg.tint = 0x222222;
panelBg.alpha = 0.9;
// Add border effect
var border = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
border.width = 710;
border.height = 510;
border.tint = 0x000000;
border.alpha = 0.8;
self.addChildAt(border, 0);
// Text area - larger and repositioned
var textBg = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
textBg.width = 660;
textBg.height = 160;
textBg.tint = 0xffffff;
textBg.alpha = 0.95;
textBg.y = 170;
self.addChild(textBg);
// Character image if provided - repositioned
if (imagePath) {
var characterImage = self.attachAsset(imagePath, {
anchorX: 0.5,
anchorY: 0.5
});
characterImage.width = 200;
characterImage.height = 200;
characterImage.y = -80;
self.addChild(characterImage);
}
// Story text - improved sizing and positioning
var storyText = new Text2(text, {
size: 28,
fill: 0x000000,
weight: 600
});
storyText.anchor.set(0.5, 0.5);
storyText.y = 170;
storyText.wordWrap = true;
storyText.wordWrapWidth = 600;
storyText.maxWidth = 600;
self.addChild(storyText);
// Scale animation entrance
self.scaleX = 0.3;
self.scaleY = 0.3;
self.alpha = 0;
self.show = function () {
tween(self, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 400,
easing: tween.backOut
});
};
self.hide = function (callback) {
tween(self, {
scaleX: 0.8,
scaleY: 0.8,
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onFinish: callback
});
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
// Removed number label to improve performance
// Numbers were causing lag due to text rendering overhead
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
// Renderizado visual desactivado para evitar interferencia con tiles del mundo
// Solo mantiene la lógica de pathfinding sin elementos visuales
// Los tiles del mundo se renderizan a través de WorldRenderer
// Actualizar las flechas solo si hay una torre seleccionada para debug
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
// Mostrar flechas solo para torres seleccionadas
while (debugArrows.length > data.targets.length) {
self.removeChild(debugArrows.pop());
}
for (var a = 0; a < data.targets.length; a++) {
var destination = data.targets[a];
var ox = destination.x - data.x;
var oy = destination.y - data.y;
var angle = Math.atan2(oy, ox);
if (!debugArrows[a]) {
debugArrows[a] = LK.getAsset('arrow', {
anchorX: -.5,
anchorY: 0.5
});
debugArrows[a].alpha = .5;
self.addChildAt(debugArrows[a], 1);
}
debugArrows[a].rotation = angle;
}
} else {
// Remover flechas si no hay torre seleccionada
self.removeArrows();
}
// Hacer el gráfico de celda invisible para no interferir con tiles
cellGraphics.alpha = 0;
};
});
var DigimonShop = Container.expand(function () {
var self = Container.call(this);
self.visible = false;
self.y = 2732;
var shopBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shopBackground.width = 2048;
shopBackground.height = 600;
shopBackground.tint = 0x222222;
shopBackground.alpha = 0.95;
var titleText = new Text2('Digimon Firewall Shop', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -250;
self.addChild(titleText);
// Create shop items container
var itemsContainer = new Container();
itemsContainer.y = -50;
self.addChild(itemsContainer);
// Digivice items data
var digiviceItems = [{
id: 'digiviceC',
name: 'Digivice C',
cost: 500,
description: 'Unlocks Champion Level'
}, {
id: 'digiviceB',
name: 'Digivice B',
cost: 2000,
description: 'Unlocks Ultimate Level'
}, {
id: 'digiviceA',
name: 'Digivice A',
cost: 8000,
description: 'Unlocks Mega Level'
}];
// Create shop item buttons
for (var i = 0; i < digiviceItems.length; i++) {
var item = digiviceItems[i];
var itemButton = new Container();
itemButton.x = -600 + i * 400;
itemButton.y = 0;
itemsContainer.addChild(itemButton);
var itemBg = itemButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
itemBg.width = 350;
itemBg.height = 200;
var itemNameText = new Text2(item.name, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
itemNameText.anchor.set(0.5, 0.5);
itemNameText.y = -50;
itemButton.addChild(itemNameText);
var itemDescText = new Text2(item.description, {
size: 35,
fill: 0xCCCCCC,
weight: 400
});
itemDescText.anchor.set(0.5, 0.5);
itemDescText.y = -10;
itemButton.addChild(itemDescText);
var itemCostText = new Text2(item.cost + ' points', {
size: 45,
fill: 0xFFD700,
weight: 800
});
itemCostText.anchor.set(0.5, 0.5);
itemCostText.y = 40;
itemButton.addChild(itemCostText);
// Create purchase functionality
(function (itemData, button, background, costText) {
button.update = function () {
var owned = storage[itemData.id] || false;
var canAfford = score >= itemData.cost;
if (owned) {
background.tint = 0x00AA00;
costText.setText('OWNED');
button.alpha = 0.7;
} else if (canAfford) {
background.tint = 0x4444FF;
costText.setText(itemData.cost + ' points');
button.alpha = 1.0;
} else {
background.tint = 0x666666;
costText.setText(itemData.cost + ' points');
button.alpha = 0.5;
}
};
button.down = function () {
var owned = storage[itemData.id] || false;
var canAfford = score >= itemData.cost;
if (owned) {
var notification = game.addChild(new Notification("You already own " + itemData.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (canAfford) {
score -= itemData.cost;
updateUI();
storage[itemData.id] = true;
var notification = game.addChild(new Notification("Purchased " + itemData.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
var notification = game.addChild(new Notification("Not enough security points for " + itemData.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
})(item, itemButton, itemBg, itemCostText);
}
var closeButton = new Container();
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = shopBackground.width / 2 - 57;
closeButton.y = -shopBackground.height / 2 + 57;
self.addChild(closeButton);
closeButton.down = function () {
self.hide();
};
self.show = function () {
self.visible = true;
tween(self, {
y: 2732 - 300
}, {
duration: 300,
easing: tween.backOut
});
};
self.hide = function () {
tween(self, {
y: 2732
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.visible = false;
}
});
};
return self;
});
var DigimonSummonMenu = Container.expand(function () {
var self = Container.call(this);
self.visible = false;
self.y = 2732;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 600;
menuBackground.tint = 0x222222;
menuBackground.alpha = 0.95;
var titleText = new Text2('Summon Digimon Allies', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -250;
self.addChild(titleText);
// Create summon items container
var itemsContainer = new Container();
itemsContainer.y = -50;
self.addChild(itemsContainer);
// Available Digimon based on unlocked Digivices
function getAvailableDigimon() {
// All Digimon that can be summoned, matching those available by microphone
var available = [{
id: 'koromon',
name: 'Koromon',
cost: 50,
description: 'Rookie Melee Fighter',
reqDigivice: null
}, {
id: 'tsunomon',
name: 'Tsunomon',
cost: 60,
description: 'Rookie Ranged Fighter',
reqDigivice: null
},
// Champion level (Digivice C required)
{
id: 'greymon',
name: 'Greymon',
cost: 150,
description: 'Champion Fire Dragon',
reqDigivice: 'digiviceC'
}, {
id: 'garurumon',
name: 'Garurumon',
cost: 180,
description: 'Champion Ice Wolf',
reqDigivice: 'digiviceC'
}, {
id: 'kabuterimon',
name: 'Kabuterimon',
cost: 160,
description: 'Champion Electric Insect',
reqDigivice: 'digiviceC'
},
// Ultimate level (Digivice B required)
{
id: 'metalgreymon',
name: 'MetalGreymon',
cost: 400,
description: 'Ultimate Cyborg Dragon',
reqDigivice: 'digiviceB'
}, {
id: 'weregarurumon',
name: 'WereGarurumon',
cost: 450,
description: 'Ultimate Beast Warrior',
reqDigivice: 'digiviceB'
}, {
id: 'megakabuterimon',
name: 'MegaKabuterimon',
cost: 420,
description: 'Ultimate Giant Insect',
reqDigivice: 'digiviceB'
},
// Mega level (Digivice A required)
{
id: 'wargreymon',
name: 'WarGreymon',
cost: 800,
description: 'Mega Dragon Warrior',
reqDigivice: 'digiviceA'
}];
return available;
}
function updateDigimonButtons() {
// Clear existing buttons
while (itemsContainer.children.length > 0) {
itemsContainer.removeChild(itemsContainer.children[0]);
}
// Only show Digimon that can actually be summoned (requirements met)
var availableDigimon = getAvailableDigimon().filter(function (d) {
// Check digivice requirement
if (d.reqDigivice && !storage[d.reqDigivice]) return false;
// Check cost
if (score < d.cost) return false;
// Check maxAlliedUnits
if (alliedUnits.length >= maxAlliedUnits) return false;
return true;
});
// If a filterLevel is set (from C, B, or A button), filter Digimon by evolution level
var filterLevel = self._filterLevel;
if (filterLevel) {
// Map Digimon id to evolution level
var digimonLevelMap = {
koromon: "rookie",
tsunomon: "rookie",
greymon: "champion",
garurumon: "champion",
kabuterimon: "champion",
metalgreymon: "ultimate",
weregarurumon: "ultimate",
megakabuterimon: "ultimate",
wargreymon: "mega"
};
availableDigimon = availableDigimon.filter(function (d) {
return (digimonLevelMap[d.id] || "").toLowerCase() === filterLevel.toLowerCase();
});
}
// Layout: 3 per row, center horizontally
var buttonsPerRow = Math.min(3, availableDigimon.length);
var buttonSpacing = 400;
var startX = -((buttonsPerRow - 1) * buttonSpacing) / 2;
for (var i = 0; i < availableDigimon.length; i++) {
var digimon = availableDigimon[i];
var row = Math.floor(i / buttonsPerRow);
var col = i % buttonsPerRow;
var digimonButton = new Container();
digimonButton.x = startX + col * buttonSpacing;
digimonButton.y = row * 180;
itemsContainer.addChild(digimonButton);
// Use correct asset for Digimon
var digimonAssetMap = {
koromon: 'agumon',
tsunomon: 'gabumon',
greymon: 'greymon',
garurumon: 'garurumon',
kabuterimon: 'kabuterimon',
metalgreymon: 'metalgreymon',
weregarurumon: 'weregarurumon',
megakabuterimon: 'megakabuterimon',
wargreymon: 'wargreymon'
};
var assetId = digimonAssetMap[digimon.id] || 'agumon';
var digimonArt = digimonButton.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
digimonArt.width = 90;
digimonArt.height = 90;
digimonArt.y = -60;
var buttonBg = digimonButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 350;
buttonBg.height = 160;
var nameText = new Text2(digimon.name, {
size: 45,
fill: 0xFFFFFF,
weight: 800
});
nameText.anchor.set(0.5, 0.5);
nameText.y = -40;
digimonButton.addChild(nameText);
var descText = new Text2(digimon.description, {
size: 28,
fill: 0xCCCCCC,
weight: 400
});
descText.anchor.set(0.5, 0.5);
descText.y = -10;
digimonButton.addChild(descText);
var costText = new Text2(digimon.cost + ' points', {
size: 35,
fill: 0xFFD700,
weight: 800
});
costText.anchor.set(0.5, 0.5);
costText.y = 30;
digimonButton.addChild(costText);
// Create functionality for each button
(function (digimonData, button, background, costText) {
button.update = function () {
var canAfford = score >= digimonData.cost;
var hasSpace = alliedUnits.length < maxAlliedUnits;
var isAvailable = !summonCooldown || LK.ticks - lastSummonTime > summonCooldown;
if (canAfford && hasSpace && isAvailable) {
background.tint = 0x4444FF;
button.alpha = 1.0;
} else {
background.tint = 0x666666;
button.alpha = 0.7;
}
};
button.down = function () {
var canAfford = score >= digimonData.cost;
var hasSpace = alliedUnits.length < maxAlliedUnits;
var isAvailable = !summonCooldown || LK.ticks - lastSummonTime > summonCooldown;
if (!canAfford) {
var notification = game.addChild(new Notification("Not enough security points for " + digimonData.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (!hasSpace) {
var notification = game.addChild(new Notification("¡Límite de aliados alcanzado!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (!isAvailable) {
var remainingCooldown = Math.ceil((summonCooldown - (LK.ticks - lastSummonTime)) / 60);
var notification = game.addChild(new Notification("Summon cooldown: " + remainingCooldown + "s"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
// Summon the Digimon
score -= digimonData.cost;
updateUI();
var newUnit = new DigimonUnit(digimonData.id, 1);
enemyLayerTop.addChild(newUnit); // Add to top layer so they appear above enemies
alliedUnits.push(newUnit);
lastSummonTime = LK.ticks;
// Play summon sound for all Digimon
LK.getSound('voiceSummon').play();
var notification = game.addChild(new Notification(digimonData.name + " summoned!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
self.hide();
}
};
})(digimon, digimonButton, buttonBg, costText);
}
}
// Close button
var closeButton = new Container();
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
self.addChild(closeButton);
closeButton.down = function () {
self.hide();
};
self.show = function (filterLevel) {
// If a filterLevel is provided, filter Digimon by evolution level
self._filterLevel = filterLevel || null;
updateDigimonButtons();
self.visible = true;
tween(self, {
y: 2732 - 300
}, {
duration: 300,
easing: tween.backOut
});
};
self.hide = function () {
self._filterLevel = null; // Reset filter when closing
tween(self, {
y: 2732
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.visible = false;
}
});
};
self.update = function () {
// Update all button states
for (var i = 0; i < itemsContainer.children.length; i++) {
var button = itemsContainer.children[i];
if (button.update) button.update();
}
};
return self;
});
var DigimonUnit = Container.expand(function (type, level) {
var self = Container.call(this);
self.type = type || 'koromon';
self.level = level || 1;
self.health = 100 + (self.level - 1) * 50;
self.maxHealth = self.health;
self.damage = 20 + (self.level - 1) * 10;
self.speed = 0.8;
self.range = CELL_SIZE * 1.5;
self.attackType = 'melee'; // 'melee', 'ranged', 'area'
self.lastAttacked = 0;
self.attackRate = 90; // Frames between attacks
self.targetEnemy = null;
self.isDead = false;
// --- Digimon UI contextual elements ---
var digimonNames = {
koromon: "Koromon",
tsunomon: "Tsunomon",
greymon: "Greymon",
garurumon: "Garurumon",
kabuterimon: "Kabuterimon",
metalgreymon: "MetalGreymon",
weregarurumon: "WereGarurumon",
megakabuterimon: "MegaKabuterimon",
wargreymon: "WarGreymon"
};
var digimonLevels = {
koromon: "Rookie",
tsunomon: "Rookie",
greymon: "Champion",
garurumon: "Champion",
kabuterimon: "Champion",
metalgreymon: "Ultimate",
weregarurumon: "Ultimate",
megakabuterimon: "Ultimate",
wargreymon: "Mega"
};
var digimonDescriptions = {
koromon: "Rookie Melee Fighter",
tsunomon: "Rookie Ranged Fighter",
greymon: "Champion Fire Dragon",
garurumon: "Champion Ice Wolf",
kabuterimon: "Champion Electric Insect",
metalgreymon: "Ultimate Cyborg Dragon",
weregarurumon: "Ultimate Beast Warrior",
megakabuterimon: "Ultimate Giant Insect",
wargreymon: "Mega Dragon Warrior"
};
var digimonColor = {
koromon: 0xFFAAAA,
tsunomon: 0xAAAAFF,
greymon: 0xFF6600,
garurumon: 0x0066FF,
kabuterimon: 0x33CC00,
metalgreymon: 0xFF3300,
weregarurumon: 0x0066CC,
megakabuterimon: 0x228B22,
wargreymon: 0xFFCC00
};
// Set properties based on Digimon type and level
switch (self.type) {
case 'koromon':
self.attackType = 'melee';
self.damage = 15 + (self.level - 1) * 8;
break;
case 'tsunomon':
self.attackType = 'ranged';
self.damage = 12 + (self.level - 1) * 6;
self.range = CELL_SIZE * 2;
break;
case 'greymon':
self.attackType = 'area';
self.damage = 25 + (self.level - 1) * 15;
self.health = 150 + (self.level - 1) * 75;
break;
case 'garurumon':
self.attackType = 'ranged';
self.damage = 20 + (self.level - 1) * 12;
self.range = CELL_SIZE * 2.5;
self.speed = 1.2;
break;
case 'kabuterimon':
self.attackType = 'area';
self.damage = 22 + (self.level - 1) * 13;
self.health = 140 + (self.level - 1) * 60;
self.range = CELL_SIZE * 2;
break;
case 'metalgreymon':
self.attackType = 'area';
self.damage = 40 + (self.level - 1) * 25;
self.health = 250 + (self.level - 1) * 100;
self.range = CELL_SIZE * 2;
break;
case 'weregarurumon':
self.attackType = 'ranged';
self.damage = 38 + (self.level - 1) * 20;
self.health = 220 + (self.level - 1) * 90;
self.range = CELL_SIZE * 3;
self.speed = 1.5;
break;
case 'megakabuterimon':
self.attackType = 'area';
self.damage = 36 + (self.level - 1) * 18;
self.health = 230 + (self.level - 1) * 90;
self.range = CELL_SIZE * 2.2;
break;
case 'wargreymon':
self.attackType = 'area';
self.damage = 60 + (self.level - 1) * 40;
self.health = 400 + (self.level - 1) * 150;
self.range = CELL_SIZE * 2.5;
self.speed = 1.0;
break;
}
self.maxHealth = self.health;
// Use correct asset for all allied units
var digimonAssetMap = {
koromon: 'agumon',
tsunomon: 'gabumon',
greymon: 'greymon',
garurumon: 'garurumon',
kabuterimon: 'kabuterimon',
metalgreymon: 'metalgreymon',
weregarurumon: 'weregarurumon',
megakabuterimon: 'megakabuterimon',
wargreymon: 'wargreymon'
};
var assetId = digimonAssetMap[self.type] || 'agumon';
var unitGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Tint and scale based on type
switch (self.type) {
case 'koromon':
unitGraphics.tint = 0xFFAAAA;
break;
case 'tsunomon':
unitGraphics.tint = 0xAAAAFF;
break;
case 'greymon':
unitGraphics.tint = 0xFF6600;
unitGraphics.scaleX = 1.3;
unitGraphics.scaleY = 1.3;
break;
case 'garurumon':
unitGraphics.tint = 0x0066FF;
unitGraphics.scaleX = 1.3;
unitGraphics.scaleY = 1.3;
break;
case 'kabuterimon':
unitGraphics.tint = 0x33CC00;
unitGraphics.scaleX = 1.2;
unitGraphics.scaleY = 1.2;
break;
case 'metalgreymon':
unitGraphics.tint = 0xFF3300;
unitGraphics.scaleX = 1.6;
unitGraphics.scaleY = 1.6;
break;
case 'weregarurumon':
unitGraphics.tint = 0x0066CC;
unitGraphics.scaleX = 1.6;
unitGraphics.scaleY = 1.6;
break;
case 'megakabuterimon':
unitGraphics.tint = 0x228B22;
unitGraphics.scaleX = 1.6;
unitGraphics.scaleY = 1.6;
break;
case 'wargreymon':
unitGraphics.tint = 0xFFCC00;
unitGraphics.scaleX = 2.0;
unitGraphics.scaleY = 2.0;
break;
}
// --- Level badge (top left of unit) ---
var badgeColors = {
Rookie: 0x00BFFF,
Champion: 0x32CD32,
Ultimate: 0xFFD700,
Mega: 0xFF4500
};
var levelName = digimonLevels[self.type] || "Rookie";
var badgeColor = badgeColors[levelName] || 0x00BFFF;
var badge = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
badge.width = 44;
badge.height = 44;
badge.x = -unitGraphics.width / 2 + 22;
badge.y = -unitGraphics.height / 2 + 22;
badge.tint = badgeColor;
var badgeText = new Text2(levelName.charAt(0), {
size: 32,
fill: 0xffffff,
weight: 800
});
badgeText.anchor.set(0.5, 0.5);
badgeText.x = badge.x;
badgeText.y = badge.y;
self.addChild(badge);
self.addChild(badgeText);
// --- Name popup (on summon, above unit) ---
var namePopup = new Text2(digimonNames[self.type] || self.type, {
size: 60,
fill: 0xffffff,
weight: 800
});
namePopup.anchor.set(0.5, 1.0);
namePopup.x = 0;
namePopup.y = -unitGraphics.height / 2 - 30;
namePopup.alpha = 0;
self.addChild(namePopup);
// Animate name popup on spawn
tween(namePopup, {
alpha: 1,
y: namePopup.y - 30
}, {
duration: 350,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(namePopup, {
alpha: 0,
y: namePopup.y - 60
}, {
duration: 700,
delay: 700,
easing: tween.easeIn,
onFinish: function onFinish() {
namePopup.visible = false;
}
});
}
});
// Play summon sound and effect for all Digimon
if (typeof LK !== "undefined" && LK.getSound) {
LK.getSound('voiceSummon').play();
}
LK.effects.flashObject(self, 0x00FF00, 400);
// --- Tooltip on touch (shows name, level, description) ---
var tooltip = new Container();
tooltip.visible = false;
var tooltipBg = tooltip.attachAsset('notification', {
anchorX: 0.5,
anchorY: 1.0
});
tooltipBg.width = 340;
tooltipBg.height = 120;
tooltipBg.tint = 0x222222;
tooltipBg.alpha = 0.95;
var tooltipName = new Text2(digimonNames[self.type] || self.type, {
size: 38,
fill: 0xffffff,
weight: 800
});
tooltipName.anchor.set(0.5, 0);
tooltipName.y = -50;
var tooltipLevel = new Text2(levelName, {
size: 28,
fill: badgeColor,
weight: 800
});
tooltipLevel.anchor.set(0.5, 0);
tooltipLevel.y = -15;
var tooltipDesc = new Text2(digimonDescriptions[self.type] || "", {
size: 24,
fill: 0xcccccc,
weight: 400
});
tooltipDesc.anchor.set(0.5, 0);
tooltipDesc.y = 15;
tooltip.addChild(tooltipBg);
tooltip.addChild(tooltipName);
tooltip.addChild(tooltipLevel);
tooltip.addChild(tooltipDesc);
tooltip.x = 0;
tooltip.y = -unitGraphics.height / 2 - 10;
self.addChild(tooltip);
// Health bar
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -unitGraphics.height / 2 - 15;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2 - 0.5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
// Movement properties
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = null;
self.pathTargets = [];
// Initialize at goal positions (moving towards spawns)
if (grid.goals && grid.goals.length > 0) {
var startGoal = grid.goals[Math.floor(Math.random() * grid.goals.length)];
self.cellX = startGoal.x;
self.cellY = startGoal.y;
self.currentCellX = startGoal.x;
self.currentCellY = startGoal.y;
self.x = grid.x + self.currentCellX * CELL_SIZE;
self.y = grid.y + self.currentCellY * CELL_SIZE;
}
// --- Touch interaction for tooltip ---
self.down = function () {
tooltip.visible = true;
tooltip.alpha = 0;
tween(tooltip, {
alpha: 1
}, {
duration: 120,
easing: tween.easeOut
});
// Hide after 2 seconds
LK.setTimeout(function () {
tween(tooltip, {
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
tooltip.visible = false;
}
});
}, 2000);
};
self.findNearbyEnemies = function () {
var nearbyEnemies = [];
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.range) {
nearbyEnemies.push(enemy);
}
}
return nearbyEnemies;
};
self.attack = function (targetEnemy) {
if (LK.ticks - self.lastAttacked < self.attackRate) return;
self.lastAttacked = LK.ticks;
switch (self.attackType) {
case 'melee':
// Direct damage
targetEnemy.health -= self.damage;
if (targetEnemy.health <= 0) targetEnemy.health = 0;else targetEnemy.healthBar.width = targetEnemy.health / targetEnemy.maxHealth * 70;
// Visual effect
var effect = new EffectIndicator(targetEnemy.x, targetEnemy.y, 'sniper');
game.addChild(effect);
break;
case 'ranged':
// Create projectile
var bullet = new Bullet(self.x, self.y, targetEnemy, self.damage, 8);
bullet.type = 'sniper';
bullet.children[0].tint = 0x00AAFF;
game.addChild(bullet);
bullets.push(bullet);
targetEnemy.bulletsTargetingThis.push(bullet);
break;
case 'area':
// Area damage to all enemies in range
var nearbyEnemies = self.findNearbyEnemies();
for (var i = 0; i < nearbyEnemies.length; i++) {
var enemy = nearbyEnemies[i];
enemy.health -= self.damage;
if (enemy.health <= 0) enemy.health = 0;else enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Visual splash effect
var splashEffect = new EffectIndicator(self.x, self.y, 'splash');
game.addChild(splashEffect);
break;
}
};
self.update = function () {
if (self.isDead) return;
// --- Visual feedback for status effects (UX/UI improvement) ---
if (self.burning && LK.ticks % 45 === 0) {
var burnFx = new EffectIndicator(self.x, self.y, 'burn');
if (game && game.addChild) game.addChild(burnFx);
}
if (self.frozen && LK.ticks % 30 === 0) {
var freezeFx = new EffectIndicator(self.x, self.y, 'freeze');
if (game && game.addChild) game.addChild(freezeFx);
}
if (self.poisoned && LK.ticks % 30 === 0) {
var poisonFx = new EffectIndicator(self.x, self.y, 'poison');
if (game && game.addChild) game.addChild(poisonFx);
}
if (self.paralyzed && LK.ticks % 30 === 0) {
var paralyzeFx = new EffectIndicator(self.x, self.y, 'paralyze');
if (game && game.addChild) game.addChild(paralyzeFx);
}
if (self.moist && LK.ticks % 60 === 0) {
var moistFx = new EffectIndicator(self.x, self.y, 'moist');
if (game && game.addChild) game.addChild(moistFx);
}
if (self.healing && LK.ticks % 30 === 0) {
var healFx = new EffectIndicator(self.x, self.y, 'heal');
if (game && game.addChild) game.addChild(healFx);
}
// Update health bar
if (self.health <= 0) {
self.isDead = true;
self.healthBar.width = 0;
// Show disappearance animation (fade out and scale up)
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 600,
easing: tween.easeIn,
onFinish: function onFinish() {
// Remove from allied units array and destroy after delay
var unitIndex = alliedUnits.indexOf(self);
if (unitIndex !== -1) alliedUnits.splice(unitIndex, 1);
self.destroy();
}
});
// Show notification of defeat
var defeatNote = game.addChild(new Notification((digimonNames[self.type] || self.type) + " defeated!"));
defeatNote.x = 2048 / 2;
defeatNote.y = grid.height - 120;
return;
}
self.healthBar.width = self.health / self.maxHealth * 70;
// Find and attack nearby enemies
var nearbyEnemies = self.findNearbyEnemies();
if (nearbyEnemies.length > 0) {
// Stop and attack closest enemy
var closestEnemy = nearbyEnemies[0];
var closestDistance = Infinity;
for (var i = 0; i < nearbyEnemies.length; i++) {
var enemy = nearbyEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
self.targetEnemy = closestEnemy;
self.attack(closestEnemy);
// Face the enemy
var dx = closestEnemy.x - self.x;
var dy = closestEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
unitGraphics.rotation = angle;
return; // Don't move while attacking
}
self.targetEnemy = null;
// --- Movement along the path toward enemies (from goal to spawn) ---
if (!self.currentTarget) {
var cell = grid.getCell(Math.round(self.currentCellX), Math.round(self.currentCellY));
if (cell && cell.targets && cell.targets.length > 0) {
var bestTarget = null;
var highestScore = -1;
for (var i = 0; i < cell.targets.length; i++) {
var target = cell.targets[i];
if (target.score > highestScore) {
highestScore = target.score;
bestTarget = target;
}
}
self.currentTarget = bestTarget;
}
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < self.speed * gameSpeed) {
self.currentCellX = self.currentTarget.x;
self.currentCellY = self.currentTarget.y;
self.cellX = Math.round(self.currentCellX);
self.cellY = Math.round(self.currentCellY);
self.currentTarget = null;
return;
}
var angle = Math.atan2(oy, ox);
self.currentCellX += Math.cos(angle) * self.speed * gameSpeed;
self.currentCellY += Math.sin(angle) * self.speed * gameSpeed;
// Face movement direction
unitGraphics.rotation = angle;
}
// Update position
self.x = grid.x + self.currentCellX * CELL_SIZE;
self.y = grid.y + self.currentCellY * CELL_SIZE;
// Check if reached spawn area (remove unit)
if (self.currentCellY <= 3) {
var unitIndex = alliedUnits.indexOf(self);
if (unitIndex !== -1) alliedUnits.splice(unitIndex, 1);
self.destroy();
}
};
return self;
});
// This update method was incorrectly placed here and should be removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'burn':
effectGraphics.tint = 0xFF4400;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.2;
break;
case 'freeze':
effectGraphics.tint = 0x66CCFF;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.3;
break;
case 'paralyze':
effectGraphics.tint = 0xFFFF00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.1;
break;
case 'moist':
effectGraphics.tint = 0x0099CC;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 0.9;
break;
case 'heal':
effectGraphics.tint = 0x00FF88;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.4;
break;
}
effectGraphics.alpha = 0.7;
self.alpha = 0;
// Animate the effect
tween(self, {
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
// Base enemy class for common functionality
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'normal';
self.speed = .01;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
self.worldNumber = currentWorld; // Track which world this enemy belongs to
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
// --- ENEMY ATTACK TOWER LOGIC ---
// Enemies will attack towers if they are adjacent or on the same cell
self.attackTowerCooldown = 0;
self.attackTowerRate = 60; // Try to attack a tower every 1 second
self.update = function () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
var slowFx = new EffectIndicator(self.x, self.y, 'slow');
if (game && game.addChild) game.addChild(slowFx);
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
var poisonFx = new EffectIndicator(self.x, self.y, 'poison');
if (game && game.addChild) game.addChild(poisonFx);
}
// Apply poison damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle burn effect (Agumon line)
if (self.burning) {
if (LK.ticks % 45 === 0) {
// Every 0.75 seconds
var burnDamage = self.burnDamage;
// Reduce burn damage if enemy is moist
if (self.moist && self.fireResistance) {
burnDamage *= self.fireResistance;
}
self.health -= burnDamage;
if (self.health <= 0) self.health = 0;else self.healthBar.width = self.health / self.maxHealth * 70;
var burnFx = new EffectIndicator(self.x, self.y, 'burn');
if (game && game.addChild) game.addChild(burnFx);
}
self.burnDuration--;
if (self.burnDuration <= 0) {
self.burning = false;
}
}
// Handle freeze effect (Gabumon line)
if (self.frozen) {
if (LK.ticks % 30 === 0) {
var freezeFx = new EffectIndicator(self.x, self.y, 'freeze');
if (game && game.addChild) game.addChild(freezeFx);
}
self.frozenDuration--;
if (self.frozenDuration <= 0) {
self.frozen = false;
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
}
}
// Handle paralysis effect (Tentomon line)
if (self.paralyzed) {
if (LK.ticks % 30 === 0) {
var paralyzeFx = new EffectIndicator(self.x, self.y, 'paralyze');
if (game && game.addChild) game.addChild(paralyzeFx);
}
self.paralyzeDuration--;
if (self.paralyzeDuration <= 0) {
self.paralyzed = false;
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
}
}
// Handle moisture effect (Gomamon line)
if (self.moist) {
if (LK.ticks % 60 === 0) {
var moistFx = new EffectIndicator(self.x, self.y, 'moist');
if (game && game.addChild) game.addChild(moistFx);
}
self.moistDuration--;
if (self.moistDuration <= 0) {
self.moist = false;
self.fireResistance = 1.0;
self.freezeVulnerability = 1.0;
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
// --- ENEMY ATTACK TOWER LOGIC ---
// Enemies will attack towers if they are adjacent or on the same cell
if (!self.isFlying && !self.isBoss) {
self.attackTowerCooldown--;
if (self.attackTowerCooldown <= 0) {
// Check for towers in adjacent cells (including current cell)
var foundTower = null;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Check if tower is in a 2x2 area adjacent to enemy's cell
var dx = Math.abs(tower.gridX + 0.5 - self.cellX);
var dy = Math.abs(tower.gridY + 0.5 - self.cellY);
if (dx <= 1.5 && dy <= 1.5) {
foundTower = tower;
break;
}
}
if (foundTower) {
// Deal damage to the tower
var baseDamage = 3;
// Increase damage with world and wave
var worldMultiplier = 1 + (self.worldNumber - 1) * 0.2;
var waveMultiplier = 1 + currentWave * 0.03;
var totalDamage = Math.ceil(baseDamage * worldMultiplier * waveMultiplier);
foundTower.health = Math.max(0, foundTower.health - totalDamage);
foundTower.towerHealthBar.width = foundTower.health / foundTower.maxHealth * 76;
// Play system damage sound
LK.getSound('systemDamage').play();
// Show damage indicator
var damageIndicator = new Notification("-" + totalDamage + " Tower Health!");
damageIndicator.x = foundTower.x;
damageIndicator.y = foundTower.y - 80;
if (game && game.addChild) game.addChild(damageIndicator);
// If tower destroyed, remove from game
if (foundTower.health <= 0) {
var idx = towers.indexOf(foundTower);
if (idx !== -1) towers.splice(idx, 1);
if (foundTower.parent) foundTower.parent.removeChild(foundTower);
}
self.attackTowerCooldown = Math.max(30, self.attackTowerRate - Math.floor(currentWave * 0.5)); // Faster attacks as game progresses
} else {
self.attackTowerCooldown = 15; // Check again soon
}
}
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
// Apply world-based scaling to all enemy types
self.applyWorldScaling = function () {
var worldMultiplier = 1;
var speedMultiplier = 1;
// Scale stats based on world progression
switch (self.worldNumber) {
case 1:
// Forest - Base stats
worldMultiplier = 1;
speedMultiplier = 1;
break;
case 2:
// Desert - 25% more health, 10% faster
worldMultiplier = 1.25;
speedMultiplier = 1.1;
break;
case 3:
// Glacier - 50% more health, slightly slower
worldMultiplier = 1.5;
speedMultiplier = 0.95;
break;
case 4:
// Village - 75% more health, 15% faster
worldMultiplier = 1.75;
speedMultiplier = 1.15;
break;
case 5:
// Tech Lab - Double health, 20% faster
worldMultiplier = 2.0;
speedMultiplier = 1.2;
break;
case 6:
// Inferno - Triple health, 25% faster
worldMultiplier = 3.0;
speedMultiplier = 1.25;
break;
default:
worldMultiplier = 1;
speedMultiplier = 1;
}
// Apply scaling
self.maxHealth = Math.floor(self.maxHealth * worldMultiplier);
self.speed *= speedMultiplier;
};
// Apply world scaling
self.applyWorldScaling();
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this virus type
var assetId = 'virus';
if (self.isBoss) {
// Use world-specific boss assets
switch (self.worldNumber) {
case 1:
assetId = 'boss_snorlax';
break;
case 2:
assetId = 'boss_rhydon';
break;
case 3:
assetId = 'boss_articuno';
break;
case 4:
assetId = 'boss_machamp';
break;
case 5:
assetId = 'boss_groudon';
break;
case 6:
assetId = 'boss_mewtwo';
break;
default:
assetId = 'virus';
}
} else if (self.type !== 'normal') {
assetId = 'virus_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale up boss enemies (but less since they already have larger base assets)
if (self.isBoss) {
enemyGraphics.scaleX = 1.2;
enemyGraphics.scaleY = 1.2;
}
// Apply world-based color tinting system
self.getWorldTint = function () {
var baseTint = 0xFFFFFF; // Default white tint
// Get base color based on world
switch (self.worldNumber) {
case 1:
// Forest - Green tints
baseTint = 0x90EE90;
break;
case 2:
// Desert - Sand/yellow tints
baseTint = 0xF4A460;
break;
case 3:
// Glacier - Blue/ice tints
baseTint = 0xADD8E6;
break;
case 4:
// Village - Brown/earth tints
baseTint = 0xD2B48C;
break;
case 5:
// Tech Lab - Metallic/silver tints
baseTint = 0xC0C0C0;
break;
case 6:
// Inferno - Red/fire tints
baseTint = 0xFF6B6B;
break;
default:
baseTint = 0xFFFFFF;
}
// Modify base tint slightly based on enemy type while keeping world theme
switch (self.type) {
case 'fast':
// Make it slightly more blue-ish while keeping world color
baseTint = tween.linear(baseTint, 0x0080FF, 0.3);
break;
case 'immune':
// Make it slightly more red-ish while keeping world color
baseTint = tween.linear(baseTint, 0xFF4444, 0.3);
break;
case 'flying':
// Make it brighter while keeping world color
baseTint = Math.min(0xFFFFFF, baseTint + 0x202020);
break;
case 'swarm':
// Make it slightly darker while keeping world color
baseTint = Math.max(0x404040, baseTint - 0x202020);
break;
}
return baseTint;
};
// Apply world-based tinting
var worldTint = self.getWorldTint();
enemyGraphics.tint = worldTint;
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
shadowGraphics.scaleX = 1.8;
shadowGraphics.scaleY = 1.8;
}
// Position shadow slightly offset
self.shadow.x = 20; // Offset right
self.shadow.y = 20; // Offset down
// Ensure shadow has the same rotation as the enemy
shadowGraphics.rotation = enemyGraphics.rotation;
}
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
self.update = function () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle burn effect (Agumon line)
if (self.burning) {
if (LK.ticks % 45 === 0) {
// Every 0.75 seconds
var burnDamage = self.burnDamage;
// Reduce burn damage if enemy is moist
if (self.moist && self.fireResistance) {
burnDamage *= self.fireResistance;
}
self.health -= burnDamage;
if (self.health <= 0) self.health = 0;else self.healthBar.width = self.health / self.maxHealth * 70;
}
self.burnDuration--;
if (self.burnDuration <= 0) {
self.burning = false;
}
}
// Handle freeze effect (Gabumon line)
if (self.frozen) {
self.frozenDuration--;
if (self.frozenDuration <= 0) {
self.frozen = false;
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
}
}
// Handle paralysis effect (Tentomon line)
if (self.paralyzed) {
self.paralyzeDuration--;
if (self.paralyzeDuration <= 0) {
self.paralyzed = false;
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
}
}
// Handle moisture effect (Gomamon line)
if (self.moist) {
self.moistDuration--;
if (self.moistDuration <= 0) {
self.moist = false;
self.fireResistance = 1.0;
self.freezeVulnerability = 1.0;
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
var GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2("+" + value, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
self.cells = [];
self.spawns = [];
self.goals = [];
for (var i = 0; i < gridWidth; i++) {
self.cells[i] = [];
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j] = {
score: 0,
pathId: 0,
towersInRange: []
};
}
}
/*
Cell Types
0: Transparent floor
1: Wall
2: Spawn
3: Goal
*/
// Create world-based maze layout
self.generateMazeForWorld = function (worldNumber) {
// Clear existing maze
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j].type = 1; // Default to wall
}
}
self.spawns = [];
self.goals = [];
// Always create entry area (top 5 rows) with 3-block wide path
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j <= 4; j++) {
if (i >= 11 && i <= 13) {
self.cells[i][j].type = 0; // Open path in center (3 blocks wide)
if (j === 0) {
self.cells[i][j].type = 2; // Spawn points
self.spawns.push(self.cells[i][j]);
}
} else {
self.cells[i][j].type = 1; // Walls on sides
}
}
}
// Always create exit area (bottom 5 rows) with 3-block wide path
for (var i = 0; i < gridWidth; i++) {
for (var j = gridHeight - 5; j < gridHeight; j++) {
if (i >= 11 && i <= 13) {
self.cells[i][j].type = 0; // Open path in center (3 blocks wide)
if (j === gridHeight - 1) {
self.cells[i][j].type = 3; // Goal points
self.goals.push(self.cells[i][j]);
}
} else {
self.cells[i][j].type = 1; // Walls on sides
}
}
}
// Create classic square-cornered labyrinth with generous tower placement areas
// Simple pattern: Create a winding path with 90-degree turns and wide wall corridors
// Path layout: Start center-top, go down, turn right, down, turn left, repeat creating a serpentine pattern
// Main path coordinates (center of 3-block wide paths)
// Reduced to only 2 curves for a simpler path
var pathCenterX = 12; // Center column
var pathPattern = [
// Start: go down from entry
{
x: pathCenterX,
startY: 5,
endY: 12,
type: 'vertical'
},
// First curve: turn left
{
y: 12,
startX: pathCenterX,
endX: 4,
type: 'horizontal'
},
// Go down on left side
{
x: 4,
startY: 12,
endY: 20,
type: 'vertical'
},
// Second curve: turn right to center
{
y: 20,
startX: 4,
endX: pathCenterX,
type: 'horizontal'
},
// Final path to exit
{
x: pathCenterX,
startY: 20,
endY: gridHeight - 5,
type: 'vertical'
}];
// Crear los segmentos del camino con 3 bloques de grosor (camino) y dejar espacio extra de 1 bloque a cada lado para asegurar zonas de 2x2 para torretas
for (var p = 0; p < pathPattern.length; p++) {
var segment = pathPattern[p];
if (segment.type === 'vertical') {
var startY = Math.min(segment.startY, segment.endY);
var endY = Math.max(segment.startY, segment.endY);
for (var y = startY; y <= endY; y++) {
// Camino de 3 bloques de ancho
for (var offset = -1; offset <= 1; offset++) {
var pathX = segment.x + offset;
if (pathX >= 0 && pathX < gridWidth && y >= 0 && y < gridHeight) {
self.cells[pathX][y].type = 0;
}
}
// Dejar espacio extra de 1 bloque a cada lado del camino para permitir torretas 2x2
for (var extra = -2; extra <= 2; extra += 4) {
var sideX = segment.x + extra;
if (sideX >= 0 && sideX < gridWidth && y >= 0 && y < gridHeight) {
// Solo marcar como espacio de torre si no es camino ni spawn/goal
if (self.cells[sideX][y].type === 1) {
// No cambiar si ya es camino/spawn/goal
self.cells[sideX][y].type = 1; // Mantener como muro, pero dejarlo para posible torre
}
}
}
// Asegurar espacio de 2x2 para torretas a la izquierda y derecha del camino
for (var offsetY = 0; offsetY <= 1; offsetY++) {
for (var extra = -2; extra <= 2; extra += 4) {
var baseX = segment.x + extra;
var baseY = y + offsetY;
if (baseX >= 0 && baseX + 1 < gridWidth && baseY >= 0 && baseY + 1 < gridHeight) {
// Solo marcar como espacio de torre si ambos son muro
if (self.cells[baseX][baseY].type === 1 && self.cells[baseX + 1][baseY].type === 1 && self.cells[baseX][baseY + 1].type === 1 && self.cells[baseX + 1][baseY + 1].type === 1) {
// Marcar como espacio de torre (type 1) para permitir torretas 2x2
self.cells[baseX][baseY].type = 1;
self.cells[baseX + 1][baseY].type = 1;
self.cells[baseX][baseY + 1].type = 1;
self.cells[baseX + 1][baseY + 1].type = 1;
}
}
}
}
}
} else if (segment.type === 'horizontal') {
var startX = Math.min(segment.startX, segment.endX);
var endX = Math.max(segment.startX, segment.endX);
for (var x = startX; x <= endX; x++) {
// Camino de 3 bloques de alto
for (var offset = -1; offset <= 1; offset++) {
var pathY = segment.y + offset;
if (x >= 0 && x < gridWidth && pathY >= 0 && pathY < gridHeight) {
self.cells[x][pathY].type = 0;
}
}
// Dejar espacio extra de 1 bloque arriba y abajo del camino para permitir torretas 2x2
for (var extra = -2; extra <= 2; extra += 4) {
var sideY = segment.y + extra;
if (x >= 0 && x < gridWidth && sideY >= 0 && sideY < gridHeight) {
if (self.cells[x][sideY].type === 1) {
self.cells[x][sideY].type = 1;
}
}
}
// Asegurar espacio de 2x2 para torretas arriba y abajo del camino
for (var offsetX = 0; offsetX <= 1; offsetX++) {
for (var extra = -2; extra <= 2; extra += 4) {
var baseX = x + offsetX;
var baseY = segment.y + extra;
if (baseX >= 0 && baseX + 1 < gridWidth && baseY >= 0 && baseY + 1 < gridHeight) {
if (self.cells[baseX][baseY].type === 1 && self.cells[baseX + 1][baseY].type === 1 && self.cells[baseX][baseY + 1].type === 1 && self.cells[baseX + 1][baseY + 1].type === 1) {
// Marcar como espacio de torre (type 1) para permitir torretas 2x2
self.cells[baseX][baseY].type = 1;
self.cells[baseX + 1][baseY].type = 1;
self.cells[baseX][baseY + 1].type = 1;
self.cells[baseX + 1][baseY + 1].type = 1;
}
}
}
}
}
}
}
// Asegurar conexiones suaves en las intersecciones y dejar espacio de 2x2 para torret
};
// Generate maze for current world
var world = Math.ceil(currentWave / 9);
if (world < 1) world = 1;
if (world > 6) world = 6;
self.generateMazeForWorld(world);
// Apply the maze layout to cells
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellType = cell.type; // Use the type set by generateMazeForWorld
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
// Only create debug cells for visible areas and reduce frequency
if (j > 3 && j <= gridHeight - 4 && (i + j) % 2 === 0) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
// Simplified pathfinding - only check cardinal directions for better performance
var targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId != pathId) {
console.warn("Spawn blocked");
return true;
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward with speed multiplier
enemy.currentCellY += enemy.speed * gameSpeed;
// Ensure enemy moves towards the center of the 3-block wide path (x=12)
var pathCenterX = 12;
if (enemy.currentCellX !== pathCenterX) {
var xDiff = pathCenterX - enemy.currentCellX;
var moveSpeed = Math.min(Math.abs(xDiff), enemy.speed * 0.5 * gameSpeed);
enemy.currentCellX += xDiff > 0 ? moveSpeed : -moveSpeed;
}
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentCellX += Math.cos(angle) * enemy.speed * gameSpeed;
enemy.currentCellY += Math.sin(angle) * enemy.speed * gameSpeed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget) {
enemy.currentTarget = cell.targets[0];
}
// Initialize stuck tracking for enemy
if (enemy.lastPosition === undefined) {
enemy.lastPosition = {
x: enemy.currentCellX,
y: enemy.currentCellY
};
enemy.stuckCounter = 0;
enemy.lastMovementTime = LK.ticks;
}
// Check if enemy is stuck (hasn't moved significantly in a while)
var currentPos = {
x: enemy.currentCellX,
y: enemy.currentCellY
};
var distanceMoved = Math.sqrt(Math.pow(currentPos.x - enemy.lastPosition.x, 2) + Math.pow(currentPos.y - enemy.lastPosition.y, 2));
// If enemy hasn't moved much in the last 60 ticks (1 second), consider it stuck
if (distanceMoved < 0.1 && LK.ticks - enemy.lastMovementTime > 60) {
enemy.stuckCounter++;
enemy.lastMovementTime = LK.ticks;
// If stuck for too long, try to find alternative path
if (enemy.stuckCounter > 3) {
// Reset stuck counter to prevent infinite loops
enemy.stuckCounter = 0;
// Try to find alternative targets from current cell
if (cell.targets && cell.targets.length > 1) {
// Find a different target than the current one
for (var i = 0; i < cell.targets.length; i++) {
var alternativeTarget = cell.targets[i];
if (alternativeTarget !== enemy.currentTarget) {
enemy.currentTarget = alternativeTarget;
break;
}
}
} else {
// If no alternative targets, try neighboring cells
var neighbors = [cell.up, cell.right, cell.down, cell.left];
var validNeighbors = [];
for (var i = 0; i < neighbors.length; i++) {
var neighbor = neighbors[i];
if (neighbor && neighbor.type !== 1 && neighbor.pathId === pathId && neighbor.targets && neighbor.targets.length > 0) {
validNeighbors.push(neighbor);
}
}
if (validNeighbors.length > 0) {
// Choose a random valid neighbor and use its target
var randomNeighbor = validNeighbors[Math.floor(Math.random() * validNeighbors.length)];
enemy.currentTarget = randomNeighbor.targets[0];
// Move slightly towards the chosen neighbor to unstuck
var neighborX = randomNeighbor.x;
var neighborY = randomNeighbor.y;
var unstuckAngle = Math.atan2(neighborY - enemy.currentCellY, neighborX - enemy.currentCellX);
enemy.currentCellX += Math.cos(unstuckAngle) * enemy.speed * 0.5;
enemy.currentCellY += Math.sin(unstuckAngle) * enemy.speed * 0.5;
}
}
}
} else if (distanceMoved >= 0.1) {
// Enemy is moving, reset stuck tracking
enemy.stuckCounter = 0;
enemy.lastMovementTime = LK.ticks;
enemy.lastPosition = {
x: currentPos.x,
y: currentPos.y
};
}
if (enemy.currentTarget) {
if (cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
// Reset position tracking when reaching target
enemy.lastPosition = {
x: enemy.currentCellX,
y: enemy.currentCellY
};
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed * gameSpeed;
enemy.currentCellY += Math.sin(angle) * enemy.speed * gameSpeed;
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var LevelSelectionMenu = Container.expand(function (worldNumber) {
var self = Container.call(this);
self.worldNumber = worldNumber;
// Position the menu at center of screen
self.x = 2048 / 2;
self.y = 2732 / 2;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 1800;
menuBackground.height = 1200;
menuBackground.tint = 0x333333;
menuBackground.alpha = 0.9;
var worldNames = ["", "Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"];
var titleText = new Text2('Select Level - ' + worldNames[worldNumber], {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -400;
self.addChild(titleText);
var worldLevels = storage.worldLevels || {
1: 1,
2: 1,
3: 1,
4: 1,
5: 1,
6: 1
};
var unlockedLevel = worldLevels[worldNumber] || 1;
// Create level buttons in a grid
var buttonsPerRow = 5;
var buttonWidth = 120;
var buttonHeight = 80;
var buttonSpacing = 160;
var startX = -((buttonsPerRow - 1) * buttonSpacing) / 2;
var startY = -200;
for (var i = 1; i <= 10; i++) {
var levelButton = new Container();
var levelButtonBg = levelButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
levelButtonBg.width = buttonWidth;
levelButtonBg.height = buttonHeight;
var isUnlocked = i <= unlockedLevel;
levelButtonBg.tint = isUnlocked ? 0x4444FF : 0x666666;
var levelButtonText = new Text2(isUnlocked ? i.toString() : "🔒", {
size: 40,
fill: isUnlocked ? 0xFFFFFF : 0x999999,
weight: 800
});
levelButtonText.anchor.set(0.5, 0.5);
levelButton.addChild(levelButtonText);
// Position buttons in grid
var row = Math.floor((i - 1) / buttonsPerRow);
var col = (i - 1) % buttonsPerRow;
levelButton.x = startX + col * buttonSpacing;
levelButton.y = startY + row * 120;
self.addChild(levelButton);
(function (levelIndex, unlocked) {
levelButton.down = function () {
if (unlocked) {
self.destroy();
game.startWorldLevel(self.worldNumber, levelIndex);
} else {
var notification = game.addChild(new Notification("Complete previous level to unlock!"));
notification.x = 2048 / 2;
notification.y = 2732 / 2 + 200;
}
};
})(i, isUnlocked);
}
// Back button
var backButton = new Container();
var backButtonBg = backButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
backButtonBg.width = 200;
backButtonBg.height = 80;
backButtonBg.tint = 0x666666;
var backButtonText = new Text2('Back', {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
backButtonText.anchor.set(0.5, 0.5);
backButton.addChild(backButtonText);
backButton.x = 0;
backButton.y = 400;
self.addChild(backButton);
backButton.down = function () {
self.destroy();
var worldSelectionMenu = new WorldSelectionMenu();
game.addChild(worldSelectionMenu);
};
return self;
});
var MainMenu = Container.expand(function () {
var self = Container.call(this);
// Position the menu at center of screen
self.x = 2048 / 2;
self.y = 2732 / 2;
// Stop any currently playing music first
LK.stopMusic();
// Start main menu music
LK.playMusic('mainMenuMusic', {
fade: {
start: 0,
end: 0.8,
duration: 1500
}
});
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 1800;
menuBackground.height = 1200;
menuBackground.tint = 0x333333;
menuBackground.alpha = 0.9;
var titleText = new Text2(getText('firewallDefensors'), {
size: 100,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -300;
self.addChild(titleText);
// Add version number below title
var versionText = new Text2('v0.1', {
size: 40,
fill: 0xCCCCCC,
weight: 400
});
versionText.anchor.set(0.5, 0.5);
versionText.y = -220;
self.addChild(versionText);
var startButton = new Container();
var startButtonBg = startButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startButtonBg.width = 400;
startButtonBg.height = 120;
startButtonBg.tint = 0x00AA00;
var startButtonText = new Text2(getText('startGame'), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
startButtonText.anchor.set(0.5, 0.5);
startButton.addChild(startButtonText);
startButton.y = 50;
self.addChild(startButton);
startButton.down = function () {
self.destroy();
game.startGame();
};
// Add tutorial button
var tutorialButton = new Container();
var tutorialButtonBg = tutorialButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
tutorialButtonBg.width = 400;
tutorialButtonBg.height = 120;
tutorialButtonBg.tint = 0x00AAAA;
var tutorialButtonText = new Text2(getText('tutorial'), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
tutorialButtonText.anchor.set(0.5, 0.5);
tutorialButton.addChild(tutorialButtonText);
tutorialButton.y = 180;
self.addChild(tutorialButton);
tutorialButton.down = function () {
self.destroy();
game.startTutorial();
};
// Add leaderboard button
var leaderboardButton = new Container();
var leaderboardButtonBg = leaderboardButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
leaderboardButtonBg.width = 400;
leaderboardButtonBg.height = 120;
leaderboardButtonBg.tint = 0x4444FF;
var leaderboardButtonText = new Text2('Leaderboard', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
leaderboardButtonText.anchor.set(0.5, 0.5);
leaderboardButton.addChild(leaderboardButtonText);
leaderboardButton.y = 310 + 130;
self.addChild(leaderboardButton);
leaderboardButton.down = function () {
LK.showLeaderboard();
};
// Add share achievement button
var shareButton = new Container();
var shareButtonBg = shareButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shareButtonBg.width = 400;
shareButtonBg.height = 120;
shareButtonBg.tint = 0x00C0C0;
var shareButtonText = new Text2('Share Achievement', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
shareButtonText.anchor.set(0.5, 0.5);
shareButton.addChild(shareButtonText);
shareButton.y = 310 + 260;
self.addChild(shareButton);
shareButton.down = function () {
var shareMsg = "I just scored " + (storage.securityPoints || 0) + " points in Firewall Defensors! Can you beat my score?";
LK.shareAchievement && LK.shareAchievement(shareMsg);
};
// Add language button
var languageButton = new Container();
var languageButtonBg = languageButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
languageButtonBg.width = 400;
languageButtonBg.height = 120;
languageButtonBg.tint = 0xFF6600;
var languageButtonText = new Text2(getText('language'), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
languageButtonText.anchor.set(0.5, 0.5);
languageButton.addChild(languageButtonText);
languageButton.y = 310;
self.addChild(languageButton);
languageButton.down = function () {
// Toggle language between English and Spanish
var newLang = currentLanguage === 'en' ? 'es' : 'en';
setLanguage(newLang);
// Recreate main menu with new language
self.destroy();
var newMainMenu = new MainMenu();
game.addChild(newMainMenu);
};
return self;
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300;
buttonBackground.height = 100;
buttonBackground.tint = 0x0088FF;
var buttonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
// Check if we can start next wave (10 waves max per world)
var worldWave = (currentWave - 1) % 10 + 1;
if (currentWave === 0) worldWave = 0;
// Show button during tutorial or when can start next wave
var showForTutorial = waveIndicator && waveIndicator.gameStarted && currentWave === 0;
var showForNextWave = waveIndicator && waveIndicator.gameStarted && worldWave < 10 && !waveInProgress && enemies.length === 0;
if (showForTutorial || showForNextWave) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves && !waveInProgress && enemies.length === 0) {
currentWave++; // Increment to the next wave directly
// Calculate current world and level for 10-wave system
currentWorld = Math.ceil(currentWave / 10);
currentLevel = (currentWave - 1) % 10 + 1;
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentLevel);
var enemyCount = waveIndicator.getEnemyCount(currentLevel);
// Update wave counter display
updateWaveCounter();
var notification = game.addChild(new Notification("Wave " + currentLevel + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Play wave start sound
LK.getSound('waveStart').play();
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 0x000000,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'default';
// Increase size of base for easier touch
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'sniper':
baseGraphics.tint = 0xFF5500;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
case 'slow':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = -20 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -25; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var costLabelShadow = new Text2(towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 4;
costLabelShadow.y = 24 + 12;
self.addChild(costLabelShadow);
// Add cost label
var costLabel = new Text2(towerCost + ' bits', {
size: 60,
fill: 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 25 + 12;
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var StorySequence = Container.expand(function (worldNumber) {
var self = Container.call(this);
self.worldNumber = worldNumber;
self.currentPanel = 0;
self.panels = [];
self.onComplete = null;
// Position at center of screen
self.x = 2048 / 2;
self.y = 2732 / 2;
// Semi-transparent background overlay
var overlay = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
overlay.width = 2048;
overlay.height = 2732;
overlay.tint = 0x000000;
overlay.alpha = 0.7;
self.getWorldStory = function (worldNumber) {
switch (worldNumber) {
case 1:
return [{
text: getText('story1_1'),
image: 'agumon'
}, {
text: getText('story1_2'),
image: 'gabumon'
}, {
text: getText('story1_3'),
image: 'tentomon'
}];
case 2:
return [{
text: getText('story2_1'),
image: 'palmon'
}, {
text: getText('story2_2'),
image: 'gomamon'
}, {
text: getText('story2_3'),
image: 'patamon'
}];
case 3:
return [{
text: getText('story3_1'),
image: null
}, {
text: getText('story3_2'),
image: null
}, {
text: getText('story3_3'),
image: null
}];
case 4:
return [{
text: getText('story4_1'),
image: null
}, {
text: getText('story4_2'),
image: null
}, {
text: getText('story4_3'),
image: null
}];
case 5:
return [{
text: getText('story5_1'),
image: null
}, {
text: getText('story5_2'),
image: null
}, {
text: getText('story5_3'),
image: null
}];
case 6:
return [{
text: getText('story6_1'),
image: null
}, {
text: getText('story6_2'),
image: null
}, {
text: getText('story6_3'),
image: null
}];
default:
return [{
text: getText('storyDefault_1'),
image: null
}, {
text: getText('storyDefault_2'),
image: null
}, {
text: getText('storyDefault_3'),
image: null
}];
}
};
// World-specific story content
var storyData = self.getWorldStory(worldNumber);
// Create panels
for (var i = 0; i < storyData.length; i++) {
var panel = new ComicPanel(storyData[i].image, storyData[i].text);
panel.x = (i - 1) * 650; // Position panels side by side
self.addChild(panel);
self.panels.push(panel);
}
// Navigation indicators
var indicatorContainer = new Container();
indicatorContainer.y = 250;
self.addChild(indicatorContainer);
for (var i = 0; i < self.panels.length; i++) {
var indicator = indicatorContainer.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = 20;
indicator.height = 20;
indicator.tint = i === 0 ? 0xffffff : 0x666666;
indicator.x = (i - (self.panels.length - 1) / 2) * 40;
}
// Skip button
var skipButton = new Container();
var skipBg = skipButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
skipBg.width = 150;
skipBg.height = 60;
skipBg.tint = 0x666666;
var skipText = new Text2('Skip', {
size: 40,
fill: 0xffffff,
weight: 800
});
skipText.anchor.set(0.5, 0.5);
skipButton.addChild(skipText);
skipButton.x = 400;
skipButton.y = -300;
self.addChild(skipButton);
skipButton.down = function () {
self.complete();
};
self.showPanel = function (index) {
if (index < 0 || index >= self.panels.length) return;
// Hide all panels
for (var i = 0; i < self.panels.length; i++) {
self.panels[i].alpha = 0;
indicatorContainer.children[i].tint = 0x666666;
}
// Show current panel
self.panels[index].show();
indicatorContainer.children[index].tint = 0xffffff;
self.currentPanel = index;
};
self.nextPanel = function () {
if (self.currentPanel < self.panels.length - 1) {
self.showPanel(self.currentPanel + 1);
} else {
self.complete();
}
};
self.complete = function () {
if (self.onComplete) {
self.onComplete();
}
self.destroy();
};
// Show first panel
self.showPanel(0);
// Auto-advance after 4 seconds or click to advance
var autoAdvanceTimer = LK.setTimeout(function () {
self.nextPanel();
}, 4000);
self.down = function () {
LK.clearTimeout(autoAdvanceTimer);
autoAdvanceTimer = LK.setTimeout(function () {
self.nextPanel();
}, 4000);
self.nextPanel();
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// Tower health system
self.maxHealth = 100;
self.health = self.maxHealth;
// Standardized method to get the current range of the tower
self.getRange = function () {
// Always calculate range based on tower type and level
switch (self.id) {
case 'sniper':
// Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost
if (self.level === self.maxLevel) {
return 12 * CELL_SIZE; // Significantly increased range for max level
}
return (5 + (self.level - 1) * 0.8) * CELL_SIZE;
case 'splash':
// Splash: base 2, +0.2 per level (max ~4 blocks at max level)
return (2 + (self.level - 1) * 0.2) * CELL_SIZE;
case 'rapid':
// Rapid: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// Slow: base 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
default:
// Default: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
switch (self.id) {
case 'agumon':
// Fire-based attacks, burns enemies
self.fireRate = 60;
self.damage = 12;
self.range = 3 * CELL_SIZE;
self.bulletSpeed = 6;
self.maxHealth = 120; // Tanky fire dragon
break;
case 'gabumon':
// Ice-based attacks, freezes enemies
self.fireRate = 55;
self.damage = 10;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 6;
self.maxHealth = 110; // Ice wolf durability
break;
case 'tentomon':
// Electric paralysis attacks
self.fireRate = 70;
self.damage = 14;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 8;
self.maxHealth = 90; // Electric insect, moderate health
break;
case 'palmon':
// Poison attacks that spread
self.fireRate = 65;
self.damage = 11;
self.range = 3 * CELL_SIZE;
self.bulletSpeed = 5;
self.maxHealth = 100; // Plant creature, balanced
break;
case 'gomamon':
// Water/moisture attacks, weakens fire resistance
self.fireRate = 60;
self.damage = 9;
self.range = 3.3 * CELL_SIZE;
self.bulletSpeed = 5;
self.maxHealth = 105; // Aquatic mammal, decent health
break;
case 'patamon':
// Healing and support abilities
self.fireRate = 80;
self.damage = 8;
self.range = 4 * CELL_SIZE; // Larger healing range
self.bulletSpeed = 7;
self.maxHealth = 85; // Angel creature, less physical but supportive
break;
}
self.health = self.maxHealth;
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
switch (self.id) {
case 'gabumon':
//{93} // Blue colors for Gabumon
baseGraphics.tint = 0x00AAFF;
break;
case 'tentomon':
//{96} // Red colors for Tentomon
baseGraphics.tint = 0xFF5500;
break;
case 'palmon':
//{99} // Green colors for Palmon
baseGraphics.tint = 0x33CC00;
break;
case 'gomamon':
//{9c} // Purple colors for Gomamon
baseGraphics.tint = 0x9900FF;
break;
case 'patamon':
//{9f} // Cyan colors for Patamon
baseGraphics.tint = 0x00FFAA;
break;
default:
//{9i} // Agumon default
baseGraphics.tint = 0xAAAAAA;
}
// Tower health bar
var towerHealthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0.5,
anchorY: 0.5
});
var towerHealthBar = self.attachAsset('healthBar', {
anchorX: 0.5,
anchorY: 0.5
});
towerHealthBarOutline.width = 80;
towerHealthBarOutline.height = 8;
towerHealthBarOutline.tint = 0x000000;
towerHealthBar.width = 76;
towerHealthBar.height = 6;
towerHealthBar.tint = 0x00ff00;
towerHealthBarOutline.y = towerHealthBar.y = -CELL_SIZE - 15;
self.towerHealthBar = towerHealthBar;
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1);
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
var gunGraphics = gunContainer.attachAsset(self.id, {
anchorX: 0.5,
anchorY: 0.5
});
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF;
break;
case 'poison':
towerLevelIndicator.tint = 0x00FFAA;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// Make last upgrade level extra expensive
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// Play tower upgrade sound
LK.getSound('towerUpgrade').play();
// --- HEALTH UPGRADE LOGIC ---
// Increase maxHealth and heal tower on upgrade
var prevMaxHealth = self.maxHealth;
// Health scaling per tower type
switch (self.id) {
case 'agumon':
self.maxHealth = 120 + (self.level - 1) * 20;
break;
case 'gabumon':
self.maxHealth = 110 + (self.level - 1) * 18;
break;
case 'tentomon':
self.maxHealth = 90 + (self.level - 1) * 15;
break;
case 'palmon':
self.maxHealth = 100 + (self.level - 1) * 18;
break;
case 'gomamon':
self.maxHealth = 105 + (self.level - 1) * 17;
break;
case 'patamon':
self.maxHealth = 85 + (self.level - 1) * 14;
break;
default:
self.maxHealth = 100 + (self.level - 1) * 15;
break;
}
// Heal tower to new max health on upgrade
self.health = self.maxHealth;
// --- DAMAGE/FIRERATE UPGRADE LOGIC ---
// Tweak last upgrade so it's not overpowered
if (self.id === 'rapid') {
if (self.level === self.maxLevel) {
// Last upgrade: only a moderate boost, not double
self.fireRate = Math.max(8, 30 - self.level * 6); // was 4, now 8 min
self.damage = 5 + self.level * 5; // was 10, now 5 per level
self.bulletSpeed = 7 + self.level * 1.2; // was 2.4, now 1.2 per level
} else {
self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades
self.damage = 5 + self.level * 3;
self.bulletSpeed = 7 + self.level * 0.7;
}
} else {
if (self.level === self.maxLevel) {
// Last upgrade: only a moderate boost, not double
self.fireRate = Math.max(10, 60 - self.level * 12); // was 5, now 10 min
self.damage = 10 + self.level * 10; // was 20, now 10 per level
self.bulletSpeed = 5 + self.level * 1.2; // was 2.4, now 1.2 per level
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
// Check if tower reached maximum level and award bonus
if (self.level === self.maxLevel) {
// Award 300 bits and security points for reaching max level
setGold(gold + 300);
score += 300;
// Save security points to storage
storage.securityPoints = score;
updateUI();
var notification = game.addChild(new Notification("Max level reached! +300 bits & security points!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough bits to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is in range
if (distance <= self.getRange()) {
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, use the original path-based targeting
// Get the cell for this enemy
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Use the cell's score (distance to exit) for prioritization
// Lower score means closer to exit
if (cell.score < closestScore) {
closestScore = cell.score;
closestEnemy = enemy;
}
}
}
}
}
if (!closestEnemy) {
self.targetEnemy = null;
}
return closestEnemy;
};
self.update = function () {
// Update tower health bar
self.towerHealthBar.width = self.health / self.maxHealth * 76;
if (self.health / self.maxHealth > 0.6) {
self.towerHealthBar.tint = 0x00ff00; // Green
} else if (self.health / self.maxHealth > 0.3) {
self.towerHealthBar.tint = 0xffff00; // Yellow
} else {
self.towerHealthBar.tint = 0xff0000; // Red
}
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
var effectiveFireRate = gameSpeed > 1 ? Math.max(1, Math.floor(self.fireRate / gameSpeed)) : self.fireRate;
if (LK.ticks - self.lastFired >= effectiveFireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
game.addChild(rangeIndicator);
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
// Add evolution arrow if digivice is available and tower level is appropriate
function canDigivolve() {
if (self.level < 2) return false; // Need at least level 2
var hasDigivice = false;
if (self.level >= 2 && self.level <= 3 && storage.digiviceC) hasDigivice = true;
if (self.level >= 4 && self.level <= 5 && storage.digiviceB) hasDigivice = true;
if (self.level >= 6 && storage.digiviceA) hasDigivice = true;
return hasDigivice;
}
if (canDigivolve()) {
var evolutionArrow = rangeIndicator.attachAsset('arrow', {
anchorX: 0.5,
anchorY: 0.5
});
evolutionArrow.width = 60;
evolutionArrow.height = 60;
evolutionArrow.tint = 0xFF6600;
evolutionArrow.x = self.getRange() + 40;
evolutionArrow.y = -40;
evolutionArrow.rotation = -Math.PI / 4; // Point diagonally up-right
// Add glow effect
evolutionArrow.alpha = 0.8;
var _glowTween = function glowTween() {
tween(evolutionArrow, {
alpha: 1.0,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(evolutionArrow, {
alpha: 0.8,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: _glowTween
});
}
});
};
_glowTween();
}
// Check if clicked on evolution arrow
var clickedOnEvolutionArrow = false;
if (rangeIndicator.children.length > 1) {
// Has evolution arrow
var evolutionArrow = rangeIndicator.children[1];
var arrowGlobalPos = rangeIndicator.toGlobal(evolutionArrow.position);
var arrowX = arrowGlobalPos.x;
var arrowY = arrowGlobalPos.y;
if (Math.abs(x - arrowX) < 40 && Math.abs(y - arrowY) < 40) {
clickedOnEvolutionArrow = true;
// Trigger digivolution
var evolutionCost = getTowerCost(self.id) * 2;
if (gold >= evolutionCost) {
setGold(gold - evolutionCost);
// Apply evolution effects
self.damage *= 1.5;
self.fireRate = Math.max(5, Math.floor(self.fireRate * 0.8));
var notification = game.addChild(new Notification(self.id.toUpperCase() + " DIGIVOLVED!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Visual evolution effect
LK.effects.flashObject(self, 0xFF6600, 1000);
// Remove evolution arrow after use
rangeIndicator.removeChild(evolutionArrow);
} else {
var notification = game.addChild(new Notification("Not enough bits to digivolve!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
}
if (!clickedOnEvolutionArrow) {
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
}
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
// Patamon healing ability - heal nearby towers instead of attacking
if (self.id === 'patamon' && LK.ticks % 120 === 0) {
// Every 2 seconds
var healingRange = self.getRange();
var healAmount = 5 + self.level * 2;
var towersHealed = 0;
for (var i = 0; i < towers.length; i++) {
var otherTower = towers[i];
if (otherTower !== self && otherTower.health < otherTower.maxHealth) {
var dx = otherTower.x - self.x;
var dy = otherTower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= healingRange) {
otherTower.health = Math.min(otherTower.maxHealth, otherTower.health + healAmount);
otherTower.towerHealthBar.width = otherTower.health / otherTower.maxHealth * 76;
towersHealed++;
// Visual healing effect
var healEffect = new EffectIndicator(otherTower.x, otherTower.y, 'heal');
game.addChild(healEffect);
}
}
}
if (towersHealed > 0) {
// Show healing indicator on Patamon
var healSelfEffect = new EffectIndicator(self.x, self.y, 'heal');
game.addChild(healSelfEffect);
}
}
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type and add special effects
bullet.type = self.id;
bullet.sourceTowerLevel = self.level;
// Special attack patterns for different evolutionary lines
switch (self.id) {
case 'agumon':
// Fire attacks that can burn enemies
// Burn chance: 5% at level 1, up to 50% at max level (linear)
bullet.burnChance = Math.min(0.05 + (self.level - 1) * 0.09, 0.5);
break;
case 'gabumon':
// Ice attacks that can freeze enemies
// Freeze chance: 5% at level 1, up to 50% at max level (linear)
bullet.freezeChance = Math.min(0.05 + (self.level - 1) * 0.09, 0.5);
break;
case 'tentomon':
// Electric attacks that can paralyze multiple enemies
// Paralyze chance: 5% at level 1, up to 55% at max level (linear)
bullet.paralyzeChance = Math.min(0.05 + (self.level - 1) * 0.10, 0.55);
bullet.paralyzeArea = CELL_SIZE * (1 + self.level * 0.2);
break;
case 'palmon':
// Poison attacks that spread to nearby enemies
// Poison spread chance: 5% at level 1, up to 50% at max level (linear)
bullet.poisonSpread = true;
bullet.poisonSpreadChance = Math.min(0.05 + (self.level - 1) * 0.09, 0.5);
bullet.poisonRadius = CELL_SIZE * (0.8 + self.level * 0.15);
break;
case 'gomamon':
// Water attacks that make enemies wet (vulnerable to freeze, resistant to burn)
bullet.moistureEffect = true;
bullet.moistureDuration = 180 + self.level * 30;
break;
}
// Customize bullet appearance based on tower type
switch (self.id) {
case 'rapid':
bullet.children[0].tint = 0x00AAFF;
bullet.children[0].width = 20;
bullet.children[0].height = 20;
break;
case 'sniper':
bullet.children[0].tint = 0xFF5500;
bullet.children[0].width = 15;
bullet.children[0].height = 15;
break;
case 'splash':
bullet.children[0].tint = 0x33CC00;
bullet.children[0].width = 40;
bullet.children[0].height = 40;
break;
case 'slow':
bullet.children[0].tint = 0x9900FF;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// --- Fire recoil effect for gunContainer ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
// Always use the original resting position for recoil, never accumulate offset
if (gunContainer._restX === undefined) {
gunContainer._restX = 0;
}
if (gunContainer._restY === undefined) {
gunContainer._restY = 0;
}
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset to resting position before animating (in case of interrupted tweens)
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Calculate recoil offset (recoil back along the gun's rotation)
var recoilDistance = 8;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
// Animate recoil back from the resting position
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original position/scale
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
// Mark cells as occupied by tower (type 1 = wall/occupied)
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 1;
}
}
}
self.refreshCellsInRange();
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'default';
self.hasEnoughGold = true;
var rangeIndicator = new Container();
self.addChild(rangeIndicator);
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3;
var previewGraphics = self.attachAsset('towerpreview', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
switch (self.towerType) {
case 'rapid':
previewGraphics.tint = 0x00AAFF;
break;
case 'sniper':
previewGraphics.tint = 0xFF5500;
break;
case 'splash':
previewGraphics.tint = 0x33CC00;
break;
case 'slow':
previewGraphics.tint = 0x9900FF;
break;
case 'poison':
previewGraphics.tint = 0x00FFAA;
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
// Check if tower would be placed within valid grid bounds
if (self.gridX < 0 || self.gridY < 0 || self.gridX + 1 >= grid.cells.length || self.gridY + 1 >= grid.cells[0].length) {
validGridPlacement = false;
} else {
// Check if all 4 cells for the 2x2 tower are available (not on enemy path)
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
// Only allow placement on wall cells (type 1) - not on path, spawn, or goal
if (!cell || cell.type !== 1) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
self.blockedByEnemy = false;
// Remove enemy blocking detection since towers can only be placed on wall tiles
// which enemies cannot occupy anyway
self.blockedByEnemy = false;
self.canPlace = validGridPlacement;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var TutorialSequence = Container.expand(function () {
var self = Container.call(this);
console.log("Starting interactive tutorial sequence");
self.currentStep = 0;
self.onComplete = null;
self.tutorialActive = true;
self.waitingForUserAction = false;
self.stepCompleted = false;
// Tutorial steps data
self.tutorialSteps = [{
textKey: 'tutorialWelcome',
action: 'intro'
}, {
textKey: 'tutorialStep1',
action: 'placeTower',
completeTextKey: 'tutorialStep1Complete'
}, {
textKey: 'tutorialStep2',
action: 'startWave',
completeTextKey: 'tutorialStep2Complete'
}, {
textKey: 'tutorialStep3',
action: 'upgradeTower',
completeTextKey: 'tutorialStep3Complete'
}, {
textKey: 'tutorialCompleted',
action: 'finish'
}];
// Semi-transparent background overlay
var overlay = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
overlay.width = 2048;
overlay.height = 2732;
overlay.tint = 0x000000;
overlay.alpha = 0.4;
// Instruction panel
var instructionBg = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
instructionBg.width = 1600;
instructionBg.height = 400;
instructionBg.tint = 0x222222;
instructionBg.alpha = 0.95;
instructionBg.x = 2048 / 2;
instructionBg.y = 400;
var instructionText = new Text2(getText(self.tutorialSteps[0].textKey), {
size: 50,
fill: 0xFFFFFF,
weight: 600
});
instructionText.anchor.set(0.5, 0.5);
instructionText.wordWrap = true;
instructionText.wordWrapWidth = 1500;
instructionText.x = 2048 / 2;
instructionText.y = 400;
self.addChild(instructionText);
// Next button
var nextButton = new Container();
var nextBg = nextButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
nextBg.width = 250;
nextBg.height = 80;
nextBg.tint = 0x00AA00;
var nextText = new Text2(getText('nextStep'), {
size: 45,
fill: 0xFFFFFF,
weight: 800
});
nextText.anchor.set(0.5, 0.5);
nextButton.addChild(nextText);
nextButton.x = 2048 / 2;
nextButton.y = 580;
self.addChild(nextButton);
// Skip button
var skipButton = new Container();
var skipBg = skipButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
skipBg.width = 200;
skipBg.height = 80;
skipBg.tint = 0x666666;
var skipText = new Text2(getText('skipTutorial'), {
size: 35,
fill: 0xffffff,
weight: 800
});
skipText.anchor.set(0.5, 0.5);
skipButton.addChild(skipText);
skipButton.x = 2048 - 150;
skipButton.y = 150;
self.addChild(skipButton);
// Button handlers
nextButton.down = function () {
self.nextStep();
};
skipButton.down = function () {
self.complete();
};
// Update button state
self.updateButtonState = function () {
if (self.waitingForUserAction && !self.stepCompleted) {
nextBg.tint = 0x666666;
nextButton.alpha = 0.5;
nextText.setText('Complete Action');
} else {
nextBg.tint = 0x00AA00;
nextButton.alpha = 1.0;
nextText.setText(getText('nextStep'));
}
};
self.nextStep = function () {
// Don't advance if waiting for user action and step not completed
if (self.waitingForUserAction && !self.stepCompleted) {
return;
}
self.currentStep++;
if (self.currentStep >= self.tutorialSteps.length) {
self.complete();
return;
}
self.waitingForUserAction = false;
self.stepCompleted = false;
var step = self.tutorialSteps[self.currentStep];
instructionText.setText(getText(step.textKey));
// Handle specific step actions
switch (step.action) {
case 'placeTower':
self.waitingForUserAction = true;
// Give enough gold for tutorial
setGold(200);
updateUI();
break;
case 'startWave':
self.waitingForUserAction = true;
waveIndicator.gameStarted = true;
break;
case 'upgradeTower':
self.waitingForUserAction = true;
// Give extra gold for upgrades
setGold(gold + 200);
updateUI();
break;
}
self.updateButtonState();
};
// Check for step completion
self.update = function () {
if (!self.waitingForUserAction || self.stepCompleted) {
return;
}
var step = self.tutorialSteps[self.currentStep];
switch (step.action) {
case 'placeTower':
if (towers.length > 0) {
self.stepCompleted = true;
if (step.completeTextKey) {
instructionText.setText(getText(step.completeTextKey));
}
self.updateButtonState();
}
break;
case 'startWave':
if (waveInProgress || currentWave > 0) {
self.stepCompleted = true;
if (step.completeTextKey) {
instructionText.setText(getText(step.completeTextKey));
}
self.updateButtonState();
}
break;
case 'upgradeTower':
// Check if any tower has been upgraded or if upgrade menu was closed
var hasUpgradedTower = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].level > 1) {
hasUpgradedTower = true;
break;
}
}
// Also check if upgrade menu was opened and closed
var upgradeMenuExists = false;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i] instanceof UpgradeMenu) {
upgradeMenuExists = true;
break;
}
}
// If tower was upgraded OR if menu was opened and then closed
if (hasUpgradedTower || !upgradeMenuExists && self.menuWasOpened) {
self.stepCompleted = true;
if (step.completeTextKey) {
instructionText.setText(getText(step.completeTextKey));
}
self.updateButtonState();
}
// Track if menu was opened
if (upgradeMenuExists) {
self.menuWasOpened = true;
}
break;
}
};
// Initialize first step
self.updateButtonState();
self.complete = function () {
self.tutorialActive = false;
// Mark tutorial as completed globally
tutorialCompleted = true;
tutorialInProgress = false;
// Stop all music first to prevent mixing
LK.stopMusic();
// Clear tutorial game state
currentWave = 0;
currentWorld = 1;
currentLevel = 1;
waveInProgress = false;
waveSpawned = false;
waveIndicator.gameStarted = false;
gold = 80;
lives = 20;
score = 0;
enemiesKilledInWave = 0;
// Clear all game entities
while (enemies.length > 0) {
var enemy = enemies.pop();
if (enemy.parent) {
enemy.parent.removeChild(enemy);
}
if (enemy.shadow && enemy.shadow.parent) {
enemy.shadow.parent.removeChild(enemy.shadow);
}
}
while (towers.length > 0) {
var tower = towers.pop();
if (tower.parent) {
tower.parent.removeChild(tower);
}
}
while (bullets.length > 0) {
var bullet = bullets.pop();
if (bullet.parent) {
bullet.parent.removeChild(bullet);
}
}
while (alliedUnits.length > 0) {
var unit = alliedUnits.pop();
if (unit.parent) {
unit.parent.removeChild(unit);
}
}
// Clear grid state
for (var i = 0; i < 24; i++) {
for (var j = 0; j < 35; j++) {
if (grid.cells[i] && grid.cells[i][j]) {
grid.cells[i][j].towersInRange = [];
}
}
}
// Reset UI
updateUI();
updateWaveCounter();
if (self.onComplete) {
self.onComplete();
}
self.destroy();
// Wait before creating main menu to ensure clean transition
LK.setTimeout(function () {
// Return to main menu after tutorial
var mainMenu = new MainMenu();
game.addChild(mainMenu);
}, 500);
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 600;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nHealth: ' + self.tower.health + '/' + self.tower.maxHealth + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 60,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' bits', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' bits', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -75;
sellButton.y = 75;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' bits');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' bits');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " bits!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 0;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
// Sincronizar el mapa de visualización después de vender torre
syncVisualizationMap();
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' bits';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2;
startBlock.tint = 0x00AA00;
// Add shadow for start text
var startTextShadow = new Text2("Start Game", {
size: 50,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 4;
startTextShadow.y = 4;
startMarker.addChild(startTextShadow);
var startText = new Text2("Start Game", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startMarker.addChild(startText);
startMarker.x = -self.indicatorWidth;
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['normal', 'fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Flying";
block.tint = 0xFFFF00;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
waveType = "Boss Normal";
break;
case 'fast':
block.tint = 0x00AAFF;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0xAA0000;
waveType = "Boss Immune";
break;
case 'flying':
block.tint = 0xFFFF00;
waveType = "Boss Flying";
break;
}
}
enemyCount = 1;
// Make the wave indicator for boss waves stand out
// Set boss wave color to the color of the wave type
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
break;
case 'fast':
block.tint = 0x00AAFF;
break;
case 'immune':
block.tint = 0xAA0000;
break;
case 'flying':
block.tint = 0xFFFF00;
break;
default:
block.tint = 0xFF0000;
break;
}
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave && enemyType !== 'swarm') {
// Add a crown or some indicator to the wave marker for boss waves
var bossIndicator = marker.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
bossIndicator.width = 30;
bossIndicator.height = 30;
bossIndicator.tint = 0xFFD700; // Gold color
bossIndicator.y = -block.height / 2 - 15;
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 4;
waveTypeShadow.y = 4;
marker.addChild(waveTypeShadow);
// Add wave type text - 30% smaller than before
var waveTypeText = new Text2(waveType, {
size: 56,
fill: 0xFFFFFF,
weight: 800
});
waveTypeText.anchor.set(0.5, 0.5);
waveTypeText.y = 0;
marker.addChild(waveTypeText);
// Add shadow for wave number - 20% larger than before
var waveNumShadow = new Text2((i + 1).toString(), {
size: 48,
fill: 0x000000,
weight: 800
});
waveNumShadow.anchor.set(1.0, 1.0);
waveNumShadow.x = blockWidth / 2 - 16 + 5;
waveNumShadow.y = block.height / 2 - 12 + 5;
marker.addChild(waveNumShadow);
// Main wave number text - 20% larger than before
var waveNum = new Text2((i + 1).toString(), {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
waveNum.anchor.set(1.0, 1.0);
waveNum.x = blockWidth / 2 - 16;
waveNum.y = block.height / 2 - 12;
marker.addChild(waveNum);
marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
self.addChild(marker);
self.waveMarkers.push(marker);
}
// Get wave type for a specific wave number
self.getWaveType = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return "normal";
}
// If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType
// then we should return a different boss type
var waveType = self.waveTypes[waveNumber - 1];
return waveType;
};
// Get enemy count for a specific wave number
self.getEnemyCount = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return 10;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Add boss prefix for boss waves (every 10th wave)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
// Fix positioning to properly show first wave when currentWave is 0
var displayWave = Math.max(0, currentWave);
var moveAmount = (progress + displayWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
// Fix comparison to properly handle wave 0 and 1
if (i - 1 < displayWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
// Waves no longer advance automatically - they must be triggered manually via NextWaveButton
// This function now only handles the initial game start
};
self.handleWaveProgression();
};
return self;
});
var WorldRenderer = Container.expand(function () {
var self = Container.call(this);
self.currentWorld = 1;
self.backgroundTiles = [];
self.pathTiles = [];
self.wallTiles = [];
self.sceneryElements = [];
self.getWorldAssets = function (worldNumber) {
switch (worldNumber) {
case 1:
return {
background: 'forestBg',
path: 'forestPath',
wall: 'forestWall',
scenery: 'forestScenery',
ambient: 0x90ee90
};
case 2:
return {
background: 'desertBg',
path: 'desertPath',
wall: 'desertWall',
scenery: 'desertScenery',
ambient: 0xffd700
};
case 3:
return {
background: 'glacierBg',
path: 'glacierPath',
wall: 'glacierWall',
scenery: 'glacierScenery',
ambient: 0xe6f3ff
};
case 4:
return {
background: 'villageBg',
path: 'villagePath',
wall: 'villageWall',
scenery: 'villageScenery',
ambient: 0xf0e68c
};
case 5:
return {
background: 'techLabBg',
path: 'techLabPath',
wall: 'techLabWall',
scenery: 'techLabScenery',
ambient: 0x87ceeb
};
case 6:
return {
background: 'infernoBg',
path: 'infernoPath',
wall: 'infernoWall',
scenery: 'infernoScenery',
ambient: 0xff6347
};
default:
return {
background: 'forestBg',
path: 'forestPath',
wall: 'forestWall',
scenery: 'forestScenery',
ambient: 0x90ee90
};
}
};
self.updateWorldGraphics = function (worldNumber, gridInstance) {
// Forzar renderizado siempre, incluso si el mundo no cambia
self.currentWorld = worldNumber;
var worldAssets = self.getWorldAssets(worldNumber);
// Clear existing tiles
while (self.backgroundTiles.length) {
self.removeChild(self.backgroundTiles.pop());
}
while (self.pathTiles.length) {
self.removeChild(self.pathTiles.pop());
}
while (self.wallTiles.length) {
self.removeChild(self.wallTiles.pop());
}
while (self.sceneryElements.length) {
self.removeChild(self.sceneryElements.pop());
}
// Create new tiles based on current world using the correct assets
var gridWidth = 24;
var gridHeight = 35;
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
// Create background tile using world-specific assets
var bgTile = self.attachAsset(worldAssets.background, {
anchorX: 0,
anchorY: 0
});
bgTile.x = i * CELL_SIZE;
bgTile.y = j * CELL_SIZE;
self.backgroundTiles.push(bgTile);
// Add world-specific scenery elements randomly
if (Math.random() < 0.15) {
// 15% chance for scenery - use world-specific scenery assets
var scenery = self.attachAsset(worldAssets.scenery, {
anchorX: 0.5,
anchorY: 0.5
});
scenery.x = i * CELL_SIZE + CELL_SIZE / 2;
scenery.y = j * CELL_SIZE + CELL_SIZE / 2;
// World-specific scenery shapes
switch (worldNumber) {
case 1:
// Forest - trees/bushes (ellipses)
scenery.scaleY = 1.2 + Math.random() * 0.8;
break;
case 2:
// Desert - cacti/rocks (tall thin or wide)
if (Math.random() < 0.5) {
scenery.scaleX = 0.6;
scenery.scaleY = 1.8; // Tall cactus
} else {
scenery.scaleX = 1.4;
scenery.scaleY = 0.7; // Wide rock
}
break;
case 3:
// Glacier - ice crystals
scenery.scaleX = 0.8 + Math.random() * 0.4;
scenery.scaleY = 0.8 + Math.random() * 0.4;
scenery.alpha = 0.7;
break;
case 4:
// Village - small structures
scenery.scaleX = 1.2;
scenery.scaleY = 1.0;
break;
case 5:
// Technology - machinery
scenery.scaleX = 0.9;
scenery.scaleY = 0.9;
scenery.alpha = 0.8;
break;
case 6:
// Hell - lava bubbles/flames
scenery.scaleX = 1.1 + Math.random() * 0.6;
scenery.scaleY = 1.1 + Math.random() * 0.6;
scenery.alpha = 0.9;
break;
}
self.sceneryElements.push(scenery);
}
}
}
// Overlay path and wall tiles on top of scenery
if (gridInstance && gridInstance.cells) {
for (var i = 0; i < Math.min(gridWidth, gridInstance.cells.length); i++) {
for (var j = 0; j < Math.min(gridHeight, gridInstance.cells[i].length); j++) {
var cell = gridInstance.cells[i][j];
if (cell.type === 0 || cell.type === 2 || cell.type === 3) {
// Path, spawn, or goal - use world-specific path assets
var pathTile = self.attachAsset(worldAssets.path, {
anchorX: 0,
anchorY: 0
});
pathTile.x = i * CELL_SIZE;
pathTile.y = j * CELL_SIZE;
pathTile.alpha = 0.95;
self.pathTiles.push(pathTile);
} else if (cell.type === 1) {
// Wall - use appropriate wall tile based on world
var wallTile = self.attachAsset(worldAssets.wall, {
anchorX: 0,
anchorY: 0
});
wallTile.x = i * CELL_SIZE;
wallTile.y = j * CELL_SIZE;
wallTile.alpha = 0.98;
self.wallTiles.push(wallTile);
}
}
}
}
// Asegurar que WorldRenderer esté en el fondo, debajo de todo
if (self.parent) {
self.parent.addChildAt(self, 0);
}
};
return self;
});
var WorldSelectionMenu = Container.expand(function () {
var self = Container.call(this);
// Position the menu at center of screen
self.x = 2048 / 2;
self.y = 2732 / 2;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 1800;
menuBackground.height = 1200;
menuBackground.tint = 0x333333;
menuBackground.alpha = 0.9;
var titleText = new Text2('Select World', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -400;
self.addChild(titleText);
var worldNames = ["Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"];
var unlockedWorlds = storage.unlockedWorlds || 1;
for (var i = 0; i < worldNames.length; i++) {
var worldButton = new Container();
var worldButtonBg = worldButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
worldButtonBg.width = 350;
worldButtonBg.height = 80;
var isUnlocked = i + 1 <= unlockedWorlds;
worldButtonBg.tint = isUnlocked ? 0x4444FF : 0x666666;
var worldButtonText = new Text2(isUnlocked ? worldNames[i] : "Locked", {
size: 40,
fill: isUnlocked ? 0xFFFFFF : 0x999999,
weight: 800
});
worldButtonText.anchor.set(0.5, 0.5);
worldButton.addChild(worldButtonText);
worldButton.y = -200 + i * 100;
self.addChild(worldButton);
(function (worldIndex, unlocked) {
worldButton.down = function () {
if (unlocked) {
self.destroy();
var levelSelectionMenu = new LevelSelectionMenu(worldIndex + 1);
game.addChild(levelSelectionMenu);
} else {
var notification = game.addChild(new Notification("Complete the previous world to unlock!"));
notification.x = 2048 / 2;
notification.y = 2732 / 2 + 200;
}
};
})(i, isUnlocked);
}
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
// Try to import facekit but make it optional to prevent loading issues
// Patamon Evolution Line (Sacred/Angel)
// Gomamon Evolution Line (Water)
// Palmon Evolution Line (Plant/Poison)
// Tentomon Evolution Line (Electric/Insect)
// Gabumon Evolution Line (Ice/Wolf)
// Agumon Evolution Line (Fire/Dragon)
// Digimon Evolution Assets for Voice Summoning
// Notify user about microphone usage for voice commands
// Music system for world progression
// Combat & Action Sounds
// Special Effects
// UI & Feedback
// Boss & Special Events
// Voice & Summoning
// We never call any camera or face detection methods, so the browser will only request microphone access.
// NOTE: We only use facekit for microphone/voice features (facekit.volume).
// Only use microphone features from facekit. Do NOT call any camera-related methods.
// This ensures the browser only requests microphone access, not camera.
var facekit = null;
try {
facekit = LK.import("@upit/facekit.v1");
} catch (e) {
console.log("Facekit not available - voice features disabled");
// Create a mock facekit object to prevent errors
facekit = {
volume: 0
};
}
console.log("This game uses microphone for voice summoning commands, not camera.");
function playWorldMusic(worldNumber, isBoss, isFinalBoss) {
// Stop any currently playing music first to prevent mixing
LK.stopMusic();
// Play world-specific music with fade in effect
var musicId;
// Check for boss music first
if (isFinalBoss) {
musicId = 'finalBossMusic';
} else if (isBoss) {
musicId = 'bossMusic';
} else {
switch (worldNumber) {
case 1:
musicId = 'forestMusic';
break;
case 2:
musicId = 'desertMusic';
break;
case 3:
musicId = 'glacierMusic';
break;
case 4:
musicId = 'villageMusic';
break;
case 5:
musicId = 'techLabMusic';
break;
case 6:
musicId = 'infernoMusic';
break;
default:
musicId = 'forestMusic';
}
}
console.log("Playing music for world " + worldNumber + ": " + musicId);
LK.playMusic(musicId, {
fade: {
start: 0,
end: isBoss || isFinalBoss ? 1.0 : 0.8,
duration: 1500
}
});
}
// Language system
var currentLanguage = storage.language || 'en';
var translations = {
en: {
firewallDefensors: 'Firewall Defensors',
startGame: 'Start Game',
tutorial: 'Tutorial',
language: 'Language',
selectWorld: 'Select World',
selectLevel: 'Select Level',
back: 'Back',
locked: 'Locked',
completeLevel: 'Complete previous level to unlock!',
completeWorld: 'Complete the previous world to unlock!',
nextWave: 'Next Wave',
shop: 'Shop',
bits: 'Bits',
systemHealth: 'System Health',
securityScore: 'Security Score',
wave: 'Wave',
upgrade: 'Upgrade',
sell: 'Sell',
maxLevel: 'Max Level',
digivolve: 'Digivolve',
owned: 'OWNED',
notEnoughBits: 'Not enough bits!',
cannotBuild: 'Cannot build here!',
pathBlocked: 'Tower would block the path!',
tutorialWelcome: 'Welcome to the Tutorial!\n\nThis game uses microphone for voice commands (not camera).\nLearn to play step by step...',
tutorialStep1: 'Step 1: Tower Placement\n\nDrag a Digimon tower from the bottom of the screen to a valid position on the battlefield.',
tutorialStep2: 'Step 2: Starting Waves\n\nClick the "Next Wave" button to start sending enemies. Towers will attack automatically.',
tutorialStep3: 'Step 3: Upgrading Towers\n\nClick on a placed tower and then press the "Upgrade" button to increase its power. Close the menu when done!',
tutorialCompleted: 'Tutorial completed!\n\nNow you know how to:\n• Place towers\n• Start waves\n• Upgrade towers\n• Shout Digimon names to summon allies (requires microphone)\n\nDefend the digital world!',
tutorialStep1Complete: 'Excellent! You placed your first tower.',
tutorialStep2Complete: 'Well done! The wave has started.',
tutorialStep3Complete: 'Perfect! You upgraded the tower.',
nextStep: 'Next',
skipTutorial: 'Skip Tutorial',
// Story translations (EN)
story1_1: "ALERT! Pokémon infiltrators have breached\nthe Digital Forest servers! They're attempting\nto steal Digimon data files!",
story1_2: "These Pokémon spies are using advanced\nstealth protocols to access our core database.\nDeploy Digimon guardians immediately!",
story1_3: "The future of the Digimon franchise\ndepends on you! Stop Pokémon from\ncorrupting our digital ecosystem!",
story2_1: "Pokémon agents have infiltrated the Desert\nData Center! They're planting malicious code\nto corrupt our systems!",
story2_2: "Fire-type Pokémon are overheating our\nservers while others steal precious\nDigimon evolution data!",
story2_3: "Their coordinated attack is more sophisticated\nthan before. Pokémon want to monopolize\nthe children's entertainment industry!",
story3_1: "Ice-type Pokémon have frozen our Glacier\nservers to slow down our defenses\nwhile they extract data!",
story3_2: "Flying Pokémon are bypassing our security\nwalls! They're trying to reach the core\nDigimon genetic database!",
story3_3: "Critical system temperatures detected!\nPokémon are trying to cause a complete\nserver meltdown!",
story4_1: "Pokémon sleeper agents hidden in the Village\nNetwork have activated! They've been\ngathering intelligence for months!",
story4_2: "Multiple Pokémon strike teams are attacking\nsimultaneously, trying to overwhelm\nour Digimon defenders!",
story4_3: "This is corporate espionage on a massive\nscale! Pokémon Company wants to steal\nour digital creature technology!",
story5_1: "MAXIMUM THREAT LEVEL! Elite Pokémon\nhackers have breached our most secure\nTechnology Labs!",
story5_2: "They're using legendary Pokémon abilities\nto bypass our quantum encryption!\nOur most sensitive data is at risk!",
story5_3: "Deploy our strongest Mega-level Digimon!\nOnly they can stop this corporate\ncyber warfare!",
story6_1: "FINAL ASSAULT! Pokémon's master plan\nis revealed - they want to delete ALL\nDigimon data permanently!",
story6_2: "Legendary Pokémon themselves are leading\nthis final attack on our core servers!\nThis is the ultimate battle for supremacy!",
story6_3: "The children's hearts are at stake!\nDefeat Pokémon's invasion and save\nthe future of digital monsters forever!",
storyDefault_1: "Pokémon infiltrators detected! Protect the Digimon database!",
storyDefault_2: "Deploy your Digimon to stop the corporate espionage!",
storyDefault_3: "Save the Digital World from Pokémon's takeover!"
},
es: {
firewallDefensors: 'Defensores del Firewall',
startGame: 'Iniciar Juego',
tutorial: 'Tutorial',
language: 'Idioma',
selectWorld: 'Seleccionar Mundo',
selectLevel: 'Seleccionar Nivel',
back: 'Atrás',
locked: 'Bloqueado',
completeLevel: '¡Completa el nivel anterior para desbloquear!',
completeWorld: '¡Completa el mundo anterior para desbloquear!',
nextWave: 'Siguiente Oleada',
shop: 'Tienda',
bits: 'Bits',
systemHealth: 'Salud del Sistema',
securityScore: 'Puntuación de Seguridad',
wave: 'Oleada',
upgrade: 'Mejorar',
sell: 'Vender',
maxLevel: 'Nivel Máximo',
digivolve: 'Digievolucionar',
owned: 'POSEÍDO',
notEnoughBits: '¡No tienes suficientes bits!',
cannotBuild: '¡No se puede construir aquí!',
pathBlocked: '¡La torreta bloquearía el camino!',
tutorialWelcome: '¡Bienvenido al Tutorial!\n\nEste juego usa el micrófono para comandos de voz (no la cámara).\nAprende a jugar paso a paso...',
tutorialStep1: 'Paso 1: Colocación de torretas\n\nArrastra una torreta Digimon desde la parte inferior de la pantalla hasta una posición válida en el campo de batalla.',
tutorialStep2: 'Paso 2: Iniciar oleadas\n\nHaz clic en el botón "Siguiente Oleada" para comenzar a enviar enemigos. Las torretas atacarán automáticamente.',
tutorialStep3: 'Paso 3: Mejorar torretas\n\nHaz clic en una torreta colocada y luego presiona el botón "Mejorar" para aumentar su poder. ¡Cierra el menú cuando termines!',
tutorialCompleted: '¡Tutorial completado!\n\nAhora ya sabes:\n• Colocar torretas\n• Iniciar oleadas\n• Mejorar torretas\n• Gritar nombres de Digimon para invocar aliados (requiere micrófono)\n\n¡Defiende el mundo digital!',
tutorialStep1Complete: 'Excelente! Has colocado tu primera torreta.',
tutorialStep2Complete: 'Bien hecho! La oleada ha comenzado.',
tutorialStep3Complete: 'Perfecto! Has mejorado la torreta.',
nextStep: 'Siguiente',
skipTutorial: 'Saltar Tutorial',
// Story translations (ES)
story1_1: "¡ALERTA! ¡Infiltradores Pokémon han violado los servidores del Bosque Digital! ¡Intentan robar archivos de datos Digimon!",
story1_2: "¡Estos espías Pokémon usan protocolos avanzados de sigilo para acceder a nuestra base de datos central! ¡Despliega guardianes Digimon de inmediato!",
story1_3: "¡El futuro de la franquicia Digimon depende de ti! ¡Detén a Pokémon antes de que corrompan nuestro ecosistema digital!",
story2_1: "¡Agentes Pokémon han infiltrado el Centro de Datos del Desierto! ¡Están plantando código malicioso para corromper nuestros sistemas!",
story2_2: "¡Pokémon de tipo fuego están sobrecalentando nuestros servidores mientras otros roban valiosos datos de evolución Digimon!",
story2_3: "¡Su ataque coordinado es más sofisticado que antes! ¡Pokémon quiere monopolizar la industria del entretenimiento infantil!",
story3_1: "¡Pokémon de tipo hielo han congelado nuestros servidores Glaciar para ralentizar nuestras defensas mientras extraen datos!",
story3_2: "¡Pokémon voladores están evadiendo nuestros muros de seguridad! ¡Intentan llegar a la base genética central de Digimon!",
story3_3: "¡Temperaturas críticas detectadas! ¡Pokémon intenta provocar un colapso total del servidor!",
story4_1: "¡Agentes durmientes Pokémon ocultos en la Red de la Aldea se han activado! ¡Han estado recolectando inteligencia durante meses!",
story4_2: "¡Múltiples equipos de ataque Pokémon atacan simultáneamente, intentando sobrepasar a nuestros defensores Digimon!",
story4_3: "¡Esto es espionaje corporativo a gran escala! ¡La Compañía Pokémon quiere robar nuestra tecnología de criaturas digitales!",
story5_1: "¡NIVEL DE AMENAZA MÁXIMO! ¡Hackers Pokémon de élite han violado nuestros Laboratorios de Tecnología más seguros!",
story5_2: "¡Usan habilidades legendarias Pokémon para evadir nuestro cifrado cuántico! ¡Nuestros datos más sensibles están en riesgo!",
story5_3: "¡Despliega nuestros Digimon Mega más fuertes! ¡Solo ellos pueden detener esta guerra cibernética corporativa!",
story6_1: "¡ASALTO FINAL! ¡El plan maestro de Pokémon se revela: quieren borrar TODOS los datos Digimon permanentemente!",
story6_2: "¡Pokémon legendarios lideran este ataque final a nuestros servidores centrales! ¡Es la batalla definitiva por la supremacía!",
story6_3: "¡El corazón de los niños está en juego! ¡Derrota la invasión Pokémon y salva el futuro de los monstruos digitales para siempre!",
storyDefault_1: "¡Infiltradores Pokémon detectados! ¡Protege la base de datos Digimon!",
storyDefault_2: "¡Despliega tus Digimon para detener el espionaje corporativo!",
storyDefault_3: "¡Salva el Mundo Digital del dominio de Pokémon!"
}
};
function getText(key) {
return translations[currentLanguage][key] || translations.en[key] || key;
}
function setLanguage(lang) {
currentLanguage = lang;
storage.language = lang;
}
// Assets adicionales para tiles específicos por mundo
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var alliedUnits = []; // Array to track summoned allied Digimon units
var selectedTower = null;
var gold = 80;
var lives = 20;
var score = storage.securityPoints || 0;
var currentWave = 0;
var totalWaves = 10; // 10 waves per world
var currentWorld = 1;
var currentLevel = 1;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var enemiesKilledInWave = 0;
var coins = [];
// Allied units management
var maxAlliedUnits = 5; // Maximum number of allied units at once
var summonCooldown = 300; // 5 seconds at 60 FPS
var lastSummonTime = 0;
// Voice summoning system
var voiceSummonCooldown = {};
var voiceDetectionActive = false;
var lastVoiceDetectionTime = 0;
var voiceDetectionCooldown = 60; // 1 second between voice detections
var coinSpawnTimer = 0;
var goldText = new Text2(getText('bits') + ': ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
// Create health bar container
var healthBarContainer = new Container();
var healthBarBg = healthBarContainer.attachAsset('healthBarOutline', {
anchorX: 0.5,
anchorY: 0.5
});
healthBarBg.width = 300;
healthBarBg.height = 20;
healthBarBg.tint = 0x000000;
var healthBarFill = healthBarContainer.attachAsset('healthBar', {
anchorX: 0.5,
anchorY: 0.5
});
healthBarFill.width = 296;
healthBarFill.height = 16;
healthBarFill.tint = 0x00FF00;
// Add health text label
var livesText = new Text2(getText('systemHealth'), {
size: 45,
fill: 0xFFFFFF,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
livesText.y = -35;
healthBarContainer.addChild(livesText);
var scoreText = new Text2(getText('securityScore') + ': ' + score, {
size: currentLanguage === 'es' ? 45 : 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
// Create speed control button
var speedButton = new Container();
var speedButtonBg = speedButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
speedButtonBg.width = 150;
speedButtonBg.height = 80;
speedButtonBg.tint = 0x00AA00;
var speedButtonText = new Text2('1x', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
speedButtonText.anchor.set(0.5, 0.5);
speedButton.addChild(speedButtonText);
var gameSpeed = 1;
var speedLevels = [1, 2];
var currentSpeedIndex = 0;
speedButton.down = function () {
currentSpeedIndex = (currentSpeedIndex + 1) % speedLevels.length;
gameSpeed = speedLevels[currentSpeedIndex];
speedButtonText.setText(gameSpeed + 'x');
// Update button color based on speed
if (gameSpeed === 1) {
speedButtonBg.tint = 0x00AA00; // Green for normal speed
} else {
speedButtonBg.tint = 0xFFAA00; // Orange for 2x speed
}
};
LK.gui.top.addChild(goldText);
LK.gui.bottom.addChild(healthBarContainer);
LK.gui.top.addChild(scoreText);
LK.gui.top.addChild(speedButton);
healthBarContainer.x = 0;
healthBarContainer.y = -300;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
speedButton.x = 0;
speedButton.y = topMargin;
function updateUI() {
goldText.setText(getText('bits') + ': ' + gold);
scoreText.setText(getText('securityScore') + ': ' + score);
// Save security points to storage
storage.securityPoints = score;
// Update health bar
var healthPercent = lives / 20; // Assuming max lives is 20
healthBarFill.width = 296 * healthPercent;
// Change color based on health
if (healthPercent > 0.6) {
healthBarFill.tint = 0x00FF00; // Green
} else if (healthPercent > 0.3) {
healthBarFill.tint = 0xFFFF00; // Yellow
} else {
healthBarFill.tint = 0xFF0000; // Red
}
}
function setGold(value) {
gold = value;
updateUI();
}
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
// Add world renderer for background graphics
var worldRenderer = new WorldRenderer();
var backgroundLayer = new Container();
backgroundLayer.addChild(worldRenderer);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
worldRenderer.x = grid.x;
worldRenderer.y = grid.y;
grid.pathFind();
// Crear una segunda copia del mapa específicamente para visualización
var mapVisualization = new Grid(24, 29 + 6);
mapVisualization.x = 150;
mapVisualization.y = 200 - CELL_SIZE * 4;
// Sincronizar el mapa de visualización con el mapa principal
function syncVisualizationMap() {
for (var i = 0; i < 24; i++) {
for (var j = 0; j < 35; j++) {
if (grid.cells[i] && grid.cells[i][j] && mapVisualization.cells[i] && mapVisualization.cells[i][j]) {
mapVisualization.cells[i][j].type = grid.cells[i][j].type;
}
}
}
}
// Forzar actualización visual del mapa cada vez que se sincroniza
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
// Sincronizar inicialmente
syncVisualizationMap();
// Render initial world graphics
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
// Only render debug on game start, not every frame
if (LK.ticks === 0) {
grid.renderDebug();
}
debugLayer.addChild(grid);
game.addChildAt(backgroundLayer, 0);
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
var cells = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cells.push({
cell: cell,
originalType: cell.type
});
cell.type = 1;
}
}
}
var blocked = grid.pathFind();
for (var i = 0; i < cells.length; i++) {
cells[i].cell.type = cells[i].originalType;
}
grid.pathFind();
grid.renderDebug();
// Sincronizar el mapa de visualización después de las verificaciones
syncVisualizationMap();
return blocked;
}
function getTowerCost(towerType) {
var cost = 5;
switch (towerType) {
case 'gabumon':
//{ju} // Rapid fire Digimon
cost = 15;
break;
case 'tentomon':
//{jw} // Long range Digimon
cost = 25;
break;
case 'palmon':
//{jy} // Area damage Digimon
cost = 35;
break;
case 'gomamon':
//{jA} // Slowing Digimon
cost = 45;
break;
case 'patamon':
//{jC} // Poison/status Digimon
cost = 55;
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
// Play tower placement sound
LK.getSound('towerPlace').play();
grid.pathFind();
grid.renderDebug();
// Sincronizar el mapa de visualización después de colocar torre
syncVisualizationMap();
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
return true;
} else {
var notification = game.addChild(new Notification("Not enough bits!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Play purchase fail sound
LK.getSound('purchaseFail').play();
return false;
}
}
game.down = function (x, y, obj) {
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
var shopVisible = digimonShop && digimonShop.visible;
var summonMenuVisible = digimonSummonMenu && digimonSummonMenu.visible;
if (upgradeMenuVisible || shopVisible || summonMenuVisible) {
return;
}
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
// Apply the same offset as in move handler to ensure consistency when starting drag
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
break;
}
}
};
game.move = function (x, y, obj) {
var shopVisible = digimonShop && digimonShop.visible;
var summonMenuVisible = digimonSummonMenu && digimonSummonMenu.visible;
if (shopVisible || summonMenuVisible) {
return;
}
if (isDragging) {
// Shift the y position upward by 1.5 tiles to show preview above finger
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
}
};
game.up = function (x, y, obj) {
var shopVisible = digimonShop && digimonShop.visible;
var summonMenuVisible = digimonSummonMenu && digimonSummonMenu.visible;
if (shopVisible || summonMenuVisible) {
// Still allow dragging to be reset even when menus are open
if (isDragging) {
isDragging = false;
towerPreview.visible = false;
}
return;
}
var clickedOnTower = false;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var towerLeft = tower.x - tower.width / 2;
var towerRight = tower.x + tower.width / 2;
var towerTop = tower.y - tower.height / 2;
var towerBottom = tower.y + tower.height / 2;
if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) {
clickedOnTower = true;
break;
}
}
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
var menuWidth = 2048;
var menuHeight = 450;
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
hideUpgradeMenu(menu);
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
grid.renderDebug();
}
}
if (isDragging) {
isDragging = false;
if (towerPreview.canPlace) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
}
};
// Simple wave counter instead of full wave indicator
var waveCounterText = new Text2('Wave: 0/9', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
waveCounterText.anchor.set(0.5, 0.5);
waveCounterText.x = 2048 / 2;
waveCounterText.y = 2732 - 80;
game.addChild(waveCounterText);
// Create a simple wave counter object to replace waveIndicator
var waveIndicator = {
gameStarted: false,
getWaveType: function getWaveType(waveNumber) {
if (waveNumber < 1 || waveNumber > 10) return "normal";
// Simple wave type logic for 10 waves
var waveTypes = ['normal', 'fast', 'immune', 'flying', 'swarm', 'normal', 'fast', 'immune', 'flying', 'boss'];
return waveTypes[waveNumber - 1];
},
getEnemyCount: function getEnemyCount(waveNumber) {
if (waveNumber < 1 || waveNumber > 10) return 10;
// Wave 10 is boss with 1 giant enemy, others have 10-30 enemies
if (waveNumber === 10) return 1;
if (waveNumber === 5) return 30; // Swarm wave
return 10;
},
getWaveTypeName: function getWaveTypeName(waveNumber) {
var type = this.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
if (waveNumber === 10) typeName = "BOSS";
return typeName;
}
};
// Function to update wave counter display
function updateWaveCounter() {
var currentWorldWave;
if (currentWave === 0) {
currentWorldWave = 0;
} else {
// Calculate the wave within the current world (1-10)
currentWorldWave = (currentWave - 1) % 10 + 1;
}
waveCounterText.setText(getText('wave') + ': ' + currentWorldWave + '/10');
}
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200;
nextWaveButton.y = 2732 - 100 + 20;
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
// Add shop button
var shopButton = new Container();
var shopButtonBg = shopButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shopButtonBg.width = 300;
shopButtonBg.height = 100;
shopButtonBg.tint = 0x4444FF;
var shopButtonText = new Text2('Shop', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
shopButtonText.anchor.set(0.5, 0.5);
shopButton.addChild(shopButtonText);
shopButton.x = 200;
shopButton.y = 2732 - 100 + 20;
game.addChild(shopButton);
var digimonShop = new DigimonShop();
digimonShop.x = 2048 / 2;
game.addChild(digimonShop);
shopButton.down = function () {
digimonShop.show();
};
// --- Add C, B, and A buttons for Digimon summon filtering ---
var summonButtonSpacing = 120;
var summonButtonStartX = shopButton.x + 220; // Place to the right of shop button
function createSummonLevelButton(label, color, filterLevel, offset) {
var btn = new Container();
var btnBg = btn.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
btnBg.width = 100;
btnBg.height = 100;
btnBg.tint = color;
var btnText = new Text2(label, {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
btnText.anchor.set(0.5, 0.5);
btn.addChild(btnText);
btn.x = summonButtonStartX + offset * summonButtonSpacing;
btn.y = shopButton.y;
btn.down = function () {
// Show summon menu filtered by evolution level
if (digimonSummonMenu && digimonSummonMenu.show) {
digimonSummonMenu.show(filterLevel);
}
};
return btn;
}
// C = Champion, B = Ultimate, A = Mega
var buttonC = createSummonLevelButton('C', 0x32CD32, 'champion', 0);
var buttonB = createSummonLevelButton('B', 0xFFD700, 'ultimate', 1);
var buttonA = createSummonLevelButton('A', 0xFF4500, 'mega', 2);
game.addChild(buttonC);
game.addChild(buttonB);
game.addChild(buttonA);
var towerTypes = ['agumon', 'gabumon', 'tentomon', 'palmon', 'gomamon', 'patamon'];
var sourceTowers = [];
var towerSpacing = 320; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
// Add summon arrow button at the end of source towers
var summonArrowButton = new Container();
var arrowBg = summonArrowButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
arrowBg.width = 200;
arrowBg.height = 200;
arrowBg.tint = 0xFF6600;
var arrowGraphics = summonArrowButton.attachAsset('arrow', {
anchorX: 0.5,
anchorY: 0.5
});
arrowGraphics.width = 80;
arrowGraphics.height = 80;
arrowGraphics.tint = 0xFFFFFF;
arrowGraphics.rotation = Math.PI / 2; // Point upward
var summonText = new Text2('SUMMON', {
size: 35,
fill: 0xFFFFFF,
weight: 800
});
summonText.anchor.set(0.5, 0.5);
summonText.y = 50;
summonArrowButton.addChild(summonText);
var summonSubText = new Text2('(No Mic)', {
size: 25,
fill: 0xCCCCCC,
weight: 600
});
summonSubText.anchor.set(0.5, 0.5);
summonSubText.y = 75;
summonArrowButton.addChild(summonSubText);
summonArrowButton.x = startX + towerTypes.length * towerSpacing;
summonArrowButton.y = towerY;
towerLayer.addChild(summonArrowButton);
// Create summon menu
var digimonSummonMenu = new DigimonSummonMenu();
digimonSummonMenu.x = 2048 / 2;
game.addChild(digimonSummonMenu);
summonArrowButton.down = function () {
digimonSummonMenu.show();
};
sourceTower = null;
enemiesToSpawn = 10;
game.update = function () {
// Update background color based on current world
var worldAssets = worldRenderer.getWorldAssets(currentWorld);
if (worldAssets) {
game.setBackgroundColor(worldAssets.ambient);
}
// Apply speed multiplier to frame-dependent updates
var effectiveSpeed = gameSpeed;
// Note: Wave progression is now manual only via NextWaveButton, no automatic timer
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
enemiesKilledInWave = 0; // Reset kill counter for new wave
// Calculate correct world and level for this wave
var levelsPerWorld = 10;
var newWorld = Math.ceil(currentWave / levelsPerWorld);
var newLevel = (currentWave - 1) % levelsPerWorld + 1;
var previousWorld = currentWorld;
currentWorld = newWorld;
currentLevel = newLevel;
if (currentWorld > 6) currentWorld = 6;
// Regenerate maze and world graphics if world changed or first wave
if (currentWorld !== previousWorld || currentWave === 1) {
grid.generateMazeForWorld(currentWorld);
mapVisualization.generateMazeForWorld(currentWorld); // Regenerar también el mapa de visualización
grid.pathFind();
grid.renderDebug();
// Sincronizar mapas después de regenerar
syncVisualizationMap();
// Update world graphics usando el mapa de visualización
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
// Change music when entering new world
playWorldMusic(currentWorld);
// World-specific notification messages
var worldNames = ["", "Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"];
var worldNotification = game.addChild(new Notification("Welcome to Digital World " + currentWorld + ": " + worldNames[currentWorld] + "!"));
worldNotification.x = 2048 / 2;
worldNotification.y = grid.height - 100;
}
// Get wave type and enemy count from the wave indicator (based on current world/level)
var worldWave = currentLevel;
var waveType = waveIndicator.getWaveType(worldWave);
var enemyCount = waveIndicator.getEnemyCount(worldWave);
// Update wave counter display
updateWaveCounter();
// Check if this is a boss wave (wave 10 of each world)
var isBossWave = worldWave === 10;
if (isBossWave) {
// Boss waves have just 1 giant enemy
enemyCount = 1;
// Get boss name based on current world
var bossNames = ["", "SNORLAX", "RHYDON", "ARTICUNO", "MACHAMP", "GROUDON", "MEWTWO"];
var bossName = bossNames[currentWorld] || "BOSS";
// Check if this is the final boss (Mewtwo in world 6)
var isFinalBoss = currentWorld === 6;
// Play boss music
playWorldMusic(currentWorld, true, isFinalBoss);
// Show boss announcement with specific name
var notification = game.addChild(new Notification("⚠️ " + bossName + " APPEARS! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy = new Enemy(waveType);
// Make wave 10 boss giant with much more health
if (isBossWave) {
enemy.isBoss = true;
enemy.maxHealth *= 50; // 50x more health for boss
enemy.health = enemy.maxHealth;
// Make boss visually larger
if (enemy.children[0]) {
enemy.children[0].scaleX = 3.0;
enemy.children[0].scaleY = 3.0;
}
// Make boss slower but more intimidating
enemy.speed *= 0.5;
}
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Scale difficulty with wave number but don't apply to boss
// as bosses already have their health multiplier
// Use exponential scaling for health
var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// All enemy types now spawn in the middle 6 tiles at the top spacing
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
// Find a column that isn't occupied by another enemy that's not yet in view
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
// Check if any enemy is already in this column but not yet in view
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
// If all columns are occupied, use original random method
var spawnX;
if (availableColumns.length > 0) {
// Choose a random unoccupied column
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
// Fallback to random if all columns are occupied
spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
}
var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
}
}
// Apply speed multiplier to enemy updates
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
// Update enemy with speed multiplier applied
if (enemy.update) {
for (var speedTick = 0; speedTick < effectiveSpeed; speedTick++) {
enemy.update();
}
}
if (enemy.health <= 0) {
// Play enemy death sound
if (enemy.isBoss) {
LK.getSound('bossDeath').play();
} else {
LK.getSound('enemyDestroyed').play();
}
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Track enemy kills for wave progression
if (enemy.waveNumber === currentWave) {
enemiesKilledInWave++;
// Give bonus gold every 5 enemies killed
if (enemiesKilledInWave % 5 === 0) {
var bonusGold = enemy.isBoss ? 15 : 10;
var bonusIndicator = new GoldIndicator(bonusGold, enemy.x, enemy.y - 50);
game.addChild(bonusIndicator);
setGold(gold + bonusGold);
var notification = game.addChild(new Notification("Kill streak bonus! +" + bonusGold + " bits!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
}
// Boss enemies give more gold and score
var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5);
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Save security points to storage
storage.securityPoints = score;
// Add a notification for boss defeat
if (enemy.isBoss) {
// Get boss name based on world
var bossNames = ["", "Snorlax", "Rhydon", "Articuno", "Machamp", "Groudon", "Mewtwo"];
var bossName = bossNames[enemy.worldNumber] || "Boss";
var notification = game.addChild(new Notification(bossName + " defeated! +" + goldEarned + " bits!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Return to normal world music after boss defeat
LK.setTimeout(function () {
LK.stopMusic();
LK.setTimeout(function () {
playWorldMusic(enemy.worldNumber, false, false);
}, 500); // Add delay to ensure clean music transition
}, 2000); // Wait 2 seconds before switching back to normal music
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
// Calculate damage based on enemy world and type
var damageDealt = 1; // Base damage
var worldDamageMultiplier = 1;
// Scale damage based on world
switch (enemy.worldNumber) {
case 1:
worldDamageMultiplier = 1;
break;
case 2:
worldDamageMultiplier = 1.2;
break;
case 3:
worldDamageMultiplier = 1.4;
break;
case 4:
worldDamageMultiplier = 1.6;
break;
case 5:
worldDamageMultiplier = 1.8;
break;
case 6:
worldDamageMultiplier = 2.0;
break;
}
// Scale damage based on enemy type
switch (enemy.type) {
case 'normal':
damageDealt = 1;
break;
case 'fast':
damageDealt = 1;
break;
// Fast but same damage
case 'immune':
damageDealt = 2;
break;
// Tanky and hits hard
case 'flying':
damageDealt = 1.5;
break;
// Aerial advantage
case 'swarm':
damageDealt = 1;
break;
// Weak individual damage
}
// Boss enemies deal significantly more damage
if (enemy.isBoss) {
damageDealt *= 5;
}
// Apply world multiplier and round
damageDealt = Math.ceil(damageDealt * worldDamageMultiplier);
lives = Math.max(0, lives - damageDealt);
// Play system damage sound
LK.getSound('systemDamage').play();
// Check for critical health warning
if (lives <= 5 && lives > 0) {
LK.getSound('criticalHealth').play();
}
updateUI();
// Show damage indicator
var damageIndicator = new Notification("-" + damageDealt + " System Health!");
damageIndicator.x = 2048 / 2;
damageIndicator.y = 2732 - 200;
game.addChild(damageIndicator);
if (lives <= 0) {
LK.showGameOver();
}
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Check for level completion and world progression
var levelsPerWorld = 10;
var completedWaves = storage.completedWaves || 0;
if (currentWave > completedWaves) {
storage.completedWaves = currentWave;
// Calculate current world and level
var currentWorldNum = Math.ceil(currentWave / levelsPerWorld);
var currentLevelNum = (currentWave - 1) % levelsPerWorld + 1;
// Update world levels in storage
var worldLevels = storage.worldLevels || {
1: 1,
2: 1,
3: 1,
4: 1,
5: 1,
6: 1
};
if (currentWorldNum >= 1 && currentWorldNum <= 6) {
worldLevels[currentWorldNum] = Math.max(worldLevels[currentWorldNum], currentLevelNum);
storage.worldLevels = worldLevels;
}
// Check if we completed a world (every 10 waves)
var worldsCompleted = Math.floor(currentWave / levelsPerWorld);
var unlockedWorlds = storage.unlockedWorlds || 1;
if (worldsCompleted + 1 > unlockedWorlds && worldsCompleted + 1 <= 6) {
storage.unlockedWorlds = worldsCompleted + 1;
var notification = game.addChild(new Notification("New world unlocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
}
// Spawn coins randomly on the field
coinSpawnTimer++;
if (coinSpawnTimer > 10800 && coins.length < 3) {
// Spawn every 3 minutes (180 seconds * 60 FPS = 10800 ticks), max 3 coins
coinSpawnTimer = 0;
// Find a random walkable position
var attempts = 0;
var spawnX, spawnY;
do {
var gridX = Math.floor(Math.random() * 24);
var gridY = Math.floor(Math.random() * 35);
var cell = grid.getCell(gridX, gridY);
if (cell && cell.type === 0) {
// Spawn on path tiles
spawnX = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
spawnY = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
break;
}
attempts++;
} while (attempts < 50);
if (attempts < 50) {
var coinValue = 5; // Fixed 5 bits for security store currency
var coin = new Coin(spawnX, spawnY, coinValue);
game.addChild(coin);
coins.push(coin);
}
}
// Update existing coins
for (var i = coins.length - 1; i >= 0; i--) {
var coin = coins[i];
if (coin.update) coin.update();
if (coin.collected) {
coins.splice(i, 1);
}
}
// Update allied units with speed multiplier
for (var i = alliedUnits.length - 1; i >= 0; i--) {
var unit = alliedUnits[i];
if (unit.update) {
for (var speedTick = 0; speedTick < effectiveSpeed; speedTick++) {
unit.update();
}
}
// Remove dead or destroyed units
if (unit.isDead || !unit.parent) {
alliedUnits.splice(i, 1);
}
}
// Voice summoning system
handleVoiceSummoning();
// Check for completion of current world (10 waves) or all worlds
var worldWave = (currentWave - 1) % 10 + 1;
if (worldWave >= 10 && enemies.length === 0 && !waveInProgress) {
// Play level complete sound
LK.getSound('levelComplete').play();
// Stop all music first to prevent mixing
LK.stopMusic();
// Clear all timeouts to prevent conflicts
// (No direct timeout clearing method available, but stopping music helps)
// Show win screen instead of immediately returning to main menu
LK.showYouWin(); // This will handle the game state reset and return to main menu properly
}
};
// Show microphone usage notification
var micNotification = game.addChild(new Notification("Microphone access may be requested for voice summoning features"));
micNotification.x = 2048 / 2;
micNotification.y = 200;
var mainMenu = new MainMenu();
game.addChild(mainMenu);
// Execute the game
console.log("Game initialized and running!");
game.startGame = function () {
var worldSelectionMenu = new WorldSelectionMenu();
game.addChild(worldSelectionMenu);
};
game.startWorld = function (worldNumber) {
currentWorld = worldNumber;
grid.generateMazeForWorld(currentWorld);
mapVisualization.generateMazeForWorld(currentWorld); // Regenerar también el mapa de visualización
grid.pathFind();
grid.renderDebug();
// Sincronizar mapas
syncVisualizationMap();
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
// Change to world-specific music
playWorldMusic(worldNumber);
// Show story sequence before starting the world
var storySequence = new StorySequence(worldNumber);
game.addChild(storySequence);
storySequence.onComplete = function () {
waveIndicator.gameStarted = true;
currentWave = (currentWorld - 1) * 10;
waveTimer = nextWaveTime;
};
};
game.startWorldLevel = function (worldNumber, levelNumber) {
currentWorld = worldNumber;
currentLevel = levelNumber;
// Set currentWave to the correct absolute wave number for this world/level
// Each world has 10 levels, so world 1: 1-10, world 2: 11-20, etc.
currentWave = (worldNumber - 1) * 10 + levelNumber;
grid.generateMazeForWorld(currentWorld);
mapVisualization.generateMazeForWorld(currentWorld);
grid.pathFind();
grid.renderDebug();
// Sincronizar mapas
syncVisualizationMap();
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
// Change to world-specific music
playWorldMusic(worldNumber);
// Show story sequence before starting the level
var storySequence = new StorySequence(worldNumber);
game.addChild(storySequence);
storySequence.onComplete = function () {
waveIndicator.gameStarted = true;
// Set waveTimer so the first wave button click starts at the selected level
waveTimer = nextWaveTime;
};
};
// Add tutorial state tracking
var tutorialCompleted = false;
var tutorialInProgress = false;
// Voice summoning system handler
// This function only uses facekit.volume (microphone input).
// No camera or face detection features are used, so only microphone permission is requested.
function handleVoiceSummoning() {
// Only process voice commands during active gameplay
if (!waveIndicator.gameStarted || LK.ticks - lastVoiceDetectionTime < voiceDetectionCooldown) {
return;
}
// Check if facekit is available and for loud voice input (shouting level)
if (facekit && facekit.volume > 0.7) {
lastVoiceDetectionTime = LK.ticks;
// Define Digimon voice commands and their requirements
var digimonVoiceCommands = {
// Champion level (requires Digivice C and base tower)
'greymon': {
baseTower: 'agumon',
requiredDigivice: 'digiviceC',
level: 'champion',
cost: 150,
description: 'Champion Fire Dragon'
},
'garurumon': {
baseTower: 'gabumon',
requiredDigivice: 'digiviceC',
level: 'champion',
cost: 180,
description: 'Champion Ice Wolf'
},
'kabuterimon': {
baseTower: 'tentomon',
requiredDigivice: 'digiviceC',
level: 'champion',
cost: 160,
description: 'Champion Electric Insect'
},
// Ultimate level (requires Digivice B and base tower)
'metalgreymon': {
baseTower: 'agumon',
requiredDigivice: 'digiviceB',
level: 'ultimate',
cost: 400,
description: 'Ultimate Cyborg Dragon'
},
'weregarurumon': {
baseTower: 'gabumon',
requiredDigivice: 'digiviceB',
level: 'ultimate',
cost: 450,
description: 'Ultimate Beast Warrior'
},
'megakabuterimon': {
baseTower: 'tentomon',
requiredDigivice: 'digiviceB',
level: 'ultimate',
cost: 420,
description: 'Ultimate Giant Insect'
},
// Mega level (requires Digivice A and base tower)
'wargreymon': {
baseTower: 'agumon',
requiredDigivice: 'digiviceA',
level: 'mega',
cost: 800,
description: 'Mega Dragon Warrior'
}
};
// Check each voice command
for (var digimonName in digimonVoiceCommands) {
var command = digimonVoiceCommands[digimonName];
// Check if player has the required Digivice
if (!storage[command.requiredDigivice]) {
continue;
}
// Count base towers of the required type
var baseTowerCount = 0;
for (var i = 0; i < towers.length; i++) {
if (towers[i].id === command.baseTower) {
baseTowerCount++;
}
}
// Need at least one base tower to summon evolution
if (baseTowerCount === 0) {
continue;
}
// Calculate cooldown based on number of base towers
var cooldownReduction = Math.max(1, baseTowerCount);
var effectiveCooldown = Math.floor(summonCooldown / cooldownReduction);
// Check if this Digimon is off cooldown
var lastSummonTime = voiceSummonCooldown[digimonName] || 0;
if (LK.ticks - lastSummonTime < effectiveCooldown) {
continue;
}
// Check if player can afford the summon
if (score < command.cost) {
continue;
}
// Check if there's space for more allied units
if (alliedUnits.length >= maxAlliedUnits) {
continue;
}
// Voice detection successful - summon the Digimon!
score -= command.cost;
updateUI();
// Create a more powerful allied unit based on evolution level
var summonedUnit = new DigimonUnit(digimonName, getEvolutionLevel(command.level));
// Apply evolution bonuses
switch (command.level) {
case 'champion':
summonedUnit.damage *= 2;
summonedUnit.health *= 2;
summonedUnit.maxHealth = summonedUnit.health;
break;
case 'ultimate':
summonedUnit.damage *= 3;
summonedUnit.health *= 3;
summonedUnit.maxHealth = summonedUnit.health;
summonedUnit.range *= 1.5;
break;
case 'mega':
summonedUnit.damage *= 5;
summonedUnit.health *= 5;
summonedUnit.maxHealth = summonedUnit.health;
summonedUnit.range *= 2;
summonedUnit.attackRate = Math.floor(summonedUnit.attackRate * 0.7);
break;
}
// Add to game world
enemyLayerTop.addChild(summonedUnit);
alliedUnits.push(summonedUnit);
// Set cooldown for this specific Digimon
voiceSummonCooldown[digimonName] = LK.ticks;
// Show success notification
var notification = game.addChild(new Notification(digimonName.toUpperCase() + " summoned by voice!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Show microphone icon animation above the summoned Digimon
var micIcon = summonedUnit.attachAsset('notification', {
anchorX: 0.5,
anchorY: 1.0
});
micIcon.width = 60;
micIcon.height = 60;
micIcon.x = 0;
micIcon.y = -summonedUnit.children[0].height / 2 - 10;
micIcon.tint = 0x00AAFF;
micIcon.alpha = 0;
tween(micIcon, {
alpha: 1,
y: micIcon.y - 20
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(micIcon, {
alpha: 0,
y: micIcon.y - 40
}, {
duration: 600,
delay: 600,
easing: tween.easeIn,
onFinish: function onFinish() {
micIcon.visible = false;
}
});
}
});
// Visual and audio feedback
LK.effects.flashScreen(0x00FF00, 500);
// Play voice summon and digivolve sounds
LK.getSound('voiceSummon').play();
LK.setTimeout(function () {
LK.getSound('digivolveSound').play();
}, 200);
// Only summon one Digimon per voice command
break;
}
}
}
// Helper function to convert evolution level to numeric level
function getEvolutionLevel(levelName) {
switch (levelName) {
case 'champion':
return 3;
case 'ultimate':
return 5;
case 'mega':
return 6;
default:
return 1;
}
}
game.startTutorial = function () {
// Clear any existing game state first
while (enemies.length > 0) {
var enemy = enemies.pop();
if (enemy.parent) {
enemy.parent.removeChild(enemy);
}
if (enemy.shadow && enemy.shadow.parent) {
enemy.shadow.parent.removeChild(enemy.shadow);
}
}
while (towers.length > 0) {
var tower = towers.pop();
if (tower.parent) {
tower.parent.removeChild(tower);
}
}
while (bullets.length > 0) {
var bullet = bullets.pop();
if (bullet.parent) {
bullet.parent.removeChild(bullet);
}
}
// IMPORTANT: Reset tutorial state BEFORE creating the sequence
tutorialCompleted = false;
tutorialInProgress = false;
// Reset game state for tutorial
currentWorld = 1;
currentLevel = 1;
currentWave = 0; // Reset to 0 so tutorial starts at wave 0/9
waveInProgress = false;
waveSpawned = false;
gold = 80;
lives = 20;
score = 0;
enemiesKilledInWave = 0;
// Update wave counter for tutorial - explicitly set to show 0/10
waveCounterText.setText('Wave: 0/10');
updateUI();
// Generate tutorial world
grid.generateMazeForWorld(currentWorld);
mapVisualization.generateMazeForWorld(currentWorld);
grid.pathFind();
grid.renderDebug();
syncVisualizationMap();
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
// Start tutorial with forest world music
playWorldMusic(1);
// Create tutorial sequence immediately after state reset
var tutorialSequence = new TutorialSequence();
game.addChild(tutorialSequence);
// Set tutorial in progress AFTER creation
tutorialInProgress = true;
tutorialSequence.onComplete = function () {
// Mark tutorial as completed
tutorialCompleted = true;
tutorialInProgress = false;
// After tutorial, ensure game is ready to start from wave 1
waveIndicator.gameStarted = true;
// Tutorial complete - show next wave button
var notification = game.addChild(new Notification("Tutorial complete! Use Next Wave button to start your first wave!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
};
};
Gabumon viendo a la derecha, perspectiva isométrica. In-Game asset. 2d. High contrast. No shadows
Gomamon. In-Game asset. 2d. High contrast. No shadows
Agumon viendo a la derecha, perspectiva isometrica. In-Game asset. 2d. High contrast. No shadows
Tentomon viendo a la derecha, perspectiva isometrica. In-Game asset. 2d. High contrast. No shadows
Patamon viendo a la derecha, perspectiva isometrica. In-Game asset. 2d. High contrast. No shadows
Pikachu visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Pidgeot visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Beedrill visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Gengar visto desde arriba. In-Game asset. 2d. High contrast. No shadows
forestScenaryElement. In-Game asset. 2d. High contrast. No shadows
Cell. In-Game asset. 2d. High contrast. No shadows
desertbg. In-Game asset. 2d. High contrast. No shadows
desertscenery. In-Game asset. 2d. High contrast. No shadows
glacierbg block. In-Game asset. 2d. High contrast. No shadows
Rattata, visto desde arriba, viendo a la derecha. In-Game asset. 2d. High contrast. No shadows
textura de camino glaciar visto desde arriba. In-Game asset. 2d. High contrast. No shadows
boton con contorno mecanico. In-Game asset. 2d. High contrast. No shadows
glacier wall, visto desde arriba efecto looping unilateral para repetir el grafico con continuidad. In-Game asset. 2d. High contrast. No shadows
Bloque de pasto verde, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Articuno visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Bloque de tierra sin césped, visto desde arriba, con efecto repetido para unirse a otros bloques, ocupar el cuadro completo de imagen In-Game asset. 2d. High contrast. No shadows
Snorlax, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Quitale el fondo blanco pero cuidado con las garras
Machamp, visto desde arriba, bien detallado. In-Game asset. 2d. High contrast. No shadows
Groudon, visto desde arriba, detalles altos. In-Game asset. 2d. High contrast. No shadows
Mewtwo, visto desde arriba, altos detalles. In-Game asset. 2d. High contrast. No shadows
Greymon, visto desde arriba, detalles altos sin errores. In-Game asset. 2d. High contrast. No shadows
Metalgreymon, visto desde arriba, detalles altos, sin errores debe ser identico. In-Game asset. 2d. High contrast. No shadows
Wargreymon, visto desde arriba, detalles muy altos, sin errores idéntico. In-Game asset. 2d. High contrast. No shadows
Garurumon, visto desde arriba, detalles muy altos, sin errores idéntico. In-Game asset. 2d. High contrast. No shadows
WereGarurumon, visto desde arriba, detalles muy altos, sin errores, original In-Game asset. 2d. High contrast. No shadows
Metalgarurumon, visto desde arriba, detalles muy épicos. In-Game asset. 2d. High contrast. No shadows
Kabuterimon. visto desde arriba. calidad y detalles altos. In-Game asset. 2d. High contrast. No shadows
MegaKabuterimon. visto desde arriba. calidad y detalles altos. In-Game asset. 2d. High contrast. No shadows
Herculeskabuterimon, visto desde arriba, calidad y detalles altamente épicos. In-Game asset. 2d. High contrast. No shadows
Rosemon, vista desde arriba, calidad y detalles altos, busto grande sexy, cuerpo completo. In-Game asset. 2d. High contrast. No shadows
Togemon, visto desde arriba, calidad y detalles altamente épicos. In-Game asset. 2d. High contrast. No shadows
Lillymon, visto desde arriba, calidad y detalles altamente épicos. In-Game asset. 2d. High contrast. No shadows
Ikkakumon, visto desde arriba, calidad y detalles altamente épicos. In-Game asset. 2d. High contrast. No shadows
Angemon, visto desde arriba, calidad y detalles altamente épicos.. In-Game asset. 2d. High contrast. No shadows
Zudomon, visto desde arriba, calidad y detalles altamente épicos, fidelidad al concepto original In-Game asset. 2d. High contrast. No shadows
mainMenuMusic
Music
forestMusic
Music
desertMusic
Music
glacierMusic
Music
villageMusic
Music
techLabMusic
Music
infernoMusic
Music
bossMusic
Music
finalBossMusic
Music
digimonAttack
Sound effect
enemyHit
Sound effect
enemyDestroyed
Sound effect
towerPlace
Sound effect
towerUpgrade
Sound effect
digivolveSound
Sound effect
splashAttack
Sound effect
poisonEffect
Sound effect
freezeEffect
Sound effect
burnEffect
Sound effect
buttonClick
Sound effect
coinCollect
Sound effect
bossWarning
Sound effect
criticalHealth
Sound effect
levelComplete
Sound effect
purchaseFail
Sound effect
purchaseSuccess
Sound effect
systemDamage
Sound effect
voiceSummon
Sound effect
waveStart
Sound effect