/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.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 = self.speed; if (speedBoostActive) { effectiveSpeed = self.speed * 3; } 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 = 60; if (speedBoostActive) { effectiveDamageRate = Math.ceil(60 / 3); } if (self.enemiesAttacking.length > 0 && LK.ticks - self.lastHealthLossTime >= effectiveDamageRate) { globalDoorHealth = Math.max(0, globalDoorHealth - self.enemiesAttacking.length); self.lastHealthLossTime = LK.ticks; // Play door hit sound LK.getSound('doorHitSound').play(); // 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) { // Prevent door repair until tutorial step 9 or later if (tutorialActive && tutorialStep < 9) { var notification = game.addChild(new Notification("Cannot repair door yet!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } 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 based on wave number if (currentWave === 10) { // Wave 10: Boss Normal self.enemyAssets = ['Enemy1', 'Enemy2', 'Enemy3']; } else if (currentWave === 20) { // Wave 20: Boss Fast self.speed *= 2; self.maxHealth = 100; self.enemyAssets = ['EnemyFast1', 'EnemyFast2', 'EnemyFast3']; } else if (currentWave === 30) { // Wave 30: Boss Flying self.isFlying = true; self.maxHealth = 80; self.enemyAssets = ['EnemyFlying1', 'EnemyFlying2', 'EnemyFlying3']; } else if (currentWave === 40) { // Wave 40: Boss Tank (should NOT be flying) self.isImmune = true; self.isFlying = false; // Explicitly set to false to override any flying behavior self.maxHealth = 240; self.speed *= 0.5; // Reduce speed by 0.5x self.enemyAssets = ['EnemyImmune1', 'EnemyImmune2', 'EnemyImmune3']; } else if (currentWave === 50) { // Wave 50: Boss Swarm - Large bosses with high health self.maxHealth = 500; // Much higher health for wave 50 bosses self.enemyAssets = ['EnemyBoss1', 'EnemyBoss2', 'EnemyBoss3']; // Use boss sprites instead of swarm // Force boss scaling for wave 50 self.isBoss = true; } else { // Default boss sprites for other boss waves 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 SpinningWheel = Container.expand(function () { var self = Container.call(this); self.isSpinning = false; self.finalSection = 0; self.betAmount = 0; // Create wheel base var wheelBase = self.attachAsset('spinningWheel', { anchorX: 0.5, anchorY: 0.5 }); wheelBase.tint = 0xDDDDDD; // Create 8 sections with different colors // Lose All is always red, others are unique bright primaries var sectionNames = ["💀 Lose All", "BET X 10", "💀 Lose All", "BET X 5", "💀 Lose All", "BET X 2", "💀 Lose All", "BET X 1"]; // Assign unique bright primary colors to each section except "Lose All" var brightPrimaries = [0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, 0xFFA500, 0x800080]; var sectionColors = []; for (var i = 0; i < sectionNames.length; i++) { if (sectionNames[i] === "Lose All") { sectionColors[i] = 0xFF0000; // Red for Lose All } else { // Assign a unique color from the brightPrimaries array, cycling if needed sectionColors[i] = brightPrimaries[i % brightPrimaries.length]; } } self.sections = []; self.sectionLabels = []; // Create visual sections for (var i = 0; i < 8; i++) { var section = new Container(); // Remove the colored background block - no longer adding sectionGraphics var angle = i * 45 * Math.PI / 180; section.x = Math.cos(angle) * 110; // Move blocks closer to center (was 150) section.y = Math.sin(angle) * 110; // Move blocks closer to center (was 150) section.rotation = angle; // Add text label var label = new Text2(sectionNames[i], { size: 32, fill: 0xFFFFFF, weight: 800 }); label.anchor.set(0.5, 0.5); label.x = 90; // Center label on shorter section block label.y = 0; section.addChild(label); self.addChild(section); self.sections.push(section); self.sectionLabels.push(sectionNames[i]); } // Black separators removed - no longer creating separator lines between sections // Arrow will be created separately and positioned outside the wheel // Remove the arrow from the spinning wheel itself self.spin = function (betAmount) { if (self.isSpinning) { return; } self.isSpinning = true; self.betAmount = betAmount; // Set high zIndex to ensure wheel appears above all other elements, including turret bullets and tutorial arrows self.zIndex = 30000; // Play wheel spin sound LK.getSound('wheelSpinSound').play(); // Random number of full rotations (3-8) plus random section var baseRotations = 3 + Math.random() * 5; var randomSection = Math.floor(Math.random() * 8); // Calculate exact center angle for the selected section // Each section is 45 degrees, so center is at section * 45 var sectionCenterAngle = randomSection * 45; var finalAngle = baseRotations * 360 + sectionCenterAngle; self.finalSection = randomSection; // Spin animation with precise centering tween(self, { rotation: finalAngle * Math.PI / 180 }, { duration: 3000, easing: tween.easeOut, onFinish: function onFinish() { // After main spin, fine-tune to ensure perfect center alignment self.fineTuneToCenter(); } }); }; self.fineTuneToCenter = function () { // Calculate current rotation in degrees var currentRotationDegrees = self.rotation * 180 / Math.PI % 360; while (currentRotationDegrees < 0) { currentRotationDegrees += 360; } while (currentRotationDegrees >= 360) { currentRotationDegrees -= 360; } // Find the nearest section center ahead of current position var nearestSectionAhead = -1; var shortestDistanceAhead = Infinity; // Check all 8 sections to find the nearest one ahead for (var i = 0; i < 8; i++) { var sectionCenter = i * 45; var distanceAhead = sectionCenter - currentRotationDegrees; // If distance is negative, add 360 to get forward distance if (distanceAhead <= 0) { distanceAhead += 360; } // Find the shortest forward distance if (distanceAhead < shortestDistanceAhead) { shortestDistanceAhead = distanceAhead; nearestSectionAhead = i; } } // Update finalSection to the nearest section ahead self.finalSection = nearestSectionAhead; // If we're very close to center, no need to adjust if (Math.abs(shortestDistanceAhead) <= 0.1 || Math.abs(shortestDistanceAhead - 360) <= 0.1) { // Already perfectly centered, proceed with result self.handleResult(); return; } // Calculate final rotation by continuing to spin forward var finalRotation = self.rotation + shortestDistanceAhead * Math.PI / 180; // Use constant slow speed: calculate duration based on angle distance var constantSpeed = 30; // degrees per second var durationMs = shortestDistanceAhead / constantSpeed * 1000; // Continue spinning smoothly at constant speed until perfectly centered tween(self, { rotation: finalRotation }, { duration: durationMs, easing: tween.linear, // Linear easing for constant speed onFinish: function onFinish() { self.handleResult(); } }); }; self.handleResult = function () { // Calculate which section is actually at the arrow position (90 degrees to the right) // The wheel rotates, so we need to find which section is at the 0-degree position (right side) var currentRotationDegrees = self.rotation * 180 / Math.PI % 360; while (currentRotationDegrees < 0) { currentRotationDegrees += 360; } while (currentRotationDegrees >= 360) { currentRotationDegrees -= 360; } // Each section is 45 degrees wide, starting at 0 degrees // The arrow points to the right (0 degrees), so we need to find which section is at 0 degrees // Add 22.5 degrees to center the calculation on section midpoints var sectionAtArrow = Math.floor((currentRotationDegrees + 22.5) / 45) % 8; var sectionName = self.sectionLabels[sectionAtArrow]; var winAmount = 0; var message = ""; switch (sectionAtArrow) { case 0: // Position 1: Lose ALL winAmount = 0; message = "Lost all bet! Better luck next time!"; break; case 1: // Position 2: BET X 10 winAmount = self.betAmount * 1; message = "Won " + winAmount + " gold! (1x)"; break; case 2: // Position 3: Lose ALL winAmount = 0; message = "Lost all bet! Better luck next time!"; break; case 3: // Position 4: BET X 50 (This should award 20X) winAmount = self.betAmount * 2; message = "Won " + winAmount + " gold! (2x)"; break; case 4: // Position 5: Lose ALL winAmount = 0; message = "Lost all bet! Better luck next time!"; break; case 5: // Position 6: BET X 20 (This should award 50X) winAmount = self.betAmount * 5; message = "Won " + winAmount + " gold! (5x)"; break; case 6: // Position 7: Lose ALL winAmount = 0; message = "Lost all bet! Better luck next time!"; break; case 7: // Position 8: BET X 100 (This should award 100X) winAmount = self.betAmount * 10; message = "JACKPOT! Won " + winAmount + " gold! (10x)"; break; } // Play win or lose sound based on result if (winAmount > 0) { LK.getSound('wheelWinSound').play(); } else { LK.getSound('wheelLoseSound').play(); } setGold(gold + winAmount); var notification = game.addChild(new Notification(message)); notification.x = 2048 / 2; notification.y = grid.height - 150; // Keep the wheel visible for at least 1 second after spinning ends, then hide LK.setTimeout(function () { // Clean up the fixed arrow if it exists if (self.wheelArrow) { self.wheelArrow.destroy(); self.wheelArrow = null; } self.destroy(); }, 1000); }; 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 = 0x0033CC; } 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 = self.fireRate; if (speedBoostActive) { effectiveFireRate = Math.ceil(self.fireRate / 3); } 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; // Play tower click sound LK.getSound('towerClickSound').play(); // 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); // Check if tower upgrades are allowed during tutorial if (tutorialActive && tutorialStep !== 5) { var notification = game.addChild(new Notification("Cannot upgrade tower yet!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } // 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 }); } }); } // Play tower upgrade sound LK.getSound('towerUpgradeSound').play(); // 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; // Create bullet in that position var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet zIndex below spinning wheel (zIndex 20000), but above most gameplay elements bullet.zIndex = 10000; game.addChild(bullet); // Ensure bullet appears below any existing spinning wheel if (bullet.zIndex) { bullet.zIndex = 10000; } bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // Play tower fire sound LK.getSound('towerFireSound').play(); // --- 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: specific boss types for specific waves var waveNumber = i + 1; if (waveNumber === 10) { // Wave 10: Boss Normal enemyType = 'normal'; waveType = "Boss Normal"; block.tint = 0xAAAAAA; } else if (waveNumber === 20) { // Wave 20: Boss Fast enemyType = 'fast'; waveType = "Boss Fast"; block.tint = 0x00AAFF; } else if (waveNumber === 30) { // Wave 30: Boss Tank enemyType = 'tank'; waveType = "Boss Tank"; block.tint = 0xAA0000; } else if (waveNumber === 40) { // Wave 40: Boss Tank enemyType = 'tank'; waveType = "Boss Tank"; block.tint = 0xAA0000; } else if (waveNumber === 50) { // Wave 50: Boss Swarm - 5 large bosses enemyType = 'swarm'; waveType = "Boss Swarm"; block.tint = 0xFF00FF; } else { // Fallback for any other boss waves (shouldn't happen with current totalWaves = 50) enemyType = 'normal'; waveType = "Boss Normal"; block.tint = 0xAAAAAA; } enemyCount = waveNumber === 50 ? 5 : 1; // Wave 50 spawns 5 bosses, others spawn 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 ****/ game.sortableChildren = true; // Initialize background music // Initialize sound effects 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 --- // Always show the tutorial at the start of the game, regardless of previous completion var tutorialActive = true; // --- Fix: Define highlightGoldButton globally so it is always available --- var goldButtonHighlighted = false; 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 // Betting system variables var currentBet = 0; // Always keep as multiple of 10 var maxBet = 999999; // Remove betting limit var minBet = 0; 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 tutorialArrow.alpha = 1.0; // Ensure arrow is fully visible without flashing // Stop any existing tweens on the arrow to prevent flashing if (typeof tween !== "undefined") { tween.stop(tutorialArrow, { alpha: true }); } 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 AND tutorial is still active if (!speedBoostButton.isActive && tutorialActive) { tutorialAllowForward = true; showTutorialOverlay(tutorialForwardIconArea, "Press the Forward Icon to Speed up the Game", "down"); } }, 5000); } else if (tutorialStep === 7) { 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 === 7) { clearTutorialOverlay(); } ; }, 4000); spawnTutorialFlyingEnemy(); } else if (tutorialStep === 8) { var checkGoldForStep9 = function checkGoldForStep9() { if (tutorialStep !== 8) { return; } if (gold < 5) { // SOLO dibuja la superposición la primera vez if (!overlayShown) { showTutorialOverlay(tutorialRedButtonArea, "Click again to keep earning more money", "up"); overlayShown = true; } } else { // El jugador llegó a 5 de oro clearTutorialOverlay(); overlayShown = false; if (highlightGoldButtonInterval) { LK.clearInterval(highlightGoldButtonInterval); highlightGoldButtonInterval = null; } advanceTutorialStep(); // pasa al paso 9 } }; // --- NUEVO: Highlight gold button and require 5 gold before step 9 --- // Only proceed to step 9 after player reaches 5 gold var overlayShown = false; // evita que la flecha se redibuje var highlightGoldButtonInterval = null; var goldButtonHighlighted = false; highlightGoldButtonInterval = LK.setInterval(checkGoldForStep9, 300); // Also check immediately in case player already has 5+ gold checkGoldForStep9(); // Do NOT advance to step 9 here. Wait for flying enemy to be killed (handled in game.update). return; } else if (tutorialStep === 9) { // Step 9: Click the gate to repair if damaged tutorialAllowFlyingEnemy = false; tutorialAllowRepair = true; // Clear any existing highlight effects from previous steps goldButtonHighlighted = false; if (typeof highlightGoldButtonInterval !== "undefined" && highlightGoldButtonInterval) { LK.clearInterval(highlightGoldButtonInterval); highlightGoldButtonInterval = null; } 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 }, "Good Luck", null); LK.setTimeout(function () { clearTutorialOverlay(); tutorialActive = false; tutorialAllowAll = true; // Save tutorial completion state storage.tutorialCompleted = true; // Start the game waveIndicator.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; // Hide the skip tutorial button when tutorial ends hideSkipTutorialButton(); }, 1200); // Show Tutorial button removed } } 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; } // --- Skip Tutorial Button Implementation --- var skipTutorialButton = null; var skipTutorialButtonText = null; var showTutorialButton = null; var showTutorialButtonText = null; function showSkipTutorialButton() { if (skipTutorialButton) { return; } skipTutorialButton = new Container(); skipTutorialButton.zIndex = 10001; // Move to bottom right, with minimal margin from the edge (closer to corner) skipTutorialButton.x = 2048 - 40 - 300; // 40px margin, 300 is half width skipTutorialButton.y = 2732 - 40 - 70; // 40px margin, 70 is half height skipTutorialButton.visible = true; var btnBg = skipTutorialButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); btnBg.width = 600; btnBg.height = 140; btnBg.tint = 0x222222; btnBg.alpha = 0.85; skipTutorialButtonText = new Text2("Skip Tutorial", { size: 70, fill: 0xffffff, weight: 800 }); skipTutorialButtonText.anchor.set(0.5, 0.5); skipTutorialButton.addChild(skipTutorialButtonText); skipTutorialButton.down = function (x, y, obj) { // Remove button if (skipTutorialButton) { skipTutorialButton.visible = false; skipTutorialButton.destroy(); skipTutorialButton = null; } // --- Begin: Cancel all pending tutorial processes and remove test monsters --- // Clear any tutorial overlay and arrow/message clearTutorialOverlay(); // Cancel any highlightGoldButtonInterval or tutorial timers if (typeof highlightGoldButtonInterval !== "undefined" && highlightGoldButtonInterval) { LK.clearInterval(highlightGoldButtonInterval); highlightGoldButtonInterval = null; } // Remove any tutorial flying enemy if (typeof tutorialFlyingEnemy !== "undefined" && tutorialFlyingEnemy) { // Remove from enemies array and destroy for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === tutorialFlyingEnemy) { if (tutorialFlyingEnemy.isFlying && tutorialFlyingEnemy.shadow) { if (typeof enemyLayerMiddle !== "undefined") { enemyLayerMiddle.removeChild(tutorialFlyingEnemy.shadow); } tutorialFlyingEnemy.shadow = null; } if (typeof enemyLayerTop !== "undefined") { enemyLayerTop.removeChild(tutorialFlyingEnemy); } enemies.splice(i, 1); tutorialFlyingEnemy.destroy(); break; } } tutorialFlyingEnemy = null; } // Remove any tutorial basic enemy if (typeof tutorialBasicEnemy !== "undefined" && tutorialBasicEnemy) { for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === tutorialBasicEnemy) { if (typeof enemyLayerBottom !== "undefined") { enemyLayerBottom.removeChild(tutorialBasicEnemy); } enemies.splice(i, 1); tutorialBasicEnemy.destroy(); break; } } tutorialBasicEnemy = null; } // --- End: Cancel all pending tutorial processes and remove test monsters --- // End tutorial, start game, give 10 gold, set door health to 100 tutorialActive = false; tutorialAllowAll = true; storage.tutorialCompleted = true; clearTutorialOverlay(); waveIndicator.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; setGold(10); globalDoorHealth = globalDoorMaxHealth; updateGlobalDoorHealthBar(); // Show Tutorial button removed }; game.addChild(skipTutorialButton); } function hideSkipTutorialButton() { if (skipTutorialButton) { skipTutorialButton.visible = false; skipTutorialButton.destroy(); skipTutorialButton = null; } } // Show Tutorial button and its implementation removed // Start tutorial or game based on completion state LK.setTimeout(function () { // Always start tutorial at game start tutorialStep = 0; // Start at step 0 so step 1 shows properly advanceTutorialStep(); showSkipTutorialButton(); // Show Tutorial button removed }, 500); // Start background music LK.playMusic('backgroundMusic', { loop: true }); 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()); } // Create betting system UI elements var betContainer = new Container(); betContainer.x = 1280; // Move further right betContainer.y = 2732 - 250; // Move down game.addChild(betContainer); // Betting amount display (center) var betAmountTextShadow = new Text2('Bet: ' + currentBet, { size: 60, fill: 0x000000, weight: 800 }); betAmountTextShadow.anchor.set(0.5, 0.5); betAmountTextShadow.x = 2; betAmountTextShadow.y = 2; betContainer.addChild(betAmountTextShadow); var betAmountText = new Text2('Bet: ' + currentBet, { size: 60, fill: 0xFFD700, weight: 800 }); betAmountText.anchor.set(0.5, 0.5); betContainer.addChild(betAmountText); // Decrease bet button (left) var decreaseBetButton = new Container(); var decreaseBetButtonGraphics = decreaseBetButton.attachAsset('decreaseBetButton', { anchorX: 0.5, anchorY: 0.5 }); decreaseBetButtonGraphics.scaleX = 2.0; decreaseBetButtonGraphics.scaleY = 2.0; decreaseBetButton.x = -250; // Left of bet display with more spacing decreaseBetButton.y = 0; betContainer.addChild(decreaseBetButton); // Increase bet button (right) var increaseBetButton = new Container(); var increaseBetButtonGraphics = increaseBetButton.attachAsset('increaseBetButton', { anchorX: 0.5, anchorY: 0.5 }); increaseBetButtonGraphics.scaleX = 2.0; increaseBetButtonGraphics.scaleY = 2.0; increaseBetButton.x = 250; // Right of bet display with more spacing increaseBetButton.y = 0; betContainer.addChild(increaseBetButton); // Bet button (below the amount display) var betButton = new Container(); var betButtonGraphics = betButton.attachAsset('betButton', { anchorX: 0.5, anchorY: 0.5 }); betButtonGraphics.scaleX = 3.0678375; betButtonGraphics.scaleY = 3.0678375; betButton.x = 0; betButton.y = 150; // Increased separation from bet amount display betContainer.addChild(betButton); // Update betting display function function updateBetDisplay() { betAmountTextShadow.setText('Bet: ' + currentBet); betAmountText.setText('Bet: ' + currentBet); } 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 // Play tower placement sound LK.getSound('placeTowerSound').play(); 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; // Stop forward loop sound and resume background music if (forwardLoopPlaying) { LK.getSound('forwardLoopSound').stop(); forwardLoopPlaying = false; LK.playMusic('backgroundMusic', { loop: true }); } } 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.scaleX = 1 / 1.5; waveIndicator.scaleY = 1 / 1.5; waveIndicator.x = 2048 - 200; 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 test button in top-left for adding 1000 gold var testButton = new Container(); var testButtonGraphics = testButton.attachAsset('redButton', { anchorX: 0.5, anchorY: 0.5 }); testButtonGraphics.scaleX = 2.0; testButtonGraphics.scaleY = 2.0; testButton.x = 200; testButton.y = 300; testButton.isPressed = false; testButton.visible = false; // Hide the test button game.addChild(testButton); // Test button functionality testButton.down = function (x, y, obj) { if (!testButton.isPressed) { testButton.isPressed = true; // Change button appearance to pressed state testButton.removeChild(testButtonGraphics); testButtonGraphics = testButton.attachAsset('redButtonPressed', { anchorX: 0.5, anchorY: 0.5 }); testButtonGraphics.scaleX = 2.0; testButtonGraphics.scaleY = 2.0; // Give 1000 gold setGold(gold + 1000); // Upgrade all towers to level 5 var towersUpgraded = 0; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (tower.level < 5) { // Set tower to level 5 tower.level = 5; tower.clicksInvested = 0; // Apply level 5 upgrades tower.fireRate = Math.max(5, 60 - tower.level * 24); tower.damage = 10 + tower.level * 20; tower.bulletSpeed = 5 + tower.level * 2.4; // Update visual indicators tower.refreshCellsInRange(); tower.updateLevelIndicators(); towersUpgraded++; } } // Show notification about towers upgraded and gold given if (towersUpgraded > 0) { var notification = game.addChild(new Notification("Upgraded " + towersUpgraded + " towers to level 5! +1000 gold")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("All towers already at max level! +1000 gold")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } }; testButton.up = function (x, y, obj) { if (testButton.isPressed) { testButton.isPressed = false; // Change button back to normal state testButton.removeChild(testButtonGraphics); testButtonGraphics = testButton.attachAsset('redButton', { anchorX: 0.5, anchorY: 0.5 }); testButtonGraphics.scaleX = 2.0; testButtonGraphics.scaleY = 2.0; } }; // 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; speedBoostButton.visible = true; // Show the speed boost button game.addChild(speedBoostButton); // Create super speed boost button (10X) var superSpeedBoostButton = new Container(); var superSpeedBoostButtonGraphics = superSpeedBoostButton.attachAsset('speedBoostButton', { anchorX: 0.5, anchorY: 0.5 }); superSpeedBoostButtonGraphics.scaleX = 4.0; superSpeedBoostButtonGraphics.scaleY = 4.0; superSpeedBoostButton.x = 2048 - 150; superSpeedBoostButton.y = 220; // Position below the original button superSpeedBoostButton.isPressed = false; superSpeedBoostButton.isActive = false; superSpeedBoostButton.cooldown = 0; superSpeedBoostButton.visible = false; // Hide the super speed boost button game.addChild(superSpeedBoostButton); // 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 // Sound control variables var forwardLoopPlaying = false; // Super speed boost variables var superSpeedBoostActive = false; // 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; // Stop background music and play forward loop sound LK.stopMusic(); forwardLoopPlaying = true; LK.getSound('forwardLoopSound').play(); // 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; } }; // Super speed boost button functionality (10X) superSpeedBoostButton.down = function (x, y, obj) { if (!superSpeedBoostButton.isPressed) { superSpeedBoostButton.isPressed = true; superSpeedBoostButton.isActive = true; superSpeedBoostActive = true; // Change button appearance to pressed state superSpeedBoostButton.removeChild(superSpeedBoostButtonGraphics); superSpeedBoostButtonGraphics = superSpeedBoostButton.attachAsset('speedBoostButtonPressed', { anchorX: 0.5, anchorY: 0.5 }); superSpeedBoostButtonGraphics.scaleX = 4.0; superSpeedBoostButtonGraphics.scaleY = 4.0; // Show activation notification var notification = game.addChild(new Notification("Super Speed Boost Active! (50X)")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; // Betting system button functionality decreaseBetButton.down = function (x, y, obj) { // Change to pressed state decreaseBetButton.removeChild(decreaseBetButtonGraphics); decreaseBetButtonGraphics = decreaseBetButton.attachAsset('decreaseBetButtonPressed', { anchorX: 0.5, anchorY: 0.5 }); decreaseBetButtonGraphics.scaleX = 2.0; decreaseBetButtonGraphics.scaleY = 2.0; if (currentBet > minBet) { // Ensure we only bet in multiples of 10 var newBet = currentBet - 10; // Round to nearest multiple of 10 (though it should already be) newBet = Math.round(newBet / 10) * 10; currentBet = Math.max(minBet, newBet); updateBetDisplay(); // Play bet decrease sound LK.getSound('betDecreaseSound').play(); } }; decreaseBetButton.up = function (x, y, obj) { // Change back to normal state decreaseBetButton.removeChild(decreaseBetButtonGraphics); decreaseBetButtonGraphics = decreaseBetButton.attachAsset('decreaseBetButton', { anchorX: 0.5, anchorY: 0.5 }); decreaseBetButtonGraphics.scaleX = 2.0; decreaseBetButtonGraphics.scaleY = 2.0; }; increaseBetButton.down = function (x, y, obj) { // Change to pressed state increaseBetButton.removeChild(increaseBetButtonGraphics); increaseBetButtonGraphics = increaseBetButton.attachAsset('increaseBetButtonPressed', { anchorX: 0.5, anchorY: 0.5 }); increaseBetButtonGraphics.scaleX = 2.0; increaseBetButtonGraphics.scaleY = 2.0; var newBet = currentBet + 10; // Check if player has at least 10 gold to place any bet if (gold < 10) { var notification = game.addChild(new Notification("You need at least 10 gold to place a bet")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } // Only allow increment if player has enough gold for the complete next bet amount if (gold >= newBet) { // Ensure we only bet in multiples of 10 // Round to nearest multiple of 10 (though it should already be) newBet = Math.round(newBet / 10) * 10; currentBet = newBet; updateBetDisplay(); // Play bet increase sound LK.getSound('betIncreaseSound').play(); } }; increaseBetButton.up = function (x, y, obj) { // Change back to normal state increaseBetButton.removeChild(increaseBetButtonGraphics); increaseBetButtonGraphics = increaseBetButton.attachAsset('increaseBetButton', { anchorX: 0.5, anchorY: 0.5 }); increaseBetButtonGraphics.scaleX = 2.0; increaseBetButtonGraphics.scaleY = 2.0; }; betButton.down = function (x, y, obj) { // Ensure bet is a multiple of 10 if (currentBet % 10 !== 0) { currentBet = Math.round(currentBet / 10) * 10; updateBetDisplay(); } if (currentBet > 0 && gold >= currentBet) { setGold(gold - currentBet); // Create spinning wheel in center of screen var spinningWheel = new SpinningWheel(); spinningWheel.x = 2048 / 2; spinningWheel.y = 2732 / 2; spinningWheel.scaleX = 1.5; spinningWheel.scaleY = 1.5; game.addChild(spinningWheel); // Create fixed arrow pointer on the right side var wheelArrow = new Container(); var arrowGraphics = wheelArrow.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5 }); // Use scale instead of width/height for better quality arrowGraphics.scaleX = 7.0; arrowGraphics.scaleY = 7.0; arrowGraphics.tint = 0xFF0000; // Red color to make it stand out arrowGraphics.rotation = Math.PI; // Point left toward the wheel wheelArrow.x = spinningWheel.x + 600; // Position further to the right of the larger wheel wheelArrow.y = spinningWheel.y; // Same vertical position as wheel center // Set zIndex above all other elements, including turret bullets and tutorial arrows wheelArrow.zIndex = 20001; game.addChild(wheelArrow); // Store reference to arrow for cleanup spinningWheel.wheelArrow = wheelArrow; // Play bet button sound LK.getSound('betButtonSound').play(); spinningWheel.spin(currentBet); var notification = game.addChild(new Notification("Spinning the wheel!")); notification.x = 2048 / 2; notification.y = grid.height - 150; // Reset bet after placing currentBet = 0; updateBetDisplay(); } else if (currentBet <= 0) { var notification = game.addChild(new Notification("Set a bet amount first!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; superSpeedBoostButton.up = function (x, y, obj) { if (superSpeedBoostButton.isPressed) { superSpeedBoostButton.isPressed = false; superSpeedBoostButton.isActive = false; superSpeedBoostActive = false; // Change button back to normal state superSpeedBoostButton.removeChild(superSpeedBoostButtonGraphics); superSpeedBoostButtonGraphics = superSpeedBoostButton.attachAsset('speedBoostButton', { anchorX: 0.5, anchorY: 0.5 }); superSpeedBoostButtonGraphics.scaleX = 4.0; superSpeedBoostButtonGraphics.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 but cap at 10 during tutorial var newGold = gold + 1; if (tutorialActive && newGold > 10) { newGold = 10; } setGold(newGold); // Show gold increment indicator below the gold counter only if gold actually increased if (!(tutorialActive && gold >= 10)) { 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; } // Play red button sound LK.getSound('redButtonSound').play(); // 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 = 1; if (superSpeedBoostActive) { speedMultiplier = 50; } else if (speedBoostActive) { speedMultiplier = 3; } // 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; } } // Eliminate all enemies while super speed boost button is pressed if (superSpeedBoostActive) { for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; // Set enemy health to 0 to eliminate them enemy.health = 0; } } // 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 === 7 && tutorialFlyingEnemy && enemy === tutorialFlyingEnemy) { tutorialFlyingEnemy = null; // ← IMPORTANTE: liberar la referencia advanceTutorialStep(); // pasa al paso 8 return; } } 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 // Play enemy death sound LK.getSound('enemyDeathSound').play(); // 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");
var storage = LK.import("@upit/storage.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 = self.speed;
if (speedBoostActive) {
effectiveSpeed = self.speed * 3;
}
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 = 60;
if (speedBoostActive) {
effectiveDamageRate = Math.ceil(60 / 3);
}
if (self.enemiesAttacking.length > 0 && LK.ticks - self.lastHealthLossTime >= effectiveDamageRate) {
globalDoorHealth = Math.max(0, globalDoorHealth - self.enemiesAttacking.length);
self.lastHealthLossTime = LK.ticks;
// Play door hit sound
LK.getSound('doorHitSound').play();
// 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) {
// Prevent door repair until tutorial step 9 or later
if (tutorialActive && tutorialStep < 9) {
var notification = game.addChild(new Notification("Cannot repair door yet!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
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 based on wave number
if (currentWave === 10) {
// Wave 10: Boss Normal
self.enemyAssets = ['Enemy1', 'Enemy2', 'Enemy3'];
} else if (currentWave === 20) {
// Wave 20: Boss Fast
self.speed *= 2;
self.maxHealth = 100;
self.enemyAssets = ['EnemyFast1', 'EnemyFast2', 'EnemyFast3'];
} else if (currentWave === 30) {
// Wave 30: Boss Flying
self.isFlying = true;
self.maxHealth = 80;
self.enemyAssets = ['EnemyFlying1', 'EnemyFlying2', 'EnemyFlying3'];
} else if (currentWave === 40) {
// Wave 40: Boss Tank (should NOT be flying)
self.isImmune = true;
self.isFlying = false; // Explicitly set to false to override any flying behavior
self.maxHealth = 240;
self.speed *= 0.5; // Reduce speed by 0.5x
self.enemyAssets = ['EnemyImmune1', 'EnemyImmune2', 'EnemyImmune3'];
} else if (currentWave === 50) {
// Wave 50: Boss Swarm - Large bosses with high health
self.maxHealth = 500; // Much higher health for wave 50 bosses
self.enemyAssets = ['EnemyBoss1', 'EnemyBoss2', 'EnemyBoss3']; // Use boss sprites instead of swarm
// Force boss scaling for wave 50
self.isBoss = true;
} else {
// Default boss sprites for other boss waves
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 SpinningWheel = Container.expand(function () {
var self = Container.call(this);
self.isSpinning = false;
self.finalSection = 0;
self.betAmount = 0;
// Create wheel base
var wheelBase = self.attachAsset('spinningWheel', {
anchorX: 0.5,
anchorY: 0.5
});
wheelBase.tint = 0xDDDDDD;
// Create 8 sections with different colors
// Lose All is always red, others are unique bright primaries
var sectionNames = ["💀 Lose All", "BET X 10", "💀 Lose All", "BET X 5", "💀 Lose All", "BET X 2", "💀 Lose All", "BET X 1"];
// Assign unique bright primary colors to each section except "Lose All"
var brightPrimaries = [0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, 0xFFA500, 0x800080];
var sectionColors = [];
for (var i = 0; i < sectionNames.length; i++) {
if (sectionNames[i] === "Lose All") {
sectionColors[i] = 0xFF0000; // Red for Lose All
} else {
// Assign a unique color from the brightPrimaries array, cycling if needed
sectionColors[i] = brightPrimaries[i % brightPrimaries.length];
}
}
self.sections = [];
self.sectionLabels = [];
// Create visual sections
for (var i = 0; i < 8; i++) {
var section = new Container();
// Remove the colored background block - no longer adding sectionGraphics
var angle = i * 45 * Math.PI / 180;
section.x = Math.cos(angle) * 110; // Move blocks closer to center (was 150)
section.y = Math.sin(angle) * 110; // Move blocks closer to center (was 150)
section.rotation = angle;
// Add text label
var label = new Text2(sectionNames[i], {
size: 32,
fill: 0xFFFFFF,
weight: 800
});
label.anchor.set(0.5, 0.5);
label.x = 90; // Center label on shorter section block
label.y = 0;
section.addChild(label);
self.addChild(section);
self.sections.push(section);
self.sectionLabels.push(sectionNames[i]);
}
// Black separators removed - no longer creating separator lines between sections
// Arrow will be created separately and positioned outside the wheel
// Remove the arrow from the spinning wheel itself
self.spin = function (betAmount) {
if (self.isSpinning) {
return;
}
self.isSpinning = true;
self.betAmount = betAmount;
// Set high zIndex to ensure wheel appears above all other elements, including turret bullets and tutorial arrows
self.zIndex = 30000;
// Play wheel spin sound
LK.getSound('wheelSpinSound').play();
// Random number of full rotations (3-8) plus random section
var baseRotations = 3 + Math.random() * 5;
var randomSection = Math.floor(Math.random() * 8);
// Calculate exact center angle for the selected section
// Each section is 45 degrees, so center is at section * 45
var sectionCenterAngle = randomSection * 45;
var finalAngle = baseRotations * 360 + sectionCenterAngle;
self.finalSection = randomSection;
// Spin animation with precise centering
tween(self, {
rotation: finalAngle * Math.PI / 180
}, {
duration: 3000,
easing: tween.easeOut,
onFinish: function onFinish() {
// After main spin, fine-tune to ensure perfect center alignment
self.fineTuneToCenter();
}
});
};
self.fineTuneToCenter = function () {
// Calculate current rotation in degrees
var currentRotationDegrees = self.rotation * 180 / Math.PI % 360;
while (currentRotationDegrees < 0) {
currentRotationDegrees += 360;
}
while (currentRotationDegrees >= 360) {
currentRotationDegrees -= 360;
}
// Find the nearest section center ahead of current position
var nearestSectionAhead = -1;
var shortestDistanceAhead = Infinity;
// Check all 8 sections to find the nearest one ahead
for (var i = 0; i < 8; i++) {
var sectionCenter = i * 45;
var distanceAhead = sectionCenter - currentRotationDegrees;
// If distance is negative, add 360 to get forward distance
if (distanceAhead <= 0) {
distanceAhead += 360;
}
// Find the shortest forward distance
if (distanceAhead < shortestDistanceAhead) {
shortestDistanceAhead = distanceAhead;
nearestSectionAhead = i;
}
}
// Update finalSection to the nearest section ahead
self.finalSection = nearestSectionAhead;
// If we're very close to center, no need to adjust
if (Math.abs(shortestDistanceAhead) <= 0.1 || Math.abs(shortestDistanceAhead - 360) <= 0.1) {
// Already perfectly centered, proceed with result
self.handleResult();
return;
}
// Calculate final rotation by continuing to spin forward
var finalRotation = self.rotation + shortestDistanceAhead * Math.PI / 180;
// Use constant slow speed: calculate duration based on angle distance
var constantSpeed = 30; // degrees per second
var durationMs = shortestDistanceAhead / constantSpeed * 1000;
// Continue spinning smoothly at constant speed until perfectly centered
tween(self, {
rotation: finalRotation
}, {
duration: durationMs,
easing: tween.linear,
// Linear easing for constant speed
onFinish: function onFinish() {
self.handleResult();
}
});
};
self.handleResult = function () {
// Calculate which section is actually at the arrow position (90 degrees to the right)
// The wheel rotates, so we need to find which section is at the 0-degree position (right side)
var currentRotationDegrees = self.rotation * 180 / Math.PI % 360;
while (currentRotationDegrees < 0) {
currentRotationDegrees += 360;
}
while (currentRotationDegrees >= 360) {
currentRotationDegrees -= 360;
}
// Each section is 45 degrees wide, starting at 0 degrees
// The arrow points to the right (0 degrees), so we need to find which section is at 0 degrees
// Add 22.5 degrees to center the calculation on section midpoints
var sectionAtArrow = Math.floor((currentRotationDegrees + 22.5) / 45) % 8;
var sectionName = self.sectionLabels[sectionAtArrow];
var winAmount = 0;
var message = "";
switch (sectionAtArrow) {
case 0:
// Position 1: Lose ALL
winAmount = 0;
message = "Lost all bet! Better luck next time!";
break;
case 1:
// Position 2: BET X 10
winAmount = self.betAmount * 1;
message = "Won " + winAmount + " gold! (1x)";
break;
case 2:
// Position 3: Lose ALL
winAmount = 0;
message = "Lost all bet! Better luck next time!";
break;
case 3:
// Position 4: BET X 50 (This should award 20X)
winAmount = self.betAmount * 2;
message = "Won " + winAmount + " gold! (2x)";
break;
case 4:
// Position 5: Lose ALL
winAmount = 0;
message = "Lost all bet! Better luck next time!";
break;
case 5:
// Position 6: BET X 20 (This should award 50X)
winAmount = self.betAmount * 5;
message = "Won " + winAmount + " gold! (5x)";
break;
case 6:
// Position 7: Lose ALL
winAmount = 0;
message = "Lost all bet! Better luck next time!";
break;
case 7:
// Position 8: BET X 100 (This should award 100X)
winAmount = self.betAmount * 10;
message = "JACKPOT! Won " + winAmount + " gold! (10x)";
break;
}
// Play win or lose sound based on result
if (winAmount > 0) {
LK.getSound('wheelWinSound').play();
} else {
LK.getSound('wheelLoseSound').play();
}
setGold(gold + winAmount);
var notification = game.addChild(new Notification(message));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Keep the wheel visible for at least 1 second after spinning ends, then hide
LK.setTimeout(function () {
// Clean up the fixed arrow if it exists
if (self.wheelArrow) {
self.wheelArrow.destroy();
self.wheelArrow = null;
}
self.destroy();
}, 1000);
};
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 = 0x0033CC;
} 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 = self.fireRate;
if (speedBoostActive) {
effectiveFireRate = Math.ceil(self.fireRate / 3);
}
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;
// Play tower click sound
LK.getSound('towerClickSound').play();
// 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);
// Check if tower upgrades are allowed during tutorial
if (tutorialActive && tutorialStep !== 5) {
var notification = game.addChild(new Notification("Cannot upgrade tower yet!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
// 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
});
}
});
}
// Play tower upgrade sound
LK.getSound('towerUpgradeSound').play();
// 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;
// Create bullet in that position
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet zIndex below spinning wheel (zIndex 20000), but above most gameplay elements
bullet.zIndex = 10000;
game.addChild(bullet);
// Ensure bullet appears below any existing spinning wheel
if (bullet.zIndex) {
bullet.zIndex = 10000;
}
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// Play tower fire sound
LK.getSound('towerFireSound').play();
// --- 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: specific boss types for specific waves
var waveNumber = i + 1;
if (waveNumber === 10) {
// Wave 10: Boss Normal
enemyType = 'normal';
waveType = "Boss Normal";
block.tint = 0xAAAAAA;
} else if (waveNumber === 20) {
// Wave 20: Boss Fast
enemyType = 'fast';
waveType = "Boss Fast";
block.tint = 0x00AAFF;
} else if (waveNumber === 30) {
// Wave 30: Boss Tank
enemyType = 'tank';
waveType = "Boss Tank";
block.tint = 0xAA0000;
} else if (waveNumber === 40) {
// Wave 40: Boss Tank
enemyType = 'tank';
waveType = "Boss Tank";
block.tint = 0xAA0000;
} else if (waveNumber === 50) {
// Wave 50: Boss Swarm - 5 large bosses
enemyType = 'swarm';
waveType = "Boss Swarm";
block.tint = 0xFF00FF;
} else {
// Fallback for any other boss waves (shouldn't happen with current totalWaves = 50)
enemyType = 'normal';
waveType = "Boss Normal";
block.tint = 0xAAAAAA;
}
enemyCount = waveNumber === 50 ? 5 : 1; // Wave 50 spawns 5 bosses, others spawn 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
****/
game.sortableChildren = true;
// Initialize background music
// Initialize sound effects
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 ---
// Always show the tutorial at the start of the game, regardless of previous completion
var tutorialActive = true;
// --- Fix: Define highlightGoldButton globally so it is always available ---
var goldButtonHighlighted = false;
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
// Betting system variables
var currentBet = 0; // Always keep as multiple of 10
var maxBet = 999999; // Remove betting limit
var minBet = 0;
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
tutorialArrow.alpha = 1.0; // Ensure arrow is fully visible without flashing
// Stop any existing tweens on the arrow to prevent flashing
if (typeof tween !== "undefined") {
tween.stop(tutorialArrow, {
alpha: true
});
}
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 AND tutorial is still active
if (!speedBoostButton.isActive && tutorialActive) {
tutorialAllowForward = true;
showTutorialOverlay(tutorialForwardIconArea, "Press the Forward Icon to Speed up the Game", "down");
}
}, 5000);
} else if (tutorialStep === 7) {
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 === 7) {
clearTutorialOverlay();
}
;
}, 4000);
spawnTutorialFlyingEnemy();
} else if (tutorialStep === 8) {
var checkGoldForStep9 = function checkGoldForStep9() {
if (tutorialStep !== 8) {
return;
}
if (gold < 5) {
// SOLO dibuja la superposición la primera vez
if (!overlayShown) {
showTutorialOverlay(tutorialRedButtonArea, "Click again to keep earning more money", "up");
overlayShown = true;
}
} else {
// El jugador llegó a 5 de oro
clearTutorialOverlay();
overlayShown = false;
if (highlightGoldButtonInterval) {
LK.clearInterval(highlightGoldButtonInterval);
highlightGoldButtonInterval = null;
}
advanceTutorialStep(); // pasa al paso 9
}
};
// --- NUEVO: Highlight gold button and require 5 gold before step 9 ---
// Only proceed to step 9 after player reaches 5 gold
var overlayShown = false; // evita que la flecha se redibuje
var highlightGoldButtonInterval = null;
var goldButtonHighlighted = false;
highlightGoldButtonInterval = LK.setInterval(checkGoldForStep9, 300);
// Also check immediately in case player already has 5+ gold
checkGoldForStep9();
// Do NOT advance to step 9 here. Wait for flying enemy to be killed (handled in game.update).
return;
} else if (tutorialStep === 9) {
// Step 9: Click the gate to repair if damaged
tutorialAllowFlyingEnemy = false;
tutorialAllowRepair = true;
// Clear any existing highlight effects from previous steps
goldButtonHighlighted = false;
if (typeof highlightGoldButtonInterval !== "undefined" && highlightGoldButtonInterval) {
LK.clearInterval(highlightGoldButtonInterval);
highlightGoldButtonInterval = null;
}
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
}, "Good Luck", null);
LK.setTimeout(function () {
clearTutorialOverlay();
tutorialActive = false;
tutorialAllowAll = true;
// Save tutorial completion state
storage.tutorialCompleted = true;
// Start the game
waveIndicator.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
// Hide the skip tutorial button when tutorial ends
hideSkipTutorialButton();
}, 1200);
// Show Tutorial button removed
}
}
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;
}
// --- Skip Tutorial Button Implementation ---
var skipTutorialButton = null;
var skipTutorialButtonText = null;
var showTutorialButton = null;
var showTutorialButtonText = null;
function showSkipTutorialButton() {
if (skipTutorialButton) {
return;
}
skipTutorialButton = new Container();
skipTutorialButton.zIndex = 10001;
// Move to bottom right, with minimal margin from the edge (closer to corner)
skipTutorialButton.x = 2048 - 40 - 300; // 40px margin, 300 is half width
skipTutorialButton.y = 2732 - 40 - 70; // 40px margin, 70 is half height
skipTutorialButton.visible = true;
var btnBg = skipTutorialButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
btnBg.width = 600;
btnBg.height = 140;
btnBg.tint = 0x222222;
btnBg.alpha = 0.85;
skipTutorialButtonText = new Text2("Skip Tutorial", {
size: 70,
fill: 0xffffff,
weight: 800
});
skipTutorialButtonText.anchor.set(0.5, 0.5);
skipTutorialButton.addChild(skipTutorialButtonText);
skipTutorialButton.down = function (x, y, obj) {
// Remove button
if (skipTutorialButton) {
skipTutorialButton.visible = false;
skipTutorialButton.destroy();
skipTutorialButton = null;
}
// --- Begin: Cancel all pending tutorial processes and remove test monsters ---
// Clear any tutorial overlay and arrow/message
clearTutorialOverlay();
// Cancel any highlightGoldButtonInterval or tutorial timers
if (typeof highlightGoldButtonInterval !== "undefined" && highlightGoldButtonInterval) {
LK.clearInterval(highlightGoldButtonInterval);
highlightGoldButtonInterval = null;
}
// Remove any tutorial flying enemy
if (typeof tutorialFlyingEnemy !== "undefined" && tutorialFlyingEnemy) {
// Remove from enemies array and destroy
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === tutorialFlyingEnemy) {
if (tutorialFlyingEnemy.isFlying && tutorialFlyingEnemy.shadow) {
if (typeof enemyLayerMiddle !== "undefined") {
enemyLayerMiddle.removeChild(tutorialFlyingEnemy.shadow);
}
tutorialFlyingEnemy.shadow = null;
}
if (typeof enemyLayerTop !== "undefined") {
enemyLayerTop.removeChild(tutorialFlyingEnemy);
}
enemies.splice(i, 1);
tutorialFlyingEnemy.destroy();
break;
}
}
tutorialFlyingEnemy = null;
}
// Remove any tutorial basic enemy
if (typeof tutorialBasicEnemy !== "undefined" && tutorialBasicEnemy) {
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === tutorialBasicEnemy) {
if (typeof enemyLayerBottom !== "undefined") {
enemyLayerBottom.removeChild(tutorialBasicEnemy);
}
enemies.splice(i, 1);
tutorialBasicEnemy.destroy();
break;
}
}
tutorialBasicEnemy = null;
}
// --- End: Cancel all pending tutorial processes and remove test monsters ---
// End tutorial, start game, give 10 gold, set door health to 100
tutorialActive = false;
tutorialAllowAll = true;
storage.tutorialCompleted = true;
clearTutorialOverlay();
waveIndicator.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
setGold(10);
globalDoorHealth = globalDoorMaxHealth;
updateGlobalDoorHealthBar();
// Show Tutorial button removed
};
game.addChild(skipTutorialButton);
}
function hideSkipTutorialButton() {
if (skipTutorialButton) {
skipTutorialButton.visible = false;
skipTutorialButton.destroy();
skipTutorialButton = null;
}
}
// Show Tutorial button and its implementation removed
// Start tutorial or game based on completion state
LK.setTimeout(function () {
// Always start tutorial at game start
tutorialStep = 0; // Start at step 0 so step 1 shows properly
advanceTutorialStep();
showSkipTutorialButton();
// Show Tutorial button removed
}, 500);
// Start background music
LK.playMusic('backgroundMusic', {
loop: true
});
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());
}
// Create betting system UI elements
var betContainer = new Container();
betContainer.x = 1280; // Move further right
betContainer.y = 2732 - 250; // Move down
game.addChild(betContainer);
// Betting amount display (center)
var betAmountTextShadow = new Text2('Bet: ' + currentBet, {
size: 60,
fill: 0x000000,
weight: 800
});
betAmountTextShadow.anchor.set(0.5, 0.5);
betAmountTextShadow.x = 2;
betAmountTextShadow.y = 2;
betContainer.addChild(betAmountTextShadow);
var betAmountText = new Text2('Bet: ' + currentBet, {
size: 60,
fill: 0xFFD700,
weight: 800
});
betAmountText.anchor.set(0.5, 0.5);
betContainer.addChild(betAmountText);
// Decrease bet button (left)
var decreaseBetButton = new Container();
var decreaseBetButtonGraphics = decreaseBetButton.attachAsset('decreaseBetButton', {
anchorX: 0.5,
anchorY: 0.5
});
decreaseBetButtonGraphics.scaleX = 2.0;
decreaseBetButtonGraphics.scaleY = 2.0;
decreaseBetButton.x = -250; // Left of bet display with more spacing
decreaseBetButton.y = 0;
betContainer.addChild(decreaseBetButton);
// Increase bet button (right)
var increaseBetButton = new Container();
var increaseBetButtonGraphics = increaseBetButton.attachAsset('increaseBetButton', {
anchorX: 0.5,
anchorY: 0.5
});
increaseBetButtonGraphics.scaleX = 2.0;
increaseBetButtonGraphics.scaleY = 2.0;
increaseBetButton.x = 250; // Right of bet display with more spacing
increaseBetButton.y = 0;
betContainer.addChild(increaseBetButton);
// Bet button (below the amount display)
var betButton = new Container();
var betButtonGraphics = betButton.attachAsset('betButton', {
anchorX: 0.5,
anchorY: 0.5
});
betButtonGraphics.scaleX = 3.0678375;
betButtonGraphics.scaleY = 3.0678375;
betButton.x = 0;
betButton.y = 150; // Increased separation from bet amount display
betContainer.addChild(betButton);
// Update betting display function
function updateBetDisplay() {
betAmountTextShadow.setText('Bet: ' + currentBet);
betAmountText.setText('Bet: ' + currentBet);
}
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
// Play tower placement sound
LK.getSound('placeTowerSound').play();
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;
// Stop forward loop sound and resume background music
if (forwardLoopPlaying) {
LK.getSound('forwardLoopSound').stop();
forwardLoopPlaying = false;
LK.playMusic('backgroundMusic', {
loop: true
});
}
}
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.scaleX = 1 / 1.5;
waveIndicator.scaleY = 1 / 1.5;
waveIndicator.x = 2048 - 200;
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 test button in top-left for adding 1000 gold
var testButton = new Container();
var testButtonGraphics = testButton.attachAsset('redButton', {
anchorX: 0.5,
anchorY: 0.5
});
testButtonGraphics.scaleX = 2.0;
testButtonGraphics.scaleY = 2.0;
testButton.x = 200;
testButton.y = 300;
testButton.isPressed = false;
testButton.visible = false; // Hide the test button
game.addChild(testButton);
// Test button functionality
testButton.down = function (x, y, obj) {
if (!testButton.isPressed) {
testButton.isPressed = true;
// Change button appearance to pressed state
testButton.removeChild(testButtonGraphics);
testButtonGraphics = testButton.attachAsset('redButtonPressed', {
anchorX: 0.5,
anchorY: 0.5
});
testButtonGraphics.scaleX = 2.0;
testButtonGraphics.scaleY = 2.0;
// Give 1000 gold
setGold(gold + 1000);
// Upgrade all towers to level 5
var towersUpgraded = 0;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.level < 5) {
// Set tower to level 5
tower.level = 5;
tower.clicksInvested = 0;
// Apply level 5 upgrades
tower.fireRate = Math.max(5, 60 - tower.level * 24);
tower.damage = 10 + tower.level * 20;
tower.bulletSpeed = 5 + tower.level * 2.4;
// Update visual indicators
tower.refreshCellsInRange();
tower.updateLevelIndicators();
towersUpgraded++;
}
}
// Show notification about towers upgraded and gold given
if (towersUpgraded > 0) {
var notification = game.addChild(new Notification("Upgraded " + towersUpgraded + " towers to level 5! +1000 gold"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
var notification = game.addChild(new Notification("All towers already at max level! +1000 gold"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
};
testButton.up = function (x, y, obj) {
if (testButton.isPressed) {
testButton.isPressed = false;
// Change button back to normal state
testButton.removeChild(testButtonGraphics);
testButtonGraphics = testButton.attachAsset('redButton', {
anchorX: 0.5,
anchorY: 0.5
});
testButtonGraphics.scaleX = 2.0;
testButtonGraphics.scaleY = 2.0;
}
};
// 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;
speedBoostButton.visible = true; // Show the speed boost button
game.addChild(speedBoostButton);
// Create super speed boost button (10X)
var superSpeedBoostButton = new Container();
var superSpeedBoostButtonGraphics = superSpeedBoostButton.attachAsset('speedBoostButton', {
anchorX: 0.5,
anchorY: 0.5
});
superSpeedBoostButtonGraphics.scaleX = 4.0;
superSpeedBoostButtonGraphics.scaleY = 4.0;
superSpeedBoostButton.x = 2048 - 150;
superSpeedBoostButton.y = 220; // Position below the original button
superSpeedBoostButton.isPressed = false;
superSpeedBoostButton.isActive = false;
superSpeedBoostButton.cooldown = 0;
superSpeedBoostButton.visible = false; // Hide the super speed boost button
game.addChild(superSpeedBoostButton);
// 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
// Sound control variables
var forwardLoopPlaying = false;
// Super speed boost variables
var superSpeedBoostActive = false;
// 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;
// Stop background music and play forward loop sound
LK.stopMusic();
forwardLoopPlaying = true;
LK.getSound('forwardLoopSound').play();
// 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;
}
};
// Super speed boost button functionality (10X)
superSpeedBoostButton.down = function (x, y, obj) {
if (!superSpeedBoostButton.isPressed) {
superSpeedBoostButton.isPressed = true;
superSpeedBoostButton.isActive = true;
superSpeedBoostActive = true;
// Change button appearance to pressed state
superSpeedBoostButton.removeChild(superSpeedBoostButtonGraphics);
superSpeedBoostButtonGraphics = superSpeedBoostButton.attachAsset('speedBoostButtonPressed', {
anchorX: 0.5,
anchorY: 0.5
});
superSpeedBoostButtonGraphics.scaleX = 4.0;
superSpeedBoostButtonGraphics.scaleY = 4.0;
// Show activation notification
var notification = game.addChild(new Notification("Super Speed Boost Active! (50X)"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
// Betting system button functionality
decreaseBetButton.down = function (x, y, obj) {
// Change to pressed state
decreaseBetButton.removeChild(decreaseBetButtonGraphics);
decreaseBetButtonGraphics = decreaseBetButton.attachAsset('decreaseBetButtonPressed', {
anchorX: 0.5,
anchorY: 0.5
});
decreaseBetButtonGraphics.scaleX = 2.0;
decreaseBetButtonGraphics.scaleY = 2.0;
if (currentBet > minBet) {
// Ensure we only bet in multiples of 10
var newBet = currentBet - 10;
// Round to nearest multiple of 10 (though it should already be)
newBet = Math.round(newBet / 10) * 10;
currentBet = Math.max(minBet, newBet);
updateBetDisplay();
// Play bet decrease sound
LK.getSound('betDecreaseSound').play();
}
};
decreaseBetButton.up = function (x, y, obj) {
// Change back to normal state
decreaseBetButton.removeChild(decreaseBetButtonGraphics);
decreaseBetButtonGraphics = decreaseBetButton.attachAsset('decreaseBetButton', {
anchorX: 0.5,
anchorY: 0.5
});
decreaseBetButtonGraphics.scaleX = 2.0;
decreaseBetButtonGraphics.scaleY = 2.0;
};
increaseBetButton.down = function (x, y, obj) {
// Change to pressed state
increaseBetButton.removeChild(increaseBetButtonGraphics);
increaseBetButtonGraphics = increaseBetButton.attachAsset('increaseBetButtonPressed', {
anchorX: 0.5,
anchorY: 0.5
});
increaseBetButtonGraphics.scaleX = 2.0;
increaseBetButtonGraphics.scaleY = 2.0;
var newBet = currentBet + 10;
// Check if player has at least 10 gold to place any bet
if (gold < 10) {
var notification = game.addChild(new Notification("You need at least 10 gold to place a bet"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
// Only allow increment if player has enough gold for the complete next bet amount
if (gold >= newBet) {
// Ensure we only bet in multiples of 10
// Round to nearest multiple of 10 (though it should already be)
newBet = Math.round(newBet / 10) * 10;
currentBet = newBet;
updateBetDisplay();
// Play bet increase sound
LK.getSound('betIncreaseSound').play();
}
};
increaseBetButton.up = function (x, y, obj) {
// Change back to normal state
increaseBetButton.removeChild(increaseBetButtonGraphics);
increaseBetButtonGraphics = increaseBetButton.attachAsset('increaseBetButton', {
anchorX: 0.5,
anchorY: 0.5
});
increaseBetButtonGraphics.scaleX = 2.0;
increaseBetButtonGraphics.scaleY = 2.0;
};
betButton.down = function (x, y, obj) {
// Ensure bet is a multiple of 10
if (currentBet % 10 !== 0) {
currentBet = Math.round(currentBet / 10) * 10;
updateBetDisplay();
}
if (currentBet > 0 && gold >= currentBet) {
setGold(gold - currentBet);
// Create spinning wheel in center of screen
var spinningWheel = new SpinningWheel();
spinningWheel.x = 2048 / 2;
spinningWheel.y = 2732 / 2;
spinningWheel.scaleX = 1.5;
spinningWheel.scaleY = 1.5;
game.addChild(spinningWheel);
// Create fixed arrow pointer on the right side
var wheelArrow = new Container();
var arrowGraphics = wheelArrow.attachAsset('arrow', {
anchorX: 0.5,
anchorY: 0.5
});
// Use scale instead of width/height for better quality
arrowGraphics.scaleX = 7.0;
arrowGraphics.scaleY = 7.0;
arrowGraphics.tint = 0xFF0000; // Red color to make it stand out
arrowGraphics.rotation = Math.PI; // Point left toward the wheel
wheelArrow.x = spinningWheel.x + 600; // Position further to the right of the larger wheel
wheelArrow.y = spinningWheel.y; // Same vertical position as wheel center
// Set zIndex above all other elements, including turret bullets and tutorial arrows
wheelArrow.zIndex = 20001;
game.addChild(wheelArrow);
// Store reference to arrow for cleanup
spinningWheel.wheelArrow = wheelArrow;
// Play bet button sound
LK.getSound('betButtonSound').play();
spinningWheel.spin(currentBet);
var notification = game.addChild(new Notification("Spinning the wheel!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Reset bet after placing
currentBet = 0;
updateBetDisplay();
} else if (currentBet <= 0) {
var notification = game.addChild(new Notification("Set a bet amount first!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
superSpeedBoostButton.up = function (x, y, obj) {
if (superSpeedBoostButton.isPressed) {
superSpeedBoostButton.isPressed = false;
superSpeedBoostButton.isActive = false;
superSpeedBoostActive = false;
// Change button back to normal state
superSpeedBoostButton.removeChild(superSpeedBoostButtonGraphics);
superSpeedBoostButtonGraphics = superSpeedBoostButton.attachAsset('speedBoostButton', {
anchorX: 0.5,
anchorY: 0.5
});
superSpeedBoostButtonGraphics.scaleX = 4.0;
superSpeedBoostButtonGraphics.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 but cap at 10 during tutorial
var newGold = gold + 1;
if (tutorialActive && newGold > 10) {
newGold = 10;
}
setGold(newGold);
// Show gold increment indicator below the gold counter only if gold actually increased
if (!(tutorialActive && gold >= 10)) {
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;
}
// Play red button sound
LK.getSound('redButtonSound').play();
// 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 = 1;
if (superSpeedBoostActive) {
speedMultiplier = 50;
} else if (speedBoostActive) {
speedMultiplier = 3;
}
// 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;
}
}
// Eliminate all enemies while super speed boost button is pressed
if (superSpeedBoostActive) {
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
// Set enemy health to 0 to eliminate them
enemy.health = 0;
}
}
// 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 === 7 && tutorialFlyingEnemy && enemy === tutorialFlyingEnemy) {
tutorialFlyingEnemy = null; // ← IMPORTANTE: liberar la referencia
advanceTutorialStep(); // pasa al paso 8
return;
}
}
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
// Play enemy death sound
LK.getSound('enemyDeathSound').play();
// 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