User prompt
En el nivel final se desbloquea a UlforceVeedramon como aliado nivel mega este no necesita torre base y su ataque es x15
User prompt
Y dale un contorno negro al texto blanco de los paneles de comic
User prompt
Esto también aplica a los siguientes mundos
User prompt
A partir del mundo glaciar se deberían usar las imágenes de los digimon aliados en los paneles comic
User prompt
Y a Wargreymon hay que darle un x7 de ataque
User prompt
Sabes que mejor triplica el daño de todos los aliados digimon ya que se ven muy inútiles
User prompt
A todos los mundos desde glaciar para abajo le faltan las reglas de comic que aplicamos en los anteriores
User prompt
Agrega un parpadeo cuando invocas manualmente un aliado Digimon junto a un sonido de succes summon ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Esta será la vida de cada jefe y ningún multiplicador de vida del mundo le afectará
User prompt
Elimina los multiplicadores de vida de cada jefe
User prompt
La vida de Mewtwo 1000
User prompt
La vida de Groudon 900
User prompt
La vida de Machamp 800
User prompt
La vida de Articuno 700
User prompt
Rhydon 600, Articuno 700, Machamp 800, Groudon 900 y Mewtwo 1000
User prompt
La vida de Snorlax 500
User prompt
La vida de los jefes se debería multiplicar por 1.2x
User prompt
Hagamos funcional la tabla de puntuaciones usando el score security y viendo el de otros jugadores ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Añadamos chino y portugués a los idiomas
User prompt
El texto de los comics será blanco con un contorno negro
User prompt
Si añadelo por favor
User prompt
Si añadelo porfa
User prompt
Sigue igual por alguna razón
User prompt
No me refiero a los rookies hablo de algunos champions, ultimates y megas los únicos que me aparecen son Greymon, Garurumon, Kabuterimon, Metalgreymon, WereGarurumon, Megakabuterimon y Wargreymon
User prompt
Cambia el de Wargreymon a 2.5x y los otros megas a 2.0x para que el de los ultimates sea 1.75x con champions a 1.5x
/**** * 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('fairy', { anchorX: 0.5, anchorY: 0.5 }); coinGraphics.width = 50; coinGraphics.height = 55; coinGraphics.tint = 0xFFD700; // Add fluttering animation var _flutterAnimation = function flutterAnimation() { tween(coinGraphics, { scaleY: 1.2, y: -8 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(coinGraphics, { scaleY: 1.0, y: 0 }, { duration: 800, easing: tween.easeInOut, onFinish: _flutterAnimation }); } }); }; _flutterAnimation(); // Add gentle rotation for fairy-like movement var _rotateAnimation = function rotateAnimation() { tween(coinGraphics, { rotation: coinGraphics.rotation + 0.3 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(coinGraphics, { rotation: coinGraphics.rotation - 0.6 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { tween(coinGraphics, { rotation: coinGraphics.rotation + 0.3 }, { duration: 1000, easing: tween.easeInOut, onFinish: _rotateAnimation }); } }); } }); }; _rotateAnimation(); // 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 with more fairy-like floating movement var newX = self.x + Math.cos(self.direction) * self.walkSpeed; var newY = self.y + Math.sin(self.direction) * self.walkSpeed * 0.7; // Slower vertical movement // Add slight vertical bobbing for fairy effect newY += Math.sin(LK.ticks * 0.05) * 0.3; // 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: 0x00FFAA, stroke: 0x00FFFF, strokeThickness: 3, weight: 800 }); storyText.anchor.set(0.5, 0.5); storyText.y = 170; storyText.wordWrap = true; storyText.wordWrapWidth = 600; storyText.maxWidth = 600; self.addChild(storyText); // Add Next button for navigation var nextButton = new Container(); var nextButtonBg = nextButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); nextButtonBg.width = 150; nextButtonBg.height = 60; nextButtonBg.tint = 0x00AA00; var nextButtonText = new Text2('Next', { size: 40, fill: 0xFFFFFF, weight: 800 }); nextButtonText.anchor.set(0.5, 0.5); nextButton.addChild(nextButtonText); nextButton.x = 0; nextButton.y = 280; self.addChild(nextButton); // Store reference to next button for external access self.nextButton = nextButton; // 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() { // Todas las líneas evolutivas principales completas var available = [ // Agumon line - COMPLETE { id: 'agumon', name: 'Agumon', description: 'Rookie Fire Dragon', reqDigivice: null }, { id: 'greymon', name: 'Greymon', description: 'Champion Fire Dragon', reqDigivice: 'digiviceC', reqBaseTower: 'agumon' }, { id: 'metalgreymon', name: 'MetalGreymon', description: 'Ultimate Cyborg Dragon', reqDigivice: 'digiviceB', reqBaseTower: 'agumon' }, { id: 'wargreymon', name: 'WarGreymon', description: 'Mega Dragon Warrior', reqDigivice: 'digiviceA', reqBaseTower: 'agumon' }, // Gabumon line - COMPLETE { id: 'gabumon', name: 'Gabumon', description: 'Rookie Ranged Wolf', reqDigivice: null }, { id: 'garurumon', name: 'Garurumon', description: 'Champion Ice Wolf', reqDigivice: 'digiviceC', reqBaseTower: 'gabumon' }, { id: 'weregarurumon', name: 'WereGarurumon', description: 'Ultimate Beast Warrior', reqDigivice: 'digiviceB', reqBaseTower: 'gabumon' }, { id: 'metalgarurumon', name: 'MetalGarurumon', description: 'Mega Ice Wolf', reqDigivice: 'digiviceA', reqBaseTower: 'gabumon' }, // Tentomon line - COMPLETE { id: 'tentomon', name: 'Tentomon', description: 'Rookie Electric Insect', reqDigivice: null }, { id: 'kabuterimon', name: 'Kabuterimon', description: 'Champion Electric Insect', reqDigivice: 'digiviceC', reqBaseTower: 'tentomon' }, { id: 'megakabuterimon', name: 'MegaKabuterimon', description: 'Ultimate Giant Insect', reqDigivice: 'digiviceB', reqBaseTower: 'tentomon' }, { id: 'herculeskabuterimon', name: 'HerculesKabuterimon', description: 'Mega Hercules Insect', reqDigivice: 'digiviceA', reqBaseTower: 'tentomon' }, // Palmon line - COMPLETE { id: 'palmon', name: 'Palmon', description: 'Rookie Plant Fighter', reqDigivice: null }, { id: 'togemon', name: 'Togemon', description: 'Champion Cactus', reqDigivice: 'digiviceC', reqBaseTower: 'palmon' }, { id: 'lillymon', name: 'Lillymon', description: 'Ultimate Flower Fairy', reqDigivice: 'digiviceB', reqBaseTower: 'palmon' }, { id: 'rosemon', name: 'Rosemon', description: 'Mega Rose Queen', reqDigivice: 'digiviceA', reqBaseTower: 'palmon' }, // Gomamon line - COMPLETE { id: 'gomamon', name: 'Gomamon', description: 'Rookie Aquatic Mammal', reqDigivice: null }, { id: 'ikkakumon', name: 'Ikkakumon', description: 'Champion Sea Beast', reqDigivice: 'digiviceC', reqBaseTower: 'gomamon' }, { id: 'zudomon', name: 'Zudomon', description: 'Ultimate Hammer Warrior', reqDigivice: 'digiviceB', reqBaseTower: 'gomamon' }, { id: 'vikemon', name: 'Vikemon', description: 'Mega Viking Beast', reqDigivice: 'digiviceA', reqBaseTower: 'gomamon' }, // Patamon line - COMPLETE { id: 'patamon', name: 'Patamon', description: 'Rookie Angel', reqDigivice: null }, { id: 'angemon', name: 'Angemon', description: 'Champion Angel', reqDigivice: 'digiviceC', reqBaseTower: 'patamon' }, { id: 'magnaangemon', name: 'MagnaAngemon', description: 'Ultimate Holy Angel', reqDigivice: 'digiviceB', reqBaseTower: 'patamon' }, { id: 'seraphimon', name: 'Seraphimon', description: 'Mega Seraph', reqDigivice: 'digiviceA', reqBaseTower: 'patamon' }]; return available; } function updateDigimonButtons() { // Clear existing buttons while (itemsContainer.children.length > 0) { itemsContainer.removeChild(itemsContainer.children[0]); } // Show ALL Digimon, but disable buttons if requirements not met var allDigimon = getAvailableDigimon(); // 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" }; allDigimon = allDigimon.filter(function (d) { return (digimonLevelMap[d.id] || "").toLowerCase() === filterLevel.toLowerCase(); }); } // If no Digimon to show, display message if (allDigimon.length === 0) { var noDigimonText = new Text2("No Digimon available for this level", { size: 40, fill: 0xFFFFFF, weight: 600 }); noDigimonText.anchor.set(0.5, 0.5); itemsContainer.addChild(noDigimonText); return; } // Layout: 3 per row, center horizontally var buttonsPerRow = Math.min(3, allDigimon.length); var buttonSpacing = 400; var startX = -((buttonsPerRow - 1) * buttonSpacing) / 2; for (var i = 0; i < allDigimon.length; i++) { var digimon = allDigimon[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 - Complete mapping for all evolutionary lines var digimonAssetMap = { // Agumon line - COMPLETE agumon: 'agumon', greymon: 'greymon', metalgreymon: 'metalgreymon', wargreymon: 'wargreymon', // Gabumon line - COMPLETE gabumon: 'gabumon', garurumon: 'garurumon', weregarurumon: 'weregarurumon', metalgarurumon: 'metalgarurumon', // Tentomon line - COMPLETE tentomon: 'tentomon', kabuterimon: 'kabuterimon', megakabuterimon: 'megakabuterimon', herculeskabuterimon: 'herculeskabuterimon', // Palmon line - COMPLETE palmon: 'palmon', togemon: 'togemon', lillymon: 'lillymon', rosemon: 'rosemon', // Gomamon line - COMPLETE gomamon: 'gomamon', ikkakumon: 'ikkakumon', zudomon: 'zudomon', vikemon: 'vikemon', // Patamon line - COMPLETE patamon: 'patamon', angemon: 'angemon', magnaangemon: 'magnaangemon', seraphimon: 'seraphimon' }; 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('', { size: 35, fill: 0xFFD700, weight: 800 }); costText.anchor.set(0.5, 0.5); costText.y = 30; digimonButton.addChild(costText); // Show reason if not available var reasonText = null; (function (digimonData, button, background, costText) { button.update = function () { var hasSpace = alliedUnits.length < maxAlliedUnits; // Calculate cooldown based on Digivice requirement var requiredCooldown = summonCooldown; // Default fallback if (digimonData.reqDigivice === 'digiviceC') { requiredCooldown = 50 * 60; // 50 seconds at 60 FPS } else if (digimonData.reqDigivice === 'digiviceB') { requiredCooldown = 100 * 60; // 1 minute 40 seconds at 60 FPS } else if (digimonData.reqDigivice === 'digiviceA') { requiredCooldown = 130 * 60; // 2 minutes 10 seconds at 60 FPS } var isAvailable = !summonCooldown || LK.ticks - lastSummonTime > requiredCooldown; var hasDigivice = !digimonData.reqDigivice || storage[digimonData.reqDigivice]; // Check for base tower requirement var hasBaseTower = true; if (digimonData.reqBaseTower) { hasBaseTower = false; for (var i = 0; i < towers.length; i++) { if (towers[i].id === digimonData.reqBaseTower) { hasBaseTower = true; break; } } } // Show disabled state and reason if not available if (!hasBaseTower) { background.tint = 0x333333; button.alpha = 0.5; if (!button._reasonText) { button._reasonText = new Text2("Requires " + digimonData.reqBaseTower.toUpperCase() + " tower in field", { size: 24, fill: 0xFF6600, weight: 800 }); button._reasonText.anchor.set(0.5, 0.5); button._reasonText.y = 60; button.addChild(button._reasonText); } costText.setText(''); // No cost shown } else if (!hasDigivice) { background.tint = 0x333333; button.alpha = 0.5; if (!button._reasonText) { button._reasonText = new Text2("Requires " + (digimonData.reqDigivice === "digiviceC" ? "Digivice C" : digimonData.reqDigivice === "digiviceB" ? "Digivice B" : "Digivice A"), { size: 24, fill: 0xFF6600, weight: 800 }); button._reasonText.anchor.set(0.5, 0.5); button._reasonText.y = 60; button.addChild(button._reasonText); } costText.setText(''); } else if (!hasSpace) { background.tint = 0x666666; button.alpha = 0.7; if (!button._reasonText) { button._reasonText = new Text2("Max allies reached", { size: 24, fill: 0xFF6600, weight: 800 }); button._reasonText.anchor.set(0.5, 0.5); button._reasonText.y = 60; button.addChild(button._reasonText); } costText.setText(''); } else if (!isAvailable) { background.tint = 0x666666; button.alpha = 0.7; // Calculate cooldown based on Digivice requirement var requiredCooldown = summonCooldown; // Default fallback if (digimonData.reqDigivice === 'digiviceC') { requiredCooldown = 50 * 60; // 50 seconds } else if (digimonData.reqDigivice === 'digiviceB') { requiredCooldown = 100 * 60; // 1 minute 40 seconds } else if (digimonData.reqDigivice === 'digiviceA') { requiredCooldown = 130 * 60; // 2 minutes 10 seconds } var remainingCooldown = Math.ceil((requiredCooldown - (LK.ticks - lastSummonTime)) / 60); if (!button._reasonText) { button._reasonText = new Text2("Cooldown: " + remainingCooldown + "s", { size: 24, fill: 0xFF6600, weight: 800 }); button._reasonText.anchor.set(0.5, 0.5); button._reasonText.y = 60; button.addChild(button._reasonText); } else { button._reasonText.setText("Cooldown: " + remainingCooldown + "s"); } costText.setText(''); } else { background.tint = 0x4444FF; button.alpha = 1.0; if (button._reasonText) { button.removeChild(button._reasonText); button._reasonText = null; } costText.setText('Ready!'); } }; button.down = function () { var hasSpace = alliedUnits.length < maxAlliedUnits; // Calculate cooldown based on Digivice requirement var requiredCooldown = summonCooldown; // Default fallback if (digimonData.reqDigivice === 'digiviceC') { requiredCooldown = 50 * 60; // 50 seconds at 60 FPS } else if (digimonData.reqDigivice === 'digiviceB') { requiredCooldown = 100 * 60; // 1 minute 40 seconds at 60 FPS } else if (digimonData.reqDigivice === 'digiviceA') { requiredCooldown = 130 * 60; // 2 minutes 10 seconds at 60 FPS } var isAvailable = !summonCooldown || LK.ticks - lastSummonTime > requiredCooldown; var hasDigivice = !digimonData.reqDigivice || storage[digimonData.reqDigivice]; // Check for base tower requirement var hasBaseTower = true; if (digimonData.reqBaseTower) { hasBaseTower = false; for (var i = 0; i < towers.length; i++) { if (towers[i].id === digimonData.reqBaseTower) { hasBaseTower = true; break; } } } if (!hasBaseTower) { var notification = game.addChild(new Notification("Requires " + digimonData.reqBaseTower.toUpperCase() + " tower in field to summon " + digimonData.name + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (!hasDigivice) { var notification = game.addChild(new Notification("Requires " + (digimonData.reqDigivice === "digiviceC" ? "Digivice C" : digimonData.reqDigivice === "digiviceB" ? "Digivice B" : "Digivice A") + " to summon " + 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) { // Calculate cooldown based on Digivice requirement var requiredCooldown = summonCooldown; // Default fallback if (digimonData.reqDigivice === 'digiviceC') { requiredCooldown = 50 * 60; // 50 seconds } else if (digimonData.reqDigivice === 'digiviceB') { requiredCooldown = 100 * 60; // 1 minute 40 seconds } else if (digimonData.reqDigivice === 'digiviceA') { requiredCooldown = 130 * 60; // 2 minutes 10 seconds } var remainingCooldown = Math.ceil((requiredCooldown - (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 (no cost, just cooldown) var newUnit = new DigimonUnit(digimonData.id, 1); // Find corresponding base tower to spawn from var spawnTower = null; if (digimonData.reqBaseTower) { for (var i = 0; i < towers.length; i++) { if (towers[i].id === digimonData.reqBaseTower) { spawnTower = towers[i]; break; } } } // Set spawn position - summoned Digimon spawn from exclusive allied spawn (spawntwo) y avanzan hacia la meta de aliados (goaltwo) if (grid.spawntwo && grid.spawntwo.length > 0) { var startSpawn = grid.spawntwo[Math.floor(Math.random() * grid.spawntwo.length)]; newUnit.cellX = startSpawn.x; newUnit.cellY = startSpawn.y; newUnit.currentCellX = startSpawn.x; newUnit.currentCellY = startSpawn.y; newUnit.x = grid.x + newUnit.currentCellX * CELL_SIZE; newUnit.y = grid.y + newUnit.currentCellY * CELL_SIZE; } 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 = 2.0; // Increased speed for better movement 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 = { // Agumon line agumon: "Agumon", greymon: "Greymon", metalgreymon: "MetalGreymon", wargreymon: "WarGreymon", // Gabumon line gabumon: "Gabumon", garurumon: "Garurumon", weregarurumon: "WereGarurumon", metalgarurumon: "MetalGarurumon", // Tentomon line tentomon: "Tentomon", kabuterimon: "Kabuterimon", megakabuterimon: "MegaKabuterimon", herculeskabuterimon: "HerculesKabuterimon", // Palmon line palmon: "Palmon", togemon: "Togemon", lillymon: "Lillymon", rosemon: "Rosemon", // Gomamon line gomamon: "Gomamon", ikkakumon: "Ikkakumon", zudomon: "Zudomon", vikemon: "Vikemon", // Patamon line patamon: "Patamon", angemon: "Angemon", magnaangemon: "MagnaAngemon", seraphimon: "Seraphimon" }; var digimonLevels = { agumon: "Rookie", greymon: "Champion", metalgreymon: "Ultimate", wargreymon: "Mega", gabumon: "Rookie", garurumon: "Champion", weregarurumon: "Ultimate", metalgarurumon: "Mega", tentomon: "Rookie", kabuterimon: "Champion", megakabuterimon: "Ultimate", herculeskabuterimon: "Mega", palmon: "Rookie", togemon: "Champion", lillymon: "Ultimate", rosemon: "Mega", gomamon: "Rookie", ikkakumon: "Champion", zudomon: "Ultimate", vikemon: "Mega", patamon: "Rookie", angemon: "Champion", magnaangemon: "Ultimate", seraphimon: "Mega" }; var digimonDescriptions = { agumon: "Rookie Fire Dragon", greymon: "Champion Fire Dragon", metalgreymon: "Ultimate Cyborg Dragon", wargreymon: "Mega Dragon Warrior", gabumon: "Rookie Ranged Wolf", garurumon: "Champion Ice Wolf", weregarurumon: "Ultimate Beast Warrior", metalgarurumon: "Mega Ice Wolf", tentomon: "Rookie Electric Insect", kabuterimon: "Champion Electric Insect", megakabuterimon: "Ultimate Giant Insect", herculeskabuterimon: "Mega Insect", palmon: "Rookie Plant Fighter", togemon: "Champion Cactus", lillymon: "Ultimate Flower Fairy", rosemon: "Mega Rose Queen", gomamon: "Rookie Aquatic Mammal", ikkakumon: "Champion Sea Beast", zudomon: "Ultimate Hammer Warrior", vikemon: "Mega Viking Beast", patamon: "Rookie Angel", angemon: "Champion Angel", magnaangemon: "Ultimate Holy Angel", seraphimon: "Mega Seraph" }; var digimonColor = { agumon: 0xFF8C00, greymon: 0xFF4500, metalgreymon: 0xFF6347, wargreymon: 0xFFD700, gabumon: 0xAAAAFF, garurumon: 0x0066FF, weregarurumon: 0x0066CC, metalgarurumon: 0x3399FF, tentomon: 0xFFAA00, kabuterimon: 0x33CC00, megakabuterimon: 0x228B22, herculeskabuterimon: 0xFFD700, palmon: 0x33CC00, togemon: 0x228B22, lillymon: 0xFFB6C1, rosemon: 0xFF69B4, gomamon: 0x00BFFF, ikkakumon: 0xB0E0E6, zudomon: 0x4682B4, vikemon: 0xAA291A, patamon: 0xFFD700, angemon: 0x87CEEB, magnaangemon: 0x14719f, seraphimon: 0x6ddca4 }; // Set properties based on Digimon type and level switch (self.type) { // Agumon line case 'agumon': self.attackType = 'melee'; self.damage = 15 + (self.level - 1) * 8; self.range = CELL_SIZE * 1.5; self.health = 100 + (self.level - 1) * 40; self.speed = 2.0; self.maxHealth = 130; // Fire rookie with decent health break; case 'greymon': self.attackType = 'ranged'; self.damage = 70; self.range = CELL_SIZE * 2.5; self.health = 150 + (self.level - 1) * 65; self.speed = 1.3; break; case 'metalgreymon': self.attackType = 'area'; self.damage = 120; self.range = CELL_SIZE * 2.8; self.health = 240 + (self.level - 1) * 100; self.speed = 1.4; break; case 'wargreymon': self.attackType = 'area'; self.damage = 180; self.range = CELL_SIZE * 4.5; self.health = 450 + (self.level - 1) * 170; self.speed = 1.3; break; // Gabumon line case 'gabumon': self.attackType = 'ranged'; self.damage = 16 + (self.level - 1) * 7; self.range = CELL_SIZE * 2.2; break; case 'garurumon': self.attackType = 'ranged'; self.damage = 70; self.range = CELL_SIZE * 2.7; self.health = 140 + (self.level - 1) * 60; self.speed = 1.2; break; case 'weregarurumon': self.attackType = 'ranged'; self.damage = 120; self.range = CELL_SIZE * 3.2; self.health = 220 + (self.level - 1) * 90; self.speed = 1.5; break; case 'metalgarurumon': self.attackType = 'ranged'; self.damage = 170; self.range = CELL_SIZE * 3.5; self.health = 400 + (self.level - 1) * 150; self.speed = 1.2; break; // Tentomon line case 'tentomon': self.attackType = 'ranged'; self.damage = 15 + (self.level - 1) * 7; self.range = CELL_SIZE * 2.0; break; case 'kabuterimon': self.attackType = 'area'; self.damage = 70; self.health = 140 + (self.level - 1) * 60; self.range = CELL_SIZE * 2.2; break; case 'megakabuterimon': self.attackType = 'area'; self.damage = 120; self.health = 230 + (self.level - 1) * 90; self.range = CELL_SIZE * 2.5; break; case 'herculeskabuterimon': self.attackType = 'area'; self.damage = 170; self.health = 400 + (self.level - 1) * 150; self.range = CELL_SIZE * 3.0; break; // Palmon line case 'palmon': self.attackType = 'melee'; self.damage = 14 + (self.level - 1) * 6; break; case 'togemon': self.attackType = 'area'; self.damage = 70; self.health = 120 + (self.level - 1) * 50; self.range = CELL_SIZE * 1.8; self.maxHealth = 120; // Champion cactus with good health break; case 'lillymon': self.attackType = 'area'; self.damage = 120; self.health = 200 + (self.level - 1) * 80; self.range = CELL_SIZE * 2.2; break; case 'rosemon': self.attackType = 'area'; self.damage = 170; self.health = 350 + (self.level - 1) * 120; self.range = CELL_SIZE * 2.7; break; // Gomamon line case 'gomamon': self.attackType = 'melee'; self.damage = 15 + (self.level - 1) * 7; break; case 'ikkakumon': self.attackType = 'area'; self.damage = 70; self.health = 130 + (self.level - 1) * 55; self.range = CELL_SIZE * 1.8; self.maxHealth = 130; // Champion sea beast with high health break; case 'zudomon': self.attackType = 'area'; self.damage = 120; self.health = 210 + (self.level - 1) * 90; self.range = CELL_SIZE * 2.3; break; case 'vikemon': self.attackType = 'area'; self.damage = 170; self.health = 400 + (self.level - 1) * 150; self.range = CELL_SIZE * 2.8; break; // Patamon line case 'patamon': self.attackType = 'ranged'; self.damage = 12 + (self.level - 1) * 6; self.range = CELL_SIZE * 2.0; break; case 'angemon': self.attackType = 'ranged'; self.damage = 70; self.range = CELL_SIZE * 2.5; self.health = 120 + (self.level - 1) * 50; self.maxHealth = 120; // Champion angel with balanced stats break; case 'magnaangemon': self.attackType = 'ranged'; self.damage = 120; self.range = CELL_SIZE * 3.0; self.health = 200 + (self.level - 1) * 80; break; case 'seraphimon': self.attackType = 'ranged'; self.damage = 170; self.range = CELL_SIZE * 3.5; self.health = 400 + (self.level - 1) * 150; break; } self.maxHealth = self.health; // Use correct asset for all allied units var digimonAssetMap = { // Agumon line agumon: 'agumon', greymon: 'greymon', metalgreymon: 'metalgreymon', wargreymon: 'wargreymon', // Gabumon line gabumon: 'gabumon', garurumon: 'garurumon', weregarurumon: 'weregarurumon', metalgarurumon: 'metalgarurumon', // Tentomon line tentomon: 'tentomon', kabuterimon: 'kabuterimon', megakabuterimon: 'megakabuterimon', herculeskabuterimon: 'herculeskabuterimon', // Palmon line palmon: 'palmon', togemon: 'togemon', lillymon: 'lillymon', rosemon: 'rosemon', // Gomamon line gomamon: 'gomamon', ikkakumon: 'ikkakumon', zudomon: 'zudomon', vikemon: 'vikemon', // Patamon line patamon: 'patamon', angemon: 'angemon', magnaangemon: 'magnaangemon', seraphimon: 'seraphimon' }; var assetId = digimonAssetMap[self.type] || 'agumon'; var unitGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Remove any tinting to keep original colors unitGraphics.tint = 0xFFFFFF; // Tint and scale based on type switch (self.type) { case 'greymon': unitGraphics.scaleX = 1.5; unitGraphics.scaleY = 1.5; break; case 'garurumon': unitGraphics.scaleX = 1.5; unitGraphics.scaleY = 1.5; break; case 'kabuterimon': unitGraphics.scaleX = 1.5; unitGraphics.scaleY = 1.5; break; case 'togemon': case 'ikkakumon': case 'angemon': unitGraphics.scaleX = 1.5; unitGraphics.scaleY = 1.5; break; case 'metalgreymon': unitGraphics.scaleX = 1.75; unitGraphics.scaleY = 1.75; break; case 'weregarurumon': unitGraphics.scaleX = 1.75; unitGraphics.scaleY = 1.75; break; case 'megakabuterimon': unitGraphics.scaleX = 1.75; unitGraphics.scaleY = 1.75; break; case 'lillymon': case 'zudomon': case 'magnaangemon': unitGraphics.scaleX = 1.75; unitGraphics.scaleY = 1.75; break; case 'wargreymon': unitGraphics.scaleX = 2.5; unitGraphics.scaleY = 2.5; break; case 'metalgarurumon': case 'herculeskabuterimon': case 'rosemon': case 'vikemon': case 'seraphimon': 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 exclusive allied spawn positions (spawntwo) if (grid.spawntwo && grid.spawntwo.length > 0) { var startSpawn = grid.spawntwo[Math.floor(Math.random() * grid.spawntwo.length)]; self.cellX = startSpawn.x; self.cellY = startSpawn.y; self.currentCellX = startSpawn.x; self.currentCellY = startSpawn.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; // All allied attacks are area attacks 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); }; 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; // --- Check if reached the end of the path (exclusive allied spawn cell) and award bits --- // Defensive: Only check if not already dead or being removed if (!self.isDead) { // Find current cell var currCellX = Math.round((self.x - grid.x) / CELL_SIZE); var currCellY = Math.round((self.y - grid.y) / CELL_SIZE); currCellX = Math.max(0, Math.min(grid.cells.length - 1, currCellX)); currCellY = Math.max(0, Math.min(grid.cells[0].length - 1, currCellY)); var currCell = grid.getCell(currCellX, currCellY); // Good practice: track last cell type to detect arrival at exclusive spawn if (self.lastCellType === undefined) self.lastCellType = currCell ? currCell.type : null; // Check if this cell is in goaltwo (exclusive allied goal) var isInGoalTwo = false; if (grid.goaltwo && grid.goaltwo.length > 0 && currCell) { for (var i = 0; i < grid.goaltwo.length; i++) { if (grid.goaltwo[i] === currCell) { isInGoalTwo = true; break; } } } if (isInGoalTwo && self.lastCellType !== 3) { // Award 10 bits setGold(gold + 10); // Show notification var rewardNote = game.addChild(new Notification("+10 bits! " + (digimonNames[self.type] || self.type) + " reached the goal!")); rewardNote.x = self.x; rewardNote.y = self.y - 60; // Remove from alliedUnits array and destroy var idx = alliedUnits.indexOf(self); if (idx !== -1) alliedUnits.splice(idx, 1); // Animate disappearance tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 400, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); // Prevent further updates self.isDead = true; return; } // Update lastCellType for next frame self.lastCellType = currCell ? currCell.type : null; } // 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; // --- Simple Enemy Following Logic --- // Find the closest enemy to follow var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If we found an enemy, move towards it if (closestEnemy) { var dx = closestEnemy.x - self.x; var dy = closestEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Only move if not too close (to avoid clustering) if (distance > self.range * 0.8) { var angle = Math.atan2(dy, dx); var moveSpeed = self.speed * gameSpeed; self.x += Math.cos(angle) * moveSpeed; self.y += Math.sin(angle) * moveSpeed; // Face the enemy if (self.children && self.children[0]) { self.children[0].rotation = angle; } } } else { // No enemies found, move towards the center of the map var centerX = grid.x + grid.cells.length * CELL_SIZE / 2; var centerY = grid.y + grid.cells[0].length * CELL_SIZE / 2; var dx = centerX - self.x; var dy = centerY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > CELL_SIZE) { var angle = Math.atan2(dy, dx); var moveSpeed = self.speed * gameSpeed * 0.5; // Slower when no enemies self.x += Math.cos(angle) * moveSpeed; self.y += Math.sin(angle) * moveSpeed; // Face movement direction if (self.children && self.children[0]) { self.children[0].rotation = angle; } } } // Keep units within reasonable bounds var minX = grid.x + CELL_SIZE; var maxX = grid.x + 23 * CELL_SIZE; var minY = grid.y + CELL_SIZE; var maxY = grid.y + 34 * CELL_SIZE; self.x = Math.max(minX, Math.min(maxX, self.x)); self.y = Math.max(minY, Math.min(maxY, self.y)); }; 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; } // --- Boss Attack Patterns --- // Only apply to boss enemies if (self.isBoss) { // World 1 Boss: Snorlax (sleep nearby towers) if (self.worldNumber === 1) { // Snorlax sleeps towers within 2.5 cells every 6 seconds if (!self._snorlaxSleepTimer) self._snorlaxSleepTimer = 0; self._snorlaxSleepTimer++; if (self._snorlaxSleepTimer > 360) { // 6 seconds at 60 FPS self._snorlaxSleepTimer = 0; // Find all towers within 2.5 cells for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.x - self.x; var dy = tower.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist <= CELL_SIZE * 2.5) { // Put tower to sleep for 4 seconds (240 ticks) if (!tower._sleepTimer || tower._sleepTimer <= 0) { tower._sleepTimer = 240; // Visual feedback: flash blue LK.effects.flashObject(tower, 0x3399FF, 800); // Show notification var note = game.addChild(new Notification("Snorlax put a tower to sleep!")); note.x = tower.x; note.y = tower.y - 100; } } } } } // World 2 Boss: Rhydon (stuns towers in a line every 7s) if (self.worldNumber === 2) { if (!self._rhydonStunTimer) self._rhydonStunTimer = 0; self._rhydonStunTimer++; if (self._rhydonStunTimer > 420) { // 7 seconds self._rhydonStunTimer = 0; // Stun towers in a horizontal line (same y, +/- 1 cell) for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dy = Math.abs(tower.y - self.y); if (dy <= CELL_SIZE * 1.2) { if (!tower._stunTimer || tower._stunTimer <= 0) { tower._stunTimer = 120; // 2 seconds LK.effects.flashObject(tower, 0xAAAAAA, 600); var note = game.addChild(new Notification("Rhydon stunned a tower!")); note.x = tower.x; note.y = tower.y - 100; } } } } } // World 3 Boss: Articuno (freezes all towers every 10s) if (self.worldNumber === 3) { if (!self._articunoFreezeTimer) self._articunoFreezeTimer = 0; self._articunoFreezeTimer++; if (self._articunoFreezeTimer > 600) { // 10 seconds self._articunoFreezeTimer = 0; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (!tower._freezeTimer || tower._freezeTimer <= 0) { tower._freezeTimer = 90; // 1.5 seconds LK.effects.flashObject(tower, 0x66CCFF, 500); var note = game.addChild(new Notification("Articuno froze a tower!")); note.x = tower.x; note.y = tower.y - 100; } } } } // World 4 Boss: Machamp (smashes nearest tower every 8s, heavy damage) if (self.worldNumber === 4) { if (!self._machampSmashTimer) self._machampSmashTimer = 0; self._machampSmashTimer++; if (self._machampSmashTimer > 480) { // 8 seconds self._machampSmashTimer = 0; // Find nearest tower var minDist = Infinity, nearest = null; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.x - self.x; var dy = tower.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist) { minDist = dist; nearest = tower; } } if (nearest) { nearest.health = Math.max(0, nearest.health - 40); nearest.towerHealthBar.width = nearest.health / nearest.maxHealth * 76; LK.effects.flashObject(nearest, 0xFF0000, 600); var note = game.addChild(new Notification("Machamp SMASH! -40 HP")); note.x = nearest.x; note.y = nearest.y - 100; } } } // World 5 Boss: Groudon (Tech Lab - high-tech cyber attacks every 8s) if (self.worldNumber === 5) { if (!self._groudonCyberTimer) self._groudonCyberTimer = 0; self._groudonCyberTimer++; if (self._groudonCyberTimer > 480) { // 8 seconds self._groudonCyberTimer = 0; // Cyber attack: overload 3 random towers with system errors var availableTowers = towers.slice(); var targetsToOverload = Math.min(3, availableTowers.length); for (var t = 0; t < targetsToOverload; t++) { if (availableTowers.length === 0) break; var randomIndex = Math.floor(Math.random() * availableTowers.length); var tower = availableTowers[randomIndex]; availableTowers.splice(randomIndex, 1); if (!tower._overloadTimer || tower._overloadTimer <= 0) { tower._overloadTimer = 150; // 2.5 seconds disabled LK.effects.flashObject(tower, 0x00FFFF, 800); var note = game.addChild(new Notification("Groudon's volcanic eruption disabled a tower!")); note.x = tower.x; note.y = tower.y - 100; } } } } // World 6 Boss: Mewtwo (Inferno - FINAL BOSS with escalating psychic attacks) if (self.worldNumber === 6) { if (!self._mewtwoPhaseTimer) self._mewtwoPhaseTimer = 0; if (!self._mewtwoPhase) self._mewtwoPhase = 1; self._mewtwoPhaseTimer++; // Phase-based attacks based on remaining health var healthPercent = self.health / self.maxHealth; if (healthPercent > 0.66) { self._mewtwoPhase = 1; } else if (healthPercent > 0.33) { self._mewtwoPhase = 2; } else { self._mewtwoPhase = 3; } // Phase 1: Mind control towers (every 6s) if (self._mewtwoPhase === 1 && self._mewtwoPhaseTimer > 360) { self._mewtwoPhaseTimer = 0; if (towers.length > 0) { var idx = Math.floor(Math.random() * towers.length); var tower = towers[idx]; if (!tower._mindControlTimer || tower._mindControlTimer <= 0) { tower._mindControlTimer = 120; // 2 seconds mind controlled LK.effects.flashObject(tower, 0xFF00FF, 600); var note = game.addChild(new Notification("Mewtwo mind-controlled a tower!")); note.x = tower.x; note.y = tower.y - 100; } } // Phase 2: Psychic barrier reflects bullets (every 8s) } else if (self._mewtwoPhase === 2 && self._mewtwoPhaseTimer > 480) { self._mewtwoPhaseTimer = 0; self._psychicBarrier = 180; // 3 seconds of bullet reflection LK.effects.flashObject(self, 0xFFFFFF, 1000); var note = game.addChild(new Notification("Mewtwo activated psychic barrier!")); note.x = 2048 / 2; note.y = grid.height - 100; // Phase 3: Desperation - multiple random attacks (every 4s) } else if (self._mewtwoPhase === 3 && self._mewtwoPhaseTimer > 240) { self._mewtwoPhaseTimer = 0; // Random between mind control, barrier, and mass disable var attackType = Math.floor(Math.random() * 3); if (attackType === 0) { // Mass mind control for (var i = 0; i < Math.min(2, towers.length); i++) { var tower = towers[Math.floor(Math.random() * towers.length)]; if (!tower._mindControlTimer || tower._mindControlTimer <= 0) { tower._mindControlTimer = 90; LK.effects.flashObject(tower, 0xFF00FF, 400); } } var note = game.addChild(new Notification("Mewtwo's desperation attack!")); } else if (attackType === 1) { // Psychic barrier self._psychicBarrier = 120; LK.effects.flashObject(self, 0xFFFFFF, 600); var note = game.addChild(new Notification("Mewtwo's psychic shield!")); } else { // Mass disable for (var i = 0; i < Math.min(3, towers.length); i++) { var tower = towers[Math.floor(Math.random() * towers.length)]; if (!tower._disableTimer || tower._disableTimer <= 0) { tower._disableTimer = 90; LK.effects.flashObject(tower, 0x666666, 400); } } var note = game.addChild(new Notification("Mewtwo disabled multiple towers!")); } note.x = 2048 / 2; note.y = grid.height - 100; } } } // --- Tower status effect handling (sleep, stun, freeze, burn, disable) --- // This is global for all towers, but we add it here for boss effects for (var i = 0; i < towers.length; i++) { var t = towers[i]; // Sleep disables fire/update if (t._sleepTimer && t._sleepTimer > 0) { t._sleepTimer--; t.alpha = 0.5; t.update = function () {}; // No-op if (t._sleepTimer === 0) { t.alpha = 1; delete t.update; } continue; } // Stun disables fire/update if (t._stunTimer && t._stunTimer > 0) { t._stunTimer--; t.alpha = 0.7; t.update = function () {}; if (t._stunTimer === 0) { t.alpha = 1; delete t.update; } continue; } // Freeze disables fire/update if (t._freezeTimer && t._freezeTimer > 0) { t._freezeTimer--; t.alpha = 0.7; t.update = function () {}; if (t._freezeTimer === 0) { t.alpha = 1; delete t.update; } continue; } // Burn: take damage over time if (t._burnTimer && t._burnTimer > 0) { t._burnTimer--; if (LK.ticks % 30 === 0) { t.health = Math.max(0, t.health - 3); t.towerHealthBar.width = t.health / t.maxHealth * 76; LK.effects.flashObject(t, 0xFF4400, 200); } if (t._burnTimer === 0) { t.alpha = 1; } } // Disable: disables fire/update if (t._disableTimer && t._disableTimer > 0) { t._disableTimer--; t.alpha = 0.3; t.update = function () {}; if (t._disableTimer === 0) { t.alpha = 1; delete t.update; } continue; } } 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 AND ALLIED UNIT LOGIC --- // Enemies will attack towers and allied units if they are adjacent or on the same cell if (!self.isFlying && !self.isBoss) { self.attackTowerCooldown--; if (self.attackTowerCooldown <= 0) { // First check for allied units to attack (priority target) var foundAlly = null; for (var i = 0; i < alliedUnits.length; i++) { var ally = alliedUnits[i]; if (ally.isDead || !ally.parent) continue; // Check if ally is in melee range (adjacent cells) var dx = Math.abs(ally.currentCellX - self.cellX); var dy = Math.abs(ally.currentCellY - self.cellY); if (dx <= 1.5 && dy <= 1.5) { foundAlly = ally; break; } } if (foundAlly) { // Deal damage to the allied unit var baseDamage = 4 + (self.worldNumber - 1) * 20; // Base damage increases by 20 per world // Increase damage with world and wave var worldMultiplier = 1 + (self.worldNumber - 1) * 0.4; var waveMultiplier = 1 + currentWave * 0.05; var totalDamage = Math.ceil(baseDamage * worldMultiplier * waveMultiplier); // Check if the allied unit is directly in front of the enemy (within 0.5 cell in the direction the enemy is facing) var facingBonus = 1; if (foundAlly && self.children && self.children[0]) { var enemyAngle = self.children[0].rotation; // Calculate vector from enemy to ally var dx = foundAlly.x - self.x; var dy = foundAlly.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { // Normalize direction var dirX = Math.cos(enemyAngle); var dirY = Math.sin(enemyAngle); // Project vector to ally onto facing direction var dot = (dx * dirX + dy * dirY) / dist; // If dot > 0.7 (roughly within 45 degrees in front), and distance is close (within 1 cell) if (dot > 0.7 && dist < CELL_SIZE * 1.2) { facingBonus = 2; // Double damage if in front } } } totalDamage *= facingBonus; foundAlly.health = Math.max(0, foundAlly.health - totalDamage); // Play enemy hit sound LK.getSound('enemyHit').play(); // Show damage indicator on ally var damageIndicator = new Notification("-" + totalDamage + " HP!"); damageIndicator.x = foundAlly.x; damageIndicator.y = foundAlly.y - 60; if (game && game.addChild) game.addChild(damageIndicator); // Visual feedback - flash ally red when hit LK.effects.flashObject(foundAlly, 0xFF0000, 300); self.attackTowerCooldown = Math.max(25, self.attackTowerRate - Math.floor(currentWave * 0.5)); } else { // No allies nearby, 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 + (self.worldNumber - 1) * 20; // Base damage increases by 20 per world // Increase damage with world and wave var worldMultiplier = 1 + (self.worldNumber - 1) * 0.4; var waveMultiplier = 1 + currentWave * 0.05; 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 = []; // Add exclusive spawn and goal arrays for allies self.spawntwo = []; self.goaltwo = []; 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 = []; // Also clear exclusive allied spawn/goal arrays self.spawntwo = []; self.goaltwo = []; // 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]); // Invertido: ahora los goals de aliados están en la entrada self.goaltwo.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]); // Invertido: ahora los spawns de aliados están en la salida self.spawntwo.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", "Inferno", "Tech Lab"]; 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.3', { size: 40, fill: 0xCCCCCC, weight: 400 }); versionText.anchor.set(0.5, 0.5); versionText.y = -220; self.addChild(versionText); // Add Beta text with Minecraft-style effects var betaText = new Text2('BETA', { size: 80, fill: 0xFFFF00, weight: 800 }); betaText.anchor.set(0.5, 0.5); betaText.x = 400; betaText.y = -200; betaText.rotation = -Math.PI / 12; // Tilt the text (15 degrees) betaText.alpha = 0.8; self.addChild(betaText); // Add floating animation similar to Minecraft var _betaFloatAnimation = function betaFloatAnimation() { tween(betaText, { y: betaText.y - 15, rotation: betaText.rotation + 0.05 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { tween(betaText, { y: betaText.y + 15, rotation: betaText.rotation - 0.05 }, { duration: 2000, easing: tween.easeInOut, onFinish: _betaFloatAnimation }); } }); }; _betaFloatAnimation(); // Add glow effect to beta text var _betaGlowAnimation = function betaGlowAnimation() { tween(betaText, { alpha: 1.0, scaleX: 1.1, scaleY: 1.1 }, { duration: 1500, easing: tween.easeInOut, onFinish: function onFinish() { tween(betaText, { alpha: 0.8, scaleX: 1.0, scaleY: 1.0 }, { duration: 1500, easing: tween.easeInOut, onFinish: _betaGlowAnimation }); } }); }; _betaGlowAnimation(); 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 = 80; 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 = 340; self.addChild(leaderboardButton); leaderboardButton.down = function () { // Defensive: try/catch in case leaderboard is not available try { if (typeof LK.showLeaderboard === "function") { LK.showLeaderboard(); } else if (typeof LK.showLeaderBoard === "function") { LK.showLeaderBoard(); } else { var notification = game.addChild(new Notification("Leaderboard not available!")); notification.x = 2048 / 2; notification.y = 2732 / 2 + 200; } } catch (e) { var notification = game.addChild(new Notification("Leaderboard not available!")); notification.x = 2048 / 2; notification.y = 2732 / 2 + 200; } }; // 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 = 210; 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, all worlds) 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; var showForWorld2Plus = waveIndicator && waveIndicator.gameStarted && worldWave < 10 && !waveInProgress && enemies.length === 0; if (showForTutorial || showForNextWave || showForWorld2Plus) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; // Update button text based on whether this is the first wave start if (isFirstWaveStart) { buttonText.setText("Start Wave"); } else { buttonText.setText("Next Wave"); } } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && !waveInProgress && enemies.length === 0) { // Handle first wave start for selected levels if (isFirstWaveStart) { // Don't increment currentWave, just clear the flag isFirstWaveStart = false; } else { // Normal wave progression - increment currentWave if (currentWave > 0 && (currentWave - 1) % 10 + 1 < 10) { currentWave++; // Increment to the next wave } else if (currentWave === 0) { currentWave = 1; // Start at wave 1 if we're at wave 0 (tutorial/fresh start) } } // 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); // Stretch notification background to fit text with extra padding var minWidth = 600; // Increased minimum width for better visibility var calculatedWidth = Math.max(minWidth, notificationText.width + 150); notificationGraphics.width = calculatedWidth; notificationGraphics.height = 140; // Increased height for better visibility self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 180; // Increased display time for better readability // Collision avoidance system self.checkCollisions = function () { if (!self.parent) return; var notifications = []; for (var i = 0; i < self.parent.children.length; i++) { var child = self.parent.children[i]; if (child instanceof Notification && child !== self && child.alpha > 0) { notifications.push(child); } } // Sort notifications by creation order (older first) notifications.sort(function (a, b) { return (a._creationTime || 0) - (b._creationTime || 0); }); // Check for overlaps and adjust position for (var i = 0; i < notifications.length; i++) { var other = notifications[i]; var dx = Math.abs(self.x - other.x); var dy = Math.abs(self.y - other.y); var minDistance = 140; // Minimum vertical distance between notifications if (dx < calculatedWidth * 0.7 && dy < minDistance) { // Move this notification up to avoid overlap var offset = minDistance - dy + 10; self.y -= offset; // Ensure notification stays within screen bounds if (self.y < 100) { self.y = other.y + minDistance; } } } }; // Store creation time for sorting self._creationTime = LK.ticks || Date.now(); // Initial collision check LK.setTimeout(function () { self.checkCollisions(); }, 1); self.update = function () { // Periodic collision checking during early life if (fadeOutTime > 150) { self.checkCollisions(); } if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 180 * 2, 1); } else { self.destroy(); } }; // Add smooth entrance animation with tween self.scaleX = 0.3; self.scaleY = 0.3; tween(self, { scaleX: 1.0, scaleY: 1.0 }, { duration: 200, easing: tween.backOut }); 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); // Only set up auto-advance for panels after the first one var autoAdvanceTimer = null; self.down = function () { if (autoAdvanceTimer) { LK.clearTimeout(autoAdvanceTimer); } 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: 'infernoBg', path: 'infernoPath', wall: 'infernoWall', scenery: 'infernoScenery', ambient: 0xff6347 }; case 6: return { background: 'techLabBg', path: 'techLabPath', wall: 'techLabWall', scenery: 'techLabScenery', ambient: 0x87ceeb }; 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", "Inferno", "Tech Lab"]; 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 ****/ // Re-inicializar todos los sonidos y música para asegurar que el audio funcione correctamente // This ensures the browser only requests microphone access, not camera. // Only use microphone features from facekit. Do NOT call any camera-related methods. // NOTE: We only use facekit for microphone/voice features (facekit.volume). // We never call any camera or face detection methods, so the browser will only request microphone access. // Voice & Summoning // Boss & Special Events // UI & Feedback // Special Effects // Combat & Action Sounds // Music system for world progression // Notify user about microphone usage for voice commands // Digimon Evolution Assets for Voice Summoning // Agumon Evolution Line (Fire/Dragon) // Gabumon Evolution Line (Ice/Wolf) // Tentomon Evolution Line (Electric/Insect) // Palmon Evolution Line (Plant/Poison) // Gomamon Evolution Line (Water) // Patamon Evolution Line (Sacred/Angel) // Try to import facekit but make it optional to prevent loading issues 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 = 'infernoMusic'; break; case 6: musicId = 'techLabMusic'; 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 Defenders', 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 Cortafuegos', 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 "Siguiente Oleada" para enviar enemigos. Las torretas atacarán automáticamente.', tutorialStep3: 'Paso 3: Mejorar torretas\n\nHaz clic en una torreta y presiona "Mejorar" para aumentar su poder. ¡Cierra el menú al terminar!', tutorialCompleted: '¡Tutorial completado!\n\nAhora sabes:\n• Colocar torretas\n• Iniciar oleadas\n• Mejorar torretas\n• Gritar nombres Digimon para invocar aliados\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\nlos servidores del Bosque Digital!\n¡Intentan robar archivos de datos Digimon!", story1_2: "¡Estos espías Pokémon usan protocolos avanzados\nde sigilo para acceder a nuestra base de datos central!\n¡Despliega guardianes Digimon de inmediato!", story1_3: "¡El futuro de la franquicia Digimon depende de ti!\n¡Detén a Pokémon antes de que corrompan\nnuestro ecosistema digital!", story2_1: "¡Agentes Pokémon han infiltrado el Centro de Datos del Desierto!\n¡Están plantando código malicioso\npara corromper nuestros sistemas!", story2_2: "¡Pokémon de tipo fuego están sobrecalentando nuestros servidores\nmientras otros roban valiosos datos\nde evolución Digimon!", story2_3: "¡Su ataque coordinado es más sofisticado que antes!\n¡Pokémon quiere monopolizar la industria\ndel entretenimiento infantil!", story3_1: "¡Pokémon de tipo hielo han congelado nuestros servidores Glaciar\npara ralentizar nuestras defensas\nmientras extraen datos!", story3_2: "¡Pokémon voladores están evadiendo nuestros muros de seguridad!\n¡Intentan llegar a la base genética central\nde Digimon!", story3_3: "¡Temperaturas críticas detectadas!\n¡Pokémon intenta provocar un colapso\ntotal del servidor!", story4_1: "¡Agentes durmientes Pokémon ocultos en la Red de la Aldea se han activado!\n¡Han estado recolectando inteligencia\ndurante meses!", story4_2: "¡Múltiples equipos de ataque Pokémon atacan simultáneamente,\nintentando sobrepasar a nuestros defensores Digimon!", story4_3: "¡Esto es espionaje corporativo a gran escala!\n¡La Compañía Pokémon quiere robar nuestra tecnología\nde criaturas digitales!", story5_1: "¡NIVEL DE AMENAZA MÁXIMO! ¡Hackers Pokémon de élite han violado\nnuestros Laboratorios de Tecnología más seguros!", story5_2: "¡Usan habilidades legendarias Pokémon para evadir nuestro cifrado cuántico!\n¡Nuestros datos más sensibles están en riesgo!", story5_3: "¡Despliega nuestros Digimon Mega más fuertes!\n¡Solo ellos pueden detener esta guerra cibernética corporativa!", story6_1: "¡ASALTO FINAL! ¡El plan maestro de Pokémon se revela:\nquieren borrar TODOS los datos Digimon permanentemente!", story6_2: "¡Pokémon legendarios lideran este ataque final a nuestros servidores centrales!\n¡Es la batalla definitiva por la supremacía!", story6_3: "¡El corazón de los niños está en juego!\n¡Derrota la invasión Pokémon y salva el futuro\nde 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; // Ensure pathId is incremented on every pathFind to keep pathfinding in sync var enemies = []; var towers = []; var bullets = []; var defenses = []; var alliedUnits = []; // Array to track summoned allied Digimon units var selectedTower = null; var gold = 80; // Base starting gold, will be updated based on world selection 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); // Contenedor para la puntuación de seguridad, con texto arriba y número abajo var scoreContainer = new Container(); var scoreLabel = new Text2(getText('securityScore'), { size: currentLanguage === 'es' ? 40 : 50, fill: 0xFF0000, weight: 800 }); scoreLabel.anchor.set(0.5, 1.0); scoreLabel.y = 0; var scoreValue = new Text2(score.toString(), { size: 60, fill: 0xFF0000, weight: 800 }); scoreValue.anchor.set(0.5, 0); scoreValue.y = 8; scoreContainer.addChild(scoreLabel); scoreContainer.addChild(scoreValue); 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 } }; // --- Add semi-transparent black background to bits and security score UI --- // Bits background var goldBg = new Container(); var goldBgRect = goldBg.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); goldBgRect.width = 320; goldBgRect.height = 90; goldBgRect.tint = 0x000000; goldBgRect.alpha = 0.5; goldBg.addChild(goldText); goldBg.x = -spacing; goldBg.y = topMargin; LK.gui.top.addChild(goldBg); // Security score background var scoreBg = new Container(); var scoreBgRect = scoreBg.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); scoreBgRect.width = 340; scoreBgRect.height = 110; scoreBgRect.tint = 0x000000; scoreBgRect.alpha = 0.5; scoreBg.addChild(scoreContainer); scoreBg.x = spacing; scoreBg.y = topMargin; LK.gui.top.addChild(scoreBg); LK.gui.bottom.addChild(healthBarContainer); LK.gui.top.addChild(speedButton); healthBarContainer.x = 0; healthBarContainer.y = -300; speedButton.x = 0; speedButton.y = topMargin; function updateUI() { goldText.setText(getText('bits') + ': ' + gold); // Actualiza el valor de la puntuación de seguridad if (scoreContainer && scoreContainer.children && scoreContainer.children.length > 1) { scoreContainer.children[1].setText(score.toString()); } // 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(); // --- Next Wave Countdown and Early Reward System --- var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); // --- 20s Countdown and Early Reward System --- var nextWaveCountdownActive = false; var nextWaveCountdownTimer = 0; var nextWaveCountdownMax = 1200; // 20 seconds at 60 FPS var nextWaveCountdownText = null; var nextWaveEarlyReward = 0; // Bits to reward for early start (calculated dynamically) // Create countdown text (hidden by default) nextWaveCountdownText = new Text2('', { size: 48, fill: 0x00FFD0, stroke: 0xFFCCCC, strokeThickness: 4, weight: 800 }); nextWaveCountdownText.anchor.set(0, 0.5); // Position countdown text to the left of the Next Wave button, vertically centered nextWaveCountdownText.x = -nextWaveButton.width / 2 - 120; nextWaveCountdownText.y = 0; nextWaveCountdownText.visible = false; nextWaveButtonContainer.addChild(nextWaveCountdownText); // Helper to start the countdown function startNextWaveCountdown() { nextWaveCountdownActive = true; nextWaveCountdownTimer = nextWaveCountdownMax; nextWaveCountdownText.visible = true; nextWaveCountdownText.setText('20'); nextWaveEarlyReward = 0; } // Helper to stop the countdown function stopNextWaveCountdown() { nextWaveCountdownActive = false; nextWaveCountdownTimer = 0; nextWaveCountdownText.visible = false; } // Patch NextWaveButton.down to handle countdown and early reward var originalNextWaveButtonDown = nextWaveButton.down; nextWaveButton.down = function () { if (!nextWaveButton.enabled) return; // If countdown is active, reward bits for early start if (nextWaveCountdownActive && nextWaveCountdownTimer > 0) { // Reward is proportional to time left: 1 bit per second left (minimum 1) var secondsLeft = Math.ceil(nextWaveCountdownTimer / 60); var reward = Math.max(1, secondsLeft); setGold(gold + reward); var rewardNote = game.addChild(new Notification("¡Oleada iniciada antes! +" + reward + " bits")); rewardNote.x = 2048 / 2; rewardNote.y = grid.height - 100; stopNextWaveCountdown(); } originalNextWaveButtonDown.apply(this, arguments); }; // Patch game.update to handle countdown logic var originalGameUpdate = game.update; game.update = function () { // Show countdown only when button is visible and enabled, and not in progress if (nextWaveButton.visible && nextWaveButton.enabled && !waveInProgress && enemies.length === 0 && !nextWaveCountdownActive) { startNextWaveCountdown(); } // Update countdown if active if (nextWaveCountdownActive) { if (nextWaveCountdownTimer > 0) { nextWaveCountdownTimer--; var secondsLeft = Math.ceil(nextWaveCountdownTimer / 60); nextWaveCountdownText.setText(secondsLeft.toString()); // If timer runs out, auto-start next wave (no reward) if (nextWaveCountdownTimer === 0) { stopNextWaveCountdown(); if (nextWaveButton.enabled) { originalNextWaveButtonDown.apply(nextWaveButton, []); } } } } else { nextWaveCountdownText.visible = false; } // Call original update originalGameUpdate.apply(this, arguments); }; // 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); // Force pathId update for all allies for (var i = 0; i < alliedUnits.length; i++) { alliedUnits[i].currentTarget = null; } // Change music when entering new world playWorldMusic(currentWorld); // World-specific notification messages var worldNames = ["", "Forest", "Desert", "Glacier", "Village", "Inferno", "Tech Lab"]; 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; // Set gold based on world number: 80 for world 1, 100 for world 2, 120 for world 3, etc. (scaled, not additive) gold = 60 + worldNumber * 20; updateUI(); 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); // Reset currentTarget for all allies to force path recalculation for (var i = 0; i < alliedUnits.length; i++) { alliedUnits[i].currentTarget = null; } // 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; // Start at the first incomplete wave for this world, or at the first wave if all are complete var worldLevels = storage.worldLevels || { 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1 }; var startLevel = worldLevels[currentWorld] || 1; currentWave = (currentWorld - 1) * 10 + startLevel; waveTimer = nextWaveTime; }; }; game.startWorldLevel = function (worldNumber, levelNumber) { currentWorld = worldNumber; // Set gold based on world number: 80 for world 1, 100 for world 2, 120 for world 3, etc. (scaled, not additive) gold = 60 + worldNumber * 20; updateUI(); // Always use the provided levelNumber when it's given if (levelNumber) { currentLevel = levelNumber; } else { var worldLevels = storage.worldLevels || { 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1 }; currentLevel = worldLevels[worldNumber] || 1; } // Calculate currentWave based on selected level, not progress currentWave = (worldNumber - 1) * 10 + currentLevel; grid.generateMazeForWorld(currentWorld); mapVisualization.generateMazeForWorld(currentWorld); grid.pathFind(); grid.renderDebug(); // Sincronizar mapas syncVisualizationMap(); worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); // Reset currentTarget for all allies to force path recalculation for (var i = 0; i < alliedUnits.length; i++) { alliedUnits[i].currentTarget = null; } // 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 flag to indicate this is the first wave start for a selected level isFirstWaveStart = 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; var isFirstWaveStart = false; // Flag to track if this is the first wave start for selected level // 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 = 60 + currentWorld * 20; // Tutorial uses world-based gold: 80, 100, 120, etc. (scaled amount) 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); // Reset currentTarget for all allies to force path recalculation for (var i = 0; i < alliedUnits.length; i++) { alliedUnits[i].currentTarget = null; } // 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; }; };
===================================================================
--- original.js
+++ change.js
@@ -905,36 +905,36 @@
var digimonButton = new Container();
digimonButton.x = startX + col * buttonSpacing;
digimonButton.y = row * 180;
itemsContainer.addChild(digimonButton);
- // Use correct asset for Digimon
+ // Use correct asset for Digimon - Complete mapping for all evolutionary lines
var digimonAssetMap = {
- // Agumon line
+ // Agumon line - COMPLETE
agumon: 'agumon',
greymon: 'greymon',
metalgreymon: 'metalgreymon',
wargreymon: 'wargreymon',
- // Gabumon line
+ // Gabumon line - COMPLETE
gabumon: 'gabumon',
garurumon: 'garurumon',
weregarurumon: 'weregarurumon',
metalgarurumon: 'metalgarurumon',
- // Tentomon line
+ // Tentomon line - COMPLETE
tentomon: 'tentomon',
kabuterimon: 'kabuterimon',
megakabuterimon: 'megakabuterimon',
herculeskabuterimon: 'herculeskabuterimon',
- // Palmon line
+ // Palmon line - COMPLETE
palmon: 'palmon',
togemon: 'togemon',
lillymon: 'lillymon',
rosemon: 'rosemon',
- // Gomamon line
+ // Gomamon line - COMPLETE
gomamon: 'gomamon',
ikkakumon: 'ikkakumon',
zudomon: 'zudomon',
vikemon: 'vikemon',
- // Patamon line
+ // Patamon line - COMPLETE
patamon: 'patamon',
angemon: 'angemon',
magnaangemon: 'magnaangemon',
seraphimon: 'seraphimon'
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
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
boton con contorno mecanico. 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
Sin fondo
hada digital. In-Game asset. 2d. High contrast. No shadows
MagnaAngemon, visto desde arriba, calidad y detalles altamente épicos.. In-Game asset. 2d. High contrast. No shadows
Seraphimon, visto desde arriba, calidad y detalles altamente épicos, Digimon. In-Game asset. 2d. High contrast. No shadows
Vikemon, visto desde arriba, calidad y detalles altamente épicos, Digimon. In-Game asset. 2d. High contrast. No shadows
Hielo oscuro, ocupar toda la imagen, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Hielo, ocupar toda la imagen, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Tierras con piedras, ocupar toda la imagen, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Aldeas, ocupar toda la imagen, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Alfombra roja, ocupar toda la imagen, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Metal con pequeños componentes de electrónica, ocupar toda la imagen, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Magma oscura, ocupar toda la imagen, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Rocas volcanicas, ocupar toda la imagen, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
Arena del desierto, ocupar toda la imagen, visto desde arriba. In-Game asset. 2d. High contrast. No shadows
UlforceVeedramon, visto desde arriba. 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
bossDeath
Sound effect