Code edit (1 edits merged)
Please save this source code
User prompt
La barra de vida de la puerta quedo hasta abajo del juego, solo debe estar un poco abajo de la puerta y centrado
User prompt
Toda la logica de la puerta funciona, pero el medidor de salud esta mal, se quedo dividido y todos bajan al mismo tiempo, que solo sea un medidor visual en la parte inferior de la puerta que mida el largo de la puerta centrado.
User prompt
Las casillas de tipo "Puerta" comparten un solo medidor de salud. No importa que casilla sea la que se golpea, mientras sea de tipo "Puerta" se descuenta al conteo general.
User prompt
Como el camino esta completamente tapado los enemigo no siguen el camino, deben poder seguirlo hasta entrar a la casilla 124, cuando hagan esto ahi es cuando se detienen.
User prompt
Convierte las casillas 136,137,138,139,140 y 141 en casillas de tipo "Puerta", cuando los enemigos llegue a esta casilla no podrán atravesarla, en su lugar comenzaran a golpearla, los enemigos todavia podran ser atacados. La puerta iniciara con 100 puntos de vida y perderá 1 de vida por segundo por cada enemigo que la este golpeando, si los enemigos son eliminados dejaran de golpear la puerta. La puerta puede ser reparada, recuperar vida, si se hace click sobre ella, cada click restaura 1 punto de vida pero se descuenta 1 oro, si no se tiene oro no se puede reparar la puerta.
User prompt
os enemigos deben detenerse en las celdas 124, 125, 126, 127, 128 y 129. Solo pueden avanzar si la puerta ubicada en las celdas 136, 137, 138, 139, 140 y 141, en la esquina inferior izquierda del mapa, ha sido destruida. Mientras esperan, los enemigos deben distribuirse aleatoriamente a lo largo del eje X en la zona de la puerta para no estar todos en la misma posición. Cuando la puerta sea destruida, las celdas mencionadas se convierten en camino transitable para que los enemigos puedan continuar. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
En la parte Inferior en las casilas 136,137,138,139,140 y 141 me gustaria colocar una puerta, que tenga durabilidad de 100 con un indicador, cuando los enemigos lleguen a este punto se alinien a lo largo de la puerta para que no todos queden en la misma posición y comiencen a "Golpearla", la puerta perdera 1 de vida por segundo por cada enemigo que este frente a ella y si llega a 0 se rompera y se perdera el juego, elimina el contador de vidas y cambia como es que se pierde el juego, cuando ya fue golpeada se puede reparar haciendo click sobre ella, con cada click se repara 1 de vida y cada click descuenta 1 de oro, no se puede reparar si no tienes dinero. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
En lugar de la 131 era la 141, la 131 es obstaculo.
User prompt
No se aplico este assets a todas las casillas por donde pueden pasar los enemigos, faltan las casillas 4,5,6,7,8,9,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,131
User prompt
Ahora genera 4 assets para el camino, por donde pueden pasar los enemigos, llamado Camino1, Camino2, Camino3, Camino4, que siga la misma dinamica de seleccionar uno al azar al inicio del juego para que se vea diferente
User prompt
Ahora genera 4 assets para donde van los obstaculos llamado Pasto1, Pasto2, Pasto3, Pasto4, que siga la misma dinamica de seleccionar uno al azar al inicio del juego para que se vea diferente
User prompt
Genera cuatro assets diferentes llamados Muro1, Muro2, Muro3, Muro4, para los muros y en cada inicio de juego seleccionaras uno de ellos al azar para colocar en donde van los muros. Esto para que en cada incio de jeugo el mapa sea diferente.
User prompt
Solo cando el Wave sea de tipo Swarm no separes el espacio de Spawn y genera 50 enemigos en lugar de 30.
User prompt
Baja el precio a 10 e incrementa 10 por cada torreta en el campo
Code edit (1 edits merged)
Please save this source code
User prompt
Que colocar una torreta cueste 50 de oro y el precio incremente 5 por cada torreta colocada en el mapa.
User prompt
Que colocar una torreta cueste 50 de oro y +5 por cada torreta ya colocada.
User prompt
Que el ahumentar el nivel de las torretas no cueste oro
Code edit (5 edits merged)
Please save this source code
User prompt
No, entre el nivel 1 al 2 son 50 click, entre el 2 y el 3 100 click, entre el 4 y el 5 150 clicks, entre el 5 y el 6 200 clicks. Y quita el nivel 6, que el nivel 5 sea el maximo, tambien edita los mensajes y los puntitos que indican el nivel.
User prompt
Si, pero cada nivel es de 50 clicks, necesito que del nivel 1 al 2 sean 50, se borre la cuenta de clicks presionados y ahora sean 100 click para pasar la torreta al nivel 2.
User prompt
Cuando presionas una torreta ahora mismo sube de nivel inmedaitamente, necesito que pase del primer al segundo nivel con 50 clicks, del segundo al tercero con 100, del tercero al cuarto con 150 y asi hasta el nivel maximo.
User prompt
Please fix the bug: 'Uncaught TypeError: self.applyGradualUpgrades is not a function' in or related to this line: 'self.applyGradualUpgrades();' Line Number: 919
User prompt
Please fix the bug: 'Uncaught TypeError: self.applyGradualUpgrades is not a function' in or related to this line: 'self.applyGradualUpgrades();' Line Number: 919
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); bulletGraphics.scaleX = 0.5; bulletGraphics.scaleY = 0.5; // --- NUEVO: Calcular radio de colisión basado en tamaño visual --- var bulletRadius = bulletGraphics.width * bulletGraphics.scaleX / 2; // Get actual enemy visual size (enemies are scaled to 50%) var enemyGraphics = self.targetEnemy.children[0]; // First child should be the enemy graphics var enemyRadius = enemyGraphics ? enemyGraphics.width * enemyGraphics.scaleX / 2 : 35; // Dirección fija al momento de disparar if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); self.vx = dx / distance * self.speed; self.vy = dy / distance * self.speed; } else { self.vx = 0; self.vy = 0; } self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } // Convert enemy position from its scaled layer to game coordinates var enemyGameX = self.targetEnemy.x * gameScale + enemyLayer.x; var enemyGameY = self.targetEnemy.y * gameScale + enemyLayer.y; var dx = enemyGameX - self.x; var dy = enemyGameY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // --- MODIFICADO: comparación de colisión considerando radios --- if (distance < bulletRadius + enemyRadius) { self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } self.destroy(); } else { // Actualizar velocidad para rastrear objetivo self.vx = dx / distance * self.speed; self.vy = dy / distance * self.speed; 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); }; }); // 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; switch (self.type) { case 'fast': self.speed *= 2; self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; break; case 'normal': default: break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; self.maxHealth *= 20; self.speed = self.speed * 0.7; } self.health = self.maxHealth; var assetId = 'enemy'; if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); enemyGraphics.scaleX = 0.5; enemyGraphics.scaleY = 0.5; if (self.isBoss) { enemyGraphics.scaleX = 0.9; enemyGraphics.scaleY = 0.9; } // --- HITBOX --- // Calcula tamaño real visible con el escalado var realWidth = enemyGraphics.width * enemyGraphics.scaleX; var realHeight = enemyGraphics.height * enemyGraphics.scaleY; // Define la hitbox centrada con tamaño escalado 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(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); shadowGraphics.tint = 0x000000; shadowGraphics.alpha = 0.4; shadowGraphics.scaleX = 0.5; shadowGraphics.scaleY = 0.5; if (self.isBoss) { shadowGraphics.scaleX = 0.9; shadowGraphics.scaleY = 0.9; } self.shadow.x = 20; self.shadow.y = 20; shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height * 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 () { // Update hitbox to use the actual scaled visual position // The enemy visual is scaled by 50%, so adjust hitbox accordingly var visualWidth = enemyGraphics.width * enemyGraphics.scaleX; var visualHeight = enemyGraphics.height * 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) { 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) { enemyGraphics.tint = 0xFFFFFF; } } } } if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height * 0.5 / 2 - 5; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.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 var obstacle = new Container(); var obstacleGraphics = obstacle.attachAsset('obstacle', { 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, 10, 15, 27, 136, 137, 138, 139, 140]; if (pathCells.indexOf(cellNumber) !== -1) { cell.type = 0; // Force as path } // 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 var wall = new Container(); var wallGraphics = wall.attachAsset('wall', { 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.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed; // 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); } 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; } // Handle normal pathfinding enemies if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { if (cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x / 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 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var Tower = Container.expand(function () { var self = Container.call(this); self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; // Progress tracking for gradual upgrades self.upgradeProgress = 0; // Current progress towards next level self.upgradeRequiredClicks = [0, 50, 100, 150, 200, 250]; // Clicks needed for each level (index 0 unused) // Standardized method to get the current range of the tower with gradual progression self.getRange = function () { var baseRange = (2 + (self.level - 1) * 0.5) * CELL_SIZE; // If not at max level, add gradual range increase based on progress if (self.level < self.maxLevel) { var nextLevelRange = (2 + self.level * 0.5) * CELL_SIZE; var rangeIncrease = (nextLevelRange - baseRange) * (self.upgradeProgress / self.upgradeRequiredClicks[self.level]); return baseRange + rangeIncrease; } return baseRange; }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.x = -90; baseGraphics.y = 150; baseGraphics.tint = 0xAAAAAA; // Reduce tower base size by 50% baseGraphics.scaleX = 0.5; baseGraphics.scaleY = 0.5; var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 12; // Reduce dot size by 50% for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -130 + dotSpacing * (i + 1); dot.y = 125 + CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); gunContainer.x = -90; gunContainer.y = 150; var gunGraphics = gunContainer.attachAsset('defense', { anchorX: 0.5, anchorY: 0.5 }); // Reduce gun size by 50% gunGraphics.scaleX = 0.5; gunGraphics.scaleY = 0.5; self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { towerLevelIndicator.tint = 0xAAAAAA; } } }; self.updateLevelIndicators(); // Initialize gradual upgrade properties self.applyGradualUpgrades(); 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 () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)) 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, but only if red button is not pressed if (LK.ticks - self.lastFired >= self.fireRate && !redButton.isPressed) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { // Hide range from previously selected tower if (rangeDisplayTower && rangeDisplayTower !== self) { rangeDisplayTower.hideRangeIndicator(); } // Show range for this tower self.showRange(); rangeDisplayTower = self; // Track pressed tower and start timer for sell functionality pressedTower = self; pressStartTime = LK.ticks; // Clear any existing timer if (pressTimer) { LK.clearTimeout(pressTimer); } // Set timer for 5 seconds (300 ticks at 60fps) pressTimer = LK.setTimeout(function () { if (pressedTower === self) { // Sell the tower var sellValue = self.getTotalValue(); setGold(gold + sellValue); // Show sell notification var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Remove tower from grid tracking if (grid.towerCells[self.gridX]) { grid.towerCells[self.gridX][self.gridY] = false; } // Hide range indicator self.hideRangeIndicator(); if (rangeDisplayTower === self) { rangeDisplayTower = null; } // Remove tower from arrays and destroy var towerIndex = towers.indexOf(self); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } self.destroy(); pressedTower = null; } }, 5000); // Gradual upgrade on click - each click costs 1 gold if (gold >= 1 && self.level < self.maxLevel) { setGold(gold - 1); self.upgradeProgress++; // Check if we've reached the required clicks for next level if (self.upgradeProgress >= self.upgradeRequiredClicks[self.level]) { self.level++; self.upgradeProgress = 0; // Reset progress for next level // Animate level indicator when reaching a new level 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 }); } }); } } // Apply gradual upgrades based on current level and progress self.applyGradualUpgrades(); self.refreshCellsInRange(); self.updateLevelIndicators(); // Update range display after upgrade self.showRange(); } else if (gold < 1) { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (self.level >= self.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; self.up = function (x, y, obj) { // Clear the press timer if this tower was being pressed if (pressedTower === self) { pressedTower = null; if (pressTimer) { LK.clearTimeout(pressTimer); pressTimer = null; } } }; self.showRange = function () { // Remove existing range indicator first self.hideRangeIndicator(); // Create range indicator var rangeIndicator = new Container(); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; rangeGraphics.tint = 0xffffff; // White color for range (original color) rangeIndicator.isTowerRange = true; // Position at tower center with offset to align with visual elements rangeIndicator.x = -88; // Offset left to center on visual tower rangeIndicator.y = 155; // Offset down to center on visual tower self.addChild(rangeIndicator); }; self.hideRangeIndicator = function () { for (var i = self.children.length - 1; i >= 0; i--) { if (self.children[i].isTowerRange) { self.removeChild(self.children[i]); } } }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { // Calculate bullet spawn position relative to gun container position // gunContainer is positioned at (-95, 150) relative to tower center // Apply the same scaling as the tower layer // Define el punto de la punta del cañón localmente en gunContainer (por ejemplo, en el eje X positivo) // Obtener la punta del cañón teniendo en cuenta el anchor // Calcula la posición global de la punta del cañón manualmente var offsetX = 30; // mueve hacia la punta (horizontal) var offsetY = -5; // sube o baja (vertical) // Aplica rotación del cañón a ese offset var cos = Math.cos(gunContainer.rotation); var sin = Math.sin(gunContainer.rotation); var rotatedX = offsetX * cos - offsetY * sin; var rotatedY = offsetX * sin + offsetY * cos; // Posición global final considerando jerarquía y escala de juego var bulletX = (self.x + gunContainer.x + rotatedX) * gameScale; var bulletY = (self.y + gunContainer.y + rotatedY) * gameScale; // Crear la bala en esa posición var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // --- Fire recoil effect for gunContainer (rotation only) --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { scaleX: true, scaleY: true }); // Store original scale values if not already stored if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset scale to resting values before animating (in case of interrupted tweens) gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Animate scale recoil effect (gun briefly shrinks then returns to normal) tween(gunContainer, { scaleX: gunContainer._restScaleX * 0.8, scaleY: gunContainer._restScaleY * 0.8 }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original scale tween(gunContainer, { scaleX: gunContainer._restScaleX, scaleY: gunContainer._restScaleY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.applyGradualUpgrades = function () { // Calculate gradual improvements based on level and progress var currentLevelBase = { fireRate: self.level === self.maxLevel ? Math.max(5, 60 - self.level * 24) : Math.max(20, 60 - self.level * 8), damage: self.level === self.maxLevel ? 10 + self.level * 20 : 10 + self.level * 5, bulletSpeed: self.level === self.maxLevel ? 5 + self.level * 2.4 : 5 + self.level * 0.5 }; // If not at max level, calculate gradual improvements if (self.level < self.maxLevel) { var nextLevelBase = { fireRate: self.level + 1 === self.maxLevel ? Math.max(5, 60 - (self.level + 1) * 24) : Math.max(20, 60 - (self.level + 1) * 8), damage: self.level + 1 === self.maxLevel ? 10 + (self.level + 1) * 20 : 10 + (self.level + 1) * 5, bulletSpeed: self.level + 1 === self.maxLevel ? 5 + (self.level + 1) * 2.4 : 5 + (self.level + 1) * 0.5 }; var progressRatio = self.upgradeProgress / self.upgradeRequiredClicks[self.level]; // Apply gradual improvements self.fireRate = Math.round(currentLevelBase.fireRate + (nextLevelBase.fireRate - currentLevelBase.fireRate) * progressRatio); self.damage = currentLevelBase.damage + (nextLevelBase.damage - currentLevelBase.damage) * progressRatio; self.bulletSpeed = currentLevelBase.bulletSpeed + (nextLevelBase.bulletSpeed - currentLevelBase.bulletSpeed) * progressRatio; } else { // At max level, use final values self.fireRate = currentLevelBase.fireRate; self.damage = currentLevelBase.damage; self.bulletSpeed = currentLevelBase.bulletSpeed; } }; 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 >= 5; // Base 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 >= 5; // Base 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 = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0xFFFF00; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Boss Normal"; break; case 'fast': block.tint = 0x00AAFF; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xAA0000; waveType = "Boss Immune"; break; case 'flying': block.tint = 0xFFFF00; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the color of the wave type switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; break; case 'fast': block.tint = 0x00AAFF; break; case 'immune': block.tint = 0xAA0000; break; case 'flying': block.tint = 0xFFFF00; break; default: block.tint = 0xFF0000; break; } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a crown or some indicator to the wave marker for boss waves var bossIndicator = marker.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); bossIndicator.width = 30; bossIndicator.height = 30; bossIndicator.tint = 0xFFD700; // Gold color bossIndicator.y = -block.height / 2 - 15; // Change the wave type text to indicate boss waveType = "BOSS"; } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2(waveType, { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (every 10th wave) if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; 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 = 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.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ var CELL_SIZE = 76; 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 = 10000; // Oro; var lives = 3; 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 goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); } function setGold(value) { gold = value; updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); 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); 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; 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 5; // Base tower cost } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } 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); 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) { // 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; if (towerPreview.canPlace) { placeTower(towerPreview.gridX, towerPreview.gridY); } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; } }; var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2 + 400; waveIndicator.y = 2732 - 80; game.addChild(waveIndicator); // Start the game immediately waveIndicator.gameStarted = true; 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.tint = 0xAAAAAA; buttonTowerGraphics.scaleX = 1.3; buttonTowerGraphics.scaleY = 1.3; buttonTower.x = 300; buttonTower.y = 2732 - 150; buttonTower.width = buttonTowerGraphics.width * 1.3; buttonTower.height = buttonTowerGraphics.height * 1.3; 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 }); redButton.x = 150; redButton.y = 2732 - 150; redButton.isPressed = false; game.addChild(redButton); // 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 }); // Give 1 gold setGold(gold + 1); // Show gold increment indicator var goldIndicator = game.addChild(new GoldIndicator(1, redButton.x, redButton.y)); // 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 }); } }; game.update = function () { 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 (var i = 0; i < enemyCount; 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 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - 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; } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Enemies no longer give gold when killed // Gold indicator and gold earning removed // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { LK.showGameOver(); } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.scaleX = 0.5;
bulletGraphics.scaleY = 0.5;
// --- NUEVO: Calcular radio de colisión basado en tamaño visual ---
var bulletRadius = bulletGraphics.width * bulletGraphics.scaleX / 2;
// Get actual enemy visual size (enemies are scaled to 50%)
var enemyGraphics = self.targetEnemy.children[0]; // First child should be the enemy graphics
var enemyRadius = enemyGraphics ? enemyGraphics.width * enemyGraphics.scaleX / 2 : 35;
// Dirección fija al momento de disparar
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
self.vx = dx / distance * self.speed;
self.vy = dy / distance * self.speed;
} else {
self.vx = 0;
self.vy = 0;
}
self.update = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
// Convert enemy position from its scaled layer to game coordinates
var enemyGameX = self.targetEnemy.x * gameScale + enemyLayer.x;
var enemyGameY = self.targetEnemy.y * gameScale + enemyLayer.y;
var dx = enemyGameX - self.x;
var dy = enemyGameY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// --- MODIFICADO: comparación de colisión considerando radios ---
if (distance < bulletRadius + enemyRadius) {
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
self.destroy();
} else {
// Actualizar velocidad para rastrear objetivo
self.vx = dx / distance * self.speed;
self.vy = dy / distance * self.speed;
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);
};
});
// 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;
switch (self.type) {
case 'fast':
self.speed *= 2;
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50;
break;
case 'normal':
default:
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
self.maxHealth *= 20;
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
var assetId = 'enemy';
if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
enemyGraphics.scaleX = 0.5;
enemyGraphics.scaleY = 0.5;
if (self.isBoss) {
enemyGraphics.scaleX = 0.9;
enemyGraphics.scaleY = 0.9;
}
// --- HITBOX ---
// Calcula tamaño real visible con el escalado
var realWidth = enemyGraphics.width * enemyGraphics.scaleX;
var realHeight = enemyGraphics.height * enemyGraphics.scaleY;
// Define la hitbox centrada con tamaño escalado
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(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
shadowGraphics.tint = 0x000000;
shadowGraphics.alpha = 0.4;
shadowGraphics.scaleX = 0.5;
shadowGraphics.scaleY = 0.5;
if (self.isBoss) {
shadowGraphics.scaleX = 0.9;
shadowGraphics.scaleY = 0.9;
}
self.shadow.x = 20;
self.shadow.y = 20;
shadowGraphics.rotation = enemyGraphics.rotation;
}
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height * 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 () {
// Update hitbox to use the actual scaled visual position
// The enemy visual is scaled by 50%, so adjust hitbox accordingly
var visualWidth = enemyGraphics.width * enemyGraphics.scaleX;
var visualHeight = enemyGraphics.height * 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) {
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) {
enemyGraphics.tint = 0xFFFFFF;
}
}
}
}
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
tween.stop(enemyGraphics, {
rotation: true
});
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height * 0.5 / 2 - 5;
};
return self;
});
var GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2("+" + value, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
self.cells = [];
self.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
var obstacle = new Container();
var obstacleGraphics = obstacle.attachAsset('obstacle', {
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, 10, 15, 27, 136, 137, 138, 139, 140];
if (pathCells.indexOf(cellNumber) !== -1) {
cell.type = 0; // Force as path
}
// 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
var wall = new Container();
var wallGraphics = wall.attachAsset('wall', {
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.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// 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);
}
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;
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget) {
enemy.currentTarget = cell.targets[0];
}
if (enemy.currentTarget) {
if (cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
enemy.x = grid.x / 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
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var Tower = Container.expand(function () {
var self = Container.call(this);
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
// Progress tracking for gradual upgrades
self.upgradeProgress = 0; // Current progress towards next level
self.upgradeRequiredClicks = [0, 50, 100, 150, 200, 250]; // Clicks needed for each level (index 0 unused)
// Standardized method to get the current range of the tower with gradual progression
self.getRange = function () {
var baseRange = (2 + (self.level - 1) * 0.5) * CELL_SIZE;
// If not at max level, add gradual range increase based on progress
if (self.level < self.maxLevel) {
var nextLevelRange = (2 + self.level * 0.5) * CELL_SIZE;
var rangeIncrease = (nextLevelRange - baseRange) * (self.upgradeProgress / self.upgradeRequiredClicks[self.level]);
return baseRange + rangeIncrease;
}
return baseRange;
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
baseGraphics.x = -90;
baseGraphics.y = 150;
baseGraphics.tint = 0xAAAAAA;
// Reduce tower base size by 50%
baseGraphics.scaleX = 0.5;
baseGraphics.scaleY = 0.5;
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 12; // Reduce dot size by 50%
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -130 + dotSpacing * (i + 1);
dot.y = 125 + CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
gunContainer.x = -90;
gunContainer.y = 150;
var gunGraphics = gunContainer.attachAsset('defense', {
anchorX: 0.5,
anchorY: 0.5
});
// Reduce gun size by 50%
gunGraphics.scaleX = 0.5;
gunGraphics.scaleY = 0.5;
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
towerLevelIndicator.tint = 0xAAAAAA;
}
}
};
self.updateLevelIndicators();
// Initialize gradual upgrade properties
self.applyGradualUpgrades();
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 () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1))
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, but only if red button is not pressed
if (LK.ticks - self.lastFired >= self.fireRate && !redButton.isPressed) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
// Hide range from previously selected tower
if (rangeDisplayTower && rangeDisplayTower !== self) {
rangeDisplayTower.hideRangeIndicator();
}
// Show range for this tower
self.showRange();
rangeDisplayTower = self;
// Track pressed tower and start timer for sell functionality
pressedTower = self;
pressStartTime = LK.ticks;
// Clear any existing timer
if (pressTimer) {
LK.clearTimeout(pressTimer);
}
// Set timer for 5 seconds (300 ticks at 60fps)
pressTimer = LK.setTimeout(function () {
if (pressedTower === self) {
// Sell the tower
var sellValue = self.getTotalValue();
setGold(gold + sellValue);
// Show sell notification
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Remove tower from grid tracking
if (grid.towerCells[self.gridX]) {
grid.towerCells[self.gridX][self.gridY] = false;
}
// Hide range indicator
self.hideRangeIndicator();
if (rangeDisplayTower === self) {
rangeDisplayTower = null;
}
// Remove tower from arrays and destroy
var towerIndex = towers.indexOf(self);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
self.destroy();
pressedTower = null;
}
}, 5000);
// Gradual upgrade on click - each click costs 1 gold
if (gold >= 1 && self.level < self.maxLevel) {
setGold(gold - 1);
self.upgradeProgress++;
// Check if we've reached the required clicks for next level
if (self.upgradeProgress >= self.upgradeRequiredClicks[self.level]) {
self.level++;
self.upgradeProgress = 0; // Reset progress for next level
// Animate level indicator when reaching a new level
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
});
}
});
}
}
// Apply gradual upgrades based on current level and progress
self.applyGradualUpgrades();
self.refreshCellsInRange();
self.updateLevelIndicators();
// Update range display after upgrade
self.showRange();
} else if (gold < 1) {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (self.level >= self.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
self.up = function (x, y, obj) {
// Clear the press timer if this tower was being pressed
if (pressedTower === self) {
pressedTower = null;
if (pressTimer) {
LK.clearTimeout(pressTimer);
pressTimer = null;
}
}
};
self.showRange = function () {
// Remove existing range indicator first
self.hideRangeIndicator();
// Create range indicator
var rangeIndicator = new Container();
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
rangeGraphics.tint = 0xffffff; // White color for range (original color)
rangeIndicator.isTowerRange = true;
// Position at tower center with offset to align with visual elements
rangeIndicator.x = -88; // Offset left to center on visual tower
rangeIndicator.y = 155; // Offset down to center on visual tower
self.addChild(rangeIndicator);
};
self.hideRangeIndicator = function () {
for (var i = self.children.length - 1; i >= 0; i--) {
if (self.children[i].isTowerRange) {
self.removeChild(self.children[i]);
}
}
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
// Calculate bullet spawn position relative to gun container position
// gunContainer is positioned at (-95, 150) relative to tower center
// Apply the same scaling as the tower layer
// Define el punto de la punta del cañón localmente en gunContainer (por ejemplo, en el eje X positivo)
// Obtener la punta del cañón teniendo en cuenta el anchor
// Calcula la posición global de la punta del cañón manualmente
var offsetX = 30; // mueve hacia la punta (horizontal)
var offsetY = -5; // sube o baja (vertical)
// Aplica rotación del cañón a ese offset
var cos = Math.cos(gunContainer.rotation);
var sin = Math.sin(gunContainer.rotation);
var rotatedX = offsetX * cos - offsetY * sin;
var rotatedY = offsetX * sin + offsetY * cos;
// Posición global final considerando jerarquía y escala de juego
var bulletX = (self.x + gunContainer.x + rotatedX) * gameScale;
var bulletY = (self.y + gunContainer.y + rotatedY) * gameScale;
// Crear la bala en esa posición
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// --- Fire recoil effect for gunContainer (rotation only) ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
scaleX: true,
scaleY: true
});
// Store original scale values if not already stored
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset scale to resting values before animating (in case of interrupted tweens)
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Animate scale recoil effect (gun briefly shrinks then returns to normal)
tween(gunContainer, {
scaleX: gunContainer._restScaleX * 0.8,
scaleY: gunContainer._restScaleY * 0.8
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original scale
tween(gunContainer, {
scaleX: gunContainer._restScaleX,
scaleY: gunContainer._restScaleY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.applyGradualUpgrades = function () {
// Calculate gradual improvements based on level and progress
var currentLevelBase = {
fireRate: self.level === self.maxLevel ? Math.max(5, 60 - self.level * 24) : Math.max(20, 60 - self.level * 8),
damage: self.level === self.maxLevel ? 10 + self.level * 20 : 10 + self.level * 5,
bulletSpeed: self.level === self.maxLevel ? 5 + self.level * 2.4 : 5 + self.level * 0.5
};
// If not at max level, calculate gradual improvements
if (self.level < self.maxLevel) {
var nextLevelBase = {
fireRate: self.level + 1 === self.maxLevel ? Math.max(5, 60 - (self.level + 1) * 24) : Math.max(20, 60 - (self.level + 1) * 8),
damage: self.level + 1 === self.maxLevel ? 10 + (self.level + 1) * 20 : 10 + (self.level + 1) * 5,
bulletSpeed: self.level + 1 === self.maxLevel ? 5 + (self.level + 1) * 2.4 : 5 + (self.level + 1) * 0.5
};
var progressRatio = self.upgradeProgress / self.upgradeRequiredClicks[self.level];
// Apply gradual improvements
self.fireRate = Math.round(currentLevelBase.fireRate + (nextLevelBase.fireRate - currentLevelBase.fireRate) * progressRatio);
self.damage = currentLevelBase.damage + (nextLevelBase.damage - currentLevelBase.damage) * progressRatio;
self.bulletSpeed = currentLevelBase.bulletSpeed + (nextLevelBase.bulletSpeed - currentLevelBase.bulletSpeed) * progressRatio;
} else {
// At max level, use final values
self.fireRate = currentLevelBase.fireRate;
self.damage = currentLevelBase.damage;
self.bulletSpeed = currentLevelBase.bulletSpeed;
}
};
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 >= 5; // Base 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 >= 5; // Base 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 = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['normal', 'fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Flying";
block.tint = 0xFFFF00;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
waveType = "Boss Normal";
break;
case 'fast':
block.tint = 0x00AAFF;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0xAA0000;
waveType = "Boss Immune";
break;
case 'flying':
block.tint = 0xFFFF00;
waveType = "Boss Flying";
break;
}
}
enemyCount = 1;
// Make the wave indicator for boss waves stand out
// Set boss wave color to the color of the wave type
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
break;
case 'fast':
block.tint = 0x00AAFF;
break;
case 'immune':
block.tint = 0xAA0000;
break;
case 'flying':
block.tint = 0xFFFF00;
break;
default:
block.tint = 0xFF0000;
break;
}
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave && enemyType !== 'swarm') {
// Add a crown or some indicator to the wave marker for boss waves
var bossIndicator = marker.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
bossIndicator.width = 30;
bossIndicator.height = 30;
bossIndicator.tint = 0xFFD700; // Gold color
bossIndicator.y = -block.height / 2 - 15;
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 4;
waveTypeShadow.y = 4;
marker.addChild(waveTypeShadow);
// Add wave type text - 30% smaller than before
var waveTypeText = new Text2(waveType, {
size: 56,
fill: 0xFFFFFF,
weight: 800
});
waveTypeText.anchor.set(0.5, 0.5);
waveTypeText.y = 0;
marker.addChild(waveTypeText);
// Add shadow for wave number - 20% larger than before
var waveNumShadow = new Text2((i + 1).toString(), {
size: 48,
fill: 0x000000,
weight: 800
});
waveNumShadow.anchor.set(1.0, 1.0);
waveNumShadow.x = blockWidth / 2 - 16 + 5;
waveNumShadow.y = block.height / 2 - 12 + 5;
marker.addChild(waveNumShadow);
// Main wave number text - 20% larger than before
var waveNum = new Text2((i + 1).toString(), {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
waveNum.anchor.set(1.0, 1.0);
waveNum.x = blockWidth / 2 - 16;
waveNum.y = block.height / 2 - 12;
marker.addChild(waveNum);
marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
self.addChild(marker);
self.waveMarkers.push(marker);
}
// Get wave type for a specific wave number
self.getWaveType = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return "normal";
}
// If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType
// then we should return a different boss type
var waveType = self.waveTypes[waveNumber - 1];
return waveType;
};
// Get enemy count for a specific wave number
self.getEnemyCount = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return 10;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Add boss prefix for boss waves (every 10th wave)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
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 = 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.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
var CELL_SIZE = 76;
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 = 10000; // Oro;
var lives = 3;
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 goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(livesText);
LK.gui.top.addChild(scoreText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
}
function setGold(value) {
gold = value;
updateUI();
}
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
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);
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;
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 5; // Base tower cost
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
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);
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) {
// 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;
if (towerPreview.canPlace) {
placeTower(towerPreview.gridX, towerPreview.gridY);
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2 + 400;
waveIndicator.y = 2732 - 80;
game.addChild(waveIndicator);
// Start the game immediately
waveIndicator.gameStarted = true;
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.tint = 0xAAAAAA;
buttonTowerGraphics.scaleX = 1.3;
buttonTowerGraphics.scaleY = 1.3;
buttonTower.x = 300;
buttonTower.y = 2732 - 150;
buttonTower.width = buttonTowerGraphics.width * 1.3;
buttonTower.height = buttonTowerGraphics.height * 1.3;
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
});
redButton.x = 150;
redButton.y = 2732 - 150;
redButton.isPressed = false;
game.addChild(redButton);
// 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
});
// Give 1 gold
setGold(gold + 1);
// Show gold increment indicator
var goldIndicator = game.addChild(new GoldIndicator(1, redButton.x, redButton.y));
// 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
});
}
};
game.update = function () {
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 (var i = 0; i < enemyCount; 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
// Find a column that isn't occupied by another enemy that's not yet in view
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
// Check if any enemy is already in this column but not yet in view
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
// If all columns are occupied, use original random method
var spawnX;
if (availableColumns.length > 0) {
// Choose a random unoccupied column
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
// Fallback to random if all columns are occupied
spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
}
var spawnY = -1 - 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;
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Enemies no longer give gold when killed
// Gold indicator and gold earning removed
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
lives = Math.max(0, lives - 1);
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
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