Code edit (12 edits merged)
Please save this source code
User prompt
El rango de la torre ya estaba centrado antes de que hiciera el cambio y ahora por alguna razón tambien cambio a color verde. Solo modifica lo del comportamiento de aprecer pero manten la posicón y el color iguales.
User prompt
Cuando doy click sobre una torre no muestra su rango, necesito que muestre el rango hasta que de click en otro lado.
User prompt
Elimina completamente el menu de Upgrade, ahora las torretas incrementaran su nivel cuando se haga click sobre ellas, cada click es como 1 oro invertido en la torreta.
User prompt
No elimines los indicadores de Wave cuando hayan salido del recuadro amarillo si no han salido completamente
User prompt
Recorre el indicador de Wave y el Indicador amarillo más a la derecha en X positivo.
User prompt
Que el tiempo de cada Wave sea 50% menos
User prompt
Cuando el indicador de Wave salga por el lado izquierdo del recuadro indicador amarillo ya no sea visible.
User prompt
Reduce el sacudido 80% ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Cuando se presione el boton rojo se sacuda un poco el juego. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (1 edits merged)
Please save this source code
User prompt
Recorre la barra de vida de los enemigos a la derecha.
User prompt
Incrementa el tiempo entre spawn de enemigos en un mismo wave
User prompt
Incrementa el espacio entre los enemigos de un mismo Wave
User prompt
Quita el boton de "Next wave"
User prompt
No, recorre el wave marker uno atras, porque esta empezando en Fast y deberia iniciar en Normal.
User prompt
Quita tambien el cuadro verde que estaba con el "Start Game"
User prompt
Quita el "Start Game" del visualizador de niveles
User prompt
Inicia el juego inmediatamente cuando inicia esta escena.
User prompt
Muestra el incremento de Oro cuadno se presiona el boton rojo ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Los enemigos no deben dar oro cuando los matas.
User prompt
Las torretas todavia disparan cuando se presiona el boton rojo, no deberian.
User prompt
Que las torretas no disparen cuando presionas el botón.
User prompt
Cuando doy click en la X no se desaparece el rango de la torreta que seleccione.
User prompt
Cuando doy click en una torre y suelto el click el menu de upgrade desaparece, me gustaria desapareciera solo cuando doy click en el boton de X y no solo cuando suelto el click
/**** * 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; // Standardized method to get the current range of the tower self.getRange = function () { // Default: base 3, +0.5 per level return (2 + (self.level - 1) * 0.5) * CELL_SIZE; }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.x = -90; baseGraphics.y = 150; baseGraphics.tint = 0xAAAAAA; // Reduce tower base size by 50% baseGraphics.scaleX = 0.5; baseGraphics.scaleY = 0.5; var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 12; // Reduce dot size by 50% for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -130 + dotSpacing * (i + 1); dot.y = 125 + CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); gunContainer.x = -90; gunContainer.y = 150; var gunGraphics = gunContainer.attachAsset('defense', { anchorX: 0.5, anchorY: 0.5 }); // Reduce gun size by 50% gunGraphics.scaleX = 0.5; gunGraphics.scaleY = 0.5; self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { towerLevelIndicator.tint = 0xAAAAAA; } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX - 20; 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; // Direct upgrade on click - each click costs 1 gold if (gold >= 1 && self.level < self.maxLevel) { setGold(gold - 1); self.level++; // Apply upgrades if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } self.refreshCellsInRange(); self.updateLevelIndicators(); // Update range display after upgrade self.showRange(); // Animate level indicator if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } } 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.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 rangeIndicator.x = 0; rangeIndicator.y = 0; self.addChild(rangeIndicator); }; self.hideRangeIndicator = function () { for (var i = self.children.length - 1; i >= 0; i--) { if (self.children[i].isTowerRange) { self.removeChild(self.children[i]); } } }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { // Calculate bullet spawn position relative to gun container position // gunContainer is positioned at (-95, 150) relative to tower center // Apply the same scaling as the tower layer // Define el punto de la punta del cañón localmente en gunContainer (por ejemplo, en el eje X positivo) // Obtener la punta del cañón teniendo en cuenta el anchor // Calcula la posición global de la punta del cañón manualmente var offsetX = 30; // mueve hacia la punta (horizontal) var offsetY = -5; // sube o baja (vertical) // Aplica rotación del cañón a ese offset var cos = Math.cos(gunContainer.rotation); var sin = Math.sin(gunContainer.rotation); var rotatedX = offsetX * cos - offsetY * sin; var rotatedY = offsetX * sin + offsetY * cos; // Posición global final considerando jerarquía y escala de juego var bulletX = (self.x + gunContainer.x + rotatedX) * gameScale; var bulletY = (self.y + gunContainer.y + rotatedY) * gameScale; // Crear la bala en esa posición var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // --- Fire recoil effect for gunContainer (rotation only) --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { scaleX: true, scaleY: true }); // Store original scale values if not already stored if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset scale to resting values before animating (in case of interrupted tweens) gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Animate scale recoil effect (gun briefly shrinks then returns to normal) tween(gunContainer, { scaleX: gunContainer._restScaleX * 0.8, scaleY: gunContainer._restScaleY * 0.8 }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original scale tween(gunContainer, { scaleX: gunContainer._restScaleX, scaleY: gunContainer._restScaleY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; // ✅ No usar gameScale aquí self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { if (grid.towerCells[gridX] && grid.towerCells[gridX][gridY] !== undefined) { grid.towerCells[gridX][gridY] = true; } } } self.refreshCellsInRange(); }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; self.hasEnoughGold = true; var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= 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 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; } } 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(); } };
===================================================================
--- original.js
+++ change.js
@@ -895,9 +895,9 @@
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
- var centerX = self.gridX + 1;
+ var centerX = self.gridX - 20;
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);
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