/**** * 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" }); /**** * 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); // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Apply slow effect // Make slow percentage scale with tower level (default 50%, up to 80% at max level) var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6 var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; // Slow by X% self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { self.targetEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'poison') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual poison effect var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Apply poison effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var Coin = Container.expand(function (x, y, value) { var self = Container.call(this); self.value = value || 5; self.x = x; self.y = y; self.collected = false; self.walkSpeed = 0.5; self.direction = Math.random() * Math.PI * 2; self.changeDirectionTimer = 0; var coinGraphics = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); coinGraphics.width = 30; coinGraphics.height = 30; coinGraphics.tint = 0xFFD700; // Add coin value text var valueText = new Text2(self.value.toString(), { size: 20, fill: 0x000000, weight: 800 }); valueText.anchor.set(0.5, 0.5); self.addChild(valueText); self.update = function () { if (self.collected) return; // Change direction occasionally self.changeDirectionTimer++; if (self.changeDirectionTimer > 120) { // Change direction every 2 seconds self.direction = Math.random() * Math.PI * 2; self.changeDirectionTimer = 0; } // Move in current direction var newX = self.x + Math.cos(self.direction) * self.walkSpeed; var newY = self.y + Math.sin(self.direction) * self.walkSpeed; // Keep within playable bounds var minX = grid.x + CELL_SIZE; var maxX = grid.x + grid.cells.length * CELL_SIZE - CELL_SIZE; var minY = grid.y + CELL_SIZE; var maxY = grid.y + grid.cells[0].length * CELL_SIZE - CELL_SIZE; if (newX < minX || newX > maxX) { self.direction = Math.PI - self.direction; // Bounce horizontally } else { self.x = newX; } if (newY < minY || newY > maxY) { self.direction = -self.direction; // Bounce vertically } else { self.y = newY; } // Check if player clicks on coin }; self.down = function () { if (!self.collected) { self.collected = true; setGold(gold + self.value); // Add 10 security score points score += 10; updateUI(); var goldIndicator = new GoldIndicator(self.value, self.x, self.y); game.addChild(goldIndicator); // Remove coin from coins array var coinIndex = coins.indexOf(self); if (coinIndex !== -1) { coins.splice(coinIndex, 1); } self.destroy(); } }; return self; }); var ComicPanel = Container.expand(function (imagePath, text) { var self = Container.call(this); // Panel background - larger panel var panelBg = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); panelBg.width = 700; panelBg.height = 500; panelBg.tint = 0x222222; panelBg.alpha = 0.9; // Add border effect var border = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); border.width = 710; border.height = 510; border.tint = 0x000000; border.alpha = 0.8; self.addChildAt(border, 0); // Text area - larger and repositioned var textBg = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); textBg.width = 660; textBg.height = 160; textBg.tint = 0xffffff; textBg.alpha = 0.95; textBg.y = 170; self.addChild(textBg); // Character image if provided - repositioned if (imagePath) { var characterImage = self.attachAsset(imagePath, { anchorX: 0.5, anchorY: 0.5 }); characterImage.width = 200; characterImage.height = 200; characterImage.y = -80; self.addChild(characterImage); } // Story text - improved sizing and positioning var storyText = new Text2(text, { size: 28, fill: 0x000000, weight: 600 }); storyText.anchor.set(0.5, 0.5); storyText.y = 170; storyText.wordWrap = true; storyText.wordWrapWidth = 600; storyText.maxWidth = 600; self.addChild(storyText); // Scale animation entrance self.scaleX = 0.3; self.scaleY = 0.3; self.alpha = 0; self.show = function () { tween(self, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 400, easing: tween.backOut }); }; self.hide = function (callback) { tween(self, { scaleX: 0.8, scaleY: 0.8, alpha: 0 }, { duration: 300, easing: tween.easeIn, onFinish: callback }); }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; // Removed number label to improve performance // Numbers were causing lag due to text rendering overhead self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { // Renderizado visual desactivado para evitar interferencia con tiles del mundo // Solo mantiene la lógica de pathfinding sin elementos visuales // Los tiles del mundo se renderizan a través de WorldRenderer // Actualizar las flechas solo si hay una torre seleccionada para debug if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { // Mostrar flechas solo para torres seleccionadas while (debugArrows.length > data.targets.length) { self.removeChild(debugArrows.pop()); } for (var a = 0; a < data.targets.length; a++) { var destination = data.targets[a]; var ox = destination.x - data.x; var oy = destination.y - data.y; var angle = Math.atan2(oy, ox); if (!debugArrows[a]) { debugArrows[a] = LK.getAsset('arrow', { anchorX: -.5, anchorY: 0.5 }); debugArrows[a].alpha = .5; self.addChildAt(debugArrows[a], 1); } debugArrows[a].rotation = angle; } } else { // Remover flechas si no hay torre seleccionada self.removeArrows(); } // Hacer el gráfico de celda invisible para no interferir con tiles cellGraphics.alpha = 0; }; }); var DigimonShop = Container.expand(function () { var self = Container.call(this); self.visible = false; self.y = 2732; var shopBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); shopBackground.width = 2048; shopBackground.height = 600; shopBackground.tint = 0x222222; shopBackground.alpha = 0.95; var titleText = new Text2('Digimon Firewall Shop', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -250; self.addChild(titleText); // Create shop items container var itemsContainer = new Container(); itemsContainer.y = -50; self.addChild(itemsContainer); // Digivice items data var digiviceItems = [{ id: 'digiviceC', name: 'Digivice C', cost: 500, description: 'Unlocks Champion Level' }, { id: 'digiviceB', name: 'Digivice B', cost: 2000, description: 'Unlocks Ultimate Level' }, { id: 'digiviceA', name: 'Digivice A', cost: 8000, description: 'Unlocks Mega Level' }]; // Create shop item buttons for (var i = 0; i < digiviceItems.length; i++) { var item = digiviceItems[i]; var itemButton = new Container(); itemButton.x = -600 + i * 400; itemButton.y = 0; itemsContainer.addChild(itemButton); var itemBg = itemButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); itemBg.width = 350; itemBg.height = 200; var itemNameText = new Text2(item.name, { size: 50, fill: 0xFFFFFF, weight: 800 }); itemNameText.anchor.set(0.5, 0.5); itemNameText.y = -50; itemButton.addChild(itemNameText); var itemDescText = new Text2(item.description, { size: 35, fill: 0xCCCCCC, weight: 400 }); itemDescText.anchor.set(0.5, 0.5); itemDescText.y = -10; itemButton.addChild(itemDescText); var itemCostText = new Text2(item.cost + ' bits', { 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 = gold >= itemData.cost; if (owned) { background.tint = 0x00AA00; costText.setText('OWNED'); button.alpha = 0.7; } else if (canAfford) { background.tint = 0x4444FF; costText.setText(itemData.cost + ' bits'); button.alpha = 1.0; } else { background.tint = 0x666666; costText.setText(itemData.cost + ' bits'); button.alpha = 0.5; } }; button.down = function () { var owned = storage[itemData.id] || false; var canAfford = gold >= 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) { setGold(gold - itemData.cost); 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 bits 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; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this virus type var assetId = 'virus'; if (self.type !== 'normal') { assetId = 'virus_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Combine poison (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ // Create world-based maze layout self.generateMazeForWorld = function (worldNumber) { // Clear existing maze for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { self.cells[i][j].type = 1; // Default to wall } } self.spawns = []; self.goals = []; // Always create entry area (top 5 rows) with 3-block wide path for (var i = 0; i < gridWidth; i++) { for (var j = 0; j <= 4; j++) { if (i >= 11 && i <= 13) { self.cells[i][j].type = 0; // Open path in center (3 blocks wide) if (j === 0) { self.cells[i][j].type = 2; // Spawn points self.spawns.push(self.cells[i][j]); } } else { self.cells[i][j].type = 1; // Walls on sides } } } // Always create exit area (bottom 5 rows) with 3-block wide path for (var i = 0; i < gridWidth; i++) { for (var j = gridHeight - 5; j < gridHeight; j++) { if (i >= 11 && i <= 13) { self.cells[i][j].type = 0; // Open path in center (3 blocks wide) if (j === gridHeight - 1) { self.cells[i][j].type = 3; // Goal points self.goals.push(self.cells[i][j]); } } else { self.cells[i][j].type = 1; // Walls on sides } } } // Create classic square-cornered labyrinth with generous tower placement areas // Simple pattern: Create a winding path with 90-degree turns and wide wall corridors // Path layout: Start center-top, go down, turn right, down, turn left, repeat creating a serpentine pattern // Main path coordinates (center of 3-block wide paths) // Reduced to only 2 curves for a simpler path var pathCenterX = 12; // Center column var pathPattern = [ // Start: go down from entry { x: pathCenterX, startY: 5, endY: 12, type: 'vertical' }, // First curve: turn left { y: 12, startX: pathCenterX, endX: 4, type: 'horizontal' }, // Go down on left side { x: 4, startY: 12, endY: 20, type: 'vertical' }, // Second curve: turn right to center { y: 20, startX: 4, endX: pathCenterX, type: 'horizontal' }, // Final path to exit { x: pathCenterX, startY: 20, endY: gridHeight - 5, type: 'vertical' }]; // Crear los segmentos del camino con 3 bloques de grosor (camino) y dejar espacio extra de 1 bloque a cada lado para asegurar zonas de 2x2 para torretas for (var p = 0; p < pathPattern.length; p++) { var segment = pathPattern[p]; if (segment.type === 'vertical') { var startY = Math.min(segment.startY, segment.endY); var endY = Math.max(segment.startY, segment.endY); for (var y = startY; y <= endY; y++) { // Camino de 3 bloques de ancho for (var offset = -1; offset <= 1; offset++) { var pathX = segment.x + offset; if (pathX >= 0 && pathX < gridWidth && y >= 0 && y < gridHeight) { self.cells[pathX][y].type = 0; } } // Dejar espacio extra de 1 bloque a cada lado del camino para permitir torretas 2x2 for (var extra = -2; extra <= 2; extra += 4) { var sideX = segment.x + extra; if (sideX >= 0 && sideX < gridWidth && y >= 0 && y < gridHeight) { // Solo marcar como espacio de torre si no es camino ni spawn/goal if (self.cells[sideX][y].type === 1) { // No cambiar si ya es camino/spawn/goal self.cells[sideX][y].type = 1; // Mantener como muro, pero dejarlo para posible torre } } } // Asegurar espacio de 2x2 para torretas a la izquierda y derecha del camino for (var offsetY = 0; offsetY <= 1; offsetY++) { for (var extra = -2; extra <= 2; extra += 4) { var baseX = segment.x + extra; var baseY = y + offsetY; if (baseX >= 0 && baseX + 1 < gridWidth && baseY >= 0 && baseY + 1 < gridHeight) { // Solo marcar como espacio de torre si ambos son muro if (self.cells[baseX][baseY].type === 1 && self.cells[baseX + 1][baseY].type === 1 && self.cells[baseX][baseY + 1].type === 1 && self.cells[baseX + 1][baseY + 1].type === 1) { // Marcar como espacio de torre (type 1) para permitir torretas 2x2 self.cells[baseX][baseY].type = 1; self.cells[baseX + 1][baseY].type = 1; self.cells[baseX][baseY + 1].type = 1; self.cells[baseX + 1][baseY + 1].type = 1; } } } } } } else if (segment.type === 'horizontal') { var startX = Math.min(segment.startX, segment.endX); var endX = Math.max(segment.startX, segment.endX); for (var x = startX; x <= endX; x++) { // Camino de 3 bloques de alto for (var offset = -1; offset <= 1; offset++) { var pathY = segment.y + offset; if (x >= 0 && x < gridWidth && pathY >= 0 && pathY < gridHeight) { self.cells[x][pathY].type = 0; } } // Dejar espacio extra de 1 bloque arriba y abajo del camino para permitir torretas 2x2 for (var extra = -2; extra <= 2; extra += 4) { var sideY = segment.y + extra; if (x >= 0 && x < gridWidth && sideY >= 0 && sideY < gridHeight) { if (self.cells[x][sideY].type === 1) { self.cells[x][sideY].type = 1; } } } // Asegurar espacio de 2x2 para torretas arriba y abajo del camino for (var offsetX = 0; offsetX <= 1; offsetX++) { for (var extra = -2; extra <= 2; extra += 4) { var baseX = x + offsetX; var baseY = segment.y + extra; if (baseX >= 0 && baseX + 1 < gridWidth && baseY >= 0 && baseY + 1 < gridHeight) { if (self.cells[baseX][baseY].type === 1 && self.cells[baseX + 1][baseY].type === 1 && self.cells[baseX][baseY + 1].type === 1 && self.cells[baseX + 1][baseY + 1].type === 1) { // Marcar como espacio de torre (type 1) para permitir torretas 2x2 self.cells[baseX][baseY].type = 1; self.cells[baseX + 1][baseY].type = 1; self.cells[baseX][baseY + 1].type = 1; self.cells[baseX + 1][baseY + 1].type = 1; } } } } } } } // Asegurar conexiones suaves en las intersecciones y dejar espacio de 2x2 para torret }; // Generate maze for current world var world = Math.ceil(currentWave / 9); if (world < 1) world = 1; if (world > 6) world = 6; self.generateMazeForWorld(world); // Apply the maze layout to cells for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = cell.type; // Use the type set by generateMazeForWorld cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; // Only create debug cells for visible areas and reduce frequency if (j > 3 && j <= gridHeight - 4 && (i + j) % 2 === 0) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; // Simplified pathfinding - only check cardinal directions for better performance var targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed; // 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); 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; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } // 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; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var LevelSelectionMenu = Container.expand(function (worldNumber) { var self = Container.call(this); self.worldNumber = worldNumber; // Position the menu at center of screen self.x = 2048 / 2; self.y = 2732 / 2; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 1800; menuBackground.height = 1200; menuBackground.tint = 0x333333; menuBackground.alpha = 0.9; var worldNames = ["", "Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"]; var titleText = new Text2('Select Level - ' + worldNames[worldNumber], { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -400; self.addChild(titleText); var worldLevels = storage.worldLevels || { 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1 }; var unlockedLevel = worldLevels[worldNumber] || 1; // Create level buttons in a grid var buttonsPerRow = 5; var buttonWidth = 120; var buttonHeight = 80; var buttonSpacing = 160; var startX = -((buttonsPerRow - 1) * buttonSpacing) / 2; var startY = -200; for (var i = 1; i <= 10; i++) { var levelButton = new Container(); var levelButtonBg = levelButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); levelButtonBg.width = buttonWidth; levelButtonBg.height = buttonHeight; var isUnlocked = i <= unlockedLevel; levelButtonBg.tint = isUnlocked ? 0x4444FF : 0x666666; var levelButtonText = new Text2(isUnlocked ? i.toString() : "🔒", { size: 40, fill: isUnlocked ? 0xFFFFFF : 0x999999, weight: 800 }); levelButtonText.anchor.set(0.5, 0.5); levelButton.addChild(levelButtonText); // Position buttons in grid var row = Math.floor((i - 1) / buttonsPerRow); var col = (i - 1) % buttonsPerRow; levelButton.x = startX + col * buttonSpacing; levelButton.y = startY + row * 120; self.addChild(levelButton); (function (levelIndex, unlocked) { levelButton.down = function () { if (unlocked) { self.destroy(); game.startWorldLevel(self.worldNumber, levelIndex); } else { var notification = game.addChild(new Notification("Complete previous level to unlock!")); notification.x = 2048 / 2; notification.y = 2732 / 2 + 200; } }; })(i, isUnlocked); } // Back button var backButton = new Container(); var backButtonBg = backButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); backButtonBg.width = 200; backButtonBg.height = 80; backButtonBg.tint = 0x666666; var backButtonText = new Text2('Back', { size: 40, fill: 0xFFFFFF, weight: 800 }); backButtonText.anchor.set(0.5, 0.5); backButton.addChild(backButtonText); backButton.x = 0; backButton.y = 400; self.addChild(backButton); backButton.down = function () { self.destroy(); var worldSelectionMenu = new WorldSelectionMenu(); game.addChild(worldSelectionMenu); }; return self; }); var MainMenu = Container.expand(function () { var self = Container.call(this); // Position the menu at center of screen self.x = 2048 / 2; self.y = 2732 / 2; 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); var startButton = new Container(); var startButtonBg = startButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startButtonBg.width = 400; startButtonBg.height = 120; startButtonBg.tint = 0x00AA00; var startButtonText = new Text2(getText('startGame'), { size: 50, fill: 0xFFFFFF, weight: 800 }); startButtonText.anchor.set(0.5, 0.5); startButton.addChild(startButtonText); startButton.y = 50; self.addChild(startButton); startButton.down = function () { self.destroy(); game.startGame(); }; // Add tutorial button var tutorialButton = new Container(); var tutorialButtonBg = tutorialButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); tutorialButtonBg.width = 400; tutorialButtonBg.height = 120; tutorialButtonBg.tint = 0x00AAAA; var tutorialButtonText = new Text2(getText('tutorial'), { size: 50, fill: 0xFFFFFF, weight: 800 }); tutorialButtonText.anchor.set(0.5, 0.5); tutorialButton.addChild(tutorialButtonText); tutorialButton.y = 180; self.addChild(tutorialButton); tutorialButton.down = function () { self.destroy(); game.startTutorial(); }; // Add language button var languageButton = new Container(); var languageButtonBg = languageButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); languageButtonBg.width = 400; languageButtonBg.height = 120; languageButtonBg.tint = 0xFF6600; var languageButtonText = new Text2(getText('language'), { size: 50, fill: 0xFFFFFF, weight: 800 }); languageButtonText.anchor.set(0.5, 0.5); languageButton.addChild(languageButtonText); languageButton.y = 310; self.addChild(languageButton); languageButton.down = function () { // Toggle language between English and Spanish var newLang = currentLanguage === 'en' ? 'es' : 'en'; setLanguage(newLang); // Recreate main menu with new language self.destroy(); var newMainMenu = new MainMenu(); game.addChild(newMainMenu); }; return self; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { // Check if we can start next wave (10 waves max per world) var worldWave = (currentWave - 1) % 10 + 1; if (currentWave === 0) worldWave = 0; // Show button during tutorial or when can start next wave var showForTutorial = waveIndicator && waveIndicator.gameStarted && currentWave === 0; var showForNextWave = waveIndicator && waveIndicator.gameStarted && worldWave < 10 && !waveInProgress && enemies.length === 0; if (showForTutorial || showForNextWave) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves && !waveInProgress && enemies.length === 0) { currentWave++; // Increment to the next wave directly // Calculate current world and level for 10-wave system currentWorld = Math.ceil(currentWave / 10); currentLevel = (currentWave - 1) % 10 + 1; waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentLevel); var enemyCount = waveIndicator.getEnemyCount(currentLevel); // Update wave counter display updateWaveCounter(); var notification = game.addChild(new Notification("Wave " + currentLevel + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Increase size of base for easier touch var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); switch (self.towerType) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'sniper': baseGraphics.tint = 0xFF5500; break; case 'splash': baseGraphics.tint = 0x33CC00; break; case 'slow': baseGraphics.tint = 0x9900FF; break; case 'poison': baseGraphics.tint = 0x00FFAA; break; default: baseGraphics.tint = 0xAAAAAA; } var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 60, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -25; // Position above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost + ' bits', { size: 60, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 25 + 12; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var StorySequence = Container.expand(function (worldNumber) { var self = Container.call(this); self.worldNumber = worldNumber; self.currentPanel = 0; self.panels = []; self.onComplete = null; // Position at center of screen self.x = 2048 / 2; self.y = 2732 / 2; // Semi-transparent background overlay var overlay = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); overlay.width = 2048; overlay.height = 2732; overlay.tint = 0x000000; overlay.alpha = 0.7; self.getWorldStory = function (worldNumber) { switch (worldNumber) { case 1: return [{ text: "ALERT! Pokémon infiltrators have breached\nthe Digital Forest servers! They're attempting\nto steal Digimon data files!", image: 'agumon' }, { text: "These Pokémon spies are using advanced\nstealth protocols to access our core database.\nDeploy Digimon guardians immediately!", image: 'gabumon' }, { text: "The future of the Digimon franchise\ndepends on you! Stop Pokémon from\ncorrupting our digital ecosystem!", image: 'tentomon' }]; case 2: return [{ text: "Pokémon agents have infiltrated the Desert\nData Center! They're planting malicious code\nto corrupt our systems!", image: 'palmon' }, { text: "Fire-type Pokémon are overheating our\nservers while others steal precious\nDigimon evolution data!", image: 'gomamon' }, { text: "Their coordinated attack is more sophisticated\nthan before. Pokémon want to monopolize\nthe children's entertainment industry!", image: 'patamon' }]; case 3: return [{ text: "Ice-type Pokémon have frozen our Glacier\nservers to slow down our defenses\nwhile they extract data!", image: null }, { text: "Flying Pokémon are bypassing our security\nwalls! They're trying to reach the core\nDigimon genetic database!", image: null }, { text: "Critical system temperatures detected!\nPokémon are trying to cause a complete\nserver meltdown!", image: null }]; case 4: return [{ text: "Pokémon sleeper agents hidden in the Village\nNetwork have activated! They've been\ngathering intelligence for months!", image: null }, { text: "Multiple Pokémon strike teams are attacking\nsimultaneously, trying to overwhelm\nour Digimon defenders!", image: null }, { text: "This is corporate espionage on a massive\nscale! Pokémon Company wants to steal\nour digital creature technology!", image: null }]; case 5: return [{ text: "MAXIMUM THREAT LEVEL! Elite Pokémon\nhackers have breached our most secure\nTechnology Labs!", image: null }, { text: "They're using legendary Pokémon abilities\nto bypass our quantum encryption!\nOur most sensitive data is at risk!", image: null }, { text: "Deploy our strongest Mega-level Digimon!\nOnly they can stop this corporate\ncyber warfare!", image: null }]; case 6: return [{ text: "FINAL ASSAULT! Pokémon's master plan\nis revealed - they want to delete ALL\nDigimon data permanently!", image: null }, { text: "Legendary Pokémon themselves are leading\nthis final attack on our core servers!\nThis is the ultimate battle for supremacy!", image: null }, { text: "The children's hearts are at stake!\nDefeat Pokémon's invasion and save\nthe future of digital monsters forever!", image: null }]; default: return [{ text: "Pokémon infiltrators detected! Protect the Digimon database!", image: null }, { text: "Deploy your Digimon to stop the corporate espionage!", image: null }, { text: "Save the Digital World from Pokémon's takeover!", image: null }]; } }; // World-specific story content var storyData = self.getWorldStory(worldNumber); // Create panels for (var i = 0; i < storyData.length; i++) { var panel = new ComicPanel(storyData[i].image, storyData[i].text); panel.x = (i - 1) * 650; // Position panels side by side self.addChild(panel); self.panels.push(panel); } // Navigation indicators var indicatorContainer = new Container(); indicatorContainer.y = 250; self.addChild(indicatorContainer); for (var i = 0; i < self.panels.length; i++) { var indicator = indicatorContainer.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = 20; indicator.height = 20; indicator.tint = i === 0 ? 0xffffff : 0x666666; indicator.x = (i - (self.panels.length - 1) / 2) * 40; } // Skip button var skipButton = new Container(); var skipBg = skipButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); skipBg.width = 150; skipBg.height = 60; skipBg.tint = 0x666666; var skipText = new Text2('Skip', { size: 40, fill: 0xffffff, weight: 800 }); skipText.anchor.set(0.5, 0.5); skipButton.addChild(skipText); skipButton.x = 400; skipButton.y = -300; self.addChild(skipButton); skipButton.down = function () { self.complete(); }; self.showPanel = function (index) { if (index < 0 || index >= self.panels.length) return; // Hide all panels for (var i = 0; i < self.panels.length; i++) { self.panels[i].alpha = 0; indicatorContainer.children[i].tint = 0x666666; } // Show current panel self.panels[index].show(); indicatorContainer.children[index].tint = 0xffffff; self.currentPanel = index; }; self.nextPanel = function () { if (self.currentPanel < self.panels.length - 1) { self.showPanel(self.currentPanel + 1); } else { self.complete(); } }; self.complete = function () { if (self.onComplete) { self.onComplete(); } self.destroy(); }; // Show first panel self.showPanel(0); // Auto-advance after 4 seconds or click to advance var autoAdvanceTimer = LK.setTimeout(function () { self.nextPanel(); }, 4000); self.down = function () { LK.clearTimeout(autoAdvanceTimer); autoAdvanceTimer = LK.setTimeout(function () { self.nextPanel(); }, 4000); self.nextPanel(); }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // 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 'gabumon': //{8L} // Rapid fire Digimon self.fireRate = 30; self.damage = 5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'tentomon': //{8N} // Long range Digimon self.fireRate = 90; self.damage = 25; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'palmon': //{8P} // Area damage Digimon self.fireRate = 75; self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; case 'gomamon': //{8R} // Slowing Digimon self.fireRate = 50; self.damage = 8; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'patamon': //{8U} // Poison/status Digimon self.fireRate = 70; self.damage = 12; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 5; break; } var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); switch (self.id) { case '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; } 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++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rapid') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect self.damage = 5 + self.level * 10; // double the effect self.bulletSpeed = 7 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades self.damage = 5 + self.level * 3; self.bulletSpeed = 7 + self.level * 0.7; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough 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 () { 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; 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 () { 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 bullet.type = self.id; // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; } // 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); self.currentStep = 0; self.onComplete = null; self.tutorialActive = true; self.autoSequenceRunning = false; // 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 = 900; instructionBg.height = 250; instructionBg.tint = 0x222222; instructionBg.alpha = 0.95; instructionBg.x = 2048 / 2; instructionBg.y = 300; var instructionText = new Text2(getText('tutorialWelcome'), { size: 55, fill: 0xFFFFFF, weight: 600 }); instructionText.anchor.set(0.5, 0.5); instructionText.wordWrap = true; instructionText.wordWrapWidth = 850; instructionText.x = 2048 / 2; instructionText.y = 300; self.addChild(instructionText); // Animated cursor for showing actions var cursor = self.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); cursor.width = 25; cursor.height = 25; cursor.tint = 0xFFFF00; cursor.visible = false; // Next button for manual progression var nextButton = new Container(); var nextBg = nextButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); nextBg.width = 200; nextBg.height = 80; nextBg.tint = 0x00AA00; var nextText = new Text2('Next', { size: 40, fill: 0xffffff, weight: 800 }); nextText.anchor.set(0.5, 0.5); nextButton.addChild(nextText); nextButton.x = 2048 / 2; nextButton.y = 500; self.addChild(nextButton); nextButton.visible = false; nextButton.down = function () { self.progressTutorial(); }; // 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); skipButton.down = function () { self.complete(); }; // Start the tutorial sequence with first step LK.setTimeout(function () { self.showStep1(); }, 1000); self.showStep1 = function () { instructionText.setText(getText('tutorialStep1')); nextButton.visible = true; self.currentTutorialStep = 1; }; self.showStep2 = function () { instructionText.setText(getText('tutorialStep2')); nextButton.visible = false; self.currentTutorialStep = 2; self.demonstrateTowerPlacement(); }; self.showStep3 = function () { instructionText.setText(getText('tutorialStep3')); nextButton.visible = false; self.currentTutorialStep = 3; self.demonstrateNextWave(); }; self.showStep4 = function () { instructionText.setText(getText('tutorialStep4')); nextButton.visible = false; self.currentTutorialStep = 4; self.demonstrateTowerUpgrade(); }; self.showFinalStep = function () { instructionText.setText(getText('tutorialCompleted')); cursor.visible = false; nextButton.visible = true; self.currentTutorialStep = 5; }; self.progressTutorial = function () { switch (self.currentTutorialStep) { case 1: self.showStep2(); break; case 2: // Step 2 is handled automatically after tower placement break; case 3: // Step 3 is handled automatically after wave start break; case 4: // Step 4 is handled automatically after upgrade break; case 5: self.finishTutorial(); break; } }; self.startAutoSequence = function () { // This function is now replaced by manual progression }; self.demonstrateTowerPlacement = function () { instructionText.setText(getText('tutorialStep2')); cursor.visible = true; // Animate cursor from source tower to placement position var sourceTowerX = 2048 / 2 - 640; var sourceTowerY = 2732 - 180; var placementX = grid.x + 6 * CELL_SIZE + CELL_SIZE / 2; var placementY = grid.y + 10 * CELL_SIZE + CELL_SIZE / 2; cursor.x = sourceTowerX; cursor.y = sourceTowerY; // Scale cursor to show it's "grabbing" the tower tween(cursor, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, onFinish: function onFinish() { // Move cursor to placement location with smooth curve tween(cursor, { x: placementX, y: placementY }, { duration: 3000, // Slower movement for better visibility easing: tween.easeInOut, onFinish: function onFinish() { // Scale down to show "placing" tween(cursor, { scaleX: 1.0, scaleY: 1.0 }, { duration: 500, //{ls} // Slower scaling animation onFinish: function onFinish() { // Actually place the tower if (placeTower(6, 10, 'agumon')) { LK.setTimeout(function () { self.showStep3(); }, 2000); // Show next step after placement } } }); } }); } }); }; self.demonstrateNextWave = function () { instructionText.setText(getText('tutorialStep3')); // Move cursor to next wave button var nextWaveX = 2048 - 200; var nextWaveY = 2732 - 100; tween(cursor, { x: nextWaveX, y: nextWaveY }, { duration: 2500, //{lF} // Slower cursor movement easing: tween.easeInOut, onFinish: function onFinish() { // Scale cursor to show clicking tween(cursor, { scaleX: 1.8, scaleY: 1.8 }, { duration: 400, //{lK} // Slower scaling onFinish: function onFinish() { tween(cursor, { scaleX: 1.0, scaleY: 1.0 }, { duration: 400, //{lQ} // Slower scaling onFinish: function onFinish() { // Start the first wave if (!waveIndicator.gameStarted) { waveIndicator.gameStarted = true; currentWave++; currentWorld = Math.ceil(currentWave / 9); currentLevel = (currentWave - 1) % 9 + 1; waveTimer = 0; waveInProgress = true; waveSpawned = false; updateWaveCounter(); } LK.setTimeout(function () { self.showStep4(); }, 3000); // Continue to upgrade step } }); } }); } }); }; self.demonstrateTowerUpgrade = function () { instructionText.setText(getText('tutorialStep4')); // Find the first tower to demonstrate upgrade if (towers.length > 0) { var tower = towers[0]; // Auto-upgrade the tower during tutorial to level 3 var _autoUpgradeLevel = function autoUpgradeLevel(targetLevel) { if (tower.level < targetLevel && tower.level < tower.maxLevel) { // Give enough gold for upgrade var baseUpgradeCost = getTowerCost(tower.id); var upgradeCost; if (tower.level === tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, tower.level - 1)); } if (gold < upgradeCost) { setGold(gold + upgradeCost); } // Upgrade the tower tower.upgrade(); // Continue upgrading until target level LK.setTimeout(function () { _autoUpgradeLevel(targetLevel); }, 800); // Slower upgrade timing for visibility } }; // Start auto-upgrading to level 3 _autoUpgradeLevel(3); tween(cursor, { x: tower.x, y: tower.y }, { duration: 2000, //{ma} // Slower cursor movement easing: tween.easeInOut, onFinish: function onFinish() { // Simulate clicking on tower tween(cursor, { scaleX: 2.0, scaleY: 2.0 }, { duration: 300, //{mf} // Slower animation onFinish: function onFinish() { tween(cursor, { scaleX: 1.0, scaleY: 1.0 }, { duration: 300, //{ml} // Slower animation onFinish: function onFinish() { // Trigger tower upgrade menu if (tower.down) { tower.down(tower.x, tower.y, {}); } LK.setTimeout(function () { self.showFinalStep(); }, 4000); // Continue to final step while keeping upgrade menu visible } }); } }); } }); } else { LK.setTimeout(function () { self.demonstrateGameplayLoop(); }, 2000); // Longer delay } }; self.demonstrateGameplayLoop = function () { // This function is now replaced by showFinalStep }; self.finishTutorial = function () { instructionText.setText(getText('tutorialFinalMessage')); // Add finish tutorial button var finishButton = new Container(); var finishBg = finishButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); finishBg.width = 400; finishBg.height = 100; finishBg.tint = 0x00AA00; var finishText = new Text2(getText('finishTutorial'), { size: 45, fill: 0xffffff, weight: 800 }); finishText.anchor.set(0.5, 0.5); finishButton.addChild(finishText); finishButton.x = 2048 / 2; finishButton.y = 500; self.addChild(finishButton); finishButton.down = function () { self.complete(); }; // Remove auto-complete timer - user must click finish button }; self.complete = function () { self.tutorialActive = false; self.autoSequenceRunning = false; // 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); } } // 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(); // Return to main menu after tutorial var mainMenu = new MainMenu(); game.addChild(mainMenu); }; 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 + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' 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); // Create digivolve button var digivolveButton = new Container(); buttonsContainer.addChild(digivolveButton); var digivolveButtonBackground = digivolveButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); digivolveButtonBackground.width = 500; digivolveButtonBackground.height = 150; var digivolveButtonText = new Text2('Digivolve', { size: 60, fill: 0xFFFFFF, weight: 800 }); digivolveButtonText.anchor.set(0.5, 0.5); digivolveButton.addChild(digivolveButtonText); // Check if digivolution is available function canDigivolve() { if (self.tower.level < 2) return false; // Need at least level 2 var hasDigivice = false; if (self.tower.level >= 2 && self.tower.level <= 3 && storage.digiviceC) hasDigivice = true; if (self.tower.level >= 4 && self.tower.level <= 5 && storage.digiviceB) hasDigivice = true; if (self.tower.level >= 6 && storage.digiviceA) hasDigivice = true; return hasDigivice; } digivolveButton.update = function () { var canEvolve = canDigivolve(); digivolveButton.visible = canEvolve; if (canEvolve) { digivolveButtonBackground.tint = 0xFF6600; } else { digivolveButtonBackground.tint = 0x666666; } }; digivolveButton.down = function () { if (canDigivolve()) { var evolutionCost = getTowerCost(self.tower.id) * 2; if (gold >= evolutionCost) { setGold(gold - evolutionCost); // Apply evolution effects self.tower.damage *= 1.5; self.tower.fireRate = Math.max(5, Math.floor(self.tower.fireRate * 0.8)); var notification = game.addChild(new Notification(self.tower.id + " digivolved!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Update stats display statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); } else { var notification = game.addChild(new Notification("Not enough bits to digivolve!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } }; upgradeButton.y = -125; digivolveButton.y = 0; sellButton.y = 125; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' bits'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' bits'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " bits!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); // Sincronizar el mapa de visualización después de vender torre syncVisualizationMap(); worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' bits'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0xFFFF00; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Boss Normal"; break; case 'fast': block.tint = 0x00AAFF; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xAA0000; waveType = "Boss Immune"; break; case 'flying': block.tint = 0xFFFF00; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the color of the wave type switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; break; case 'fast': block.tint = 0x00AAFF; break; case 'immune': block.tint = 0xAA0000; break; case 'flying': block.tint = 0xFFFF00; break; default: block.tint = 0xFF0000; break; } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a crown or some indicator to the wave marker for boss waves var bossIndicator = marker.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); bossIndicator.width = 30; bossIndicator.height = 30; bossIndicator.tint = 0xFFD700; // Gold color bossIndicator.y = -block.height / 2 - 15; // Change the wave type text to indicate boss waveType = "BOSS"; } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2(waveType, { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (every 10th wave) if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; // Fix positioning to properly show first wave when currentWave is 0 var displayWave = Math.max(0, currentWave); var moveAmount = (progress + displayWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; for (var i = 0; i < totalWaves + 1; i++) { var marker = self.waveMarkers[i]; if (i === 0) { continue; } var block = marker.children[0]; // Fix comparison to properly handle wave 0 and 1 if (i - 1 < displayWave) { block.alpha = .5; } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } // Waves no longer advance automatically - they must be triggered manually via NextWaveButton // This function now only handles the initial game start }; self.handleWaveProgression(); }; return self; }); var WorldRenderer = Container.expand(function () { var self = Container.call(this); self.currentWorld = 1; self.backgroundTiles = []; self.pathTiles = []; self.wallTiles = []; self.sceneryElements = []; self.getWorldAssets = function (worldNumber) { switch (worldNumber) { case 1: return { background: 'forestBg', path: 'forestPath', wall: 'forestWall', scenery: 'forestScenery', ambient: 0x90ee90 }; case 2: return { background: 'desertBg', path: 'desertPath', wall: 'desertWall', scenery: 'desertScenery', ambient: 0xffd700 }; case 3: return { background: 'glacierBg', path: 'glacierPath', wall: 'glacierWall', scenery: 'glacierScenery', ambient: 0xe6f3ff }; case 4: return { background: 'villageBg', path: 'villagePath', wall: 'villageWall', scenery: 'villageScenery', ambient: 0xf0e68c }; case 5: return { background: 'techLabBg', path: 'techLabPath', wall: 'techLabWall', scenery: 'techLabScenery', ambient: 0x87ceeb }; case 6: return { background: 'infernoBg', path: 'infernoPath', wall: 'infernoWall', scenery: 'infernoScenery', ambient: 0xff6347 }; default: return { background: 'forestBg', path: 'forestPath', wall: 'forestWall', scenery: 'forestScenery', ambient: 0x90ee90 }; } }; self.updateWorldGraphics = function (worldNumber, gridInstance) { // Forzar renderizado siempre, incluso si el mundo no cambia self.currentWorld = worldNumber; var worldAssets = self.getWorldAssets(worldNumber); // Clear existing tiles while (self.backgroundTiles.length) { self.removeChild(self.backgroundTiles.pop()); } while (self.pathTiles.length) { self.removeChild(self.pathTiles.pop()); } while (self.wallTiles.length) { self.removeChild(self.wallTiles.pop()); } while (self.sceneryElements.length) { self.removeChild(self.sceneryElements.pop()); } // Create new tiles based on current world using the correct assets var gridWidth = 24; var gridHeight = 35; for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { // Create background tile using world-specific assets var bgTile = self.attachAsset(worldAssets.background, { anchorX: 0, anchorY: 0 }); bgTile.x = i * CELL_SIZE; bgTile.y = j * CELL_SIZE; self.backgroundTiles.push(bgTile); // Add world-specific scenery elements randomly if (Math.random() < 0.15) { // 15% chance for scenery - use world-specific scenery assets var scenery = self.attachAsset(worldAssets.scenery, { anchorX: 0.5, anchorY: 0.5 }); scenery.x = i * CELL_SIZE + CELL_SIZE / 2; scenery.y = j * CELL_SIZE + CELL_SIZE / 2; // World-specific scenery shapes switch (worldNumber) { case 1: // Forest - trees/bushes (ellipses) scenery.scaleY = 1.2 + Math.random() * 0.8; break; case 2: // Desert - cacti/rocks (tall thin or wide) if (Math.random() < 0.5) { scenery.scaleX = 0.6; scenery.scaleY = 1.8; // Tall cactus } else { scenery.scaleX = 1.4; scenery.scaleY = 0.7; // Wide rock } break; case 3: // Glacier - ice crystals scenery.scaleX = 0.8 + Math.random() * 0.4; scenery.scaleY = 0.8 + Math.random() * 0.4; scenery.alpha = 0.7; break; case 4: // Village - small structures scenery.scaleX = 1.2; scenery.scaleY = 1.0; break; case 5: // Technology - machinery scenery.scaleX = 0.9; scenery.scaleY = 0.9; scenery.alpha = 0.8; break; case 6: // Hell - lava bubbles/flames scenery.scaleX = 1.1 + Math.random() * 0.6; scenery.scaleY = 1.1 + Math.random() * 0.6; scenery.alpha = 0.9; break; } self.sceneryElements.push(scenery); } } } // Overlay path and wall tiles on top of scenery if (gridInstance && gridInstance.cells) { for (var i = 0; i < Math.min(gridWidth, gridInstance.cells.length); i++) { for (var j = 0; j < Math.min(gridHeight, gridInstance.cells[i].length); j++) { var cell = gridInstance.cells[i][j]; if (cell.type === 0 || cell.type === 2 || cell.type === 3) { // Path, spawn, or goal - use world-specific path assets var pathTile = self.attachAsset(worldAssets.path, { anchorX: 0, anchorY: 0 }); pathTile.x = i * CELL_SIZE; pathTile.y = j * CELL_SIZE; pathTile.alpha = 0.95; self.pathTiles.push(pathTile); } else if (cell.type === 1) { // Wall - use appropriate wall tile based on world var wallTile = self.attachAsset(worldAssets.wall, { anchorX: 0, anchorY: 0 }); wallTile.x = i * CELL_SIZE; wallTile.y = j * CELL_SIZE; wallTile.alpha = 0.98; self.wallTiles.push(wallTile); } } } } // Asegurar que WorldRenderer esté en el fondo, debajo de todo if (self.parent) { self.parent.addChildAt(self, 0); } }; return self; }); var WorldSelectionMenu = Container.expand(function () { var self = Container.call(this); // Position the menu at center of screen self.x = 2048 / 2; self.y = 2732 / 2; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 1800; menuBackground.height = 1200; menuBackground.tint = 0x333333; menuBackground.alpha = 0.9; var titleText = new Text2('Select World', { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -400; self.addChild(titleText); var worldNames = ["Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"]; var unlockedWorlds = storage.unlockedWorlds || 1; for (var i = 0; i < worldNames.length; i++) { var worldButton = new Container(); var worldButtonBg = worldButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); worldButtonBg.width = 350; worldButtonBg.height = 80; var isUnlocked = i + 1 <= unlockedWorlds; worldButtonBg.tint = isUnlocked ? 0x4444FF : 0x666666; var worldButtonText = new Text2(isUnlocked ? worldNames[i] : "Locked", { size: 40, fill: isUnlocked ? 0xFFFFFF : 0x999999, weight: 800 }); worldButtonText.anchor.set(0.5, 0.5); worldButton.addChild(worldButtonText); worldButton.y = -200 + i * 100; self.addChild(worldButton); (function (worldIndex, unlocked) { worldButton.down = function () { if (unlocked) { self.destroy(); var levelSelectionMenu = new LevelSelectionMenu(worldIndex + 1); game.addChild(levelSelectionMenu); } else { var notification = game.addChild(new Notification("Complete the previous world to unlock!")); notification.x = 2048 / 2; notification.y = 2732 / 2 + 200; } }; })(i, isUnlocked); } return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ // Language system var currentLanguage = storage.language || 'en'; var translations = { en: { firewallDefensors: 'Firewall Defensors', startGame: 'Start Game', tutorial: 'Tutorial', language: 'Language', selectWorld: 'Select World', selectLevel: 'Select Level', back: 'Back', locked: 'Locked', completeLevel: 'Complete previous level to unlock!', completeWorld: 'Complete the previous world to unlock!', nextWave: 'Next Wave', shop: 'Shop', bits: 'Bits', systemHealth: 'System Health', securityScore: 'Security Score', wave: 'Wave', upgrade: 'Upgrade', sell: 'Sell', maxLevel: 'Max Level', digivolve: 'Digivolve', owned: 'OWNED', notEnoughBits: 'Not enough bits!', cannotBuild: 'Cannot build here!', pathBlocked: 'Tower would block the path!', tutorialWelcome: 'Welcome to Automated Tutorial!\n\nWatch how the game is normally played...', tutorialStep1: 'These are the main game elements:\n- Bits (money) top left\n- System health bottom\n- Digimon towers bottom', tutorialStep2: 'Step 1: Place a tower\nThe cursor will show how to drag a tower...', tutorialStep3: 'Step 2: Start enemy wave\nNow we will activate the first wave...', tutorialStep4: 'Step 3: Upgrade towers\nClick on a tower to upgrade it...', tutorialCompleted: 'Tutorial completed!\n\nNow observe the game in action:\n- Towers attack automatically\n- You earn bits for defeating enemies\n- Use bits for more towers and upgrades', tutorialFinalMessage: 'Tutorial completed!\n\nNow you can:\n• Use "Next Wave" to start waves\n• Place and upgrade towers\n• Defend the digital world\n\nClick "Finish Tutorial" to continue!', finishTutorial: 'Finish Tutorial', skipTutorial: 'Skip Tutorial' }, es: { firewallDefensors: 'Defensores del Firewall', startGame: 'Iniciar Juego', tutorial: 'Tutorial', language: 'Idioma', selectWorld: 'Seleccionar Mundo', selectLevel: 'Seleccionar Nivel', back: 'Atrás', locked: 'Bloqueado', completeLevel: '¡Completa el nivel anterior para desbloquear!', completeWorld: '¡Completa el mundo anterior para desbloquear!', nextWave: 'Siguiente Oleada', shop: 'Tienda', bits: 'Bits', systemHealth: 'Salud del Sistema', securityScore: 'Puntuación de Seguridad', wave: 'Oleada', upgrade: 'Mejorar', sell: 'Vender', maxLevel: 'Nivel Máximo', digivolve: 'Digievolucionar', owned: 'POSEÍDO', notEnoughBits: '¡No tienes suficientes bits!', cannotBuild: '¡No se puede construir aquí!', pathBlocked: '¡La torreta bloquearía el camino!', tutorialWelcome: '¡Bienvenido al Tutorial Automático!\n\nObserva cómo se juega normalmente...', tutorialStep1: 'Estos son los elementos principales del juego:\n- Bits (dinero) arriba izquierda\n- Salud del sistema abajo\n- Torretas Digimon abajo', tutorialStep2: 'Paso 1: Colocar una torreta\nEl cursor mostrará cómo arrastrar una torreta...', tutorialStep3: 'Paso 2: Iniciar oleada de enemigos\nAhora activaremos la primera oleada...', tutorialStep4: 'Paso 3: Mejorar torretas\nHaz clic en una torreta para mejorarla...', tutorialCompleted: '¡Tutorial completado!\n\nAhora observa el juego en acción:\n- Las torretas atacan automáticamente\n- Ganas bits por derrotar enemigos\n- Usa los bits para más torretas y mejoras', tutorialFinalMessage: '¡Tutorial completado!\n\nAhora puedes:\n• Usar "Siguiente Oleada" para iniciar oleadas\n• Colocar y mejorar torretas\n• Defender el mundo digital\n\n¡Haz clic en "Finalizar Tutorial" para continuar!', finishTutorial: 'Finalizar Tutorial', skipTutorial: 'Saltar Tutorial' } }; function getText(key) { return translations[currentLanguage][key] || translations.en[key] || key; } function setLanguage(lang) { currentLanguage = lang; storage.language = lang; } // Assets adicionales para tiles específicos por mundo var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; var gold = 80; var lives = 20; var score = 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 = []; var coinSpawnTimer = 0; var goldText = new Text2(getText('bits') + ': ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); // Create health bar container var healthBarContainer = new Container(); var healthBarBg = healthBarContainer.attachAsset('healthBarOutline', { anchorX: 0.5, anchorY: 0.5 }); healthBarBg.width = 300; healthBarBg.height = 20; healthBarBg.tint = 0x000000; var healthBarFill = healthBarContainer.attachAsset('healthBar', { anchorX: 0.5, anchorY: 0.5 }); healthBarFill.width = 296; healthBarFill.height = 16; healthBarFill.tint = 0x00FF00; // Add health text label var livesText = new Text2(getText('systemHealth'), { size: 45, fill: 0xFFFFFF, weight: 800 }); livesText.anchor.set(0.5, 0.5); livesText.y = -35; healthBarContainer.addChild(livesText); var scoreText = new Text2(getText('securityScore') + ': ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; // Create speed control button var speedButton = new Container(); var speedButtonBg = speedButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); speedButtonBg.width = 150; speedButtonBg.height = 80; speedButtonBg.tint = 0x00AA00; var speedButtonText = new Text2('1x', { size: 50, fill: 0xFFFFFF, weight: 800 }); speedButtonText.anchor.set(0.5, 0.5); speedButton.addChild(speedButtonText); var gameSpeed = 1; var speedLevels = [1, 2, 4]; 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 if (gameSpeed === 2) { speedButtonBg.tint = 0xFFAA00; // Orange for 2x speed } else { speedButtonBg.tint = 0xFF0000; // Red for 4x speed } }; LK.gui.top.addChild(goldText); LK.gui.bottom.addChild(healthBarContainer); LK.gui.top.addChild(scoreText); LK.gui.top.addChild(speedButton); healthBarContainer.x = 0; healthBarContainer.y = -300; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; speedButton.x = 0; speedButton.y = topMargin; function updateUI() { goldText.setText(getText('bits') + ': ' + gold); scoreText.setText(getText('securityScore') + ': ' + score); // 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); 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; return false; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { 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) { 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 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 = (currentWave - 1) % 10 + 1; if (currentWave === 0) currentWorldWave = 0; waveCounterText.setText(getText('wave') + ': ' + currentWorldWave + '/10'); } var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); // Add shop button var shopButton = new Container(); var shopButtonBg = shopButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); shopButtonBg.width = 300; shopButtonBg.height = 100; shopButtonBg.tint = 0x4444FF; var shopButtonText = new Text2('Shop', { size: 50, fill: 0xFFFFFF, weight: 800 }); shopButtonText.anchor.set(0.5, 0.5); shopButton.addChild(shopButtonText); shopButton.x = 200; shopButton.y = 2732 - 100 + 20; game.addChild(shopButton); var digimonShop = new DigimonShop(); digimonShop.x = 2048 / 2; game.addChild(digimonShop); shopButton.down = function () { digimonShop.show(); }; 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); } 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 // Check if we entered a new world and regenerate maze (9 waves per world) var newWorld = Math.ceil(currentWave / 9); if (newWorld > currentWorld || currentWave === 1) { currentWorld = newWorld; if (currentWorld > 6) currentWorld = 6; 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); // World-specific notification messages var worldNames = ["", "Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"]; var worldNotification = game.addChild(new Notification("Welcome to Digital World " + currentWorld + ": " + worldNames[currentWorld] + "!")); worldNotification.x = 2048 / 2; worldNotification.y = grid.height - 100; } // Get wave type and enemy count from the wave indicator (based on current world wave) var worldWave = (currentWave - 1) % 10 + 1; 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 worldWave = (currentWave - 1) % 10 + 1; var isBossWave = worldWave === 10; if (isBossWave) { // Boss waves have just 1 giant enemy enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ FINAL BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var worldWave = (currentWave - 1) % 9 + 1; var enemy = new Enemy(waveType); // Make wave 10 boss giant with much more health if (worldWave === 10) { 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; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // All enemy types now spawn in the middle 6 tiles at the top spacing var gridWidth = 24; var midPoint = Math.floor(gridWidth / 2); // 12 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading enemy.cellX = spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; } } // Update enemies without speed multiplier (speed button only affects tower fire rate) for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; // Update enemy normally (no speed multiplier) if (enemy.update) enemy.update(); if (enemy.health <= 0) { 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; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " bits!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { LK.showGameOver(); } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } // 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); } } // Check for completion of current world (10 waves) or all worlds var worldWave = (currentWave - 1) % 10 + 1; if (worldWave >= 10 && enemies.length === 0 && !waveInProgress) { if (currentWorld >= 6) { // Completed all worlds LK.showYouWin(); } else { // Completed current world, show victory message var notification = game.addChild(new Notification("World " + currentWorld + " Complete! Advancing to next world...")); notification.x = 2048 / 2; notification.y = 2732 / 2; // Start next world after delay LK.setTimeout(function () { currentWorld++; currentWave = (currentWorld - 1) * 10; // Reset for next world waveIndicator.gameStarted = true; updateWaveCounter(); }, 3000); } } }; var mainMenu = new MainMenu(); game.addChild(mainMenu); game.startGame = function () { var worldSelectionMenu = new WorldSelectionMenu(); game.addChild(worldSelectionMenu); }; game.startWorld = function (worldNumber) { currentWorld = worldNumber; grid.generateMazeForWorld(currentWorld); mapVisualization.generateMazeForWorld(currentWorld); // Regenerar también el mapa de visualización grid.pathFind(); grid.renderDebug(); // Sincronizar mapas syncVisualizationMap(); worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); // Show story sequence before starting the world var storySequence = new StorySequence(worldNumber); game.addChild(storySequence); storySequence.onComplete = function () { waveIndicator.gameStarted = true; currentWave = (currentWorld - 1) * 10; waveTimer = nextWaveTime; }; }; game.startWorldLevel = function (worldNumber, levelNumber) { currentWorld = worldNumber; currentLevel = levelNumber; // Calculate the wave number based on world and level currentWave = (worldNumber - 1) * 10 + levelNumber; grid.generateMazeForWorld(currentWorld); mapVisualization.generateMazeForWorld(currentWorld); grid.pathFind(); grid.renderDebug(); // Sincronizar mapas syncVisualizationMap(); worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); // Show story sequence before starting the level var storySequence = new StorySequence(worldNumber); game.addChild(storySequence); storySequence.onComplete = function () { waveIndicator.gameStarted = true; waveTimer = nextWaveTime; }; }; game.startTutorial = function () { // Set up a simple tutorial level currentWorld = 1; currentLevel = 1; currentWave = 0; // Reset to 0 so tutorial starts at wave 0/9 // Update wave counter for tutorial - explicitly set to show 0/10 waveCounterText.setText('Wave: 0/10'); // Generate tutorial world grid.generateMazeForWorld(currentWorld); mapVisualization.generateMazeForWorld(currentWorld); grid.pathFind(); grid.renderDebug(); syncVisualizationMap(); worldRenderer.updateWorldGraphics(currentWorld, mapVisualization); // Show tutorial sequence var tutorialSequence = new TutorialSequence(); game.addChild(tutorialSequence); tutorialSequence.onComplete = function () { // After tutorial, ensure game is ready to start from wave 1 waveIndicator.gameStarted = true; // Tutorial complete - show next wave button var notification = game.addChild(new Notification("Tutorial complete! Use Next Wave button to start your first wave!")); notification.x = 2048 / 2; notification.y = grid.height - 150; }; };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
unlockedWorlds: 1,
completedWaves: 0,
digiviceC: false,
digiviceB: false,
digiviceA: false,
worldLevels: {},
language: "en"
});
/****
* 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);
// Splash damage to nearby enemies
var splashRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var splashDx = otherEnemy.x - self.targetEnemy.x;
var splashDy = otherEnemy.y - self.targetEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
// Apply splash damage (50% of original damage)
otherEnemy.health -= self.damage * 0.5;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
// Apply slow effect
// Make slow percentage scale with tower level (default 50%, up to 80% at max level)
var slowPct = 0.5;
if (self.sourceTowerLevel !== undefined) {
// Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
self.targetEnemy.slowDuration = 180; // Reset duration
}
}
} else if (self.type === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
var Coin = Container.expand(function (x, y, value) {
var self = Container.call(this);
self.value = value || 5;
self.x = x;
self.y = y;
self.collected = false;
self.walkSpeed = 0.5;
self.direction = Math.random() * Math.PI * 2;
self.changeDirectionTimer = 0;
var coinGraphics = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
coinGraphics.width = 30;
coinGraphics.height = 30;
coinGraphics.tint = 0xFFD700;
// Add coin value text
var valueText = new Text2(self.value.toString(), {
size: 20,
fill: 0x000000,
weight: 800
});
valueText.anchor.set(0.5, 0.5);
self.addChild(valueText);
self.update = function () {
if (self.collected) return;
// Change direction occasionally
self.changeDirectionTimer++;
if (self.changeDirectionTimer > 120) {
// Change direction every 2 seconds
self.direction = Math.random() * Math.PI * 2;
self.changeDirectionTimer = 0;
}
// Move in current direction
var newX = self.x + Math.cos(self.direction) * self.walkSpeed;
var newY = self.y + Math.sin(self.direction) * self.walkSpeed;
// Keep within playable bounds
var minX = grid.x + CELL_SIZE;
var maxX = grid.x + grid.cells.length * CELL_SIZE - CELL_SIZE;
var minY = grid.y + CELL_SIZE;
var maxY = grid.y + grid.cells[0].length * CELL_SIZE - CELL_SIZE;
if (newX < minX || newX > maxX) {
self.direction = Math.PI - self.direction; // Bounce horizontally
} else {
self.x = newX;
}
if (newY < minY || newY > maxY) {
self.direction = -self.direction; // Bounce vertically
} else {
self.y = newY;
}
// Check if player clicks on coin
};
self.down = function () {
if (!self.collected) {
self.collected = true;
setGold(gold + self.value);
// Add 10 security score points
score += 10;
updateUI();
var goldIndicator = new GoldIndicator(self.value, self.x, self.y);
game.addChild(goldIndicator);
// Remove coin from coins array
var coinIndex = coins.indexOf(self);
if (coinIndex !== -1) {
coins.splice(coinIndex, 1);
}
self.destroy();
}
};
return self;
});
var ComicPanel = Container.expand(function (imagePath, text) {
var self = Container.call(this);
// Panel background - larger panel
var panelBg = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
panelBg.width = 700;
panelBg.height = 500;
panelBg.tint = 0x222222;
panelBg.alpha = 0.9;
// Add border effect
var border = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
border.width = 710;
border.height = 510;
border.tint = 0x000000;
border.alpha = 0.8;
self.addChildAt(border, 0);
// Text area - larger and repositioned
var textBg = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
textBg.width = 660;
textBg.height = 160;
textBg.tint = 0xffffff;
textBg.alpha = 0.95;
textBg.y = 170;
self.addChild(textBg);
// Character image if provided - repositioned
if (imagePath) {
var characterImage = self.attachAsset(imagePath, {
anchorX: 0.5,
anchorY: 0.5
});
characterImage.width = 200;
characterImage.height = 200;
characterImage.y = -80;
self.addChild(characterImage);
}
// Story text - improved sizing and positioning
var storyText = new Text2(text, {
size: 28,
fill: 0x000000,
weight: 600
});
storyText.anchor.set(0.5, 0.5);
storyText.y = 170;
storyText.wordWrap = true;
storyText.wordWrapWidth = 600;
storyText.maxWidth = 600;
self.addChild(storyText);
// Scale animation entrance
self.scaleX = 0.3;
self.scaleY = 0.3;
self.alpha = 0;
self.show = function () {
tween(self, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 400,
easing: tween.backOut
});
};
self.hide = function (callback) {
tween(self, {
scaleX: 0.8,
scaleY: 0.8,
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onFinish: callback
});
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
// Removed number label to improve performance
// Numbers were causing lag due to text rendering overhead
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
// Renderizado visual desactivado para evitar interferencia con tiles del mundo
// Solo mantiene la lógica de pathfinding sin elementos visuales
// Los tiles del mundo se renderizan a través de WorldRenderer
// Actualizar las flechas solo si hay una torre seleccionada para debug
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
// Mostrar flechas solo para torres seleccionadas
while (debugArrows.length > data.targets.length) {
self.removeChild(debugArrows.pop());
}
for (var a = 0; a < data.targets.length; a++) {
var destination = data.targets[a];
var ox = destination.x - data.x;
var oy = destination.y - data.y;
var angle = Math.atan2(oy, ox);
if (!debugArrows[a]) {
debugArrows[a] = LK.getAsset('arrow', {
anchorX: -.5,
anchorY: 0.5
});
debugArrows[a].alpha = .5;
self.addChildAt(debugArrows[a], 1);
}
debugArrows[a].rotation = angle;
}
} else {
// Remover flechas si no hay torre seleccionada
self.removeArrows();
}
// Hacer el gráfico de celda invisible para no interferir con tiles
cellGraphics.alpha = 0;
};
});
var DigimonShop = Container.expand(function () {
var self = Container.call(this);
self.visible = false;
self.y = 2732;
var shopBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shopBackground.width = 2048;
shopBackground.height = 600;
shopBackground.tint = 0x222222;
shopBackground.alpha = 0.95;
var titleText = new Text2('Digimon Firewall Shop', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -250;
self.addChild(titleText);
// Create shop items container
var itemsContainer = new Container();
itemsContainer.y = -50;
self.addChild(itemsContainer);
// Digivice items data
var digiviceItems = [{
id: 'digiviceC',
name: 'Digivice C',
cost: 500,
description: 'Unlocks Champion Level'
}, {
id: 'digiviceB',
name: 'Digivice B',
cost: 2000,
description: 'Unlocks Ultimate Level'
}, {
id: 'digiviceA',
name: 'Digivice A',
cost: 8000,
description: 'Unlocks Mega Level'
}];
// Create shop item buttons
for (var i = 0; i < digiviceItems.length; i++) {
var item = digiviceItems[i];
var itemButton = new Container();
itemButton.x = -600 + i * 400;
itemButton.y = 0;
itemsContainer.addChild(itemButton);
var itemBg = itemButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
itemBg.width = 350;
itemBg.height = 200;
var itemNameText = new Text2(item.name, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
itemNameText.anchor.set(0.5, 0.5);
itemNameText.y = -50;
itemButton.addChild(itemNameText);
var itemDescText = new Text2(item.description, {
size: 35,
fill: 0xCCCCCC,
weight: 400
});
itemDescText.anchor.set(0.5, 0.5);
itemDescText.y = -10;
itemButton.addChild(itemDescText);
var itemCostText = new Text2(item.cost + ' bits', {
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 = gold >= itemData.cost;
if (owned) {
background.tint = 0x00AA00;
costText.setText('OWNED');
button.alpha = 0.7;
} else if (canAfford) {
background.tint = 0x4444FF;
costText.setText(itemData.cost + ' bits');
button.alpha = 1.0;
} else {
background.tint = 0x666666;
costText.setText(itemData.cost + ' bits');
button.alpha = 0.5;
}
};
button.down = function () {
var owned = storage[itemData.id] || false;
var canAfford = gold >= 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) {
setGold(gold - itemData.cost);
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 bits 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;
});
// This update method was incorrectly placed here and should be removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
}
effectGraphics.alpha = 0.7;
self.alpha = 0;
// Animate the effect
tween(self, {
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
// Base enemy class for common functionality
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'normal';
self.speed = .01;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this virus type
var assetId = 'virus';
if (self.type !== 'normal') {
assetId = 'virus_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale up boss enemies
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Fall back to regular enemy asset if specific type asset not found
// Apply tint to differentiate enemy types
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
break;
case 'immune':
enemyGraphics.tint = 0xAA0000; // Red for immune enemies
break;
case 'flying':
enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
break;
case 'swarm':
enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
break;
}*/
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
shadowGraphics.scaleX = 1.8;
shadowGraphics.scaleY = 1.8;
}
// Position shadow slightly offset
self.shadow.x = 20; // Offset right
self.shadow.y = 20; // Offset down
// Ensure shadow has the same rotation as the enemy
shadowGraphics.rotation = enemyGraphics.rotation;
}
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
self.update = function () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
var GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2("+" + value, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
self.cells = [];
self.spawns = [];
self.goals = [];
for (var i = 0; i < gridWidth; i++) {
self.cells[i] = [];
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j] = {
score: 0,
pathId: 0,
towersInRange: []
};
}
}
/*
Cell Types
0: Transparent floor
1: Wall
2: Spawn
3: Goal
*/
// Create world-based maze layout
self.generateMazeForWorld = function (worldNumber) {
// Clear existing maze
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j].type = 1; // Default to wall
}
}
self.spawns = [];
self.goals = [];
// Always create entry area (top 5 rows) with 3-block wide path
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j <= 4; j++) {
if (i >= 11 && i <= 13) {
self.cells[i][j].type = 0; // Open path in center (3 blocks wide)
if (j === 0) {
self.cells[i][j].type = 2; // Spawn points
self.spawns.push(self.cells[i][j]);
}
} else {
self.cells[i][j].type = 1; // Walls on sides
}
}
}
// Always create exit area (bottom 5 rows) with 3-block wide path
for (var i = 0; i < gridWidth; i++) {
for (var j = gridHeight - 5; j < gridHeight; j++) {
if (i >= 11 && i <= 13) {
self.cells[i][j].type = 0; // Open path in center (3 blocks wide)
if (j === gridHeight - 1) {
self.cells[i][j].type = 3; // Goal points
self.goals.push(self.cells[i][j]);
}
} else {
self.cells[i][j].type = 1; // Walls on sides
}
}
}
// Create classic square-cornered labyrinth with generous tower placement areas
// Simple pattern: Create a winding path with 90-degree turns and wide wall corridors
// Path layout: Start center-top, go down, turn right, down, turn left, repeat creating a serpentine pattern
// Main path coordinates (center of 3-block wide paths)
// Reduced to only 2 curves for a simpler path
var pathCenterX = 12; // Center column
var pathPattern = [
// Start: go down from entry
{
x: pathCenterX,
startY: 5,
endY: 12,
type: 'vertical'
},
// First curve: turn left
{
y: 12,
startX: pathCenterX,
endX: 4,
type: 'horizontal'
},
// Go down on left side
{
x: 4,
startY: 12,
endY: 20,
type: 'vertical'
},
// Second curve: turn right to center
{
y: 20,
startX: 4,
endX: pathCenterX,
type: 'horizontal'
},
// Final path to exit
{
x: pathCenterX,
startY: 20,
endY: gridHeight - 5,
type: 'vertical'
}];
// Crear los segmentos del camino con 3 bloques de grosor (camino) y dejar espacio extra de 1 bloque a cada lado para asegurar zonas de 2x2 para torretas
for (var p = 0; p < pathPattern.length; p++) {
var segment = pathPattern[p];
if (segment.type === 'vertical') {
var startY = Math.min(segment.startY, segment.endY);
var endY = Math.max(segment.startY, segment.endY);
for (var y = startY; y <= endY; y++) {
// Camino de 3 bloques de ancho
for (var offset = -1; offset <= 1; offset++) {
var pathX = segment.x + offset;
if (pathX >= 0 && pathX < gridWidth && y >= 0 && y < gridHeight) {
self.cells[pathX][y].type = 0;
}
}
// Dejar espacio extra de 1 bloque a cada lado del camino para permitir torretas 2x2
for (var extra = -2; extra <= 2; extra += 4) {
var sideX = segment.x + extra;
if (sideX >= 0 && sideX < gridWidth && y >= 0 && y < gridHeight) {
// Solo marcar como espacio de torre si no es camino ni spawn/goal
if (self.cells[sideX][y].type === 1) {
// No cambiar si ya es camino/spawn/goal
self.cells[sideX][y].type = 1; // Mantener como muro, pero dejarlo para posible torre
}
}
}
// Asegurar espacio de 2x2 para torretas a la izquierda y derecha del camino
for (var offsetY = 0; offsetY <= 1; offsetY++) {
for (var extra = -2; extra <= 2; extra += 4) {
var baseX = segment.x + extra;
var baseY = y + offsetY;
if (baseX >= 0 && baseX + 1 < gridWidth && baseY >= 0 && baseY + 1 < gridHeight) {
// Solo marcar como espacio de torre si ambos son muro
if (self.cells[baseX][baseY].type === 1 && self.cells[baseX + 1][baseY].type === 1 && self.cells[baseX][baseY + 1].type === 1 && self.cells[baseX + 1][baseY + 1].type === 1) {
// Marcar como espacio de torre (type 1) para permitir torretas 2x2
self.cells[baseX][baseY].type = 1;
self.cells[baseX + 1][baseY].type = 1;
self.cells[baseX][baseY + 1].type = 1;
self.cells[baseX + 1][baseY + 1].type = 1;
}
}
}
}
}
} else if (segment.type === 'horizontal') {
var startX = Math.min(segment.startX, segment.endX);
var endX = Math.max(segment.startX, segment.endX);
for (var x = startX; x <= endX; x++) {
// Camino de 3 bloques de alto
for (var offset = -1; offset <= 1; offset++) {
var pathY = segment.y + offset;
if (x >= 0 && x < gridWidth && pathY >= 0 && pathY < gridHeight) {
self.cells[x][pathY].type = 0;
}
}
// Dejar espacio extra de 1 bloque arriba y abajo del camino para permitir torretas 2x2
for (var extra = -2; extra <= 2; extra += 4) {
var sideY = segment.y + extra;
if (x >= 0 && x < gridWidth && sideY >= 0 && sideY < gridHeight) {
if (self.cells[x][sideY].type === 1) {
self.cells[x][sideY].type = 1;
}
}
}
// Asegurar espacio de 2x2 para torretas arriba y abajo del camino
for (var offsetX = 0; offsetX <= 1; offsetX++) {
for (var extra = -2; extra <= 2; extra += 4) {
var baseX = x + offsetX;
var baseY = segment.y + extra;
if (baseX >= 0 && baseX + 1 < gridWidth && baseY >= 0 && baseY + 1 < gridHeight) {
if (self.cells[baseX][baseY].type === 1 && self.cells[baseX + 1][baseY].type === 1 && self.cells[baseX][baseY + 1].type === 1 && self.cells[baseX + 1][baseY + 1].type === 1) {
// Marcar como espacio de torre (type 1) para permitir torretas 2x2
self.cells[baseX][baseY].type = 1;
self.cells[baseX + 1][baseY].type = 1;
self.cells[baseX][baseY + 1].type = 1;
self.cells[baseX + 1][baseY + 1].type = 1;
}
}
}
}
}
}
}
// Asegurar conexiones suaves en las intersecciones y dejar espacio de 2x2 para torret
};
// Generate maze for current world
var world = Math.ceil(currentWave / 9);
if (world < 1) world = 1;
if (world > 6) world = 6;
self.generateMazeForWorld(world);
// Apply the maze layout to cells
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellType = cell.type; // Use the type set by generateMazeForWorld
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
// Only create debug cells for visible areas and reduce frequency
if (j > 3 && j <= gridHeight - 4 && (i + j) % 2 === 0) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
// Simplified pathfinding - only check cardinal directions for better performance
var targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId != pathId) {
console.warn("Spawn blocked");
return true;
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// 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);
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;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget) {
enemy.currentTarget = cell.targets[0];
}
// 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;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var LevelSelectionMenu = Container.expand(function (worldNumber) {
var self = Container.call(this);
self.worldNumber = worldNumber;
// Position the menu at center of screen
self.x = 2048 / 2;
self.y = 2732 / 2;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 1800;
menuBackground.height = 1200;
menuBackground.tint = 0x333333;
menuBackground.alpha = 0.9;
var worldNames = ["", "Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"];
var titleText = new Text2('Select Level - ' + worldNames[worldNumber], {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -400;
self.addChild(titleText);
var worldLevels = storage.worldLevels || {
1: 1,
2: 1,
3: 1,
4: 1,
5: 1,
6: 1
};
var unlockedLevel = worldLevels[worldNumber] || 1;
// Create level buttons in a grid
var buttonsPerRow = 5;
var buttonWidth = 120;
var buttonHeight = 80;
var buttonSpacing = 160;
var startX = -((buttonsPerRow - 1) * buttonSpacing) / 2;
var startY = -200;
for (var i = 1; i <= 10; i++) {
var levelButton = new Container();
var levelButtonBg = levelButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
levelButtonBg.width = buttonWidth;
levelButtonBg.height = buttonHeight;
var isUnlocked = i <= unlockedLevel;
levelButtonBg.tint = isUnlocked ? 0x4444FF : 0x666666;
var levelButtonText = new Text2(isUnlocked ? i.toString() : "🔒", {
size: 40,
fill: isUnlocked ? 0xFFFFFF : 0x999999,
weight: 800
});
levelButtonText.anchor.set(0.5, 0.5);
levelButton.addChild(levelButtonText);
// Position buttons in grid
var row = Math.floor((i - 1) / buttonsPerRow);
var col = (i - 1) % buttonsPerRow;
levelButton.x = startX + col * buttonSpacing;
levelButton.y = startY + row * 120;
self.addChild(levelButton);
(function (levelIndex, unlocked) {
levelButton.down = function () {
if (unlocked) {
self.destroy();
game.startWorldLevel(self.worldNumber, levelIndex);
} else {
var notification = game.addChild(new Notification("Complete previous level to unlock!"));
notification.x = 2048 / 2;
notification.y = 2732 / 2 + 200;
}
};
})(i, isUnlocked);
}
// Back button
var backButton = new Container();
var backButtonBg = backButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
backButtonBg.width = 200;
backButtonBg.height = 80;
backButtonBg.tint = 0x666666;
var backButtonText = new Text2('Back', {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
backButtonText.anchor.set(0.5, 0.5);
backButton.addChild(backButtonText);
backButton.x = 0;
backButton.y = 400;
self.addChild(backButton);
backButton.down = function () {
self.destroy();
var worldSelectionMenu = new WorldSelectionMenu();
game.addChild(worldSelectionMenu);
};
return self;
});
var MainMenu = Container.expand(function () {
var self = Container.call(this);
// Position the menu at center of screen
self.x = 2048 / 2;
self.y = 2732 / 2;
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);
var startButton = new Container();
var startButtonBg = startButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startButtonBg.width = 400;
startButtonBg.height = 120;
startButtonBg.tint = 0x00AA00;
var startButtonText = new Text2(getText('startGame'), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
startButtonText.anchor.set(0.5, 0.5);
startButton.addChild(startButtonText);
startButton.y = 50;
self.addChild(startButton);
startButton.down = function () {
self.destroy();
game.startGame();
};
// Add tutorial button
var tutorialButton = new Container();
var tutorialButtonBg = tutorialButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
tutorialButtonBg.width = 400;
tutorialButtonBg.height = 120;
tutorialButtonBg.tint = 0x00AAAA;
var tutorialButtonText = new Text2(getText('tutorial'), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
tutorialButtonText.anchor.set(0.5, 0.5);
tutorialButton.addChild(tutorialButtonText);
tutorialButton.y = 180;
self.addChild(tutorialButton);
tutorialButton.down = function () {
self.destroy();
game.startTutorial();
};
// Add language button
var languageButton = new Container();
var languageButtonBg = languageButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
languageButtonBg.width = 400;
languageButtonBg.height = 120;
languageButtonBg.tint = 0xFF6600;
var languageButtonText = new Text2(getText('language'), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
languageButtonText.anchor.set(0.5, 0.5);
languageButton.addChild(languageButtonText);
languageButton.y = 310;
self.addChild(languageButton);
languageButton.down = function () {
// Toggle language between English and Spanish
var newLang = currentLanguage === 'en' ? 'es' : 'en';
setLanguage(newLang);
// Recreate main menu with new language
self.destroy();
var newMainMenu = new MainMenu();
game.addChild(newMainMenu);
};
return self;
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300;
buttonBackground.height = 100;
buttonBackground.tint = 0x0088FF;
var buttonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
// Check if we can start next wave (10 waves max per world)
var worldWave = (currentWave - 1) % 10 + 1;
if (currentWave === 0) worldWave = 0;
// Show button during tutorial or when can start next wave
var showForTutorial = waveIndicator && waveIndicator.gameStarted && currentWave === 0;
var showForNextWave = waveIndicator && waveIndicator.gameStarted && worldWave < 10 && !waveInProgress && enemies.length === 0;
if (showForTutorial || showForNextWave) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves && !waveInProgress && enemies.length === 0) {
currentWave++; // Increment to the next wave directly
// Calculate current world and level for 10-wave system
currentWorld = Math.ceil(currentWave / 10);
currentLevel = (currentWave - 1) % 10 + 1;
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentLevel);
var enemyCount = waveIndicator.getEnemyCount(currentLevel);
// Update wave counter display
updateWaveCounter();
var notification = game.addChild(new Notification("Wave " + currentLevel + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 0x000000,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'default';
// Increase size of base for easier touch
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'sniper':
baseGraphics.tint = 0xFF5500;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
case 'slow':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = -20 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -25; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var costLabelShadow = new Text2(towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 4;
costLabelShadow.y = 24 + 12;
self.addChild(costLabelShadow);
// Add cost label
var costLabel = new Text2(towerCost + ' bits', {
size: 60,
fill: 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 25 + 12;
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var StorySequence = Container.expand(function (worldNumber) {
var self = Container.call(this);
self.worldNumber = worldNumber;
self.currentPanel = 0;
self.panels = [];
self.onComplete = null;
// Position at center of screen
self.x = 2048 / 2;
self.y = 2732 / 2;
// Semi-transparent background overlay
var overlay = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
overlay.width = 2048;
overlay.height = 2732;
overlay.tint = 0x000000;
overlay.alpha = 0.7;
self.getWorldStory = function (worldNumber) {
switch (worldNumber) {
case 1:
return [{
text: "ALERT! Pokémon infiltrators have breached\nthe Digital Forest servers! They're attempting\nto steal Digimon data files!",
image: 'agumon'
}, {
text: "These Pokémon spies are using advanced\nstealth protocols to access our core database.\nDeploy Digimon guardians immediately!",
image: 'gabumon'
}, {
text: "The future of the Digimon franchise\ndepends on you! Stop Pokémon from\ncorrupting our digital ecosystem!",
image: 'tentomon'
}];
case 2:
return [{
text: "Pokémon agents have infiltrated the Desert\nData Center! They're planting malicious code\nto corrupt our systems!",
image: 'palmon'
}, {
text: "Fire-type Pokémon are overheating our\nservers while others steal precious\nDigimon evolution data!",
image: 'gomamon'
}, {
text: "Their coordinated attack is more sophisticated\nthan before. Pokémon want to monopolize\nthe children's entertainment industry!",
image: 'patamon'
}];
case 3:
return [{
text: "Ice-type Pokémon have frozen our Glacier\nservers to slow down our defenses\nwhile they extract data!",
image: null
}, {
text: "Flying Pokémon are bypassing our security\nwalls! They're trying to reach the core\nDigimon genetic database!",
image: null
}, {
text: "Critical system temperatures detected!\nPokémon are trying to cause a complete\nserver meltdown!",
image: null
}];
case 4:
return [{
text: "Pokémon sleeper agents hidden in the Village\nNetwork have activated! They've been\ngathering intelligence for months!",
image: null
}, {
text: "Multiple Pokémon strike teams are attacking\nsimultaneously, trying to overwhelm\nour Digimon defenders!",
image: null
}, {
text: "This is corporate espionage on a massive\nscale! Pokémon Company wants to steal\nour digital creature technology!",
image: null
}];
case 5:
return [{
text: "MAXIMUM THREAT LEVEL! Elite Pokémon\nhackers have breached our most secure\nTechnology Labs!",
image: null
}, {
text: "They're using legendary Pokémon abilities\nto bypass our quantum encryption!\nOur most sensitive data is at risk!",
image: null
}, {
text: "Deploy our strongest Mega-level Digimon!\nOnly they can stop this corporate\ncyber warfare!",
image: null
}];
case 6:
return [{
text: "FINAL ASSAULT! Pokémon's master plan\nis revealed - they want to delete ALL\nDigimon data permanently!",
image: null
}, {
text: "Legendary Pokémon themselves are leading\nthis final attack on our core servers!\nThis is the ultimate battle for supremacy!",
image: null
}, {
text: "The children's hearts are at stake!\nDefeat Pokémon's invasion and save\nthe future of digital monsters forever!",
image: null
}];
default:
return [{
text: "Pokémon infiltrators detected! Protect the Digimon database!",
image: null
}, {
text: "Deploy your Digimon to stop the corporate espionage!",
image: null
}, {
text: "Save the Digital World from Pokémon's takeover!",
image: null
}];
}
};
// World-specific story content
var storyData = self.getWorldStory(worldNumber);
// Create panels
for (var i = 0; i < storyData.length; i++) {
var panel = new ComicPanel(storyData[i].image, storyData[i].text);
panel.x = (i - 1) * 650; // Position panels side by side
self.addChild(panel);
self.panels.push(panel);
}
// Navigation indicators
var indicatorContainer = new Container();
indicatorContainer.y = 250;
self.addChild(indicatorContainer);
for (var i = 0; i < self.panels.length; i++) {
var indicator = indicatorContainer.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = 20;
indicator.height = 20;
indicator.tint = i === 0 ? 0xffffff : 0x666666;
indicator.x = (i - (self.panels.length - 1) / 2) * 40;
}
// Skip button
var skipButton = new Container();
var skipBg = skipButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
skipBg.width = 150;
skipBg.height = 60;
skipBg.tint = 0x666666;
var skipText = new Text2('Skip', {
size: 40,
fill: 0xffffff,
weight: 800
});
skipText.anchor.set(0.5, 0.5);
skipButton.addChild(skipText);
skipButton.x = 400;
skipButton.y = -300;
self.addChild(skipButton);
skipButton.down = function () {
self.complete();
};
self.showPanel = function (index) {
if (index < 0 || index >= self.panels.length) return;
// Hide all panels
for (var i = 0; i < self.panels.length; i++) {
self.panels[i].alpha = 0;
indicatorContainer.children[i].tint = 0x666666;
}
// Show current panel
self.panels[index].show();
indicatorContainer.children[index].tint = 0xffffff;
self.currentPanel = index;
};
self.nextPanel = function () {
if (self.currentPanel < self.panels.length - 1) {
self.showPanel(self.currentPanel + 1);
} else {
self.complete();
}
};
self.complete = function () {
if (self.onComplete) {
self.onComplete();
}
self.destroy();
};
// Show first panel
self.showPanel(0);
// Auto-advance after 4 seconds or click to advance
var autoAdvanceTimer = LK.setTimeout(function () {
self.nextPanel();
}, 4000);
self.down = function () {
LK.clearTimeout(autoAdvanceTimer);
autoAdvanceTimer = LK.setTimeout(function () {
self.nextPanel();
}, 4000);
self.nextPanel();
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// 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 'gabumon':
//{8L} // Rapid fire Digimon
self.fireRate = 30;
self.damage = 5;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'tentomon':
//{8N} // Long range Digimon
self.fireRate = 90;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'palmon':
//{8P} // Area damage Digimon
self.fireRate = 75;
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'gomamon':
//{8R} // Slowing Digimon
self.fireRate = 50;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'patamon':
//{8U} // Poison/status Digimon
self.fireRate = 70;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
switch (self.id) {
case '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;
}
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++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
if (self.id === 'rapid') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect
self.damage = 5 + self.level * 10; // double the effect
self.bulletSpeed = 7 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades
self.damage = 5 + self.level * 3;
self.bulletSpeed = 7 + self.level * 0.7;
}
} else {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for all other towers (double the effect)
self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect
self.damage = 10 + self.level * 20; // double the effect
self.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough 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 () {
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;
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 () {
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
bullet.type = self.id;
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
// 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);
self.currentStep = 0;
self.onComplete = null;
self.tutorialActive = true;
self.autoSequenceRunning = false;
// 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 = 900;
instructionBg.height = 250;
instructionBg.tint = 0x222222;
instructionBg.alpha = 0.95;
instructionBg.x = 2048 / 2;
instructionBg.y = 300;
var instructionText = new Text2(getText('tutorialWelcome'), {
size: 55,
fill: 0xFFFFFF,
weight: 600
});
instructionText.anchor.set(0.5, 0.5);
instructionText.wordWrap = true;
instructionText.wordWrapWidth = 850;
instructionText.x = 2048 / 2;
instructionText.y = 300;
self.addChild(instructionText);
// Animated cursor for showing actions
var cursor = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
cursor.width = 25;
cursor.height = 25;
cursor.tint = 0xFFFF00;
cursor.visible = false;
// Next button for manual progression
var nextButton = new Container();
var nextBg = nextButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
nextBg.width = 200;
nextBg.height = 80;
nextBg.tint = 0x00AA00;
var nextText = new Text2('Next', {
size: 40,
fill: 0xffffff,
weight: 800
});
nextText.anchor.set(0.5, 0.5);
nextButton.addChild(nextText);
nextButton.x = 2048 / 2;
nextButton.y = 500;
self.addChild(nextButton);
nextButton.visible = false;
nextButton.down = function () {
self.progressTutorial();
};
// 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);
skipButton.down = function () {
self.complete();
};
// Start the tutorial sequence with first step
LK.setTimeout(function () {
self.showStep1();
}, 1000);
self.showStep1 = function () {
instructionText.setText(getText('tutorialStep1'));
nextButton.visible = true;
self.currentTutorialStep = 1;
};
self.showStep2 = function () {
instructionText.setText(getText('tutorialStep2'));
nextButton.visible = false;
self.currentTutorialStep = 2;
self.demonstrateTowerPlacement();
};
self.showStep3 = function () {
instructionText.setText(getText('tutorialStep3'));
nextButton.visible = false;
self.currentTutorialStep = 3;
self.demonstrateNextWave();
};
self.showStep4 = function () {
instructionText.setText(getText('tutorialStep4'));
nextButton.visible = false;
self.currentTutorialStep = 4;
self.demonstrateTowerUpgrade();
};
self.showFinalStep = function () {
instructionText.setText(getText('tutorialCompleted'));
cursor.visible = false;
nextButton.visible = true;
self.currentTutorialStep = 5;
};
self.progressTutorial = function () {
switch (self.currentTutorialStep) {
case 1:
self.showStep2();
break;
case 2:
// Step 2 is handled automatically after tower placement
break;
case 3:
// Step 3 is handled automatically after wave start
break;
case 4:
// Step 4 is handled automatically after upgrade
break;
case 5:
self.finishTutorial();
break;
}
};
self.startAutoSequence = function () {
// This function is now replaced by manual progression
};
self.demonstrateTowerPlacement = function () {
instructionText.setText(getText('tutorialStep2'));
cursor.visible = true;
// Animate cursor from source tower to placement position
var sourceTowerX = 2048 / 2 - 640;
var sourceTowerY = 2732 - 180;
var placementX = grid.x + 6 * CELL_SIZE + CELL_SIZE / 2;
var placementY = grid.y + 10 * CELL_SIZE + CELL_SIZE / 2;
cursor.x = sourceTowerX;
cursor.y = sourceTowerY;
// Scale cursor to show it's "grabbing" the tower
tween(cursor, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
onFinish: function onFinish() {
// Move cursor to placement location with smooth curve
tween(cursor, {
x: placementX,
y: placementY
}, {
duration: 3000,
// Slower movement for better visibility
easing: tween.easeInOut,
onFinish: function onFinish() {
// Scale down to show "placing"
tween(cursor, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 500,
//{ls} // Slower scaling animation
onFinish: function onFinish() {
// Actually place the tower
if (placeTower(6, 10, 'agumon')) {
LK.setTimeout(function () {
self.showStep3();
}, 2000); // Show next step after placement
}
}
});
}
});
}
});
};
self.demonstrateNextWave = function () {
instructionText.setText(getText('tutorialStep3'));
// Move cursor to next wave button
var nextWaveX = 2048 - 200;
var nextWaveY = 2732 - 100;
tween(cursor, {
x: nextWaveX,
y: nextWaveY
}, {
duration: 2500,
//{lF} // Slower cursor movement
easing: tween.easeInOut,
onFinish: function onFinish() {
// Scale cursor to show clicking
tween(cursor, {
scaleX: 1.8,
scaleY: 1.8
}, {
duration: 400,
//{lK} // Slower scaling
onFinish: function onFinish() {
tween(cursor, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 400,
//{lQ} // Slower scaling
onFinish: function onFinish() {
// Start the first wave
if (!waveIndicator.gameStarted) {
waveIndicator.gameStarted = true;
currentWave++;
currentWorld = Math.ceil(currentWave / 9);
currentLevel = (currentWave - 1) % 9 + 1;
waveTimer = 0;
waveInProgress = true;
waveSpawned = false;
updateWaveCounter();
}
LK.setTimeout(function () {
self.showStep4();
}, 3000); // Continue to upgrade step
}
});
}
});
}
});
};
self.demonstrateTowerUpgrade = function () {
instructionText.setText(getText('tutorialStep4'));
// Find the first tower to demonstrate upgrade
if (towers.length > 0) {
var tower = towers[0];
// Auto-upgrade the tower during tutorial to level 3
var _autoUpgradeLevel = function autoUpgradeLevel(targetLevel) {
if (tower.level < targetLevel && tower.level < tower.maxLevel) {
// Give enough gold for upgrade
var baseUpgradeCost = getTowerCost(tower.id);
var upgradeCost;
if (tower.level === tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, tower.level - 1));
}
if (gold < upgradeCost) {
setGold(gold + upgradeCost);
}
// Upgrade the tower
tower.upgrade();
// Continue upgrading until target level
LK.setTimeout(function () {
_autoUpgradeLevel(targetLevel);
}, 800); // Slower upgrade timing for visibility
}
};
// Start auto-upgrading to level 3
_autoUpgradeLevel(3);
tween(cursor, {
x: tower.x,
y: tower.y
}, {
duration: 2000,
//{ma} // Slower cursor movement
easing: tween.easeInOut,
onFinish: function onFinish() {
// Simulate clicking on tower
tween(cursor, {
scaleX: 2.0,
scaleY: 2.0
}, {
duration: 300,
//{mf} // Slower animation
onFinish: function onFinish() {
tween(cursor, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
//{ml} // Slower animation
onFinish: function onFinish() {
// Trigger tower upgrade menu
if (tower.down) {
tower.down(tower.x, tower.y, {});
}
LK.setTimeout(function () {
self.showFinalStep();
}, 4000); // Continue to final step while keeping upgrade menu visible
}
});
}
});
}
});
} else {
LK.setTimeout(function () {
self.demonstrateGameplayLoop();
}, 2000); // Longer delay
}
};
self.demonstrateGameplayLoop = function () {
// This function is now replaced by showFinalStep
};
self.finishTutorial = function () {
instructionText.setText(getText('tutorialFinalMessage'));
// Add finish tutorial button
var finishButton = new Container();
var finishBg = finishButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
finishBg.width = 400;
finishBg.height = 100;
finishBg.tint = 0x00AA00;
var finishText = new Text2(getText('finishTutorial'), {
size: 45,
fill: 0xffffff,
weight: 800
});
finishText.anchor.set(0.5, 0.5);
finishButton.addChild(finishText);
finishButton.x = 2048 / 2;
finishButton.y = 500;
self.addChild(finishButton);
finishButton.down = function () {
self.complete();
};
// Remove auto-complete timer - user must click finish button
};
self.complete = function () {
self.tutorialActive = false;
self.autoSequenceRunning = false;
// 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);
}
}
// 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();
// Return to main menu after tutorial
var mainMenu = new MainMenu();
game.addChild(mainMenu);
};
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 + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' 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);
// Create digivolve button
var digivolveButton = new Container();
buttonsContainer.addChild(digivolveButton);
var digivolveButtonBackground = digivolveButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
digivolveButtonBackground.width = 500;
digivolveButtonBackground.height = 150;
var digivolveButtonText = new Text2('Digivolve', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
digivolveButtonText.anchor.set(0.5, 0.5);
digivolveButton.addChild(digivolveButtonText);
// Check if digivolution is available
function canDigivolve() {
if (self.tower.level < 2) return false; // Need at least level 2
var hasDigivice = false;
if (self.tower.level >= 2 && self.tower.level <= 3 && storage.digiviceC) hasDigivice = true;
if (self.tower.level >= 4 && self.tower.level <= 5 && storage.digiviceB) hasDigivice = true;
if (self.tower.level >= 6 && storage.digiviceA) hasDigivice = true;
return hasDigivice;
}
digivolveButton.update = function () {
var canEvolve = canDigivolve();
digivolveButton.visible = canEvolve;
if (canEvolve) {
digivolveButtonBackground.tint = 0xFF6600;
} else {
digivolveButtonBackground.tint = 0x666666;
}
};
digivolveButton.down = function () {
if (canDigivolve()) {
var evolutionCost = getTowerCost(self.tower.id) * 2;
if (gold >= evolutionCost) {
setGold(gold - evolutionCost);
// Apply evolution effects
self.tower.damage *= 1.5;
self.tower.fireRate = Math.max(5, Math.floor(self.tower.fireRate * 0.8));
var notification = game.addChild(new Notification(self.tower.id + " digivolved!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Update stats display
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
} else {
var notification = game.addChild(new Notification("Not enough bits to digivolve!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
};
upgradeButton.y = -125;
digivolveButton.y = 0;
sellButton.y = 125;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' bits');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' bits');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " bits!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 0;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
// Sincronizar el mapa de visualización después de vender torre
syncVisualizationMap();
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' bits';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2;
startBlock.tint = 0x00AA00;
// Add shadow for start text
var startTextShadow = new Text2("Start Game", {
size: 50,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 4;
startTextShadow.y = 4;
startMarker.addChild(startTextShadow);
var startText = new Text2("Start Game", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startMarker.addChild(startText);
startMarker.x = -self.indicatorWidth;
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['normal', 'fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Flying";
block.tint = 0xFFFF00;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
waveType = "Boss Normal";
break;
case 'fast':
block.tint = 0x00AAFF;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0xAA0000;
waveType = "Boss Immune";
break;
case 'flying':
block.tint = 0xFFFF00;
waveType = "Boss Flying";
break;
}
}
enemyCount = 1;
// Make the wave indicator for boss waves stand out
// Set boss wave color to the color of the wave type
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
break;
case 'fast':
block.tint = 0x00AAFF;
break;
case 'immune':
block.tint = 0xAA0000;
break;
case 'flying':
block.tint = 0xFFFF00;
break;
default:
block.tint = 0xFF0000;
break;
}
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave && enemyType !== 'swarm') {
// Add a crown or some indicator to the wave marker for boss waves
var bossIndicator = marker.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
bossIndicator.width = 30;
bossIndicator.height = 30;
bossIndicator.tint = 0xFFD700; // Gold color
bossIndicator.y = -block.height / 2 - 15;
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 4;
waveTypeShadow.y = 4;
marker.addChild(waveTypeShadow);
// Add wave type text - 30% smaller than before
var waveTypeText = new Text2(waveType, {
size: 56,
fill: 0xFFFFFF,
weight: 800
});
waveTypeText.anchor.set(0.5, 0.5);
waveTypeText.y = 0;
marker.addChild(waveTypeText);
// Add shadow for wave number - 20% larger than before
var waveNumShadow = new Text2((i + 1).toString(), {
size: 48,
fill: 0x000000,
weight: 800
});
waveNumShadow.anchor.set(1.0, 1.0);
waveNumShadow.x = blockWidth / 2 - 16 + 5;
waveNumShadow.y = block.height / 2 - 12 + 5;
marker.addChild(waveNumShadow);
// Main wave number text - 20% larger than before
var waveNum = new Text2((i + 1).toString(), {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
waveNum.anchor.set(1.0, 1.0);
waveNum.x = blockWidth / 2 - 16;
waveNum.y = block.height / 2 - 12;
marker.addChild(waveNum);
marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
self.addChild(marker);
self.waveMarkers.push(marker);
}
// Get wave type for a specific wave number
self.getWaveType = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return "normal";
}
// If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType
// then we should return a different boss type
var waveType = self.waveTypes[waveNumber - 1];
return waveType;
};
// Get enemy count for a specific wave number
self.getEnemyCount = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return 10;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Add boss prefix for boss waves (every 10th wave)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
// Fix positioning to properly show first wave when currentWave is 0
var displayWave = Math.max(0, currentWave);
var moveAmount = (progress + displayWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
// Fix comparison to properly handle wave 0 and 1
if (i - 1 < displayWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
// Waves no longer advance automatically - they must be triggered manually via NextWaveButton
// This function now only handles the initial game start
};
self.handleWaveProgression();
};
return self;
});
var WorldRenderer = Container.expand(function () {
var self = Container.call(this);
self.currentWorld = 1;
self.backgroundTiles = [];
self.pathTiles = [];
self.wallTiles = [];
self.sceneryElements = [];
self.getWorldAssets = function (worldNumber) {
switch (worldNumber) {
case 1:
return {
background: 'forestBg',
path: 'forestPath',
wall: 'forestWall',
scenery: 'forestScenery',
ambient: 0x90ee90
};
case 2:
return {
background: 'desertBg',
path: 'desertPath',
wall: 'desertWall',
scenery: 'desertScenery',
ambient: 0xffd700
};
case 3:
return {
background: 'glacierBg',
path: 'glacierPath',
wall: 'glacierWall',
scenery: 'glacierScenery',
ambient: 0xe6f3ff
};
case 4:
return {
background: 'villageBg',
path: 'villagePath',
wall: 'villageWall',
scenery: 'villageScenery',
ambient: 0xf0e68c
};
case 5:
return {
background: 'techLabBg',
path: 'techLabPath',
wall: 'techLabWall',
scenery: 'techLabScenery',
ambient: 0x87ceeb
};
case 6:
return {
background: 'infernoBg',
path: 'infernoPath',
wall: 'infernoWall',
scenery: 'infernoScenery',
ambient: 0xff6347
};
default:
return {
background: 'forestBg',
path: 'forestPath',
wall: 'forestWall',
scenery: 'forestScenery',
ambient: 0x90ee90
};
}
};
self.updateWorldGraphics = function (worldNumber, gridInstance) {
// Forzar renderizado siempre, incluso si el mundo no cambia
self.currentWorld = worldNumber;
var worldAssets = self.getWorldAssets(worldNumber);
// Clear existing tiles
while (self.backgroundTiles.length) {
self.removeChild(self.backgroundTiles.pop());
}
while (self.pathTiles.length) {
self.removeChild(self.pathTiles.pop());
}
while (self.wallTiles.length) {
self.removeChild(self.wallTiles.pop());
}
while (self.sceneryElements.length) {
self.removeChild(self.sceneryElements.pop());
}
// Create new tiles based on current world using the correct assets
var gridWidth = 24;
var gridHeight = 35;
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
// Create background tile using world-specific assets
var bgTile = self.attachAsset(worldAssets.background, {
anchorX: 0,
anchorY: 0
});
bgTile.x = i * CELL_SIZE;
bgTile.y = j * CELL_SIZE;
self.backgroundTiles.push(bgTile);
// Add world-specific scenery elements randomly
if (Math.random() < 0.15) {
// 15% chance for scenery - use world-specific scenery assets
var scenery = self.attachAsset(worldAssets.scenery, {
anchorX: 0.5,
anchorY: 0.5
});
scenery.x = i * CELL_SIZE + CELL_SIZE / 2;
scenery.y = j * CELL_SIZE + CELL_SIZE / 2;
// World-specific scenery shapes
switch (worldNumber) {
case 1:
// Forest - trees/bushes (ellipses)
scenery.scaleY = 1.2 + Math.random() * 0.8;
break;
case 2:
// Desert - cacti/rocks (tall thin or wide)
if (Math.random() < 0.5) {
scenery.scaleX = 0.6;
scenery.scaleY = 1.8; // Tall cactus
} else {
scenery.scaleX = 1.4;
scenery.scaleY = 0.7; // Wide rock
}
break;
case 3:
// Glacier - ice crystals
scenery.scaleX = 0.8 + Math.random() * 0.4;
scenery.scaleY = 0.8 + Math.random() * 0.4;
scenery.alpha = 0.7;
break;
case 4:
// Village - small structures
scenery.scaleX = 1.2;
scenery.scaleY = 1.0;
break;
case 5:
// Technology - machinery
scenery.scaleX = 0.9;
scenery.scaleY = 0.9;
scenery.alpha = 0.8;
break;
case 6:
// Hell - lava bubbles/flames
scenery.scaleX = 1.1 + Math.random() * 0.6;
scenery.scaleY = 1.1 + Math.random() * 0.6;
scenery.alpha = 0.9;
break;
}
self.sceneryElements.push(scenery);
}
}
}
// Overlay path and wall tiles on top of scenery
if (gridInstance && gridInstance.cells) {
for (var i = 0; i < Math.min(gridWidth, gridInstance.cells.length); i++) {
for (var j = 0; j < Math.min(gridHeight, gridInstance.cells[i].length); j++) {
var cell = gridInstance.cells[i][j];
if (cell.type === 0 || cell.type === 2 || cell.type === 3) {
// Path, spawn, or goal - use world-specific path assets
var pathTile = self.attachAsset(worldAssets.path, {
anchorX: 0,
anchorY: 0
});
pathTile.x = i * CELL_SIZE;
pathTile.y = j * CELL_SIZE;
pathTile.alpha = 0.95;
self.pathTiles.push(pathTile);
} else if (cell.type === 1) {
// Wall - use appropriate wall tile based on world
var wallTile = self.attachAsset(worldAssets.wall, {
anchorX: 0,
anchorY: 0
});
wallTile.x = i * CELL_SIZE;
wallTile.y = j * CELL_SIZE;
wallTile.alpha = 0.98;
self.wallTiles.push(wallTile);
}
}
}
}
// Asegurar que WorldRenderer esté en el fondo, debajo de todo
if (self.parent) {
self.parent.addChildAt(self, 0);
}
};
return self;
});
var WorldSelectionMenu = Container.expand(function () {
var self = Container.call(this);
// Position the menu at center of screen
self.x = 2048 / 2;
self.y = 2732 / 2;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 1800;
menuBackground.height = 1200;
menuBackground.tint = 0x333333;
menuBackground.alpha = 0.9;
var titleText = new Text2('Select World', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -400;
self.addChild(titleText);
var worldNames = ["Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"];
var unlockedWorlds = storage.unlockedWorlds || 1;
for (var i = 0; i < worldNames.length; i++) {
var worldButton = new Container();
var worldButtonBg = worldButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
worldButtonBg.width = 350;
worldButtonBg.height = 80;
var isUnlocked = i + 1 <= unlockedWorlds;
worldButtonBg.tint = isUnlocked ? 0x4444FF : 0x666666;
var worldButtonText = new Text2(isUnlocked ? worldNames[i] : "Locked", {
size: 40,
fill: isUnlocked ? 0xFFFFFF : 0x999999,
weight: 800
});
worldButtonText.anchor.set(0.5, 0.5);
worldButton.addChild(worldButtonText);
worldButton.y = -200 + i * 100;
self.addChild(worldButton);
(function (worldIndex, unlocked) {
worldButton.down = function () {
if (unlocked) {
self.destroy();
var levelSelectionMenu = new LevelSelectionMenu(worldIndex + 1);
game.addChild(levelSelectionMenu);
} else {
var notification = game.addChild(new Notification("Complete the previous world to unlock!"));
notification.x = 2048 / 2;
notification.y = 2732 / 2 + 200;
}
};
})(i, isUnlocked);
}
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
// Language system
var currentLanguage = storage.language || 'en';
var translations = {
en: {
firewallDefensors: 'Firewall Defensors',
startGame: 'Start Game',
tutorial: 'Tutorial',
language: 'Language',
selectWorld: 'Select World',
selectLevel: 'Select Level',
back: 'Back',
locked: 'Locked',
completeLevel: 'Complete previous level to unlock!',
completeWorld: 'Complete the previous world to unlock!',
nextWave: 'Next Wave',
shop: 'Shop',
bits: 'Bits',
systemHealth: 'System Health',
securityScore: 'Security Score',
wave: 'Wave',
upgrade: 'Upgrade',
sell: 'Sell',
maxLevel: 'Max Level',
digivolve: 'Digivolve',
owned: 'OWNED',
notEnoughBits: 'Not enough bits!',
cannotBuild: 'Cannot build here!',
pathBlocked: 'Tower would block the path!',
tutorialWelcome: 'Welcome to Automated Tutorial!\n\nWatch how the game is normally played...',
tutorialStep1: 'These are the main game elements:\n- Bits (money) top left\n- System health bottom\n- Digimon towers bottom',
tutorialStep2: 'Step 1: Place a tower\nThe cursor will show how to drag a tower...',
tutorialStep3: 'Step 2: Start enemy wave\nNow we will activate the first wave...',
tutorialStep4: 'Step 3: Upgrade towers\nClick on a tower to upgrade it...',
tutorialCompleted: 'Tutorial completed!\n\nNow observe the game in action:\n- Towers attack automatically\n- You earn bits for defeating enemies\n- Use bits for more towers and upgrades',
tutorialFinalMessage: 'Tutorial completed!\n\nNow you can:\n• Use "Next Wave" to start waves\n• Place and upgrade towers\n• Defend the digital world\n\nClick "Finish Tutorial" to continue!',
finishTutorial: 'Finish Tutorial',
skipTutorial: 'Skip Tutorial'
},
es: {
firewallDefensors: 'Defensores del Firewall',
startGame: 'Iniciar Juego',
tutorial: 'Tutorial',
language: 'Idioma',
selectWorld: 'Seleccionar Mundo',
selectLevel: 'Seleccionar Nivel',
back: 'Atrás',
locked: 'Bloqueado',
completeLevel: '¡Completa el nivel anterior para desbloquear!',
completeWorld: '¡Completa el mundo anterior para desbloquear!',
nextWave: 'Siguiente Oleada',
shop: 'Tienda',
bits: 'Bits',
systemHealth: 'Salud del Sistema',
securityScore: 'Puntuación de Seguridad',
wave: 'Oleada',
upgrade: 'Mejorar',
sell: 'Vender',
maxLevel: 'Nivel Máximo',
digivolve: 'Digievolucionar',
owned: 'POSEÍDO',
notEnoughBits: '¡No tienes suficientes bits!',
cannotBuild: '¡No se puede construir aquí!',
pathBlocked: '¡La torreta bloquearía el camino!',
tutorialWelcome: '¡Bienvenido al Tutorial Automático!\n\nObserva cómo se juega normalmente...',
tutorialStep1: 'Estos son los elementos principales del juego:\n- Bits (dinero) arriba izquierda\n- Salud del sistema abajo\n- Torretas Digimon abajo',
tutorialStep2: 'Paso 1: Colocar una torreta\nEl cursor mostrará cómo arrastrar una torreta...',
tutorialStep3: 'Paso 2: Iniciar oleada de enemigos\nAhora activaremos la primera oleada...',
tutorialStep4: 'Paso 3: Mejorar torretas\nHaz clic en una torreta para mejorarla...',
tutorialCompleted: '¡Tutorial completado!\n\nAhora observa el juego en acción:\n- Las torretas atacan automáticamente\n- Ganas bits por derrotar enemigos\n- Usa los bits para más torretas y mejoras',
tutorialFinalMessage: '¡Tutorial completado!\n\nAhora puedes:\n• Usar "Siguiente Oleada" para iniciar oleadas\n• Colocar y mejorar torretas\n• Defender el mundo digital\n\n¡Haz clic en "Finalizar Tutorial" para continuar!',
finishTutorial: 'Finalizar Tutorial',
skipTutorial: 'Saltar Tutorial'
}
};
function getText(key) {
return translations[currentLanguage][key] || translations.en[key] || key;
}
function setLanguage(lang) {
currentLanguage = lang;
storage.language = lang;
}
// Assets adicionales para tiles específicos por mundo
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
var gold = 80;
var lives = 20;
var score = 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 = [];
var coinSpawnTimer = 0;
var goldText = new Text2(getText('bits') + ': ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
// Create health bar container
var healthBarContainer = new Container();
var healthBarBg = healthBarContainer.attachAsset('healthBarOutline', {
anchorX: 0.5,
anchorY: 0.5
});
healthBarBg.width = 300;
healthBarBg.height = 20;
healthBarBg.tint = 0x000000;
var healthBarFill = healthBarContainer.attachAsset('healthBar', {
anchorX: 0.5,
anchorY: 0.5
});
healthBarFill.width = 296;
healthBarFill.height = 16;
healthBarFill.tint = 0x00FF00;
// Add health text label
var livesText = new Text2(getText('systemHealth'), {
size: 45,
fill: 0xFFFFFF,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
livesText.y = -35;
healthBarContainer.addChild(livesText);
var scoreText = new Text2(getText('securityScore') + ': ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
// Create speed control button
var speedButton = new Container();
var speedButtonBg = speedButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
speedButtonBg.width = 150;
speedButtonBg.height = 80;
speedButtonBg.tint = 0x00AA00;
var speedButtonText = new Text2('1x', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
speedButtonText.anchor.set(0.5, 0.5);
speedButton.addChild(speedButtonText);
var gameSpeed = 1;
var speedLevels = [1, 2, 4];
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 if (gameSpeed === 2) {
speedButtonBg.tint = 0xFFAA00; // Orange for 2x speed
} else {
speedButtonBg.tint = 0xFF0000; // Red for 4x speed
}
};
LK.gui.top.addChild(goldText);
LK.gui.bottom.addChild(healthBarContainer);
LK.gui.top.addChild(scoreText);
LK.gui.top.addChild(speedButton);
healthBarContainer.x = 0;
healthBarContainer.y = -300;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
speedButton.x = 0;
speedButton.y = topMargin;
function updateUI() {
goldText.setText(getText('bits') + ': ' + gold);
scoreText.setText(getText('securityScore') + ': ' + score);
// 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);
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;
return false;
}
}
game.down = function (x, y, obj) {
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
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) {
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 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 = (currentWave - 1) % 10 + 1;
if (currentWave === 0) currentWorldWave = 0;
waveCounterText.setText(getText('wave') + ': ' + currentWorldWave + '/10');
}
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200;
nextWaveButton.y = 2732 - 100 + 20;
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
// Add shop button
var shopButton = new Container();
var shopButtonBg = shopButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shopButtonBg.width = 300;
shopButtonBg.height = 100;
shopButtonBg.tint = 0x4444FF;
var shopButtonText = new Text2('Shop', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
shopButtonText.anchor.set(0.5, 0.5);
shopButton.addChild(shopButtonText);
shopButton.x = 200;
shopButton.y = 2732 - 100 + 20;
game.addChild(shopButton);
var digimonShop = new DigimonShop();
digimonShop.x = 2048 / 2;
game.addChild(digimonShop);
shopButton.down = function () {
digimonShop.show();
};
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);
}
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
// Check if we entered a new world and regenerate maze (9 waves per world)
var newWorld = Math.ceil(currentWave / 9);
if (newWorld > currentWorld || currentWave === 1) {
currentWorld = newWorld;
if (currentWorld > 6) currentWorld = 6;
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);
// World-specific notification messages
var worldNames = ["", "Forest", "Desert", "Glacier", "Village", "Tech Lab", "Inferno"];
var worldNotification = game.addChild(new Notification("Welcome to Digital World " + currentWorld + ": " + worldNames[currentWorld] + "!"));
worldNotification.x = 2048 / 2;
worldNotification.y = grid.height - 100;
}
// Get wave type and enemy count from the wave indicator (based on current world wave)
var worldWave = (currentWave - 1) % 10 + 1;
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 worldWave = (currentWave - 1) % 10 + 1;
var isBossWave = worldWave === 10;
if (isBossWave) {
// Boss waves have just 1 giant enemy
enemyCount = 1;
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ FINAL BOSS WAVE! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var worldWave = (currentWave - 1) % 9 + 1;
var enemy = new Enemy(waveType);
// Make wave 10 boss giant with much more health
if (worldWave === 10) {
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;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// All enemy types now spawn in the middle 6 tiles at the top spacing
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
// Find a column that isn't occupied by another enemy that's not yet in view
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
// Check if any enemy is already in this column but not yet in view
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
// If all columns are occupied, use original random method
var spawnX;
if (availableColumns.length > 0) {
// Choose a random unoccupied column
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
// Fallback to random if all columns are occupied
spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
}
var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
}
}
// Update enemies without speed multiplier (speed button only affects tower fire rate)
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
// Update enemy normally (no speed multiplier)
if (enemy.update) enemy.update();
if (enemy.health <= 0) {
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;
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " bits!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
lives = Math.max(0, lives - 1);
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// 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);
}
}
// Check for completion of current world (10 waves) or all worlds
var worldWave = (currentWave - 1) % 10 + 1;
if (worldWave >= 10 && enemies.length === 0 && !waveInProgress) {
if (currentWorld >= 6) {
// Completed all worlds
LK.showYouWin();
} else {
// Completed current world, show victory message
var notification = game.addChild(new Notification("World " + currentWorld + " Complete! Advancing to next world..."));
notification.x = 2048 / 2;
notification.y = 2732 / 2;
// Start next world after delay
LK.setTimeout(function () {
currentWorld++;
currentWave = (currentWorld - 1) * 10;
// Reset for next world
waveIndicator.gameStarted = true;
updateWaveCounter();
}, 3000);
}
}
};
var mainMenu = new MainMenu();
game.addChild(mainMenu);
game.startGame = function () {
var worldSelectionMenu = new WorldSelectionMenu();
game.addChild(worldSelectionMenu);
};
game.startWorld = function (worldNumber) {
currentWorld = worldNumber;
grid.generateMazeForWorld(currentWorld);
mapVisualization.generateMazeForWorld(currentWorld); // Regenerar también el mapa de visualización
grid.pathFind();
grid.renderDebug();
// Sincronizar mapas
syncVisualizationMap();
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
// Show story sequence before starting the world
var storySequence = new StorySequence(worldNumber);
game.addChild(storySequence);
storySequence.onComplete = function () {
waveIndicator.gameStarted = true;
currentWave = (currentWorld - 1) * 10;
waveTimer = nextWaveTime;
};
};
game.startWorldLevel = function (worldNumber, levelNumber) {
currentWorld = worldNumber;
currentLevel = levelNumber;
// Calculate the wave number based on world and level
currentWave = (worldNumber - 1) * 10 + levelNumber;
grid.generateMazeForWorld(currentWorld);
mapVisualization.generateMazeForWorld(currentWorld);
grid.pathFind();
grid.renderDebug();
// Sincronizar mapas
syncVisualizationMap();
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
// Show story sequence before starting the level
var storySequence = new StorySequence(worldNumber);
game.addChild(storySequence);
storySequence.onComplete = function () {
waveIndicator.gameStarted = true;
waveTimer = nextWaveTime;
};
};
game.startTutorial = function () {
// Set up a simple tutorial level
currentWorld = 1;
currentLevel = 1;
currentWave = 0; // Reset to 0 so tutorial starts at wave 0/9
// Update wave counter for tutorial - explicitly set to show 0/10
waveCounterText.setText('Wave: 0/10');
// Generate tutorial world
grid.generateMazeForWorld(currentWorld);
mapVisualization.generateMazeForWorld(currentWorld);
grid.pathFind();
grid.renderDebug();
syncVisualizationMap();
worldRenderer.updateWorldGraphics(currentWorld, mapVisualization);
// Show tutorial sequence
var tutorialSequence = new TutorialSequence();
game.addChild(tutorialSequence);
tutorialSequence.onComplete = function () {
// 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;
};
};
Digimon Virus terrestre. In-Game asset. 2d. High contrast. No shadows
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
forestBackgroundtile. In-Game asset. 2d. High contrast. No shadows
forestPathTile. In-Game asset. 2d. High contrast. No shadows
forestScenaryElement. In-Game asset. 2d. High contrast. No shadows
Cell. In-Game asset. 2d. High contrast. No shadows
desertbg. In-Game asset. 2d. High contrast. No shadows
desertscenery. In-Game asset. 2d. High contrast. No shadows
glacierbg block. In-Game asset. 2d. High contrast. No shadows