User prompt
Baja en Y el boton de incrementar ORO
User prompt
Agrega un boton de prueba en la parte superior izquierda que agrega 1000 de oro cuando se presiona
User prompt
Que el Jefe de la oleada 10 sea jefe normal, el jefe de la oleada 20 sea rapido, el jefe de la oleada 30 sea tanque, el jefe de la oleada 40 sea volador y el jefe de la oleada 50 sea Swarm. Cada uno usando la skin de su mounstruo pero grande.
User prompt
Si el jugador ya jugo el tutorial una vez ya no lo vuelvas a mostrar aunque reinicie el juego. ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'storage.getItem is not a function' in or related to this line: 'if (storage.getItem("tutorialCompleted") === true) {' Line Number: 2110
User prompt
Please fix the bug: 'storage.get is not a function' in or related to this line: 'if (storage.get("tutorialCompleted") === true) {' Line Number: 2110
User prompt
Please fix the bug: 'storage.getItem is not a function' in or related to this line: 'if (storage.getItem("tutorialCompleted") === true) {' Line Number: 2110
User prompt
Please fix the bug: 'storage.get is not a function' in or related to this line: 'if (storage.get("tutorialCompleted") === true) {' Line Number: 2110
User prompt
Please fix the bug: 'storage.getItem is not a function' in or related to this line: 'if (storage.getItem("tutorialCompleted") === true) {' Line Number: 2110
User prompt
Please fix the bug: 'storage.get is not a function' in or related to this line: 'if (storage.get("tutorialCompleted") === true) {' Line Number: 2110
User prompt
Please fix the bug: 'storage.getItem is not a function' in or related to this line: 'if (storage.getItem("tutorialCompleted") === true) {' Line Number: 2110
User prompt
Please fix the bug: 'storage.get is not a function' in or related to this line: 'if (storage.get("tutorialCompleted") === true) {' Line Number: 2110
User prompt
Si el jugador ya jugo el tutorial una vez ya no lo vuelvas a mostrar aunque reinicie el juego.
User prompt
Incorpora un boton para poder girar la ruleta, cuando oprimas el boton te descuenta 100 oros, aparece en el centro del juego una ruleta y gira para detenerse aleatoriamente en algunos de estos 6 premios: +2 Monedas cada que presionas el boton por 60 segundos. (Durante 60 segundos, cada vez que presiones el boton te dara el doble de monedas, en lugar de +1 seran +2) X2 Ahumento de nivel en torretas por 60 segundos. (Cada vez que hagas click en una torreta para subir de nivel contara como si hubieras hecho click dos veces durante 60 segundos) +1000 de Oro, (simplemente te da 100 de oro) Eliminar a todos los enemigos del mapa (Elimina automaticamente a todos los enemigos en pantalla) Aparecen 3 jefes normales (Hace Spawn de tres jefes normales)
User prompt
Que el jefe del nivel 50 sea Swarm
User prompt
Si durante el tutorial ya se tienen 10 oros y se presiona el boton no mostrar los +1
Code edit (1 edits merged)
Please save this source code
User prompt
Ya no se visualiza correctamente el nivel de las torretas, cambia el color de los recuadros llenos a verde
User prompt
Solo durante el 5, deshabilita si pasa del 5 y antes de iniciar el juego. Iniciado el juago ya se retira el limitante
User prompt
Solo se puede incrementar el nivel de las torretas durante el paso 5 del tutorial o terminando el tutorial
User prompt
Solo se puede incrementar el nivel de las torretas durante el paso 5 del tutorial o depues de que termine el tutorial en adelante
User prompt
No puedes pasar de 10 Oros si estas en el tutorial
Code edit (1 edits merged)
Please save this source code
User prompt
Que no se pueda reparar la puerta si no hasta el paso 9 del tutorial en adelante
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of null (reading 'x')' in or related to this line: 'showTutorialOverlay({' Line Number: 2310
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); bulletGraphics.scaleX = 0.5; bulletGraphics.scaleY = 0.5; // --- NUEVO: Calcular radio de colisión basado en tamaño visual --- var bulletRadius = bulletGraphics.width * bulletGraphics.scaleX / 2; // Get actual enemy visual size (enemies are scaled to 50%) var enemyGraphics = self.targetEnemy.children[0]; // First child should be the enemy graphics var enemyRadius = enemyGraphics ? enemyGraphics.width * enemyGraphics.scaleX / 2 : 35; // Dirección fija al momento de disparar if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); self.vx = dx / distance * self.speed; self.vy = dy / distance * self.speed; } else { self.vx = 0; self.vy = 0; } self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } // Convert enemy position from its scaled layer to game coordinates var enemyGameX = self.targetEnemy.x * gameScale + enemyLayer.x; var enemyGameY = self.targetEnemy.y * gameScale + enemyLayer.y; var dx = enemyGameX - self.x; var dy = enemyGameY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // --- MODIFICADO: comparación de colisión considerando radios --- if (distance < bulletRadius + enemyRadius) { 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; } self.destroy(); } else { // Actualizar velocidad para rastrear objetivo var effectiveSpeed = speedBoostActive ? self.speed * 3 : self.speed; self.vx = dx / distance * effectiveSpeed; self.vy = dy / distance * effectiveSpeed; // Rotate bullet to point toward target enemy var angle = Math.atan2(self.vy, self.vx); bulletGraphics.rotation = angle; self.x += self.vx; self.y += self.vy; } }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); // Add cell number label (small red text) var cellNumberLabel = new Text2('1', { size: 20, fill: 0xFF0000, weight: 800 }); cellNumberLabel.anchor.set(0.5, 0.5); self.addChild(cellNumberLabel); self.setCellNumber = function (number) { cellNumberLabel.setText(number.toString()); }; self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); cellGraphics.tint = 0x880000; return; } numberLabel.visible = true; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; } else { cellGraphics.tint = 0x88 - tint << 8 | tint; } 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; } break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0x008800; numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); }; }); var Door = Container.expand(function (gridX, gridY) { var self = Container.call(this); self.gridX = gridX; self.gridY = gridY; self.enemiesAttacking = []; self.lastHealthLossTime = 0; self.isDestroyed = false; self.originalDoorAsset = selectedDoorAsset; // Store original door asset for this door self.destroyedAsset = 'Destruida' + (Math.floor(Math.random() * 4) + 1); // Random destroyed asset var doorGraphics = self.attachAsset(self.originalDoorAsset, { anchorX: 0.5, anchorY: 0.5 }); self.updateHealthDisplay = function () { // Individual doors no longer have health displays // The global health bar will be updated instead }; self.updateDoorVisual = function () { // Check if door should be destroyed (when global health is 0) var shouldBeDestroyed = globalDoorHealth <= 0; if (shouldBeDestroyed && !self.isDestroyed) { // Change to destroyed visual self.isDestroyed = true; self.removeChild(self.children[0]); // Remove current graphics var destroyedGraphics = self.attachAsset(self.destroyedAsset, { anchorX: 0.5, anchorY: 0.5 }); } else if (!shouldBeDestroyed && self.isDestroyed) { // Change back to original door visual self.isDestroyed = false; self.removeChild(self.children[0]); // Remove destroyed graphics var doorGraphics = self.attachAsset(self.originalDoorAsset, { anchorX: 0.5, anchorY: 0.5 }); } }; self.takeDamage = function () { var effectiveDamageRate = speedBoostActive ? Math.ceil(60 / 3) : 60; if (self.enemiesAttacking.length > 0 && LK.ticks - self.lastHealthLossTime >= effectiveDamageRate) { globalDoorHealth = Math.max(0, globalDoorHealth - self.enemiesAttacking.length); self.lastHealthLossTime = LK.ticks; // Update global door health bar updateGlobalDoorHealthBar(); // Update door visual state self.updateDoorVisual(); } }; self.repair = function () { if (gold >= 1 && globalDoorHealth < globalDoorMaxHealth) { setGold(gold - 1); globalDoorHealth = Math.min(globalDoorMaxHealth, globalDoorHealth + 1); // Update global door health bar updateGlobalDoorHealthBar(); // Update door visual state self.updateDoorVisual(); return true; } return false; }; self.addAttackingEnemy = function (enemy) { if (self.enemiesAttacking.indexOf(enemy) === -1) { self.enemiesAttacking.push(enemy); } }; self.removeAttackingEnemy = function (enemy) { var index = self.enemiesAttacking.indexOf(enemy); if (index !== -1) { self.enemiesAttacking.splice(index, 1); } }; self.update = function () { self.takeDamage(); }; self.down = function (x, y, obj) { if (self.repair()) { var notification = game.addChild(new Notification("Door repaired! (-1 gold)")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (gold < 1) { var notification = game.addChild(new Notification("Not enough gold to repair!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Door is at full health!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; self.animationFrame = 0; self.animationTimer = 0; self.animationSpeed = 15; self.currentAssetIndex = 0; switch (self.type) { case 'fast': self.speed *= 2; self.maxHealth = 100; self.enemyAssets = ['EnemyFast1', 'EnemyFast2', 'EnemyFast3']; break; case 'tank': self.isImmune = true; self.maxHealth = 240; self.speed *= 0.5; // Reduce speed by 0.5x self.enemyAssets = ['EnemyImmune1', 'EnemyImmune2', 'EnemyImmune3']; break; case 'flying': self.isFlying = true; self.maxHealth = 80; self.enemyAssets = ['EnemyFlying1', 'EnemyFlying2', 'EnemyFlying3']; break; case 'swarm': self.maxHealth = 15; self.enemyAssets = ['EnemySwarm1', 'EnemySwarm2', 'EnemySwarm3']; break; case 'normal': default: self.enemyAssets = ['Enemy1', 'Enemy2', 'Enemy3']; break; } // Apply tutorial speed boost for normal enemies only if (tutorialActive && self.type === 'normal') { self.speed *= 2; // Double speed during tutorial } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; self.maxHealth *= 20; self.speed = self.speed * 0.7; // Override enemy assets for boss enemies to use boss sprites self.enemyAssets = ['EnemyBoss1', 'EnemyBoss2', 'EnemyBoss3']; } self.health = self.maxHealth; // Carga múltiples sprites y alterna visibilidad sin parpadeo self.enemyGraphicsList = []; self.enemyAssets.forEach(function (assetName, index) { var gfx = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); gfx.visible = index === 0; // Solo el primero visible gfx.scaleX = self.isBoss ? 0.9 : 0.5; gfx.scaleY = self.isBoss ? 0.9 : 0.5; self.enemyGraphicsList.push(gfx); }); self.enemyGraphics = self.enemyGraphicsList[0]; // --- HITBOX --- var realWidth = self.enemyGraphics.width * self.enemyGraphics.scaleX; var realHeight = self.enemyGraphics.height * self.enemyGraphics.scaleY; self.hitbox = { x: self.x - realWidth / 2, y: self.y - realHeight / 2, width: realWidth, height: realHeight }; // --------------- if (self.isFlying) { self.shadow = new Container(); var shadowGraphics = self.shadow.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); shadowGraphics.tint = 0x000000; shadowGraphics.alpha = 0; // Start with 0% transparency shadowGraphics.scaleX = self.isBoss ? 0.9 : 0.5; shadowGraphics.scaleY = self.isBoss ? 0.9 : 0.5; // Position shadow outside visible area initially to prevent visible spawn/move effect self.shadow.x = -1000; self.shadow.y = -1000; shadowGraphics.rotation = self.enemyGraphics.rotation; // Tween shadow alpha from 0 to 0.5 over 1 second tween(shadowGraphics, { alpha: 0.5 }, { duration: 1000 }); } 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 = -self.enemyGraphics.height * 0.5 / 2 - 5; healthBarOutline.x = -healthBarOutline.width / 2 + 15; healthBarBG.x = healthBar.x = -healthBar.width / 2 + 15; healthBarOutline.scaleX = healthBarOutline.scaleY = 0.5; healthBarBG.scaleX = healthBarBG.scaleY = 0.5; healthBar.scaleX = healthBar.scaleY = 0.5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { // Cambio de animación sin parpadeo - now applies to all enemy types self.animationTimer++; if (self.animationTimer >= self.animationSpeed) { self.animationTimer = 0; self.currentAssetIndex = (self.currentAssetIndex + 1) % self.enemyGraphicsList.length; var newGfx = self.enemyGraphicsList[self.currentAssetIndex]; // Copia la rotación del sprite anterior al nuevo newGfx.rotation = self.enemyGraphics.rotation; self.enemyGraphicsList.forEach(function (gfx, index) { gfx.visible = index === self.currentAssetIndex; }); self.enemyGraphics = newGfx; } var visualWidth = self.enemyGraphics.width * self.enemyGraphics.scaleX; var visualHeight = self.enemyGraphics.height * self.enemyGraphics.scaleY; self.hitbox = { x: self.x - visualWidth / 2, y: self.y - visualHeight / 2, width: visualWidth, height: visualHeight }; if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } if (self.isImmune) { self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { if (self.slowed) { if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; if (!self.poisoned) { self.enemyGraphics.tint = 0xFFFFFF; } } } if (self.poisoned) { if (!self.poisonEffect) { self.poisonEffect = true; } 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; if (!self.slowed) { self.enemyGraphics.tint = 0xFFFFFF; } } } } if (self.isImmune) { self.enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { self.enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { self.enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { self.enemyGraphics.tint = 0x9900FF; } else { self.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 (self.enemyGraphics.targetRotation === undefined) { self.enemyGraphics.targetRotation = angle; self.enemyGraphics.rotation = angle; } else { if (Math.abs(angle - self.enemyGraphics.targetRotation) > 0.05) { tween.stop(self.enemyGraphics, { rotation: true }); var currentRotation = self.enemyGraphics.rotation; var angleDiff = angle - currentRotation; while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } self.enemyGraphics.targetRotation = angle; tween(self.enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -self.enemyGraphics.height * 0.5 / 2 - 5; }; return self; }); var GoldIndicator = Container.expand(function (amount, x, y) { var self = Container.call(this); self.x = x; self.y = y; var goldTextShadow = new Text2('+' + amount, { size: 60, fill: 0x000000, weight: 800 }); goldTextShadow.anchor.set(0.5, 0.5); goldTextShadow.scaleX = 1.1; // Adjusted scale for better visibility goldTextShadow.scaleY = 1.1; // Adjusted scale for better visibility goldTextShadow.x = -2; // Offset for shadow effect goldTextShadow.y = -2; // Offset for shadow effect self.addChild(goldTextShadow); var goldText = new Text2('+' + amount, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); var fadeTime = 60; var moveSpeed = 2; self.update = function () { self.y -= moveSpeed; fadeTime--; if (fadeTime <= 0) { self.destroy(); } else { self.alpha = fadeTime / 60; } }; return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.towerCells = []; // Separate tracking for tower placement self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; self.towerCells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; self.towerCells[i][j] = false; // Track tower placement separately } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; if (i > 5 - 3 && i <= 5 + 3) { if (j === 0) { cellType = 2; self.spawns.push(cell); } else if (j <= 4) { cellType = 0; } else if (j === gridHeight - 1) { cellType = 3; self.goals.push(cell); } else if (j >= gridHeight - 4) { cellType = 0; } } cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; // Calculate cell number (1-based indexing) // Only count cells in the playable area (j > 3 && j <= gridHeight - 4) var cellNumber = (j - 4) * gridWidth + i + 1; debugCell.setCellNumber(cellNumber); // Place obstacles on specific cells var obstacleCells = [14, 17, 18, 19, 20, 21, 22, 23, 26, 35, 38, 39, 40, 41, 42, 43, 44, 45, 47, 50, 59, 62, 64, 65, 66, 67, 68, 69, 70, 71, 74, 83, 86, 87, 88, 89, 90, 91, 92, 93, 95, 98, 107, 110, 112, 113, 114, 115, 116, 117, 118, 119, 122, 12, 130, 131, 16]; if (obstacleCells.indexOf(cellNumber) !== -1) { // Create obstacle using randomly selected grass asset var obstacle = new Container(); var obstacleGraphics = obstacle.attachAsset(selectedGrassAsset, { anchorX: 0.5, anchorY: 0.5 }); obstacle.x = i * CELL_SIZE; obstacle.y = j * CELL_SIZE; self.addChild(obstacle); // Mark cell as wall so enemies cannot traverse cell.type = 1; } // Force specific cells to be path regardless of other conditions var pathCells = [3, 4, 5, 6, 7, 8, 9, 10, 15, 27, 28, 29, 30, 31, 32, 33, 34, 46, 51, 52, 53, 54, 55, 56, 57, 58, 63, 75, 76, 77, 78, 79, 80, 81, 82, 94, 99, 100, 101, 102, 103, 104, 105, 106, 111, 123, 124, 125, 126, 127, 128, 129, 141, 136, 137, 138, 139, 140]; if (pathCells.indexOf(cellNumber) !== -1) { cell.type = 0; // Force as path // Create path using randomly selected path asset var path = new Container(); var pathGraphics = path.attachAsset(selectedPathAsset, { anchorX: 0.5, anchorY: 0.5 }); path.x = i * CELL_SIZE; path.y = j * CELL_SIZE; self.addChild(path); } // Place doors on specific cells if (doorCells.indexOf(cellNumber) !== -1) { // Create door instance var door = new Door(i, j); door.x = i * CELL_SIZE; door.y = j * CELL_SIZE; self.addChild(door); doors.push(door); // Mark cell as door type (type 4) cell.type = 4; cell.door = door; } // Place walls on specific cells var wallCells = [1, 2, 11, 12, 13, 24, 25, 36, 37, 48, 49, 60, 61, 72, 73, 84, 85, 96, 97, 108, 109, 120, 121, 132, 133, 134, 135, 142, 143, 144]; if (wallCells.indexOf(cellNumber) !== -1) { // Create wall using randomly selected wall asset var wall = new Container(); var wallGraphics = wall.attachAsset(selectedWallAsset, { anchorX: 0.5, anchorY: 0.5 }); wall.x = i * CELL_SIZE; wall.y = j * CELL_SIZE; self.addChild(wall); // Mark cell as wall so enemies cannot traverse and towers cannot be placed cell.type = 1; } } } } 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]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 1 && node.left.type != 1) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.right.type != 1) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.right.type != 1) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.left.type != 1) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } 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 && cell.type == 3) { return true; } // Also check if enemy has reached the bottom of the screen (for enemies moving directly to goals) if (enemy.currentCellY >= 18) { // Grid height is 19 (13+6), so 18 is the bottom row 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; // 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 / gameScale + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y / gameScale + 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); } // Check if enemy has moved past the bottom of the playable area if (enemy.currentCellY >= 18) { // Grid height is 19 (13+6), so 18 is the bottom row return true; } 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 / gameScale + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y / gameScale + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Check if enemy has reached cell 124 (position where they should choose a door) var currentCellNumber = (Math.round(enemy.currentCellY) - 4) * 12 + Math.round(enemy.currentCellX) + 1; if (currentCellNumber === 124 && !enemy.selectedDoorCell) { // Enemy reached decision point - randomly select a door cell to attack var doorCellNumbers = [136, 137, 138, 139, 140, 141]; // Use a more robust random selection method with explicit array length var randomIndex = Math.floor(Math.random() * doorCellNumbers.length); var selectedDoorCellNumber = doorCellNumbers[randomIndex]; // Convert cell number back to grid coordinates with explicit calculation var targetGridY = Math.floor((selectedDoorCellNumber - 1) / 12) + 4; var targetGridX = (selectedDoorCellNumber - 1) % 12; // Verify coordinates for cell 140 specifically if (selectedDoorCellNumber === 140) { // Cell 140: (140-1) = 139, 139/12 = 11.58, floor = 11, +4 = 15 // 139 % 12 = 7 targetGridY = 15; targetGridX = 7; } // Set the enemy's target to the selected door cell enemy.selectedDoorCell = { x: targetGridX, y: targetGridY }; enemy.currentTarget = enemy.selectedDoorCell; // Debug logging to verify all cells are being selected console.log("Enemy selected door cell:", selectedDoorCellNumber, "at grid:", targetGridX, targetGridY); } // Check if enemy is trying to move into a door cell var targetCell = grid.getCell(Math.round(enemy.currentCellX), Math.round(enemy.currentCellY)); if (targetCell && targetCell.type === 4 && targetCell.door && globalDoorHealth > 0) { // Enemy encounters a door - start attacking it if (!enemy.attackingDoor) { enemy.attackingDoor = targetCell.door; targetCell.door.addAttackingEnemy(enemy); } // Stop moving and attack the door enemy.currentTarget = undefined; enemy.x = grid.x / gameScale + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y / gameScale + enemy.currentCellY * CELL_SIZE; return false; } // If enemy was attacking a door but door is now destroyed, clear attacking state and continue moving if (enemy.attackingDoor && globalDoorHealth <= 0) { enemy.attackingDoor.removeAttackingEnemy(enemy); enemy.attackingDoor = null; // Instead of resetting target, set enemy to move directly to the goal // Find the closest goal cell for direct movement var closestGoal = self.goals[0]; if (self.goals.length > 1) { var closestDist = Infinity; for (var g = 0; g < self.goals.length; g++) { var goal = self.goals[g]; var dx = goal.x - enemy.currentCellX; var dy = goal.y - enemy.currentCellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; closestGoal = goal; } } } enemy.currentTarget = closestGoal; // Set direct target to goal } // Handle normal pathfinding enemies if (!enemy.currentTarget) { // If enemy has a selected door cell, prioritize moving to it if (enemy.selectedDoorCell) { enemy.currentTarget = enemy.selectedDoorCell; } else { enemy.currentTarget = cell.targets[0]; } } if (enemy.currentTarget) { // Don't override target if enemy has selected a specific door cell if (!enemy.selectedDoorCell && 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); // Only clear target if we weren't heading to a specific door if (!enemy.selectedDoorCell) { enemy.currentTarget = undefined; } return; } // Special movement logic for enemies targeting door cells if (enemy.selectedDoorCell) { // First move to correct X position, then move down in Y var targetX = enemy.selectedDoorCell.x; var targetY = enemy.selectedDoorCell.y; var xDistance = Math.abs(targetX - enemy.currentCellX); var yDistance = Math.abs(targetY - enemy.currentCellY); // If not at correct X position, move horizontally first if (xDistance > 0.1) { if (targetX > enemy.currentCellX) { enemy.currentCellX += enemy.speed; } else { enemy.currentCellX -= enemy.speed; } // Rotate enemy to face movement direction var angle = targetX > enemy.currentCellX ? 0 : Math.PI; 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 }); var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } else { // At correct X position, now move down in Y enemy.currentCellY += enemy.speed; // Rotate enemy to face downward 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 }); var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } else { // Normal pathfinding movement for enemies without door target var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } } enemy.x = grid.x / gameScale + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y / gameScale + enemy.currentCellY * CELL_SIZE; }; }); 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, style: 'bold' }); 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 Tower = Container.expand(function () { var self = Container.call(this); self.level = 1; self.maxLevel = 5; self.clicksInvested = 0; // Track total clicks invested in this tower self.clicksNeededForNextLevel = 50; // Clicks needed to reach next level self.gridX = 0; self.gridY = 0; // Method to calculate clicks needed for current level (progressive: 20, 50, 100, 200) self.getClicksNeededForCurrentLevel = function () { if (self.level >= self.maxLevel) { return 0; } // New click requirements per level switch (self.level) { case 1: return 20; // Level 1->2: 20 clicks case 2: return 50; // Level 2->3: 50 clicks case 3: return 100; // Level 3->4: 100 clicks case 4: return 200; // Level 4->5: 200 clicks default: return 0; } }; // Standardized method to get the current range of the tower self.getRange = function () { // Default: base 3, +0.5 per level return (2 + (self.level - 1) * 0.5) * CELL_SIZE; }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.x = -90; baseGraphics.y = 150; baseGraphics.tint = 0xAAAAAA; // Reduce tower base size by 50% baseGraphics.scaleX = 0.5; baseGraphics.scaleY = 0.5; var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 12; // Reduce dot size by 50% 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 = -130 + dotSpacing * (i + 1); dot.y = 125 + CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); gunContainer.x = -90; gunContainer.y = 150; var gunGraphics = gunContainer.attachAsset('defense', { anchorX: 0.5, anchorY: 0.5 }); // Reduce gun size by 50% gunGraphics.scaleX = 0.5; gunGraphics.scaleY = 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 { 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 - 200; 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 () { // Use the cost at the time this tower was placed (10 + 10 * number of towers before this one) // Since we don't track individual tower placement order, use current cost as approximation var baseTowerCost = 10; // Original base cost // Calculate total clicks invested with progressive requirements var totalClicksInvested = 0; for (var level = 1; level < self.level; level++) { totalClicksInvested += level * 10; // Sum up all previous level investments } totalClicksInvested += self.clicksInvested; // Add current level progress var totalInvestment = baseTowerCost + totalClicksInvested; return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)) var baseUpgradeCost = 5; // Base tower cost 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 upgrades if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestDistance = Infinity; // Calculamos la posición real del cañón var gunWorldX = self.x + -180 * 0.5; var gunWorldY = self.y + 300 * 0.5; // Debug circle removed - using persistent range display instead for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calcula la distancia desde el cañón, no desde el centro lógico var dx = enemy.x - gunWorldX; var dy = enemy.y - gunWorldY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.getRange()) { if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } } self.targetEnemy = closestEnemy; return closestEnemy; }; self.update = function () { self.findTarget(); if (self.targetEnemy) { // Calculate the angle from the gun position to the target enemy // Gun container is at (-95, 150) relative to tower center, scaled by 50% var gunWorldX = self.x + -95 * 0.5; var gunWorldY = self.y + 300 * 0.5; var dx = self.targetEnemy.x - gunWorldX; var dy = self.targetEnemy.y - gunWorldY; var distance = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx); gunContainer.rotation = angle; // Fire automatically when target is in range and fire rate allows // Apply speed boost multiplier to firing rate var effectiveFireRate = speedBoostActive ? Math.ceil(self.fireRate / 3) : self.fireRate; if (LK.ticks - self.lastFired >= effectiveFireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { // Hide range from previously selected tower if (rangeDisplayTower && rangeDisplayTower !== self) { rangeDisplayTower.hideRangeIndicator(); } // Show range for this tower self.showRange(); rangeDisplayTower = self; // Track pressed tower and start timer for sell functionality pressedTower = self; pressStartTime = LK.ticks; // Clear any existing timer if (pressTimer) { LK.clearTimeout(pressTimer); } // Set timer for 5 seconds (300 ticks at 60fps) pressTimer = LK.setTimeout(function () { if (pressedTower === self) { // Sell the tower var sellValue = self.getTotalValue(); setGold(gold + sellValue); // Show sell notification var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Remove tower from grid tracking if (grid.towerCells[self.gridX]) { grid.towerCells[self.gridX][self.gridY] = false; } // Hide range indicator self.hideRangeIndicator(); if (rangeDisplayTower === self) { rangeDisplayTower = null; } // Remove tower from arrays and destroy var towerIndex = towers.indexOf(self); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } // Decrement towers placed counter when selling if (towersPlaced > 0) { towersPlaced--; } updateTowerPrice(); // Update tower price display self.destroy(); pressedTower = null; } }, 5000); // Progressive upgrade system - clicks no longer cost gold if (self.level < self.maxLevel) { self.clicksInvested++; // Check if we have enough clicks to level up (progressive requirements) var clicksNeeded = self.getClicksNeededForCurrentLevel(); if (self.clicksInvested >= clicksNeeded) { self.level++; // Reset click counter for next level self.clicksInvested = 0; // Apply upgrades if (self.level === self.maxLevel) { // Extra powerful last upgrade (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(); // Update range display after upgrade self.showRange(); // Animate level indicator 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 }); } }); } // Show level up notification var notification = game.addChild(new Notification("Tower upgraded to level " + self.level + "!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Check if this is the tutorial tower reaching level 2 if (tutorialActive && tutorialStep === 5 && self === tutorialTower && self.level >= 2) { advanceTutorialStep(); } } else { // Show progress notification (progressive click requirements) var totalClicksNeeded = self.getClicksNeededForCurrentLevel(); var clicksRemaining = totalClicksNeeded - self.clicksInvested; var notification = game.addChild(new Notification(clicksRemaining + " more clicks to level " + (self.level + 1))); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (self.level >= self.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; self.up = function (x, y, obj) { // Clear the press timer if this tower was being pressed if (pressedTower === self) { pressedTower = null; if (pressTimer) { LK.clearTimeout(pressTimer); pressTimer = null; } } }; self.showRange = function () { // Remove existing range indicator first self.hideRangeIndicator(); // Create range indicator var rangeIndicator = new Container(); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; rangeGraphics.tint = 0xffffff; // White color for range (original color) rangeIndicator.isTowerRange = true; // Position at tower center with offset to align with visual elements rangeIndicator.x = -88; // Offset left to center on visual tower rangeIndicator.y = 155; // Offset down to center on visual tower self.addChild(rangeIndicator); }; self.hideRangeIndicator = function () { for (var i = self.children.length - 1; i >= 0; i--) { if (self.children[i].isTowerRange) { self.removeChild(self.children[i]); } } }; 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) { // Calculate bullet spawn position relative to gun container position // gunContainer is positioned at (-95, 150) relative to tower center // Apply the same scaling as the tower layer // Define el punto de la punta del cañón localmente en gunContainer (por ejemplo, en el eje X positivo) // Obtener la punta del cañón teniendo en cuenta el anchor // Calcula la posición global de la punta del cañón manualmente var offsetX = 30; // mueve hacia la punta (horizontal) var offsetY = -5; // sube o baja (vertical) // Aplica rotación del cañón a ese offset var cos = Math.cos(gunContainer.rotation); var sin = Math.sin(gunContainer.rotation); var rotatedX = offsetX * cos - offsetY * sin; var rotatedY = offsetX * sin + offsetY * cos; // Posición global final considerando jerarquía y escala de juego var bulletX = (self.x + gunContainer.x + rotatedX) * gameScale; var bulletY = (self.y + gunContainer.y + rotatedY) * gameScale; // Crear la bala en esa posición var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // --- Fire recoil effect for gunContainer (rotation only) --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { scaleX: true, scaleY: true }); // Store original scale values if not already stored if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset scale to resting values before animating (in case of interrupted tweens) gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Animate scale recoil effect (gun briefly shrinks then returns to normal) tween(gunContainer, { scaleX: gunContainer._restScaleX * 0.8, scaleY: gunContainer._restScaleY * 0.8 }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original scale tween(gunContainer, { scaleX: gunContainer._restScaleX, scaleY: gunContainer._restScaleY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; // ✅ No usar gameScale aquí self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { if (grid.towerCells[gridX] && grid.towerCells[gridX][gridY] !== undefined) { grid.towerCells[gridX][gridY] = true; } } } 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(); // Check against current tower cost // 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(); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic var previewCircleScale = 1.83; // Cambia este valor para agrandar o achicar el círculo rangeGraphics.width = rangeGraphics.height = previewRange * 2 * previewCircleScale; previewGraphics.tint = 0xAAAAAA; if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } }; self.updatePlacementStatus = function () { var cell = grid.getCell(self.gridX, self.gridY); // Check if this cell is specifically an obstacle (not a wall) var cellNumber = (self.gridY - 4) * 12 + self.gridX + 1; var obstacleCells = [14, 17, 18, 19, 20, 21, 22, 23, 26, 35, 38, 39, 40, 41, 42, 43, 44, 45, 47, 50, 59, 62, 64, 65, 66, 67, 68, 69, 70, 71, 74, 83, 86, 87, 88, 89, 90, 91, 92, 93, 95, 98, 107, 110, 112, 113, 114, 115, 116, 117, 118, 119, 122, 130, 131, 16]; var wallCells = [1, 2, 11, 12, 13, 24, 25, 36, 37, 48, 49, 60, 61, 72, 73, 84, 85, 96, 97, 108, 109, 120, 121, 132, 133, 134, 135, 142, 143, 144]; // Check if there's already a tower at this exact position using separate tower tracking var existingTower = grid.towerCells[self.gridX] && grid.towerCells[self.gridX][self.gridY]; // Check if this is a wall cell (towers cannot be placed on walls) var isWallCell = cellNumber >= 1 && cellNumber <= 144 && wallCells.indexOf(cellNumber) !== -1; // Allow placement on obstacle cells that don't have towers and are not walls var isObstacleCell = cellNumber >= 1 && cellNumber <= 144 && obstacleCells.indexOf(cellNumber) !== -1; // Only check the exact cell for tower placement, not neighboring cells var validGridPlacement = cell && !existingTower && isObstacleCell && !isWallCell; self.canPlace = validGridPlacement; self.hasEnoughGold = gold >= getTowerCost(); // Check against current tower cost self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = (x - grid.x) / gameScale; var gridPosY = (y - grid.y) / gameScale; // Para X dejamos el redondeo normal self.gridX = Math.round(gridPosX / CELL_SIZE); // Para Y, calculamos la fracción dentro de la celda var yInCell = gridPosY / CELL_SIZE % 1; // Si el mouse pasó más del 90% de la celda, pasamos a la siguiente celda (redondeo hacia arriba) if (yInCell > 0.9) { self.gridY = Math.ceil(gridPosY / CELL_SIZE); } else { self.gridY = Math.ceil(gridPosY / CELL_SIZE); } // Actualizamos la posición visual acorde a gridX, gridY self.x = grid.x + self.gridX * CELL_SIZE * gameScale; self.y = grid.y + self.gridY * CELL_SIZE * gameScale; self.checkPlacement(); }; 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; 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 = "Tank"; enemyType = "tank"; 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', 'tank', '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 'tank': block.tint = 0xAA0000; waveType = "Boss Tank"; 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 'tank': 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 tank block.tint = 0xAA0000; waveType = "Tank"; enemyType = "tank"; 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.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { waveTimer++; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; if (currentWave != 1) { var waveType = self.getWaveTypeName(currentWave); var enemyCount = self.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } } }; self.update = function () { var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave - 1) * 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; i++) { var marker = self.waveMarkers[i]; var block = marker.children[0]; if (i < currentWave) { block.alpha = .5; } // Hide markers that are completely outside the left side of the yellow indicator rectangle var leftEdge = self.positionIndicator.x - (blockWidth - 16) / 2; if (marker.x + blockWidth / 2 < leftEdge) { marker.visible = false; } else { marker.visible = true; } } self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ 3; 75; var CELL_SIZE = 76; var selectedWallAsset = 'Muro' + (Math.floor(Math.random() * 4) + 1); // Randomly select Muro1, Muro2, Muro3, or Muro4 var selectedGrassAsset = 'Pasto' + (Math.floor(Math.random() * 4) + 1); // Randomly select Pasto1, Pasto2, Pasto3, or Pasto4 var selectedPathAsset = 'Camino' + (Math.floor(Math.random() * 4) + 1); // Randomly select Camino1, Camino2, Camino3, or Camino4 var selectedDoorAsset = 'Puerta' + (Math.floor(Math.random() * 4) + 1); // Randomly select door asset for this game var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; var rangeDisplayTower = null; // Track which tower is showing range var pressedTower = null; // Track which tower is being pressed var pressStartTime = 0; // Track when the press started var pressTimer = null; // Timer for checking press duration var gold = 0; // Start with 0 gold for tutorial var lives = 1; var score = 0; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 4; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave var towersPlaced = 0; // Track total towers placed for progressive pricing var doorCells = [136, 137, 138, 139, 140, 141]; // Door cell numbers var doors = []; // Track door instances var globalDoorHealth = 95; // Start door health at 95/100 for tutorial var globalDoorMaxHealth = 100; // Maximum health for all doors var globalDoorHealthBar = null; // Global health bar container var globalDoorHealthBarBg = null; // Global door health bar background var globalDoorHealthBarFill = null; // Global door health bar fill var globalDoorHealthText = null; // Global door health bar text var topMargin = 100; // Define topMargin variable // --- Tutorial System --- var tutorialActive = true; var tutorialStep = 0; var tutorialOverlay = null; var tutorialArrow = null; var tutorialMessage = null; var tutorialTargetArea = null; var tutorialRedButtonPresses = 0; var tutorialTowerPlaced = false; var tutorialTower = null; var tutorialTowerUpgradeClicks = 0; var tutorialEnemyKilled = false; var tutorialFlyingEnemyKilled = false; var tutorialGateRepaired = false; var tutorialForwardButton = null; var tutorialForwardButtonPressed = false; var tutorialFlyingEnemy = null; var tutorialBasicEnemy = null; var tutorialRepairMessageShown = false; var tutorialGoodLuckShown = false; var tutorialAllowTowerPlacement = false; var tutorialAllowUpgrade = false; var tutorialAllowRepair = false; var tutorialAllowForward = false; var tutorialAllowRedButton = true; var tutorialAllowDragTower = false; var tutorialAllowPlaceCell = 67; // Only allow cell 67 for first tower var tutorialAllowTurretIcon = false; var tutorialAllowGateRepair = false; var tutorialAllowFlyingEnemy = false; var tutorialAllowBasicEnemy = false; var tutorialAllowSpeedBoost = false; var tutorialAllowGameStart = false; var tutorialAllowAll = false; var tutorialCellOffsetX = -85; // Offset for tutorial cell highlighting var tutorialCellOffsetY = -85; // Offset for tutorial cell highlighting var tutorialOffsetStep5X = 530; // Offset for tutorial step 5 highlight var tutorialOffsetStep5Y = 820; // Offset for tutorial step 5 highlight var tutorialForwardIconArea = { x: 2048 - 150, y: 120, w: 240, h: 180 }; // Area for speed/forward button var tutorialRedButtonArea = { x: 250, y: 2732 - 220, w: 350, h: 350 }; // Area for red button var tutorialTurretIconArea = { x: 700, y: 2732 - 210, w: 350, h: 350 }; // Area for turret icon var tutorialGateArea = { x: 1050, y: 2250, w: 1200, h: 300 }; // Area for gate repair function showTutorialOverlay(area, message, arrowDir) { // Add null check for area parameter if (!area) { console.error("showTutorialOverlay called with null/undefined area parameter"); return; } // Remove previous overlay/arrow/message if (tutorialOverlay) { tutorialOverlay.destroy(); tutorialOverlay = null; } if (tutorialArrow) { tutorialArrow.destroy(); tutorialArrow = null; } if (tutorialMessage) { tutorialMessage.destroy(); tutorialMessage = null; } // Overlay: darken everything except area tutorialOverlay = new Container(); tutorialOverlay.zIndex = 9999; tutorialOverlay.alpha = 0.5; // Four rectangles: top, left, right, bottom var darkColor = 0x000000; var topRect = tutorialOverlay.attachAsset('notification', { anchorX: 0, anchorY: 0 }); topRect.width = 2048; topRect.height = Math.max(0, area.y - area.h / 2); topRect.tint = darkColor; var leftRect = tutorialOverlay.attachAsset('notification', { anchorX: 0, anchorY: 0 }); leftRect.width = Math.max(0, area.x - area.w / 2); leftRect.height = area.h; leftRect.y = area.y - area.h / 2; leftRect.tint = darkColor; var rightRect = tutorialOverlay.attachAsset('notification', { anchorX: 0, anchorY: 0 }); rightRect.width = Math.max(0, 2048 - (area.x + area.w / 2)); rightRect.height = area.h; rightRect.x = area.x + area.w / 2; rightRect.y = area.y - area.h / 2; rightRect.tint = darkColor; var bottomRect = tutorialOverlay.attachAsset('notification', { anchorX: 0, anchorY: 0 }); bottomRect.width = 2048; bottomRect.height = Math.max(0, 2732 - (area.y + area.h / 2)); bottomRect.y = area.y + area.h / 2; bottomRect.tint = darkColor; game.addChild(tutorialOverlay); var fontSize = 80; if (message === "Good Luck") { fontSize = 200; // O incluso 250 si quieres algo más grande } // Message - position below area if it's in the top part of screen (forward button area) tutorialMessage = new Text2(message, { size: fontSize, fill: 0xffffff, weight: 800, align: 'center' }); tutorialMessage.anchor.set(0.5, 0.5); tutorialMessage.x = 2048 / 2; // Check if this is the forward button area (top of screen) and position below instead if (area.y < 300) { tutorialMessage.y = area.y + area.h / 2 + 220; // Position below the area } else { tutorialMessage.y = area.y - area.h / 2 - 220; // Position above the area (normal) } tutorialMessage.zIndex = 10000; // Ensure message appears above all other objects game.addChild(tutorialMessage); // Arrow (optional) - point up if below area, down if above if (arrowDir) { tutorialArrow = new Container(); var arrowGfx = tutorialArrow.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5 }); arrowGfx.scaleX = 6; arrowGfx.scaleY = 6; // Point up if message is below area, down if above if (area.y < 300) { arrowGfx.rotation = -Math.PI / 2; // Point upward tutorialArrow.y = area.y + area.h / 2 + 60; } else { arrowGfx.rotation = Math.PI / 2; // Point downward tutorialArrow.y = area.y - area.h / 2 - 60; } tutorialArrow.x = area.x; tutorialArrow.zIndex = 10000; // Ensure arrow appears above all other objects game.addChild(tutorialArrow); } } function clearTutorialOverlay() { if (tutorialOverlay) { tutorialOverlay.destroy(); tutorialOverlay = null; } if (tutorialArrow) { tutorialArrow.destroy(); tutorialArrow = null; } if (tutorialMessage) { tutorialMessage.destroy(); tutorialMessage = null; } } function advanceTutorialStep() { clearTutorialOverlay(); tutorialStep++; console.log("Tutorial Step " + tutorialStep + " started"); // Step logic if (tutorialStep === 1) { // Step 1: Press red button to earn gold tutorialAllowRedButton = true; tutorialAllowDragTower = false; tutorialAllowTurretIcon = false; tutorialAllowUpgrade = false; tutorialAllowRepair = false; tutorialAllowForward = false; tutorialAllowAll = false; showTutorialOverlay(tutorialRedButtonArea, "Press the Red Button to earn Gold", "up"); } else if (tutorialStep === 2) { // Step 2: Keep pressing until 10 gold showTutorialOverlay(tutorialRedButtonArea, "Keep Pressing until you Reach 10 Gold", "up"); } else if (tutorialStep === 3) { // Step 3a: Show turret icon highlight first tutorialAllowRedButton = false; tutorialAllowTurretIcon = true; tutorialAllowDragTower = false; // Don't allow dragging yet tutorialAllowPlaceCell = 67; showTutorialOverlay(tutorialTurretIconArea, "Click the Turret Icon", "up"); } else if (tutorialStep === 4) { // Step 3b: Show placement highlight after clicking turret icon tutorialAllowTurretIcon = false; tutorialAllowDragTower = true; // Calculate cell 67's grid position var cell67GridX = (tutorialAllowPlaceCell - 1) % 12; var cell67GridY = Math.floor((tutorialAllowPlaceCell - 1) / 12) + 4; // Calculate center of cell 67 in game coordinates var cell67CenterX = grid.x + cell67GridX * CELL_SIZE * gameScale + CELL_SIZE * gameScale / 2 + tutorialCellOffsetX; var cell67CenterY = grid.y + cell67GridY * CELL_SIZE * gameScale + CELL_SIZE * gameScale / 2 + tutorialCellOffsetY; showTutorialOverlay({ x: cell67CenterX, y: cell67CenterY, w: CELL_SIZE * gameScale + 20, h: CELL_SIZE * gameScale + 20 }, "Drag the Turret to the Highlighted Area", null); } else if (tutorialStep === 5) { // Step 5: Tap the turret to upgrade to level 2 (10 clicks) tutorialAllowTurretIcon = false; tutorialAllowDragTower = false; tutorialAllowUpgrade = true; showTutorialOverlay({ x: tutorialTower.x + tutorialOffsetStep5X, y: tutorialTower.y + tutorialOffsetStep5Y, w: 200, h: 200 }, "Tap the Turret to Upgrade it to Level 2", "up"); } else if (tutorialStep === 6) { // Step 6: Kill all monsters before they reach the kingdom! tutorialAllowUpgrade = false; tutorialAllowBasicEnemy = true; // No highlight, just clear overlay and wait clearTutorialOverlay(); // Spawn 1 basic enemy spawnTutorialBasicEnemy(); // After 5 seconds, highlight the forward button LK.setTimeout(function () { // Only show highlight if speed boost button is not already active if (!speedBoostButton.isActive) { tutorialAllowForward = true; showTutorialOverlay(tutorialForwardIconArea, "Press the Forward Icon to Speed up the Game", "down"); } }, 5000); } else if (tutorialStep === 7) { advanceTutorialStep(); } else if (tutorialStep === 8) { // Step 8: Spawn flying enemy, warn about flying tutorialAllowForward = false; tutorialAllowFlyingEnemy = true; var flyingEnemyArea = { x: 1120, y: 200, w: 300, h: 800 }; showTutorialOverlay(flyingEnemyArea, "Watch out for Flying Enemies!\nThey Fly over the Gate and go straight to the Kingdom", "down"); LK.setTimeout(function () { if (tutorialStep === 8) { clearTutorialOverlay(); } ; }, 5000); spawnTutorialFlyingEnemy(); } else if (tutorialStep === 9) { // Step 9: Click the gate to repair if damaged tutorialAllowFlyingEnemy = false; tutorialAllowRepair = true; showTutorialOverlay(tutorialGateArea, "Click the gate to repair it if it's damaged", "down"); } else if (tutorialStep === 10) { // Step 10: Good luck, start game tutorialAllowRepair = false; // Update existing tutorial message instead of creating new overlay showTutorialOverlay({ x: 2048 / 2, y: 2732 / 2, w: 0, h: 0 }, "START", null); LK.setTimeout(function () { clearTutorialOverlay(); tutorialActive = false; tutorialAllowAll = true; // Start the game waveIndicator.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; }, 1200); } } function spawnTutorialBasicEnemy() { // Only spawn if not already present if (tutorialBasicEnemy) { return; } var enemy = new Enemy('normal'); enemy.cellX = 6; enemy.cellY = 1; enemy.currentCellX = 6; enemy.currentCellY = 1; enemy.waveNumber = -1; enemy.maxHealth = 50; enemy.health = 50; enemyLayerBottom.addChild(enemy); enemies.push(enemy); tutorialBasicEnemy = enemy; } function spawnTutorialFlyingEnemy() { if (tutorialFlyingEnemy) { return; } var enemy = new Enemy('flying'); enemy.cellX = 6; enemy.cellY = 1; enemy.currentCellX = 6; enemy.currentCellY = 1; enemy.waveNumber = -2; enemy.maxHealth = 40; enemy.health = 40; enemyLayerTop.addChild(enemy); if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } enemies.push(enemy); tutorialFlyingEnemy = enemy; } // Start tutorial LK.setTimeout(function () { tutorialStep = 0; // Start at step 0 so step 1 shows properly advanceTutorialStep(); }, 500); var goldTextShadow = new Text2('Gold: ' + gold, { size: 90, fill: 0x000000, weight: 800 }); goldTextShadow.anchor.set(0.5, 0.5); LK.gui.top.addChild(goldTextShadow); goldTextShadow.x = 0; goldTextShadow.y = topMargin - 25; var goldText = new Text2('Gold: ' + gold, { size: 90, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); LK.gui.top.addChild(goldText); goldText.x = 0; goldText.y = topMargin - 30; function updateUI() { // Update the text and preserve the stroke styling goldTextShadow.destroy(); // Destroy the old shadow Text2 object goldTextShadow = new Text2('Gold: ' + gold, { size: 90, fill: 0x000000, weight: 800 }); goldTextShadow.anchor.set(0.5, 0.5); LK.gui.top.addChild(goldTextShadow); goldTextShadow.x = 0; goldTextShadow.y = topMargin - 25; goldText.destroy(); // Destroy the old Text2 object goldText = new Text2('Gold: ' + gold, { size: 90, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); LK.gui.top.addChild(goldText); goldText.x = 0; goldText.y = topMargin - 30; updateTowerPrice(); } function setGold(value) { gold = value; updateUI(); } // Create dirt background with multiple smaller tiles var dirtBackground = new Container(); // Create 6 background tiles arranged in a 2x3 grid var tilesX = 2; // 2 tiles horizontally var tilesY = 3; // 3 tiles vertically var tileScale = 0.55; // Make each tile slightly larger to create overlap var tileWidth = 2055 * tileScale; // Use actual asset width var tileHeight = 2738.26 * tileScale; // Use actual asset height // Calculate overlap amount (5% of tile size) var overlapX = tileWidth * 0.05; var overlapY = tileHeight * 0.05; // Calculate total coverage and starting positions with overlap var totalWidth = tilesX * tileWidth - (tilesX - 1) * overlapX; var totalHeight = tilesY * tileHeight - (tilesY - 1) * overlapY; var startX = (2048 - totalWidth) / 2; var startY = (2732 - totalHeight) / 2; for (var i = 0; i < tilesX; i++) { for (var j = 0; j < tilesY; j++) { var backgroundTile = dirtBackground.attachAsset('dirtBackground', { anchorX: 0.5, anchorY: 0.5 }); // Scale each tile with fixed scale (no variation) backgroundTile.scaleX = tileScale; backgroundTile.scaleY = tileScale; // Position tiles with overlap to eliminate gaps backgroundTile.x = startX + i * (tileWidth - overlapX) + tileWidth / 2; backgroundTile.y = startY + j * (tileHeight - overlapY) + tileHeight / 2; // No rotation or variation to maintain consistent orientation } } dirtBackground.x = 0; dirtBackground.y = 0; game.addChild(dirtBackground); var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(12, 13 + 6); // Calculate scale to fill full screen width (2048px) with the 11-cell width var gameScale = 2048 / (12 * CELL_SIZE); // Scale factor to fill screen width grid.scaleX = gameScale; grid.scaleY = gameScale; grid.x = 40 * gameScale; // Move 40 units right (10 + 20 + 10) and scale grid.y = (200 - CELL_SIZE * 4 - 20 - 30) * gameScale; // Move up 50 units total (20 + 30) and scale the Y position accordingly grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); // Create global door health bar after doors are created createGlobalDoorHealthBar(); game.addChild(debugLayer); // Scale tower layer to match grid towerLayer.scaleX = gameScale; towerLayer.scaleY = gameScale; game.addChild(towerLayer); // Scale enemy layer to match grid enemyLayer.scaleX = gameScale; enemyLayer.scaleY = gameScale; game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; // Create simple tower asset next to red button for dragging var buttonTower = new Container(); var buttonTowerGraphics = buttonTower.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); buttonTowerGraphics.scaleX = 3.5; buttonTowerGraphics.scaleY = 3.5; buttonTower.x = 700; buttonTower.y = 2732 - 210; buttonTower.width = buttonTowerGraphics.width * 3.5; buttonTower.height = buttonTowerGraphics.height * 3.5; game.addChild(buttonTower); // Add a shadow text for tower price outline var towerPriceTextShadow = new Text2('Price: ' + getTowerCost(), { size: 60, fill: 0x000000, weight: 800 }); towerPriceTextShadow.anchor.set(0.5, 0.5); towerPriceTextShadow.x = buttonTower.x + 2; towerPriceTextShadow.y = buttonTower.y + buttonTower.height / 2 + 40 + 2; game.addChild(towerPriceTextShadow); // Add a text display for the current tower price below the draggable tower var towerPriceText = new Text2('Price: ' + getTowerCost(), { size: 60, fill: 0xFFFFFF, weight: 800 }); towerPriceText.anchor.set(0.5, 0.5); towerPriceText.x = buttonTower.x; towerPriceText.y = buttonTower.y + buttonTower.height / 2 + 40; // Position below the tower game.addChild(towerPriceText); // Update the tower price text whenever the UI is updated function updateTowerPrice() { towerPriceTextShadow.setText('Price: ' + getTowerCost()); towerPriceText.setText('Price: ' + getTowerCost()); } var isDragging = false; function wouldBlockPath(gridX, gridY) { // Use offset coordinates for consistency with placement verification var checkGridX = Math.floor(gridX + 0.5); var checkGridY = Math.floor(gridY + 0.5); var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(checkGridX + i, checkGridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost() { return 10 + towersPlaced * 10; // Base cost 10, increases by 10 per tower } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function createGlobalDoorHealthBar() { if (doors.length === 0) { return; } // Create health bar container globalDoorHealthBar = new Container(); // Calculate position - center of all doors, slightly below var centerX = 457.5; var centerY = 1040; globalDoorHealthBar.x = centerX; globalDoorHealthBar.y = centerY; // Health bar background globalDoorHealthBarBg = globalDoorHealthBar.attachAsset('doorHealthBg', { anchorX: 0.5, anchorY: 0.5 }); // Health bar fill globalDoorHealthBarFill = globalDoorHealthBar.attachAsset('doorHealthBar', { anchorX: 0.5, anchorY: 0.5 }); // Health text globalDoorHealthText = new Text2(" ", { size: 18, fill: 0xFFFFFF, weight: 800 }); globalDoorHealthText.anchor.set(0.5, 0.5); globalDoorHealthBar.addChild(globalDoorHealthText); // Add to game with proper scaling towerLayer.addChild(globalDoorHealthBar); // Después de 1 segundo, fuerza la actualización del texto a "99/100" LK.setTimeout(function () { globalDoorHealthText.setText(" "); globalDoorHealthText.setText("95/100"); updateGlobalDoorHealthBar(); }, 1000); } function updateGlobalDoorHealthBar() { if (!globalDoorHealthBar) { return; } var healthPercent = globalDoorHealth / globalDoorMaxHealth; // Calculate the actual width based on health percentage - subtract a small amount to make 95% visible var actualWidth = Math.max(0, 450 * healthPercent - 3); // Subtract 3 pixels to ensure visible difference globalDoorHealthBarFill.width = actualWidth; // Change anchor to make it decrease from right to left globalDoorHealthBarFill.anchor.set(0, 0.5); // Position the fill bar so it aligns with the left edge of the background globalDoorHealthBarFill.x = -225; // Half of the background width (450/2) to align left edge globalDoorHealthText.setText(globalDoorHealth + "/" + globalDoorMaxHealth); // Force update the health bar fill width to ensure it reflects the actual health immediately after text update LK.setTimeout(function () { globalDoorHealthBarFill.width = actualWidth; }, 1); // Update all door visuals when global health changes for (var i = 0; i < doors.length; i++) { doors[i].updateDoorVisual(); } } function placeTower(gridX, gridY) { var towerCost = getTowerCost(); if (gold >= towerCost) { // Double-check that the cell is available for placement var cellNumber = (gridY - 4) * 12 + gridX + 1; var obstacleCells = [14, 17, 18, 19, 20, 21, 22, 23, 26, 35, 38, 39, 40, 41, 42, 43, 44, 45, 47, 50, 59, 62, 64, 65, 66, 67, 68, 69, 70, 71, 74, 83, 86, 87, 88, 89, 90, 91, 92, 93, 95, 98, 107, 110, 112, 113, 114, 115, 116, 117, 118, 119, 122, 130, 131, 16]; var wallCells = [1, 2, 11, 12, 13, 24, 25, 36, 37, 48, 49, 60, 61, 72, 73, 84, 85, 96, 97, 108, 109, 120, 121, 132, 133, 134, 135, 142, 143, 144]; var isObstacleCell = cellNumber >= 1 && cellNumber <= 144 && obstacleCells.indexOf(cellNumber) !== -1; var isWallCell = cellNumber >= 1 && cellNumber <= 144 && wallCells.indexOf(cellNumber) !== -1; var existingTower = grid.towerCells[gridX] && grid.towerCells[gridX][gridY]; if (isObstacleCell && !isWallCell && !existingTower) { var tower = new Tower(); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); towersPlaced++; // Increment tower counter updateTowerPrice(); // Update tower price display grid.renderDebug(); return true; } else { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { // --- Tutorial gating --- if (tutorialActive && !tutorialAllowAll) { // Step 1/2: Only allow red button if ((tutorialStep === 1 || tutorialStep === 2) && tutorialAllowRedButton) { if (x >= tutorialRedButtonArea.x - tutorialRedButtonArea.w / 2 && x <= tutorialRedButtonArea.x + tutorialRedButtonArea.w / 2 && y >= tutorialRedButtonArea.y - tutorialRedButtonArea.h / 2 && y <= tutorialRedButtonArea.y + tutorialRedButtonArea.h / 2) { // Simulate redButton.down if (!redButton.isPressed) { redButton.isPressed = true; redButton.removeChild(redButtonGraphics); redButtonGraphics = redButton.attachAsset('redButtonPressed', { anchorX: 0.5, anchorY: 0.5 }); redButtonGraphics.scaleX = 4.0; redButtonGraphics.scaleY = 4.0; setGold(gold + 1); tutorialRedButtonPresses++; // Show gold indicator var randomXOffset = Math.random() * 450 - 250; var goldIndicator = game.addChild(new GoldIndicator(1, LK.gui.top.x + goldText.x + 350 + randomXOffset, LK.gui.top.y + goldText.y + 140)); goldIndicator.scaleX = 1.5; goldIndicator.scaleY = 1.5; } // Step up only after first press (step 1->2) and at 3 gold (step 2->3) if (tutorialStep === 1 && tutorialRedButtonPresses >= 1) { advanceTutorialStep(); } else if (tutorialStep === 2 && gold >= 10) { advanceTutorialStep(); } return; } else { // Ignore all other clicks return; } } // Step 3: Only allow turret icon click to advance to step 4 if (tutorialStep === 3 && tutorialAllowTurretIcon) { if (x >= tutorialTurretIconArea.x - tutorialTurretIconArea.w / 2 && x <= tutorialTurretIconArea.x + tutorialTurretIconArea.w / 2 && y >= tutorialTurretIconArea.y - tutorialTurretIconArea.h / 2 && y <= tutorialTurretIconArea.y + tutorialTurretIconArea.h / 2) { advanceTutorialStep(); // Advance to step 4 to show placement highlight towerPreview.visible = true; isDragging = true; towerPreview.updateAppearance(); towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); return; } else { return; } } // Step 4: Only allow tower dragging (placement highlight is shown) if (tutorialStep === 4 && tutorialAllowDragTower) { if (x >= tutorialTurretIconArea.x - tutorialTurretIconArea.w / 2 && x <= tutorialTurretIconArea.x + tutorialTurretIconArea.w / 2 && y >= tutorialTurretIconArea.y - tutorialTurretIconArea.h / 2 && y <= tutorialTurretIconArea.y + tutorialTurretIconArea.h / 2) { towerPreview.visible = true; isDragging = true; towerPreview.updateAppearance(); towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); return; } else { return; } } // Step 5: Only allow upgrade on tutorial tower if (tutorialStep === 5 && tutorialAllowUpgrade) { if (tutorialTower) { var tx = tutorialTower.x, ty = tutorialTower.y, tw = 200, th = 200; if (x >= tx - tw / 2 && x <= tx + tw / 2 && y >= ty - th / 2 && y <= ty + th / 2) { // Simulate tower.down for upgrade tutorialTowerUpgradeClicks++; tutorialTower.down(x, y, obj); return; } else { return; } } } // Step 6/7: Allow forward icon (step 6 sets up the highlight with timeout, step 7 handles the press) if (tutorialStep === 6 && tutorialAllowForward) { if (x >= tutorialForwardIconArea.x - tutorialForwardIconArea.w / 2 && x <= tutorialForwardIconArea.x + tutorialForwardIconArea.w / 2 && y >= tutorialForwardIconArea.y - tutorialForwardIconArea.h / 2 && y <= tutorialForwardIconArea.y + tutorialForwardIconArea.h / 2) { // Clear tutorial overlay immediately when forward is pressed clearTutorialOverlay(); // Simulate speedBoostButton.down if (!speedBoostButton.isPressed) { speedBoostButton.isPressed = true; speedBoostButton.isActive = true; speedBoostActive = true; speedBoostButton.removeChild(speedBoostButtonGraphics); speedBoostButtonGraphics = speedBoostButton.attachAsset('speedBoostButtonPressed', { anchorX: 0.5, anchorY: 0.5 }); speedBoostButtonGraphics.scaleX = 4.0; speedBoostButtonGraphics.scaleY = 4.0; } return; } else { return; } } // Step 9: Only allow gate repair if (tutorialStep === 9) { console.log("Checking door health:", globalDoorHealth); if (globalDoorHealth >= 99) { console.log("Door fully repaired. Advancing tutorial."); advanceTutorialStep(); } return; } // Block all other actions return; } // --- Normal game code below --- // Check for the normal tower next to red button (buttonTower) if (x >= buttonTower.x - buttonTower.width / 2 && x <= buttonTower.x + buttonTower.width / 2 && y >= buttonTower.y - buttonTower.height / 2 && y <= buttonTower.y + buttonTower.height / 2) { towerPreview.visible = true; isDragging = true; towerPreview.updateAppearance(); // Apply the same offset as in move handler to ensure consistency when starting drag towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); return; } // Check if clicking on any existing tower - if not, hide range 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; } } // If not clicking on a tower, hide the range display if (!clickedOnTower && rangeDisplayTower) { rangeDisplayTower.hideRangeIndicator(); rangeDisplayTower = null; } }; 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; } } // Clear press timer when releasing anywhere if (pressedTower) { pressedTower = null; if (pressTimer) { LK.clearTimeout(pressTimer); pressTimer = null; } } if (isDragging) { isDragging = false; // --- Tutorial gating for tower placement --- if (tutorialActive && tutorialStep === 4 && tutorialAllowDragTower) { // Only allow placement on cell 67 var cellNumber = (towerPreview.gridY - 4) * 12 + towerPreview.gridX + 1; if (towerPreview.canPlace && cellNumber === tutorialAllowPlaceCell) { placeTower(towerPreview.gridX, towerPreview.gridY); // Find the just-placed tower and mark as tutorialTower for (var i = 0; i < towers.length; i++) { var t = towers[i]; var tCell = (t.gridY - 4) * 12 + t.gridX + 1; if (tCell === tutorialAllowPlaceCell) { tutorialTower = t; break; } } advanceTutorialStep(); } else { var notification = game.addChild(new Notification("Place the turret on the highlighted cell!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; return; } // --- End tutorial gating for placement --- if (towerPreview.canPlace) { placeTower(towerPreview.gridX, towerPreview.gridY); } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; } }; var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2 + 400; waveIndicator.y = 2732 - 80; game.addChild(waveIndicator); // Do not start the game until tutorial is complete waveIndicator.gameStarted = false; currentWave = 0; waveTimer = nextWaveTime; // Create simple tower asset next to red button for dragging var buttonTower = new Container(); var buttonTowerGraphics = buttonTower.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); buttonTowerGraphics.scaleX = 3.5; buttonTowerGraphics.scaleY = 3.5; buttonTower.x = 700; buttonTower.y = 2732 - 210; buttonTower.width = buttonTowerGraphics.width * 3.5; buttonTower.height = buttonTowerGraphics.height * 3.5; game.addChild(buttonTower); sourceTower = null; enemiesToSpawn = 10; // Create red button for manual firing and gold generation var redButton = new Container(); var redButtonGraphics = redButton.attachAsset('redButton', { anchorX: 0.5, anchorY: 0.5 }); redButtonGraphics.scaleX = 4.0; redButtonGraphics.scaleY = 4.0; redButton.x = 250; redButton.y = 2732 - 220; redButton.isPressed = false; game.addChild(redButton); // Create speed boost button var speedBoostButton = new Container(); var speedBoostButtonGraphics = speedBoostButton.attachAsset('speedBoostButton', { anchorX: 0.5, anchorY: 0.5 }); speedBoostButtonGraphics.scaleX = 4.0; speedBoostButtonGraphics.scaleY = 4.0; speedBoostButton.x = 2048 - 150; speedBoostButton.y = 120; speedBoostButton.isPressed = false; speedBoostButton.isActive = false; speedBoostButton.cooldown = 0; game.addChild(speedBoostButton); // Speed boost variables var speedBoostActive = false; var speedBoostDuration = 0; var speedBoostCooldown = 0; var speedBoostMaxDuration = 600; // 10 seconds at 60fps var speedBoostMaxCooldown = 1800; // 30 seconds at 60fps // Speed boost button functionality speedBoostButton.down = function (x, y, obj) { if (!speedBoostButton.isPressed) { speedBoostButton.isPressed = true; speedBoostButton.isActive = true; speedBoostActive = true; // Change button appearance to pressed state speedBoostButton.removeChild(speedBoostButtonGraphics); speedBoostButtonGraphics = speedBoostButton.attachAsset('speedBoostButtonPressed', { anchorX: 0.5, anchorY: 0.5 }); speedBoostButtonGraphics.scaleX = 4.0; speedBoostButtonGraphics.scaleY = 4.0; // Show activation notification var notification = game.addChild(new Notification("Speed Boost Active!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; speedBoostButton.up = function (x, y, obj) { if (speedBoostButton.isPressed) { speedBoostButton.isPressed = false; speedBoostButton.isActive = false; speedBoostActive = false; // Change button back to normal state speedBoostButton.removeChild(speedBoostButtonGraphics); speedBoostButtonGraphics = speedBoostButton.attachAsset('speedBoostButton', { anchorX: 0.5, anchorY: 0.5 }); speedBoostButtonGraphics.scaleX = 4.0; speedBoostButtonGraphics.scaleY = 4.0; } }; // Button functionality redButton.down = function (x, y, obj) { if (!redButton.isPressed) { redButton.isPressed = true; // Change button appearance to pressed state redButton.removeChild(redButtonGraphics); redButtonGraphics = redButton.attachAsset('redButtonPressed', { anchorX: 0.5, anchorY: 0.5 }); redButtonGraphics.scaleX = 4.0; redButtonGraphics.scaleY = 4.0; // Give 1 gold setGold(gold + 1); // Show gold increment indicator below the gold counter var randomXOffset = Math.random() * 450 - 250; // Random value between -250 and +200 var goldIndicator = game.addChild(new GoldIndicator(1, LK.gui.top.x + goldText.x + 350 + randomXOffset, LK.gui.top.y + goldText.y + 140)); goldIndicator.scaleX = 1.5; goldIndicator.scaleY = 1.5; // Towers should not fire when red button is pressed // Red button now only gives gold } }; redButton.up = function (x, y, obj) { if (redButton.isPressed) { redButton.isPressed = false; // Change button back to normal state redButton.removeChild(redButtonGraphics); redButtonGraphics = redButton.attachAsset('redButton', { anchorX: 0.5, anchorY: 0.5 }); redButtonGraphics.scaleX = 4.0; redButtonGraphics.scaleY = 4.0; } }; game.update = function () { // Speed boost is now controlled by button press/release - no duration or cooldown needed // Apply speed boost to game mechanics var speedMultiplier = speedBoostActive ? 3 : 1; // Process wave progression with speed boost for (var speedTick = 0; speedTick < speedMultiplier; speedTick++) { waveIndicator.handleWaveProgression(); } if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; if (isBossWave && waveType !== 'swarm') { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Spawn the appropriate number of enemies // For Swarm waves, spawn 50 enemies instead of 30 and remove spacing var actualEnemyCount = waveType === 'swarm' ? 50 : enemyCount; for (var i = 0; i < actualEnemyCount; i++) { var enemy = new Enemy(waveType); // 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 = 12; var midPoint = Math.floor(gridWidth / 2); // 6 var spawnX, spawnY; if (waveType === 'swarm') { // For swarm waves, spawn without spacing - enemies spawn closely together spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 3 to 8 spawnY = -1 - i * 0.3; // Minimal spacing for swarm waves } else { // 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 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 } spawnY = -1 - i * 2 - Math.random() * 2; // Increased spacing between enemies } 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; } } // Process enemies with speed boost for (var speedTick = 0; speedTick < speedMultiplier; speedTick++) { for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { // --- Tutorial: check if this is the tutorial basic or flying enemy --- if (tutorialActive) { if (tutorialStep === 6 && tutorialBasicEnemy && enemy === tutorialBasicEnemy) { tutorialBasicEnemy = null; // Immediately spawn flying enemy when basic enemy is killed advanceTutorialStep(); // Don't advance tutorial step here - let forward button press handle it } if (tutorialStep === 8 && tutorialFlyingEnemy && enemy === tutorialFlyingEnemy) { tutorialFlyingEnemy = null; // Show door highlight overlay with repair message advanceTutorialStep(); } } for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Clean up door attacking if enemy was attacking a door if (enemy.attackingDoor) { enemy.attackingDoor.removeAttackingEnemy(enemy); enemy.attackingDoor = null; } // Enemies no longer give gold when killed // Gold indicator and gold earning removed // 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!")); 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 door attacking if enemy was attacking a door if (enemy.attackingDoor) { enemy.attackingDoor.removeAttackingEnemy(enemy); enemy.attackingDoor = null; } // 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(); } } } } // Process bullets with speed boost for (var speedTick = 0; speedTick < speedMultiplier; speedTick++) { 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); } } } // Process towers with speed boost (for firing rate) for (var speedTick = 0; speedTick < speedMultiplier; speedTick++) { for (var t = 0; t < towers.length; t++) { var tower = towers[t]; if (tower.update) { tower.update(); } } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.scaleX = 0.5;
bulletGraphics.scaleY = 0.5;
// --- NUEVO: Calcular radio de colisión basado en tamaño visual ---
var bulletRadius = bulletGraphics.width * bulletGraphics.scaleX / 2;
// Get actual enemy visual size (enemies are scaled to 50%)
var enemyGraphics = self.targetEnemy.children[0]; // First child should be the enemy graphics
var enemyRadius = enemyGraphics ? enemyGraphics.width * enemyGraphics.scaleX / 2 : 35;
// Dirección fija al momento de disparar
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
self.vx = dx / distance * self.speed;
self.vy = dy / distance * self.speed;
} else {
self.vx = 0;
self.vy = 0;
}
self.update = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
// Convert enemy position from its scaled layer to game coordinates
var enemyGameX = self.targetEnemy.x * gameScale + enemyLayer.x;
var enemyGameY = self.targetEnemy.y * gameScale + enemyLayer.y;
var dx = enemyGameX - self.x;
var dy = enemyGameY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// --- MODIFICADO: comparación de colisión considerando radios ---
if (distance < bulletRadius + enemyRadius) {
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;
}
self.destroy();
} else {
// Actualizar velocidad para rastrear objetivo
var effectiveSpeed = speedBoostActive ? self.speed * 3 : self.speed;
self.vx = dx / distance * effectiveSpeed;
self.vy = dy / distance * effectiveSpeed;
// Rotate bullet to point toward target enemy
var angle = Math.atan2(self.vy, self.vx);
bulletGraphics.rotation = angle;
self.x += self.vx;
self.y += self.vy;
}
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
self.addChild(numberLabel);
// Add cell number label (small red text)
var cellNumberLabel = new Text2('1', {
size: 20,
fill: 0xFF0000,
weight: 800
});
cellNumberLabel.anchor.set(0.5, 0.5);
self.addChild(cellNumberLabel);
self.setCellNumber = function (number) {
cellNumberLabel.setText(number.toString());
};
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.setText("-");
cellGraphics.tint = 0x880000;
return;
}
numberLabel.visible = true;
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff;
} else {
cellGraphics.tint = 0x88 - tint << 8 | tint;
}
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;
}
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.tint = 0x008800;
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 1000) / 10);
};
});
var Door = Container.expand(function (gridX, gridY) {
var self = Container.call(this);
self.gridX = gridX;
self.gridY = gridY;
self.enemiesAttacking = [];
self.lastHealthLossTime = 0;
self.isDestroyed = false;
self.originalDoorAsset = selectedDoorAsset; // Store original door asset for this door
self.destroyedAsset = 'Destruida' + (Math.floor(Math.random() * 4) + 1); // Random destroyed asset
var doorGraphics = self.attachAsset(self.originalDoorAsset, {
anchorX: 0.5,
anchorY: 0.5
});
self.updateHealthDisplay = function () {
// Individual doors no longer have health displays
// The global health bar will be updated instead
};
self.updateDoorVisual = function () {
// Check if door should be destroyed (when global health is 0)
var shouldBeDestroyed = globalDoorHealth <= 0;
if (shouldBeDestroyed && !self.isDestroyed) {
// Change to destroyed visual
self.isDestroyed = true;
self.removeChild(self.children[0]); // Remove current graphics
var destroyedGraphics = self.attachAsset(self.destroyedAsset, {
anchorX: 0.5,
anchorY: 0.5
});
} else if (!shouldBeDestroyed && self.isDestroyed) {
// Change back to original door visual
self.isDestroyed = false;
self.removeChild(self.children[0]); // Remove destroyed graphics
var doorGraphics = self.attachAsset(self.originalDoorAsset, {
anchorX: 0.5,
anchorY: 0.5
});
}
};
self.takeDamage = function () {
var effectiveDamageRate = speedBoostActive ? Math.ceil(60 / 3) : 60;
if (self.enemiesAttacking.length > 0 && LK.ticks - self.lastHealthLossTime >= effectiveDamageRate) {
globalDoorHealth = Math.max(0, globalDoorHealth - self.enemiesAttacking.length);
self.lastHealthLossTime = LK.ticks;
// Update global door health bar
updateGlobalDoorHealthBar();
// Update door visual state
self.updateDoorVisual();
}
};
self.repair = function () {
if (gold >= 1 && globalDoorHealth < globalDoorMaxHealth) {
setGold(gold - 1);
globalDoorHealth = Math.min(globalDoorMaxHealth, globalDoorHealth + 1);
// Update global door health bar
updateGlobalDoorHealthBar();
// Update door visual state
self.updateDoorVisual();
return true;
}
return false;
};
self.addAttackingEnemy = function (enemy) {
if (self.enemiesAttacking.indexOf(enemy) === -1) {
self.enemiesAttacking.push(enemy);
}
};
self.removeAttackingEnemy = function (enemy) {
var index = self.enemiesAttacking.indexOf(enemy);
if (index !== -1) {
self.enemiesAttacking.splice(index, 1);
}
};
self.update = function () {
self.takeDamage();
};
self.down = function (x, y, obj) {
if (self.repair()) {
var notification = game.addChild(new Notification("Door repaired! (-1 gold)"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (gold < 1) {
var notification = game.addChild(new Notification("Not enough gold to repair!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
var notification = game.addChild(new Notification("Door is at full health!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
return self;
});
// Base enemy class for common functionality
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'normal';
self.speed = .01;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
self.animationFrame = 0;
self.animationTimer = 0;
self.animationSpeed = 15;
self.currentAssetIndex = 0;
switch (self.type) {
case 'fast':
self.speed *= 2;
self.maxHealth = 100;
self.enemyAssets = ['EnemyFast1', 'EnemyFast2', 'EnemyFast3'];
break;
case 'tank':
self.isImmune = true;
self.maxHealth = 240;
self.speed *= 0.5; // Reduce speed by 0.5x
self.enemyAssets = ['EnemyImmune1', 'EnemyImmune2', 'EnemyImmune3'];
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
self.enemyAssets = ['EnemyFlying1', 'EnemyFlying2', 'EnemyFlying3'];
break;
case 'swarm':
self.maxHealth = 15;
self.enemyAssets = ['EnemySwarm1', 'EnemySwarm2', 'EnemySwarm3'];
break;
case 'normal':
default:
self.enemyAssets = ['Enemy1', 'Enemy2', 'Enemy3'];
break;
}
// Apply tutorial speed boost for normal enemies only
if (tutorialActive && self.type === 'normal') {
self.speed *= 2; // Double speed during tutorial
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
self.maxHealth *= 20;
self.speed = self.speed * 0.7;
// Override enemy assets for boss enemies to use boss sprites
self.enemyAssets = ['EnemyBoss1', 'EnemyBoss2', 'EnemyBoss3'];
}
self.health = self.maxHealth;
// Carga múltiples sprites y alterna visibilidad sin parpadeo
self.enemyGraphicsList = [];
self.enemyAssets.forEach(function (assetName, index) {
var gfx = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
gfx.visible = index === 0; // Solo el primero visible
gfx.scaleX = self.isBoss ? 0.9 : 0.5;
gfx.scaleY = self.isBoss ? 0.9 : 0.5;
self.enemyGraphicsList.push(gfx);
});
self.enemyGraphics = self.enemyGraphicsList[0];
// --- HITBOX ---
var realWidth = self.enemyGraphics.width * self.enemyGraphics.scaleX;
var realHeight = self.enemyGraphics.height * self.enemyGraphics.scaleY;
self.hitbox = {
x: self.x - realWidth / 2,
y: self.y - realHeight / 2,
width: realWidth,
height: realHeight
};
// ---------------
if (self.isFlying) {
self.shadow = new Container();
var shadowGraphics = self.shadow.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
shadowGraphics.tint = 0x000000;
shadowGraphics.alpha = 0; // Start with 0% transparency
shadowGraphics.scaleX = self.isBoss ? 0.9 : 0.5;
shadowGraphics.scaleY = self.isBoss ? 0.9 : 0.5;
// Position shadow outside visible area initially to prevent visible spawn/move effect
self.shadow.x = -1000;
self.shadow.y = -1000;
shadowGraphics.rotation = self.enemyGraphics.rotation;
// Tween shadow alpha from 0 to 0.5 over 1 second
tween(shadowGraphics, {
alpha: 0.5
}, {
duration: 1000
});
}
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 = -self.enemyGraphics.height * 0.5 / 2 - 5;
healthBarOutline.x = -healthBarOutline.width / 2 + 15;
healthBarBG.x = healthBar.x = -healthBar.width / 2 + 15;
healthBarOutline.scaleX = healthBarOutline.scaleY = 0.5;
healthBarBG.scaleX = healthBarBG.scaleY = 0.5;
healthBar.scaleX = healthBar.scaleY = 0.5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
self.update = function () {
// Cambio de animación sin parpadeo - now applies to all enemy types
self.animationTimer++;
if (self.animationTimer >= self.animationSpeed) {
self.animationTimer = 0;
self.currentAssetIndex = (self.currentAssetIndex + 1) % self.enemyGraphicsList.length;
var newGfx = self.enemyGraphicsList[self.currentAssetIndex];
// Copia la rotación del sprite anterior al nuevo
newGfx.rotation = self.enemyGraphics.rotation;
self.enemyGraphicsList.forEach(function (gfx, index) {
gfx.visible = index === self.currentAssetIndex;
});
self.enemyGraphics = newGfx;
}
var visualWidth = self.enemyGraphics.width * self.enemyGraphics.scaleX;
var visualHeight = self.enemyGraphics.height * self.enemyGraphics.scaleY;
self.hitbox = {
x: self.x - visualWidth / 2,
y: self.y - visualHeight / 2,
width: visualWidth,
height: visualHeight
};
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
if (self.isImmune) {
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
if (self.slowed) {
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
if (!self.poisoned) {
self.enemyGraphics.tint = 0xFFFFFF;
}
}
}
if (self.poisoned) {
if (!self.poisonEffect) {
self.poisonEffect = true;
}
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;
if (!self.slowed) {
self.enemyGraphics.tint = 0xFFFFFF;
}
}
}
}
if (self.isImmune) {
self.enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
self.enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned) {
self.enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
self.enemyGraphics.tint = 0x9900FF;
} else {
self.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 (self.enemyGraphics.targetRotation === undefined) {
self.enemyGraphics.targetRotation = angle;
self.enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - self.enemyGraphics.targetRotation) > 0.05) {
tween.stop(self.enemyGraphics, {
rotation: true
});
var currentRotation = self.enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
self.enemyGraphics.targetRotation = angle;
tween(self.enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -self.enemyGraphics.height * 0.5 / 2 - 5;
};
return self;
});
var GoldIndicator = Container.expand(function (amount, x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
var goldTextShadow = new Text2('+' + amount, {
size: 60,
fill: 0x000000,
weight: 800
});
goldTextShadow.anchor.set(0.5, 0.5);
goldTextShadow.scaleX = 1.1; // Adjusted scale for better visibility
goldTextShadow.scaleY = 1.1; // Adjusted scale for better visibility
goldTextShadow.x = -2; // Offset for shadow effect
goldTextShadow.y = -2; // Offset for shadow effect
self.addChild(goldTextShadow);
var goldText = new Text2('+' + amount, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
var fadeTime = 60;
var moveSpeed = 2;
self.update = function () {
self.y -= moveSpeed;
fadeTime--;
if (fadeTime <= 0) {
self.destroy();
} else {
self.alpha = fadeTime / 60;
}
};
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
self.cells = [];
self.towerCells = []; // Separate tracking for tower placement
self.spawns = [];
self.goals = [];
for (var i = 0; i < gridWidth; i++) {
self.cells[i] = [];
self.towerCells[i] = [];
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j] = {
score: 0,
pathId: 0,
towersInRange: []
};
self.towerCells[i][j] = false; // Track tower placement separately
}
}
/*
Cell Types
0: Transparent floor
1: Wall
2: Spawn
3: Goal
*/
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0;
if (i > 5 - 3 && i <= 5 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = 0;
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = 0;
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
// Calculate cell number (1-based indexing)
// Only count cells in the playable area (j > 3 && j <= gridHeight - 4)
var cellNumber = (j - 4) * gridWidth + i + 1;
debugCell.setCellNumber(cellNumber);
// Place obstacles on specific cells
var obstacleCells = [14, 17, 18, 19, 20, 21, 22, 23, 26, 35, 38, 39, 40, 41, 42, 43, 44, 45, 47, 50, 59, 62, 64, 65, 66, 67, 68, 69, 70, 71, 74, 83, 86, 87, 88, 89, 90, 91, 92, 93, 95, 98, 107, 110, 112, 113, 114, 115, 116, 117, 118, 119, 122, 12, 130, 131, 16];
if (obstacleCells.indexOf(cellNumber) !== -1) {
// Create obstacle using randomly selected grass asset
var obstacle = new Container();
var obstacleGraphics = obstacle.attachAsset(selectedGrassAsset, {
anchorX: 0.5,
anchorY: 0.5
});
obstacle.x = i * CELL_SIZE;
obstacle.y = j * CELL_SIZE;
self.addChild(obstacle);
// Mark cell as wall so enemies cannot traverse
cell.type = 1;
}
// Force specific cells to be path regardless of other conditions
var pathCells = [3, 4, 5, 6, 7, 8, 9, 10, 15, 27, 28, 29, 30, 31, 32, 33, 34, 46, 51, 52, 53, 54, 55, 56, 57, 58, 63, 75, 76, 77, 78, 79, 80, 81, 82, 94, 99, 100, 101, 102, 103, 104, 105, 106, 111, 123, 124, 125, 126, 127, 128, 129, 141, 136, 137, 138, 139, 140];
if (pathCells.indexOf(cellNumber) !== -1) {
cell.type = 0; // Force as path
// Create path using randomly selected path asset
var path = new Container();
var pathGraphics = path.attachAsset(selectedPathAsset, {
anchorX: 0.5,
anchorY: 0.5
});
path.x = i * CELL_SIZE;
path.y = j * CELL_SIZE;
self.addChild(path);
}
// Place doors on specific cells
if (doorCells.indexOf(cellNumber) !== -1) {
// Create door instance
var door = new Door(i, j);
door.x = i * CELL_SIZE;
door.y = j * CELL_SIZE;
self.addChild(door);
doors.push(door);
// Mark cell as door type (type 4)
cell.type = 4;
cell.door = door;
}
// Place walls on specific cells
var wallCells = [1, 2, 11, 12, 13, 24, 25, 36, 37, 48, 49, 60, 61, 72, 73, 84, 85, 96, 97, 108, 109, 120, 121, 132, 133, 134, 135, 142, 143, 144];
if (wallCells.indexOf(cellNumber) !== -1) {
// Create wall using randomly selected wall asset
var wall = new Container();
var wallGraphics = wall.attachAsset(selectedWallAsset, {
anchorX: 0.5,
anchorY: 0.5
});
wall.x = i * CELL_SIZE;
wall.y = j * CELL_SIZE;
self.addChild(wall);
// Mark cell as wall so enemies cannot traverse and towers cannot be placed
cell.type = 1;
}
}
}
}
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];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.left.type != 1) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.right.type != 1) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.right.type != 1) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.left.type != 1) {
processNode(node.downLeft, targetScore, node);
}
targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
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 && cell.type == 3) {
return true;
}
// Also check if enemy has reached the bottom of the screen (for enemies moving directly to goals)
if (enemy.currentCellY >= 18) {
// Grid height is 19 (13+6), so 18 is the bottom row
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;
// 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 / gameScale + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y / gameScale + 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);
}
// Check if enemy has moved past the bottom of the playable area
if (enemy.currentCellY >= 18) {
// Grid height is 19 (13+6), so 18 is the bottom row
return true;
}
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 / gameScale + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y / gameScale + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Check if enemy has reached cell 124 (position where they should choose a door)
var currentCellNumber = (Math.round(enemy.currentCellY) - 4) * 12 + Math.round(enemy.currentCellX) + 1;
if (currentCellNumber === 124 && !enemy.selectedDoorCell) {
// Enemy reached decision point - randomly select a door cell to attack
var doorCellNumbers = [136, 137, 138, 139, 140, 141];
// Use a more robust random selection method with explicit array length
var randomIndex = Math.floor(Math.random() * doorCellNumbers.length);
var selectedDoorCellNumber = doorCellNumbers[randomIndex];
// Convert cell number back to grid coordinates with explicit calculation
var targetGridY = Math.floor((selectedDoorCellNumber - 1) / 12) + 4;
var targetGridX = (selectedDoorCellNumber - 1) % 12;
// Verify coordinates for cell 140 specifically
if (selectedDoorCellNumber === 140) {
// Cell 140: (140-1) = 139, 139/12 = 11.58, floor = 11, +4 = 15
// 139 % 12 = 7
targetGridY = 15;
targetGridX = 7;
}
// Set the enemy's target to the selected door cell
enemy.selectedDoorCell = {
x: targetGridX,
y: targetGridY
};
enemy.currentTarget = enemy.selectedDoorCell;
// Debug logging to verify all cells are being selected
console.log("Enemy selected door cell:", selectedDoorCellNumber, "at grid:", targetGridX, targetGridY);
}
// Check if enemy is trying to move into a door cell
var targetCell = grid.getCell(Math.round(enemy.currentCellX), Math.round(enemy.currentCellY));
if (targetCell && targetCell.type === 4 && targetCell.door && globalDoorHealth > 0) {
// Enemy encounters a door - start attacking it
if (!enemy.attackingDoor) {
enemy.attackingDoor = targetCell.door;
targetCell.door.addAttackingEnemy(enemy);
}
// Stop moving and attack the door
enemy.currentTarget = undefined;
enemy.x = grid.x / gameScale + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y / gameScale + enemy.currentCellY * CELL_SIZE;
return false;
}
// If enemy was attacking a door but door is now destroyed, clear attacking state and continue moving
if (enemy.attackingDoor && globalDoorHealth <= 0) {
enemy.attackingDoor.removeAttackingEnemy(enemy);
enemy.attackingDoor = null;
// Instead of resetting target, set enemy to move directly to the goal
// Find the closest goal cell for direct movement
var closestGoal = self.goals[0];
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var g = 0; g < self.goals.length; g++) {
var goal = self.goals[g];
var dx = goal.x - enemy.currentCellX;
var dy = goal.y - enemy.currentCellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
closestGoal = goal;
}
}
}
enemy.currentTarget = closestGoal; // Set direct target to goal
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget) {
// If enemy has a selected door cell, prioritize moving to it
if (enemy.selectedDoorCell) {
enemy.currentTarget = enemy.selectedDoorCell;
} else {
enemy.currentTarget = cell.targets[0];
}
}
if (enemy.currentTarget) {
// Don't override target if enemy has selected a specific door cell
if (!enemy.selectedDoorCell && 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);
// Only clear target if we weren't heading to a specific door
if (!enemy.selectedDoorCell) {
enemy.currentTarget = undefined;
}
return;
}
// Special movement logic for enemies targeting door cells
if (enemy.selectedDoorCell) {
// First move to correct X position, then move down in Y
var targetX = enemy.selectedDoorCell.x;
var targetY = enemy.selectedDoorCell.y;
var xDistance = Math.abs(targetX - enemy.currentCellX);
var yDistance = Math.abs(targetY - enemy.currentCellY);
// If not at correct X position, move horizontally first
if (xDistance > 0.1) {
if (targetX > enemy.currentCellX) {
enemy.currentCellX += enemy.speed;
} else {
enemy.currentCellX -= enemy.speed;
}
// Rotate enemy to face movement direction
var angle = targetX > enemy.currentCellX ? 0 : Math.PI;
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
});
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
} else {
// At correct X position, now move down in Y
enemy.currentCellY += enemy.speed;
// Rotate enemy to face downward
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
});
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
} else {
// Normal pathfinding movement for enemies without door target
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
}
enemy.x = grid.x / gameScale + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y / gameScale + enemy.currentCellY * CELL_SIZE;
};
});
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,
style: 'bold'
});
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 Tower = Container.expand(function () {
var self = Container.call(this);
self.level = 1;
self.maxLevel = 5;
self.clicksInvested = 0; // Track total clicks invested in this tower
self.clicksNeededForNextLevel = 50; // Clicks needed to reach next level
self.gridX = 0;
self.gridY = 0;
// Method to calculate clicks needed for current level (progressive: 20, 50, 100, 200)
self.getClicksNeededForCurrentLevel = function () {
if (self.level >= self.maxLevel) {
return 0;
}
// New click requirements per level
switch (self.level) {
case 1:
return 20;
// Level 1->2: 20 clicks
case 2:
return 50;
// Level 2->3: 50 clicks
case 3:
return 100;
// Level 3->4: 100 clicks
case 4:
return 200;
// Level 4->5: 200 clicks
default:
return 0;
}
};
// Standardized method to get the current range of the tower
self.getRange = function () {
// Default: base 3, +0.5 per level
return (2 + (self.level - 1) * 0.5) * CELL_SIZE;
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
baseGraphics.x = -90;
baseGraphics.y = 150;
baseGraphics.tint = 0xAAAAAA;
// Reduce tower base size by 50%
baseGraphics.scaleX = 0.5;
baseGraphics.scaleY = 0.5;
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 12; // Reduce dot size by 50%
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 = -130 + dotSpacing * (i + 1);
dot.y = 125 + CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
gunContainer.x = -90;
gunContainer.y = 150;
var gunGraphics = gunContainer.attachAsset('defense', {
anchorX: 0.5,
anchorY: 0.5
});
// Reduce gun size by 50%
gunGraphics.scaleX = 0.5;
gunGraphics.scaleY = 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 {
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 - 200;
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 () {
// Use the cost at the time this tower was placed (10 + 10 * number of towers before this one)
// Since we don't track individual tower placement order, use current cost as approximation
var baseTowerCost = 10; // Original base cost
// Calculate total clicks invested with progressive requirements
var totalClicksInvested = 0;
for (var level = 1; level < self.level; level++) {
totalClicksInvested += level * 10; // Sum up all previous level investments
}
totalClicksInvested += self.clicksInvested; // Add current level progress
var totalInvestment = baseTowerCost + totalClicksInvested;
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1))
var baseUpgradeCost = 5; // Base tower cost
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 upgrades
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect
self.damage = 10 + self.level * 20; // double the effect
self.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
self.refreshCellsInRange();
self.updateLevelIndicators();
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestDistance = Infinity;
// Calculamos la posición real del cañón
var gunWorldX = self.x + -180 * 0.5;
var gunWorldY = self.y + 300 * 0.5;
// Debug circle removed - using persistent range display instead
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calcula la distancia desde el cañón, no desde el centro lógico
var dx = enemy.x - gunWorldX;
var dy = enemy.y - gunWorldY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.getRange()) {
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
}
self.targetEnemy = closestEnemy;
return closestEnemy;
};
self.update = function () {
self.findTarget();
if (self.targetEnemy) {
// Calculate the angle from the gun position to the target enemy
// Gun container is at (-95, 150) relative to tower center, scaled by 50%
var gunWorldX = self.x + -95 * 0.5;
var gunWorldY = self.y + 300 * 0.5;
var dx = self.targetEnemy.x - gunWorldX;
var dy = self.targetEnemy.y - gunWorldY;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
// Fire automatically when target is in range and fire rate allows
// Apply speed boost multiplier to firing rate
var effectiveFireRate = speedBoostActive ? Math.ceil(self.fireRate / 3) : self.fireRate;
if (LK.ticks - self.lastFired >= effectiveFireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
// Hide range from previously selected tower
if (rangeDisplayTower && rangeDisplayTower !== self) {
rangeDisplayTower.hideRangeIndicator();
}
// Show range for this tower
self.showRange();
rangeDisplayTower = self;
// Track pressed tower and start timer for sell functionality
pressedTower = self;
pressStartTime = LK.ticks;
// Clear any existing timer
if (pressTimer) {
LK.clearTimeout(pressTimer);
}
// Set timer for 5 seconds (300 ticks at 60fps)
pressTimer = LK.setTimeout(function () {
if (pressedTower === self) {
// Sell the tower
var sellValue = self.getTotalValue();
setGold(gold + sellValue);
// Show sell notification
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Remove tower from grid tracking
if (grid.towerCells[self.gridX]) {
grid.towerCells[self.gridX][self.gridY] = false;
}
// Hide range indicator
self.hideRangeIndicator();
if (rangeDisplayTower === self) {
rangeDisplayTower = null;
}
// Remove tower from arrays and destroy
var towerIndex = towers.indexOf(self);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
// Decrement towers placed counter when selling
if (towersPlaced > 0) {
towersPlaced--;
}
updateTowerPrice(); // Update tower price display
self.destroy();
pressedTower = null;
}
}, 5000);
// Progressive upgrade system - clicks no longer cost gold
if (self.level < self.maxLevel) {
self.clicksInvested++;
// Check if we have enough clicks to level up (progressive requirements)
var clicksNeeded = self.getClicksNeededForCurrentLevel();
if (self.clicksInvested >= clicksNeeded) {
self.level++;
// Reset click counter for next level
self.clicksInvested = 0;
// Apply upgrades
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (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();
// Update range display after upgrade
self.showRange();
// Animate level indicator
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
});
}
});
}
// Show level up notification
var notification = game.addChild(new Notification("Tower upgraded to level " + self.level + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Check if this is the tutorial tower reaching level 2
if (tutorialActive && tutorialStep === 5 && self === tutorialTower && self.level >= 2) {
advanceTutorialStep();
}
} else {
// Show progress notification (progressive click requirements)
var totalClicksNeeded = self.getClicksNeededForCurrentLevel();
var clicksRemaining = totalClicksNeeded - self.clicksInvested;
var notification = game.addChild(new Notification(clicksRemaining + " more clicks to level " + (self.level + 1)));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (self.level >= self.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
self.up = function (x, y, obj) {
// Clear the press timer if this tower was being pressed
if (pressedTower === self) {
pressedTower = null;
if (pressTimer) {
LK.clearTimeout(pressTimer);
pressTimer = null;
}
}
};
self.showRange = function () {
// Remove existing range indicator first
self.hideRangeIndicator();
// Create range indicator
var rangeIndicator = new Container();
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
rangeGraphics.tint = 0xffffff; // White color for range (original color)
rangeIndicator.isTowerRange = true;
// Position at tower center with offset to align with visual elements
rangeIndicator.x = -88; // Offset left to center on visual tower
rangeIndicator.y = 155; // Offset down to center on visual tower
self.addChild(rangeIndicator);
};
self.hideRangeIndicator = function () {
for (var i = self.children.length - 1; i >= 0; i--) {
if (self.children[i].isTowerRange) {
self.removeChild(self.children[i]);
}
}
};
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) {
// Calculate bullet spawn position relative to gun container position
// gunContainer is positioned at (-95, 150) relative to tower center
// Apply the same scaling as the tower layer
// Define el punto de la punta del cañón localmente en gunContainer (por ejemplo, en el eje X positivo)
// Obtener la punta del cañón teniendo en cuenta el anchor
// Calcula la posición global de la punta del cañón manualmente
var offsetX = 30; // mueve hacia la punta (horizontal)
var offsetY = -5; // sube o baja (vertical)
// Aplica rotación del cañón a ese offset
var cos = Math.cos(gunContainer.rotation);
var sin = Math.sin(gunContainer.rotation);
var rotatedX = offsetX * cos - offsetY * sin;
var rotatedY = offsetX * sin + offsetY * cos;
// Posición global final considerando jerarquía y escala de juego
var bulletX = (self.x + gunContainer.x + rotatedX) * gameScale;
var bulletY = (self.y + gunContainer.y + rotatedY) * gameScale;
// Crear la bala en esa posición
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// --- Fire recoil effect for gunContainer (rotation only) ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
scaleX: true,
scaleY: true
});
// Store original scale values if not already stored
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset scale to resting values before animating (in case of interrupted tweens)
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Animate scale recoil effect (gun briefly shrinks then returns to normal)
tween(gunContainer, {
scaleX: gunContainer._restScaleX * 0.8,
scaleY: gunContainer._restScaleY * 0.8
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original scale
tween(gunContainer, {
scaleX: gunContainer._restScaleX,
scaleY: gunContainer._restScaleY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
// ✅ No usar gameScale aquí
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
if (grid.towerCells[gridX] && grid.towerCells[gridX][gridY] !== undefined) {
grid.towerCells[gridX][gridY] = true;
}
}
}
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(); // Check against current tower cost
// 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();
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
var previewCircleScale = 1.83; // Cambia este valor para agrandar o achicar el círculo
rangeGraphics.width = rangeGraphics.height = previewRange * 2 * previewCircleScale;
previewGraphics.tint = 0xAAAAAA;
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var cell = grid.getCell(self.gridX, self.gridY);
// Check if this cell is specifically an obstacle (not a wall)
var cellNumber = (self.gridY - 4) * 12 + self.gridX + 1;
var obstacleCells = [14, 17, 18, 19, 20, 21, 22, 23, 26, 35, 38, 39, 40, 41, 42, 43, 44, 45, 47, 50, 59, 62, 64, 65, 66, 67, 68, 69, 70, 71, 74, 83, 86, 87, 88, 89, 90, 91, 92, 93, 95, 98, 107, 110, 112, 113, 114, 115, 116, 117, 118, 119, 122, 130, 131, 16];
var wallCells = [1, 2, 11, 12, 13, 24, 25, 36, 37, 48, 49, 60, 61, 72, 73, 84, 85, 96, 97, 108, 109, 120, 121, 132, 133, 134, 135, 142, 143, 144];
// Check if there's already a tower at this exact position using separate tower tracking
var existingTower = grid.towerCells[self.gridX] && grid.towerCells[self.gridX][self.gridY];
// Check if this is a wall cell (towers cannot be placed on walls)
var isWallCell = cellNumber >= 1 && cellNumber <= 144 && wallCells.indexOf(cellNumber) !== -1;
// Allow placement on obstacle cells that don't have towers and are not walls
var isObstacleCell = cellNumber >= 1 && cellNumber <= 144 && obstacleCells.indexOf(cellNumber) !== -1;
// Only check the exact cell for tower placement, not neighboring cells
var validGridPlacement = cell && !existingTower && isObstacleCell && !isWallCell;
self.canPlace = validGridPlacement;
self.hasEnoughGold = gold >= getTowerCost(); // Check against current tower cost
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = (x - grid.x) / gameScale;
var gridPosY = (y - grid.y) / gameScale;
// Para X dejamos el redondeo normal
self.gridX = Math.round(gridPosX / CELL_SIZE);
// Para Y, calculamos la fracción dentro de la celda
var yInCell = gridPosY / CELL_SIZE % 1;
// Si el mouse pasó más del 90% de la celda, pasamos a la siguiente celda (redondeo hacia arriba)
if (yInCell > 0.9) {
self.gridY = Math.ceil(gridPosY / CELL_SIZE);
} else {
self.gridY = Math.ceil(gridPosY / CELL_SIZE);
}
// Actualizamos la posición visual acorde a gridX, gridY
self.x = grid.x + self.gridX * CELL_SIZE * gameScale;
self.y = grid.y + self.gridY * CELL_SIZE * gameScale;
self.checkPlacement();
};
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;
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 = "Tank";
enemyType = "tank";
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', 'tank', '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 'tank':
block.tint = 0xAA0000;
waveType = "Boss Tank";
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 'tank':
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 tank
block.tint = 0xAA0000;
waveType = "Tank";
enemyType = "tank";
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.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave - 1) * 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; i++) {
var marker = self.waveMarkers[i];
var block = marker.children[0];
if (i < currentWave) {
block.alpha = .5;
}
// Hide markers that are completely outside the left side of the yellow indicator rectangle
var leftEdge = self.positionIndicator.x - (blockWidth - 16) / 2;
if (marker.x + blockWidth / 2 < leftEdge) {
marker.visible = false;
} else {
marker.visible = true;
}
}
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
3;
75;
var CELL_SIZE = 76;
var selectedWallAsset = 'Muro' + (Math.floor(Math.random() * 4) + 1); // Randomly select Muro1, Muro2, Muro3, or Muro4
var selectedGrassAsset = 'Pasto' + (Math.floor(Math.random() * 4) + 1); // Randomly select Pasto1, Pasto2, Pasto3, or Pasto4
var selectedPathAsset = 'Camino' + (Math.floor(Math.random() * 4) + 1); // Randomly select Camino1, Camino2, Camino3, or Camino4
var selectedDoorAsset = 'Puerta' + (Math.floor(Math.random() * 4) + 1); // Randomly select door asset for this game
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
var rangeDisplayTower = null; // Track which tower is showing range
var pressedTower = null; // Track which tower is being pressed
var pressStartTime = 0; // Track when the press started
var pressTimer = null; // Timer for checking press duration
var gold = 0; // Start with 0 gold for tutorial
var lives = 1;
var score = 0;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 4;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var towersPlaced = 0; // Track total towers placed for progressive pricing
var doorCells = [136, 137, 138, 139, 140, 141]; // Door cell numbers
var doors = []; // Track door instances
var globalDoorHealth = 95; // Start door health at 95/100 for tutorial
var globalDoorMaxHealth = 100; // Maximum health for all doors
var globalDoorHealthBar = null; // Global health bar container
var globalDoorHealthBarBg = null; // Global door health bar background
var globalDoorHealthBarFill = null; // Global door health bar fill
var globalDoorHealthText = null; // Global door health bar text
var topMargin = 100; // Define topMargin variable
// --- Tutorial System ---
var tutorialActive = true;
var tutorialStep = 0;
var tutorialOverlay = null;
var tutorialArrow = null;
var tutorialMessage = null;
var tutorialTargetArea = null;
var tutorialRedButtonPresses = 0;
var tutorialTowerPlaced = false;
var tutorialTower = null;
var tutorialTowerUpgradeClicks = 0;
var tutorialEnemyKilled = false;
var tutorialFlyingEnemyKilled = false;
var tutorialGateRepaired = false;
var tutorialForwardButton = null;
var tutorialForwardButtonPressed = false;
var tutorialFlyingEnemy = null;
var tutorialBasicEnemy = null;
var tutorialRepairMessageShown = false;
var tutorialGoodLuckShown = false;
var tutorialAllowTowerPlacement = false;
var tutorialAllowUpgrade = false;
var tutorialAllowRepair = false;
var tutorialAllowForward = false;
var tutorialAllowRedButton = true;
var tutorialAllowDragTower = false;
var tutorialAllowPlaceCell = 67; // Only allow cell 67 for first tower
var tutorialAllowTurretIcon = false;
var tutorialAllowGateRepair = false;
var tutorialAllowFlyingEnemy = false;
var tutorialAllowBasicEnemy = false;
var tutorialAllowSpeedBoost = false;
var tutorialAllowGameStart = false;
var tutorialAllowAll = false;
var tutorialCellOffsetX = -85; // Offset for tutorial cell highlighting
var tutorialCellOffsetY = -85; // Offset for tutorial cell highlighting
var tutorialOffsetStep5X = 530; // Offset for tutorial step 5 highlight
var tutorialOffsetStep5Y = 820; // Offset for tutorial step 5 highlight
var tutorialForwardIconArea = {
x: 2048 - 150,
y: 120,
w: 240,
h: 180
}; // Area for speed/forward button
var tutorialRedButtonArea = {
x: 250,
y: 2732 - 220,
w: 350,
h: 350
}; // Area for red button
var tutorialTurretIconArea = {
x: 700,
y: 2732 - 210,
w: 350,
h: 350
}; // Area for turret icon
var tutorialGateArea = {
x: 1050,
y: 2250,
w: 1200,
h: 300
}; // Area for gate repair
function showTutorialOverlay(area, message, arrowDir) {
// Add null check for area parameter
if (!area) {
console.error("showTutorialOverlay called with null/undefined area parameter");
return;
}
// Remove previous overlay/arrow/message
if (tutorialOverlay) {
tutorialOverlay.destroy();
tutorialOverlay = null;
}
if (tutorialArrow) {
tutorialArrow.destroy();
tutorialArrow = null;
}
if (tutorialMessage) {
tutorialMessage.destroy();
tutorialMessage = null;
}
// Overlay: darken everything except area
tutorialOverlay = new Container();
tutorialOverlay.zIndex = 9999;
tutorialOverlay.alpha = 0.5;
// Four rectangles: top, left, right, bottom
var darkColor = 0x000000;
var topRect = tutorialOverlay.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
topRect.width = 2048;
topRect.height = Math.max(0, area.y - area.h / 2);
topRect.tint = darkColor;
var leftRect = tutorialOverlay.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
leftRect.width = Math.max(0, area.x - area.w / 2);
leftRect.height = area.h;
leftRect.y = area.y - area.h / 2;
leftRect.tint = darkColor;
var rightRect = tutorialOverlay.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
rightRect.width = Math.max(0, 2048 - (area.x + area.w / 2));
rightRect.height = area.h;
rightRect.x = area.x + area.w / 2;
rightRect.y = area.y - area.h / 2;
rightRect.tint = darkColor;
var bottomRect = tutorialOverlay.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
bottomRect.width = 2048;
bottomRect.height = Math.max(0, 2732 - (area.y + area.h / 2));
bottomRect.y = area.y + area.h / 2;
bottomRect.tint = darkColor;
game.addChild(tutorialOverlay);
var fontSize = 80;
if (message === "Good Luck") {
fontSize = 200; // O incluso 250 si quieres algo más grande
}
// Message - position below area if it's in the top part of screen (forward button area)
tutorialMessage = new Text2(message, {
size: fontSize,
fill: 0xffffff,
weight: 800,
align: 'center'
});
tutorialMessage.anchor.set(0.5, 0.5);
tutorialMessage.x = 2048 / 2;
// Check if this is the forward button area (top of screen) and position below instead
if (area.y < 300) {
tutorialMessage.y = area.y + area.h / 2 + 220; // Position below the area
} else {
tutorialMessage.y = area.y - area.h / 2 - 220; // Position above the area (normal)
}
tutorialMessage.zIndex = 10000; // Ensure message appears above all other objects
game.addChild(tutorialMessage);
// Arrow (optional) - point up if below area, down if above
if (arrowDir) {
tutorialArrow = new Container();
var arrowGfx = tutorialArrow.attachAsset('arrow', {
anchorX: 0.5,
anchorY: 0.5
});
arrowGfx.scaleX = 6;
arrowGfx.scaleY = 6;
// Point up if message is below area, down if above
if (area.y < 300) {
arrowGfx.rotation = -Math.PI / 2; // Point upward
tutorialArrow.y = area.y + area.h / 2 + 60;
} else {
arrowGfx.rotation = Math.PI / 2; // Point downward
tutorialArrow.y = area.y - area.h / 2 - 60;
}
tutorialArrow.x = area.x;
tutorialArrow.zIndex = 10000; // Ensure arrow appears above all other objects
game.addChild(tutorialArrow);
}
}
function clearTutorialOverlay() {
if (tutorialOverlay) {
tutorialOverlay.destroy();
tutorialOverlay = null;
}
if (tutorialArrow) {
tutorialArrow.destroy();
tutorialArrow = null;
}
if (tutorialMessage) {
tutorialMessage.destroy();
tutorialMessage = null;
}
}
function advanceTutorialStep() {
clearTutorialOverlay();
tutorialStep++;
console.log("Tutorial Step " + tutorialStep + " started");
// Step logic
if (tutorialStep === 1) {
// Step 1: Press red button to earn gold
tutorialAllowRedButton = true;
tutorialAllowDragTower = false;
tutorialAllowTurretIcon = false;
tutorialAllowUpgrade = false;
tutorialAllowRepair = false;
tutorialAllowForward = false;
tutorialAllowAll = false;
showTutorialOverlay(tutorialRedButtonArea, "Press the Red Button to earn Gold", "up");
} else if (tutorialStep === 2) {
// Step 2: Keep pressing until 10 gold
showTutorialOverlay(tutorialRedButtonArea, "Keep Pressing until you Reach 10 Gold", "up");
} else if (tutorialStep === 3) {
// Step 3a: Show turret icon highlight first
tutorialAllowRedButton = false;
tutorialAllowTurretIcon = true;
tutorialAllowDragTower = false; // Don't allow dragging yet
tutorialAllowPlaceCell = 67;
showTutorialOverlay(tutorialTurretIconArea, "Click the Turret Icon", "up");
} else if (tutorialStep === 4) {
// Step 3b: Show placement highlight after clicking turret icon
tutorialAllowTurretIcon = false;
tutorialAllowDragTower = true;
// Calculate cell 67's grid position
var cell67GridX = (tutorialAllowPlaceCell - 1) % 12;
var cell67GridY = Math.floor((tutorialAllowPlaceCell - 1) / 12) + 4;
// Calculate center of cell 67 in game coordinates
var cell67CenterX = grid.x + cell67GridX * CELL_SIZE * gameScale + CELL_SIZE * gameScale / 2 + tutorialCellOffsetX;
var cell67CenterY = grid.y + cell67GridY * CELL_SIZE * gameScale + CELL_SIZE * gameScale / 2 + tutorialCellOffsetY;
showTutorialOverlay({
x: cell67CenterX,
y: cell67CenterY,
w: CELL_SIZE * gameScale + 20,
h: CELL_SIZE * gameScale + 20
}, "Drag the Turret to the Highlighted Area", null);
} else if (tutorialStep === 5) {
// Step 5: Tap the turret to upgrade to level 2 (10 clicks)
tutorialAllowTurretIcon = false;
tutorialAllowDragTower = false;
tutorialAllowUpgrade = true;
showTutorialOverlay({
x: tutorialTower.x + tutorialOffsetStep5X,
y: tutorialTower.y + tutorialOffsetStep5Y,
w: 200,
h: 200
}, "Tap the Turret to Upgrade it to Level 2", "up");
} else if (tutorialStep === 6) {
// Step 6: Kill all monsters before they reach the kingdom!
tutorialAllowUpgrade = false;
tutorialAllowBasicEnemy = true;
// No highlight, just clear overlay and wait
clearTutorialOverlay();
// Spawn 1 basic enemy
spawnTutorialBasicEnemy();
// After 5 seconds, highlight the forward button
LK.setTimeout(function () {
// Only show highlight if speed boost button is not already active
if (!speedBoostButton.isActive) {
tutorialAllowForward = true;
showTutorialOverlay(tutorialForwardIconArea, "Press the Forward Icon to Speed up the Game", "down");
}
}, 5000);
} else if (tutorialStep === 7) {
advanceTutorialStep();
} else if (tutorialStep === 8) {
// Step 8: Spawn flying enemy, warn about flying
tutorialAllowForward = false;
tutorialAllowFlyingEnemy = true;
var flyingEnemyArea = {
x: 1120,
y: 200,
w: 300,
h: 800
};
showTutorialOverlay(flyingEnemyArea, "Watch out for Flying Enemies!\nThey Fly over the Gate and go straight to the Kingdom", "down");
LK.setTimeout(function () {
if (tutorialStep === 8) {
clearTutorialOverlay();
}
;
}, 5000);
spawnTutorialFlyingEnemy();
} else if (tutorialStep === 9) {
// Step 9: Click the gate to repair if damaged
tutorialAllowFlyingEnemy = false;
tutorialAllowRepair = true;
showTutorialOverlay(tutorialGateArea, "Click the gate to repair it if it's damaged", "down");
} else if (tutorialStep === 10) {
// Step 10: Good luck, start game
tutorialAllowRepair = false;
// Update existing tutorial message instead of creating new overlay
showTutorialOverlay({
x: 2048 / 2,
y: 2732 / 2,
w: 0,
h: 0
}, "START", null);
LK.setTimeout(function () {
clearTutorialOverlay();
tutorialActive = false;
tutorialAllowAll = true;
// Start the game
waveIndicator.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
}, 1200);
}
}
function spawnTutorialBasicEnemy() {
// Only spawn if not already present
if (tutorialBasicEnemy) {
return;
}
var enemy = new Enemy('normal');
enemy.cellX = 6;
enemy.cellY = 1;
enemy.currentCellX = 6;
enemy.currentCellY = 1;
enemy.waveNumber = -1;
enemy.maxHealth = 50;
enemy.health = 50;
enemyLayerBottom.addChild(enemy);
enemies.push(enemy);
tutorialBasicEnemy = enemy;
}
function spawnTutorialFlyingEnemy() {
if (tutorialFlyingEnemy) {
return;
}
var enemy = new Enemy('flying');
enemy.cellX = 6;
enemy.cellY = 1;
enemy.currentCellX = 6;
enemy.currentCellY = 1;
enemy.waveNumber = -2;
enemy.maxHealth = 40;
enemy.health = 40;
enemyLayerTop.addChild(enemy);
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
enemies.push(enemy);
tutorialFlyingEnemy = enemy;
}
// Start tutorial
LK.setTimeout(function () {
tutorialStep = 0; // Start at step 0 so step 1 shows properly
advanceTutorialStep();
}, 500);
var goldTextShadow = new Text2('Gold: ' + gold, {
size: 90,
fill: 0x000000,
weight: 800
});
goldTextShadow.anchor.set(0.5, 0.5);
LK.gui.top.addChild(goldTextShadow);
goldTextShadow.x = 0;
goldTextShadow.y = topMargin - 25;
var goldText = new Text2('Gold: ' + gold, {
size: 90,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
LK.gui.top.addChild(goldText);
goldText.x = 0;
goldText.y = topMargin - 30;
function updateUI() {
// Update the text and preserve the stroke styling
goldTextShadow.destroy(); // Destroy the old shadow Text2 object
goldTextShadow = new Text2('Gold: ' + gold, {
size: 90,
fill: 0x000000,
weight: 800
});
goldTextShadow.anchor.set(0.5, 0.5);
LK.gui.top.addChild(goldTextShadow);
goldTextShadow.x = 0;
goldTextShadow.y = topMargin - 25;
goldText.destroy(); // Destroy the old Text2 object
goldText = new Text2('Gold: ' + gold, {
size: 90,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
LK.gui.top.addChild(goldText);
goldText.x = 0;
goldText.y = topMargin - 30;
updateTowerPrice();
}
function setGold(value) {
gold = value;
updateUI();
}
// Create dirt background with multiple smaller tiles
var dirtBackground = new Container();
// Create 6 background tiles arranged in a 2x3 grid
var tilesX = 2; // 2 tiles horizontally
var tilesY = 3; // 3 tiles vertically
var tileScale = 0.55; // Make each tile slightly larger to create overlap
var tileWidth = 2055 * tileScale; // Use actual asset width
var tileHeight = 2738.26 * tileScale; // Use actual asset height
// Calculate overlap amount (5% of tile size)
var overlapX = tileWidth * 0.05;
var overlapY = tileHeight * 0.05;
// Calculate total coverage and starting positions with overlap
var totalWidth = tilesX * tileWidth - (tilesX - 1) * overlapX;
var totalHeight = tilesY * tileHeight - (tilesY - 1) * overlapY;
var startX = (2048 - totalWidth) / 2;
var startY = (2732 - totalHeight) / 2;
for (var i = 0; i < tilesX; i++) {
for (var j = 0; j < tilesY; j++) {
var backgroundTile = dirtBackground.attachAsset('dirtBackground', {
anchorX: 0.5,
anchorY: 0.5
});
// Scale each tile with fixed scale (no variation)
backgroundTile.scaleX = tileScale;
backgroundTile.scaleY = tileScale;
// Position tiles with overlap to eliminate gaps
backgroundTile.x = startX + i * (tileWidth - overlapX) + tileWidth / 2;
backgroundTile.y = startY + j * (tileHeight - overlapY) + tileHeight / 2;
// No rotation or variation to maintain consistent orientation
}
}
dirtBackground.x = 0;
dirtBackground.y = 0;
game.addChild(dirtBackground);
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
var grid = new Grid(12, 13 + 6);
// Calculate scale to fill full screen width (2048px) with the 11-cell width
var gameScale = 2048 / (12 * CELL_SIZE); // Scale factor to fill screen width
grid.scaleX = gameScale;
grid.scaleY = gameScale;
grid.x = 40 * gameScale; // Move 40 units right (10 + 20 + 10) and scale
grid.y = (200 - CELL_SIZE * 4 - 20 - 30) * gameScale; // Move up 50 units total (20 + 30) and scale the Y position accordingly
grid.pathFind();
grid.renderDebug();
debugLayer.addChild(grid);
// Create global door health bar after doors are created
createGlobalDoorHealthBar();
game.addChild(debugLayer);
// Scale tower layer to match grid
towerLayer.scaleX = gameScale;
towerLayer.scaleY = gameScale;
game.addChild(towerLayer);
// Scale enemy layer to match grid
enemyLayer.scaleX = gameScale;
enemyLayer.scaleY = gameScale;
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
// Create simple tower asset next to red button for dragging
var buttonTower = new Container();
var buttonTowerGraphics = buttonTower.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
buttonTowerGraphics.scaleX = 3.5;
buttonTowerGraphics.scaleY = 3.5;
buttonTower.x = 700;
buttonTower.y = 2732 - 210;
buttonTower.width = buttonTowerGraphics.width * 3.5;
buttonTower.height = buttonTowerGraphics.height * 3.5;
game.addChild(buttonTower);
// Add a shadow text for tower price outline
var towerPriceTextShadow = new Text2('Price: ' + getTowerCost(), {
size: 60,
fill: 0x000000,
weight: 800
});
towerPriceTextShadow.anchor.set(0.5, 0.5);
towerPriceTextShadow.x = buttonTower.x + 2;
towerPriceTextShadow.y = buttonTower.y + buttonTower.height / 2 + 40 + 2;
game.addChild(towerPriceTextShadow);
// Add a text display for the current tower price below the draggable tower
var towerPriceText = new Text2('Price: ' + getTowerCost(), {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
towerPriceText.anchor.set(0.5, 0.5);
towerPriceText.x = buttonTower.x;
towerPriceText.y = buttonTower.y + buttonTower.height / 2 + 40; // Position below the tower
game.addChild(towerPriceText);
// Update the tower price text whenever the UI is updated
function updateTowerPrice() {
towerPriceTextShadow.setText('Price: ' + getTowerCost());
towerPriceText.setText('Price: ' + getTowerCost());
}
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
// Use offset coordinates for consistency with placement verification
var checkGridX = Math.floor(gridX + 0.5);
var checkGridY = Math.floor(gridY + 0.5);
var cells = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(checkGridX + i, checkGridY + j);
if (cell) {
cells.push({
cell: cell,
originalType: cell.type
});
cell.type = 1;
}
}
}
var blocked = grid.pathFind();
for (var i = 0; i < cells.length; i++) {
cells[i].cell.type = cells[i].originalType;
}
grid.pathFind();
grid.renderDebug();
return blocked;
}
function getTowerCost() {
return 10 + towersPlaced * 10; // Base cost 10, increases by 10 per tower
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function createGlobalDoorHealthBar() {
if (doors.length === 0) {
return;
}
// Create health bar container
globalDoorHealthBar = new Container();
// Calculate position - center of all doors, slightly below
var centerX = 457.5;
var centerY = 1040;
globalDoorHealthBar.x = centerX;
globalDoorHealthBar.y = centerY;
// Health bar background
globalDoorHealthBarBg = globalDoorHealthBar.attachAsset('doorHealthBg', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar fill
globalDoorHealthBarFill = globalDoorHealthBar.attachAsset('doorHealthBar', {
anchorX: 0.5,
anchorY: 0.5
});
// Health text
globalDoorHealthText = new Text2(" ", {
size: 18,
fill: 0xFFFFFF,
weight: 800
});
globalDoorHealthText.anchor.set(0.5, 0.5);
globalDoorHealthBar.addChild(globalDoorHealthText);
// Add to game with proper scaling
towerLayer.addChild(globalDoorHealthBar);
// Después de 1 segundo, fuerza la actualización del texto a "99/100"
LK.setTimeout(function () {
globalDoorHealthText.setText(" ");
globalDoorHealthText.setText("95/100");
updateGlobalDoorHealthBar();
}, 1000);
}
function updateGlobalDoorHealthBar() {
if (!globalDoorHealthBar) {
return;
}
var healthPercent = globalDoorHealth / globalDoorMaxHealth;
// Calculate the actual width based on health percentage - subtract a small amount to make 95% visible
var actualWidth = Math.max(0, 450 * healthPercent - 3); // Subtract 3 pixels to ensure visible difference
globalDoorHealthBarFill.width = actualWidth;
// Change anchor to make it decrease from right to left
globalDoorHealthBarFill.anchor.set(0, 0.5);
// Position the fill bar so it aligns with the left edge of the background
globalDoorHealthBarFill.x = -225; // Half of the background width (450/2) to align left edge
globalDoorHealthText.setText(globalDoorHealth + "/" + globalDoorMaxHealth);
// Force update the health bar fill width to ensure it reflects the actual health immediately after text update
LK.setTimeout(function () {
globalDoorHealthBarFill.width = actualWidth;
}, 1);
// Update all door visuals when global health changes
for (var i = 0; i < doors.length; i++) {
doors[i].updateDoorVisual();
}
}
function placeTower(gridX, gridY) {
var towerCost = getTowerCost();
if (gold >= towerCost) {
// Double-check that the cell is available for placement
var cellNumber = (gridY - 4) * 12 + gridX + 1;
var obstacleCells = [14, 17, 18, 19, 20, 21, 22, 23, 26, 35, 38, 39, 40, 41, 42, 43, 44, 45, 47, 50, 59, 62, 64, 65, 66, 67, 68, 69, 70, 71, 74, 83, 86, 87, 88, 89, 90, 91, 92, 93, 95, 98, 107, 110, 112, 113, 114, 115, 116, 117, 118, 119, 122, 130, 131, 16];
var wallCells = [1, 2, 11, 12, 13, 24, 25, 36, 37, 48, 49, 60, 61, 72, 73, 84, 85, 96, 97, 108, 109, 120, 121, 132, 133, 134, 135, 142, 143, 144];
var isObstacleCell = cellNumber >= 1 && cellNumber <= 144 && obstacleCells.indexOf(cellNumber) !== -1;
var isWallCell = cellNumber >= 1 && cellNumber <= 144 && wallCells.indexOf(cellNumber) !== -1;
var existingTower = grid.towerCells[gridX] && grid.towerCells[gridX][gridY];
if (isObstacleCell && !isWallCell && !existingTower) {
var tower = new Tower();
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
towersPlaced++; // Increment tower counter
updateTowerPrice(); // Update tower price display
grid.renderDebug();
return true;
} else {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
// --- Tutorial gating ---
if (tutorialActive && !tutorialAllowAll) {
// Step 1/2: Only allow red button
if ((tutorialStep === 1 || tutorialStep === 2) && tutorialAllowRedButton) {
if (x >= tutorialRedButtonArea.x - tutorialRedButtonArea.w / 2 && x <= tutorialRedButtonArea.x + tutorialRedButtonArea.w / 2 && y >= tutorialRedButtonArea.y - tutorialRedButtonArea.h / 2 && y <= tutorialRedButtonArea.y + tutorialRedButtonArea.h / 2) {
// Simulate redButton.down
if (!redButton.isPressed) {
redButton.isPressed = true;
redButton.removeChild(redButtonGraphics);
redButtonGraphics = redButton.attachAsset('redButtonPressed', {
anchorX: 0.5,
anchorY: 0.5
});
redButtonGraphics.scaleX = 4.0;
redButtonGraphics.scaleY = 4.0;
setGold(gold + 1);
tutorialRedButtonPresses++;
// Show gold indicator
var randomXOffset = Math.random() * 450 - 250;
var goldIndicator = game.addChild(new GoldIndicator(1, LK.gui.top.x + goldText.x + 350 + randomXOffset, LK.gui.top.y + goldText.y + 140));
goldIndicator.scaleX = 1.5;
goldIndicator.scaleY = 1.5;
}
// Step up only after first press (step 1->2) and at 3 gold (step 2->3)
if (tutorialStep === 1 && tutorialRedButtonPresses >= 1) {
advanceTutorialStep();
} else if (tutorialStep === 2 && gold >= 10) {
advanceTutorialStep();
}
return;
} else {
// Ignore all other clicks
return;
}
}
// Step 3: Only allow turret icon click to advance to step 4
if (tutorialStep === 3 && tutorialAllowTurretIcon) {
if (x >= tutorialTurretIconArea.x - tutorialTurretIconArea.w / 2 && x <= tutorialTurretIconArea.x + tutorialTurretIconArea.w / 2 && y >= tutorialTurretIconArea.y - tutorialTurretIconArea.h / 2 && y <= tutorialTurretIconArea.y + tutorialTurretIconArea.h / 2) {
advanceTutorialStep(); // Advance to step 4 to show placement highlight
towerPreview.visible = true;
isDragging = true;
towerPreview.updateAppearance();
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
return;
} else {
return;
}
}
// Step 4: Only allow tower dragging (placement highlight is shown)
if (tutorialStep === 4 && tutorialAllowDragTower) {
if (x >= tutorialTurretIconArea.x - tutorialTurretIconArea.w / 2 && x <= tutorialTurretIconArea.x + tutorialTurretIconArea.w / 2 && y >= tutorialTurretIconArea.y - tutorialTurretIconArea.h / 2 && y <= tutorialTurretIconArea.y + tutorialTurretIconArea.h / 2) {
towerPreview.visible = true;
isDragging = true;
towerPreview.updateAppearance();
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
return;
} else {
return;
}
}
// Step 5: Only allow upgrade on tutorial tower
if (tutorialStep === 5 && tutorialAllowUpgrade) {
if (tutorialTower) {
var tx = tutorialTower.x,
ty = tutorialTower.y,
tw = 200,
th = 200;
if (x >= tx - tw / 2 && x <= tx + tw / 2 && y >= ty - th / 2 && y <= ty + th / 2) {
// Simulate tower.down for upgrade
tutorialTowerUpgradeClicks++;
tutorialTower.down(x, y, obj);
return;
} else {
return;
}
}
}
// Step 6/7: Allow forward icon (step 6 sets up the highlight with timeout, step 7 handles the press)
if (tutorialStep === 6 && tutorialAllowForward) {
if (x >= tutorialForwardIconArea.x - tutorialForwardIconArea.w / 2 && x <= tutorialForwardIconArea.x + tutorialForwardIconArea.w / 2 && y >= tutorialForwardIconArea.y - tutorialForwardIconArea.h / 2 && y <= tutorialForwardIconArea.y + tutorialForwardIconArea.h / 2) {
// Clear tutorial overlay immediately when forward is pressed
clearTutorialOverlay();
// Simulate speedBoostButton.down
if (!speedBoostButton.isPressed) {
speedBoostButton.isPressed = true;
speedBoostButton.isActive = true;
speedBoostActive = true;
speedBoostButton.removeChild(speedBoostButtonGraphics);
speedBoostButtonGraphics = speedBoostButton.attachAsset('speedBoostButtonPressed', {
anchorX: 0.5,
anchorY: 0.5
});
speedBoostButtonGraphics.scaleX = 4.0;
speedBoostButtonGraphics.scaleY = 4.0;
}
return;
} else {
return;
}
}
// Step 9: Only allow gate repair
if (tutorialStep === 9) {
console.log("Checking door health:", globalDoorHealth);
if (globalDoorHealth >= 99) {
console.log("Door fully repaired. Advancing tutorial.");
advanceTutorialStep();
}
return;
}
// Block all other actions
return;
}
// --- Normal game code below ---
// Check for the normal tower next to red button (buttonTower)
if (x >= buttonTower.x - buttonTower.width / 2 && x <= buttonTower.x + buttonTower.width / 2 && y >= buttonTower.y - buttonTower.height / 2 && y <= buttonTower.y + buttonTower.height / 2) {
towerPreview.visible = true;
isDragging = true;
towerPreview.updateAppearance();
// Apply the same offset as in move handler to ensure consistency when starting drag
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
return;
}
// Check if clicking on any existing tower - if not, hide range
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;
}
}
// If not clicking on a tower, hide the range display
if (!clickedOnTower && rangeDisplayTower) {
rangeDisplayTower.hideRangeIndicator();
rangeDisplayTower = null;
}
};
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;
}
}
// Clear press timer when releasing anywhere
if (pressedTower) {
pressedTower = null;
if (pressTimer) {
LK.clearTimeout(pressTimer);
pressTimer = null;
}
}
if (isDragging) {
isDragging = false;
// --- Tutorial gating for tower placement ---
if (tutorialActive && tutorialStep === 4 && tutorialAllowDragTower) {
// Only allow placement on cell 67
var cellNumber = (towerPreview.gridY - 4) * 12 + towerPreview.gridX + 1;
if (towerPreview.canPlace && cellNumber === tutorialAllowPlaceCell) {
placeTower(towerPreview.gridX, towerPreview.gridY);
// Find the just-placed tower and mark as tutorialTower
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
var tCell = (t.gridY - 4) * 12 + t.gridX + 1;
if (tCell === tutorialAllowPlaceCell) {
tutorialTower = t;
break;
}
}
advanceTutorialStep();
} else {
var notification = game.addChild(new Notification("Place the turret on the highlighted cell!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
return;
}
// --- End tutorial gating for placement ---
if (towerPreview.canPlace) {
placeTower(towerPreview.gridX, towerPreview.gridY);
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2 + 400;
waveIndicator.y = 2732 - 80;
game.addChild(waveIndicator);
// Do not start the game until tutorial is complete
waveIndicator.gameStarted = false;
currentWave = 0;
waveTimer = nextWaveTime;
// Create simple tower asset next to red button for dragging
var buttonTower = new Container();
var buttonTowerGraphics = buttonTower.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
buttonTowerGraphics.scaleX = 3.5;
buttonTowerGraphics.scaleY = 3.5;
buttonTower.x = 700;
buttonTower.y = 2732 - 210;
buttonTower.width = buttonTowerGraphics.width * 3.5;
buttonTower.height = buttonTowerGraphics.height * 3.5;
game.addChild(buttonTower);
sourceTower = null;
enemiesToSpawn = 10;
// Create red button for manual firing and gold generation
var redButton = new Container();
var redButtonGraphics = redButton.attachAsset('redButton', {
anchorX: 0.5,
anchorY: 0.5
});
redButtonGraphics.scaleX = 4.0;
redButtonGraphics.scaleY = 4.0;
redButton.x = 250;
redButton.y = 2732 - 220;
redButton.isPressed = false;
game.addChild(redButton);
// Create speed boost button
var speedBoostButton = new Container();
var speedBoostButtonGraphics = speedBoostButton.attachAsset('speedBoostButton', {
anchorX: 0.5,
anchorY: 0.5
});
speedBoostButtonGraphics.scaleX = 4.0;
speedBoostButtonGraphics.scaleY = 4.0;
speedBoostButton.x = 2048 - 150;
speedBoostButton.y = 120;
speedBoostButton.isPressed = false;
speedBoostButton.isActive = false;
speedBoostButton.cooldown = 0;
game.addChild(speedBoostButton);
// Speed boost variables
var speedBoostActive = false;
var speedBoostDuration = 0;
var speedBoostCooldown = 0;
var speedBoostMaxDuration = 600; // 10 seconds at 60fps
var speedBoostMaxCooldown = 1800; // 30 seconds at 60fps
// Speed boost button functionality
speedBoostButton.down = function (x, y, obj) {
if (!speedBoostButton.isPressed) {
speedBoostButton.isPressed = true;
speedBoostButton.isActive = true;
speedBoostActive = true;
// Change button appearance to pressed state
speedBoostButton.removeChild(speedBoostButtonGraphics);
speedBoostButtonGraphics = speedBoostButton.attachAsset('speedBoostButtonPressed', {
anchorX: 0.5,
anchorY: 0.5
});
speedBoostButtonGraphics.scaleX = 4.0;
speedBoostButtonGraphics.scaleY = 4.0;
// Show activation notification
var notification = game.addChild(new Notification("Speed Boost Active!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
speedBoostButton.up = function (x, y, obj) {
if (speedBoostButton.isPressed) {
speedBoostButton.isPressed = false;
speedBoostButton.isActive = false;
speedBoostActive = false;
// Change button back to normal state
speedBoostButton.removeChild(speedBoostButtonGraphics);
speedBoostButtonGraphics = speedBoostButton.attachAsset('speedBoostButton', {
anchorX: 0.5,
anchorY: 0.5
});
speedBoostButtonGraphics.scaleX = 4.0;
speedBoostButtonGraphics.scaleY = 4.0;
}
};
// Button functionality
redButton.down = function (x, y, obj) {
if (!redButton.isPressed) {
redButton.isPressed = true;
// Change button appearance to pressed state
redButton.removeChild(redButtonGraphics);
redButtonGraphics = redButton.attachAsset('redButtonPressed', {
anchorX: 0.5,
anchorY: 0.5
});
redButtonGraphics.scaleX = 4.0;
redButtonGraphics.scaleY = 4.0;
// Give 1 gold
setGold(gold + 1);
// Show gold increment indicator below the gold counter
var randomXOffset = Math.random() * 450 - 250; // Random value between -250 and +200
var goldIndicator = game.addChild(new GoldIndicator(1, LK.gui.top.x + goldText.x + 350 + randomXOffset, LK.gui.top.y + goldText.y + 140));
goldIndicator.scaleX = 1.5;
goldIndicator.scaleY = 1.5;
// Towers should not fire when red button is pressed
// Red button now only gives gold
}
};
redButton.up = function (x, y, obj) {
if (redButton.isPressed) {
redButton.isPressed = false;
// Change button back to normal state
redButton.removeChild(redButtonGraphics);
redButtonGraphics = redButton.attachAsset('redButton', {
anchorX: 0.5,
anchorY: 0.5
});
redButtonGraphics.scaleX = 4.0;
redButtonGraphics.scaleY = 4.0;
}
};
game.update = function () {
// Speed boost is now controlled by button press/release - no duration or cooldown needed
// Apply speed boost to game mechanics
var speedMultiplier = speedBoostActive ? 3 : 1;
// Process wave progression with speed boost
for (var speedTick = 0; speedTick < speedMultiplier; speedTick++) {
waveIndicator.handleWaveProgression();
}
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
// Get wave type and enemy count from the wave indicator
var waveType = waveIndicator.getWaveType(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
// Check if this is a boss wave
var isBossWave = currentWave % 10 === 0 && currentWave > 0;
if (isBossWave && waveType !== 'swarm') {
// Boss waves have just 1 enemy regardless of what the wave indicator says
enemyCount = 1;
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
// Spawn the appropriate number of enemies
// For Swarm waves, spawn 50 enemies instead of 30 and remove spacing
var actualEnemyCount = waveType === 'swarm' ? 50 : enemyCount;
for (var i = 0; i < actualEnemyCount; i++) {
var enemy = new Enemy(waveType);
// 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 = 12;
var midPoint = Math.floor(gridWidth / 2); // 6
var spawnX, spawnY;
if (waveType === 'swarm') {
// For swarm waves, spawn without spacing - enemies spawn closely together
spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 3 to 8
spawnY = -1 - i * 0.3; // Minimal spacing for swarm waves
} else {
// 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
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
}
spawnY = -1 - i * 2 - Math.random() * 2; // Increased spacing between enemies
}
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;
}
}
// Process enemies with speed boost
for (var speedTick = 0; speedTick < speedMultiplier; speedTick++) {
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
if (enemy.health <= 0) {
// --- Tutorial: check if this is the tutorial basic or flying enemy ---
if (tutorialActive) {
if (tutorialStep === 6 && tutorialBasicEnemy && enemy === tutorialBasicEnemy) {
tutorialBasicEnemy = null;
// Immediately spawn flying enemy when basic enemy is killed
advanceTutorialStep();
// Don't advance tutorial step here - let forward button press handle it
}
if (tutorialStep === 8 && tutorialFlyingEnemy && enemy === tutorialFlyingEnemy) {
tutorialFlyingEnemy = null;
// Show door highlight overlay with repair message
advanceTutorialStep();
}
}
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Clean up door attacking if enemy was attacking a door
if (enemy.attackingDoor) {
enemy.attackingDoor.removeAttackingEnemy(enemy);
enemy.attackingDoor = null;
}
// Enemies no longer give gold when killed
// Gold indicator and gold earning removed
// 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!"));
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 door attacking if enemy was attacking a door
if (enemy.attackingDoor) {
enemy.attackingDoor.removeAttackingEnemy(enemy);
enemy.attackingDoor = null;
}
// 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();
}
}
}
}
// Process bullets with speed boost
for (var speedTick = 0; speedTick < speedMultiplier; speedTick++) {
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);
}
}
}
// Process towers with speed boost (for firing rate)
for (var speedTick = 0; speedTick < speedMultiplier; speedTick++) {
for (var t = 0; t < towers.length; t++) {
var tower = towers[t];
if (tower.update) {
tower.update();
}
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
};
White circle with two eyes, seen from above.. In-Game asset. 2d. High contrast. No shadows
Top-down view of a single cartoon-style castle wall tile, seen from above, 2D game style, flat lighting, seamless edges, stone texture, bright colors, square shape, repeatable sprite, pixel-art or hand-drawn style.. In-Game asset. 2d. High contrast. No shadows
Top-down view of a single cartoon-style grass tile, seen from above, 2D game style, seamless and repeatable texture, bright colors, hand-drawn or pixel-art style, soft grass texture, 32x32 sprite.. In-Game asset. 2d. High contrast. No shadows
Top-down view of a single cartoon-style dirt and gravel path tile, seen from above, 2D game style, seamless and repeatable texture, hand-drawn or pixel-art style, natural colors, a mix of packed earth and scattered small stones, 32x32 sprite.. In-Game asset. 2d. High contrast. No shadows
Vista cenital de una sola baldosa de puerta de madera, aislada, vista desde arriba, estilo pixel art 2D para videojuego, textura continua y repetible, sprite de 32x32. Sin manija, sin bisagras, sin marco, solo tablones de madera con clavos decorativos. Textura de vetas estilizadas. Arte estilo hecho a mano. Alto contraste, sin sombras. Sin fondo.
Vista cenital de una baldosa de puerta de madera destruida, aislada, vista desde arriba, estilo pixel art 2D para videojuego, textura continua y repetible, sprite de 32x32. Sin manija, sin marco, sin bisagras. Tablones de madera rotos, astillas y clavos decorativos dispersos. Vetas de madera estilizadas. Alto contraste, arte estilo hecho a mano. Sin sombras. Sin fondo.. In-Game asset. 2d. High contrast. No shadows
Botón presionado rojo 2D, pixel cartoon.
Top-down view of a cartoon-style grass field, seen from above, 2D game style, bright colors, hand-drawn or pixel-art style, soft grass texture, 2000x3000 pixels In-Game asset. 2d. High contrast. No shadows
Un enemigo nivel básico visto desde arriba, pixel cartoon.. In-Game asset. 2d. High contrast. No shadows
Mueve sus pies de posición los de la izquierda más arriba y los de la derecha más abajo
Mueve las alas más abajo
Sin modificar el cuerpo, mueve los pies y las manos como si caminara así de frente a la camara.
Mueve los pies y las manos como si estuviera caminando de frente hacia la camara
Ballesta vista desde arriba, pixel cartoon.. In-Game asset. 2d. High contrast. No shadows
Flecha de arco vista desde arriba, pixel cartoon.. In-Game asset. 2d. High contrast. No shadows
Este mismo pero agrega soldados muy pequeños sobre las torres
Esto mismo pero los pies derechos
Esto mismo pero con los pies derechos
Caminando de frente hacia la camara.
Solo junta los pies, no modifiques nada mas!
Un boton de color banco para delantar el juego de velocidad, dos felchas negras apuntando a la derecha.. In-Game asset. 2d. High contrast. No shadows
White square. In-Game asset. 2d. High contrast. No shadows
Una ruleta, dividida en 8 partes, cada parte separada por una linea negra y cada parte rellena con un color brillante atractivo, sin fondo, sin flecha apuntando al resultado. In-Game asset. 2d. High contrast. No shadows
Un boton grande de apuestas que diga "BET" como en el casino. In-Game asset. 2d. High contrast. No shadows
Un boton azul fuerte tipo Casino que diga "+". In-Game asset. 2d. High contrast. No shadows
Un boton azul fuerte tipo Casino que diga "-". In-Game asset. 2d. High contrast. No shadows