User prompt
pon este texto a la derecha del asset enemy del tutorial con letra blanca Squire The most basic enemy doesn't have much health but usually comes in groups.
User prompt
pon este texto a la derecha del asset enemy del tutorial con letra blanca. Squire The most basic enemy doesn't have much health but usually comes in groups.
User prompt
haz el 'swarm' del tutorial más pequeño ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
en la zona del tutorial, pon enemy del mismo tamaño y posición que default enemy_inmune del mismo tamaño y posición que rapid enemy_fast del mismo tamaño y posición que sniper enemy_sworm del mismo tamaño y posición que splash enemy_flying del mismo tamaño y posición que slow un texto que ponga 'ENEMIES' en blanco en negrita encima del texto 'TROOPS' la diferencia será que cuando le das a que 'si' estos asets no apareceran, apareceran cuando le des click al asset 'next', y se volverán a ocultar cuando le des click al 'asset' back ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
en la zona del tutorial, pon enemy del mismo tamaño y posición que default enemy_inmune del mismo tamaño y posición que rapid enemy_fast del mismo tamaño y posición que sniper enemy_sworm del mismo tamaño y posición que splash enemy_flying del mismo tamaño y posición que slow un texto que ponga 'ENEMIES' en blanco en negrita encima del texto 'TROOPS' la diferencia será que cuando le das a que 'si' estos asets no apareceran, apareceran cuando le des click al asset 'next', y se volverán a ocultar cuando le des click al 'asset' back ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
haz que cuando toques next los enemigos no se oculten
User prompt
cuando toques el asset 'next' dejarán de estar ocultos los assets enemy enemy_inmune enemy_fast enemy_sworm enemy_flying cuando toques back se volverán a ocultar ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
cuando toques el asset 'next' dejarán de estar ocultos los assets enemy enemy_inmune enemy_fast enemy_sworm enemy_flying cuando toques back se volverán a ocultar ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
cuando toques 'next' dejarán de estar ocultos los assets enemy enemy_inmune enemy_fast enemy_sworm enemy_flying ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
haz que solo sean visibles en el tutorial después de tocar next, al tocar back se esconderán otra vez enemy enemy_inmune enemy_fast enemy_sworm enemy_flying el texto 'enemies' ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Pon el texto 'ENEMIES' de color blanco en negrita, del mismo tamaño y en la misma posición que el texto 'TOOPS' en la parte del tutorial
User prompt
haz lo mismo con enemy ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
estira un poco verticalmente swarm en la zona del tutorial ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
estira un poco swarm en la zona del tutorial ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
haz que swarm en el tutorial tenga la mitad de su tamaño
User prompt
en la zona del tutorial, pon enemy del mismo tamaño y posición que default enemy_inmune del mismo tamaño y posición que rapid enemy_fast del mismo tamaño y posición que sniper enemy_sworm del mismo tamaño y posición que splash enemy_flying del mismo tamaño y posición que slow
User prompt
haz los assets algo más grandes y muevelos un poco a la derecha ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
pon los assets enemy, enemy_inmune, enemy_fast, enemy_swarm, enemy_flying en ese ordenm uno debajo de otro centrado a la izquierda
User prompt
pon el asset 'finish' en la misma posición que 'next'
User prompt
pon back a la misma altura que 'next'
User prompt
haz que todo esto vuelva a mostrarse junto al asset 'next' cuando pulses el asset 'back' ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
tambien el texto 'troops' ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
haz que cuando toques el botón next se oculte el asset de tutorial de default, rapid, splash, sniper, slow, poison, junto a sus textos de su derecha ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
subele unos pixeles el texto 'inferno tower'
User prompt
sube unos pixeles el texto 'inferno tower'
/**** * 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 assetName = 'bullet'; // default bullet asset if (self.type === 'splash') { assetName = 'bola'; } var bulletGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distanceSquared = dx * dx + dy * dy; // Use squared distance var speedSquared = self.speed * self.speed; // Cache squared speed if (distanceSquared < speedSquared) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 3; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Apply percentage-based damage var damagePercentage = 0.1; // Base 10% damage if (self.sourceTowerLevel !== undefined) { // Scale damage percentage with tower level: 10% at level 1, up to 20% at level 6 var damagePercentages = [0.1, 0.12, 0.14, 0.16, 0.18, 0.2]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); damagePercentage = damagePercentages[idx]; } // Apply percentage damage based on enemy's max health var percentageDamage = Math.floor(self.targetEnemy.maxHealth * damagePercentage); self.targetEnemy.health -= percentageDamage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply slow effect // Make slow percentage scale with tower level (default 50%, up to 80% at max level) var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6 var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; // Slow by X% self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { self.targetEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'poison') { // Create visual poison effect at impact location var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Poison area damage to nearby enemies (including the target) var poisonRadius = CELL_SIZE * 1.4; // 2x2 cells area (sqrt(2) * CELL_SIZE for diagonal coverage) // Collect enemies in range and sort by distance var enemiesInRange = []; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; var poisonDx = otherEnemy.x - self.targetEnemy.x; var poisonDy = otherEnemy.y - self.targetEnemy.y; var poisonDistance = Math.sqrt(poisonDx * poisonDx + poisonDy * poisonDy); if (poisonDistance <= poisonRadius && !otherEnemy.isImmune) { enemiesInRange.push({ enemy: otherEnemy, distance: poisonDistance }); } } // Sort by distance (closest first) and limit based on tower level enemiesInRange.sort(function (a, b) { return a.distance - b.distance; }); // Base limit is 5, +1 per level (sourceTowerLevel from tower) var maxEnemies = 5; if (self.sourceTowerLevel !== undefined) { maxEnemies = 5 + (self.sourceTowerLevel - 1); } var enemiesToAffect = Math.min(maxEnemies, enemiesInRange.length); // Apply poison effect to the closest enemies within limit for (var i = 0; i < enemiesToAffect; i++) { var otherEnemy = enemiesInRange[i].enemy; // Apply poison effect otherEnemy.poisoned = true; otherEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick otherEnemy.poisonDuration = 300; // 5 seconds at 60 FPS // Apply permanent slow effect from poison if (!otherEnemy.poisonSlowed) { if (!otherEnemy.originalSpeed) { otherEnemy.originalSpeed = otherEnemy.speed; } otherEnemy.speed *= 0.5; // Permanently slow by 50% otherEnemy.poisonSlowed = true; } } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); // List of cell numbers to exclude from having 'cell' asset (except those with '-b' suffix) var excludedCells = [1, 2, 41, 42, 63, 64, 85, 86, 107, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 165, 166, 187, 188, 209, 210, 231, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 51, 52, 73, 74, 95, 96, 117, 118, 139, 140, 161, 162, 183, 184, 205, 206, 227, 228, 249, 250, 45, 46, 47, 48, 49, 50, 67, 68, 69, 70, 71, 72, 89, 90, 111, 112, 133, 134, 155, 156, 177, 178, 199, 200, 221, 222, 243, 244, 309, 310, 331, 332, 353, 354, 375, 376, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 429, 430, 451, 452, 473, 474, 495, 496, 517, 518, 539, 540, 519, 520, 521, 522, 523, 524, 525, 526, 541, 542, 543, 544, 545, 546, 547, 548, 569, 570, 591, 592, 605, 606]; var cellGraphics = null; // We'll determine whether to show the cell asset later in the render method based on the cell's sequential number and suffix var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0x000000, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); 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) { // Check if we need to show cell asset for this cell var shouldShowCell = true; if (data.sequentialNumber) { var hasBSuffix = false; // Check if this cell would have '-b' suffix if (data.type === 0 || data.type === 2) { // path cells var isBorderCell = data.x === 0 || data.x === 23 || data.y === 4; hasBSuffix = isBorderCell && data.sequentialNumber; } else if (data.type === 1) { // wall cells (all have '-b') hasBSuffix = data.sequentialNumber; } else if (data.type === 3) { // goal cells var isBorderCell = data.x === 0 || data.x === 23 || data.y === 4; hasBSuffix = isBorderCell && data.sequentialNumber; } // Only hide cell asset if the sequential number is in excluded list AND it doesn't have '-b' suffix if (excludedCells.indexOf(data.sequentialNumber) !== -1 && !hasBSuffix) { shouldShowCell = false; } } // Create or remove cell graphics based on shouldShowCell if (shouldShowCell && !cellGraphics) { cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; // Make 10% of cells darker randomly if (Math.random() < 0.1) { // Apply darker tint by reducing brightness var darkerTint = cellGraphics.tint; var r = darkerTint >> 16 & 0xFF; var g = darkerTint >> 8 & 0xFF; var b = darkerTint & 0xFF; // Reduce brightness by 40% r = Math.floor(r * 0.6); g = Math.floor(g * 0.6); b = Math.floor(b * 0.6); cellGraphics.tint = r << 16 | g << 8 | b; } } else if (!shouldShowCell && cellGraphics) { self.removeChild(cellGraphics); cellGraphics = null; } switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); if (cellGraphics) cellGraphics.tint = 0x880000; return; } // Simple sequential numbering for path cells if (!data.sequentialNumber) { // Assign sequential numbers to path cells only var pathCellCount = 0; for (var row = 0; row < grid.cells[0].length; row++) { for (var col = 0; col < grid.cells.length; col++) { var cell = grid.cells[col][row]; if (cell && (cell.type === 0 || cell.type === 2) && cell.pathId === pathId) { pathCellCount++; cell.sequentialNumber = pathCellCount; } } } } // Check if this cell is on the border var isBorderCell = data.x === 0 || data.x === 23 || data.y === 4; var displayText = data.sequentialNumber ? data.sequentialNumber.toString() : "-"; if (isBorderCell && displayText !== "-") { // Special case: rename cell 27-b to 1 if (displayText === "27") { displayText = "1"; } else if (displayText === "28") { displayText = "2"; } else if (displayText === "25") { displayText = "3"; } else if (displayText === "26") { displayText = "4"; } else if (displayText === "29") { displayText = "5"; } else if (displayText === "30") { displayText = "6"; } else { displayText += "-b"; } } else if (displayText === "27") { // Handle case where cell 27 is not on border but we still want to rename it displayText = "1"; } else if (displayText === "28") { displayText = "2"; } else if (displayText === "25") { displayText = "3"; } else if (displayText === "26") { displayText = "4"; } else if (displayText === "29") { displayText = "5"; } else if (displayText === "30") { displayText = "6"; } numberLabel.setText(displayText); numberLabel.visible = true; // Removed tower in range highlighting to prevent map darkening if (cellGraphics) cellGraphics.tint = 0xFFFFFF; 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; debugArrows[a].visible = false; // Hide the arrows self.addChildAt(debugArrows[a], 1); } debugArrows[a].rotation = angle; debugArrows[a].visible = false; // Hide the arrows } break; } case 1: { self.removeArrows(); if (cellGraphics) cellGraphics.tint = 0xaaaaaa; // Simple sequential numbering for wall cells if (!data.sequentialNumber) { var wallCellCount = 0; for (var row = 0; row < grid.cells[0].length; row++) { for (var col = 0; col < grid.cells.length; col++) { var cell = grid.cells[col][row]; if (cell && cell.type === 1) { wallCellCount++; cell.sequentialNumber = wallCellCount; } } } } var displayText = data.sequentialNumber ? data.sequentialNumber.toString() : "-"; // Add '-b' to all grey cells (wall cells) if (displayText !== "-") { // Special case: rename cell 27-b to 1 if (displayText === "27") { displayText = "1"; } else if (displayText === "28") { displayText = "2"; } else if (displayText === "25") { displayText = "3"; } else if (displayText === "26") { displayText = "4"; } else if (displayText === "29") { displayText = "5"; } else if (displayText === "30") { displayText = "6"; } else { displayText += "-b"; } } numberLabel.setText(displayText); numberLabel.visible = true; break; } case 3: { self.removeArrows(); if (cellGraphics) cellGraphics.tint = 0x008800; // Simple sequential numbering for goal cells if (!data.sequentialNumber) { var goalCellCount = 0; for (var row = 0; row < grid.cells[0].length; row++) { for (var col = 0; col < grid.cells.length; col++) { var cell = grid.cells[col][row]; if (cell && cell.type === 3) { goalCellCount++; cell.sequentialNumber = goalCellCount; } } } } // Check if this goal cell is on the border var isBorderCell = data.x === 0 || data.x === 23 || data.y === 4; var displayText = data.sequentialNumber ? data.sequentialNumber.toString() : "-"; if (isBorderCell && displayText !== "-") { // Special case: rename cell 27-b to 1 if (displayText === "27") { displayText = "1"; } else if (displayText === "28") { displayText = "2"; } else if (displayText === "25") { displayText = "3"; } else if (displayText === "26") { displayText = "4"; } else if (displayText === "29") { displayText = "5"; } else if (displayText === "30") { displayText = "6"; } else { displayText += "-b"; } } else if (displayText === "27") { // Handle case where cell 27 is not on border but we still want to rename it displayText = "1"; } else if (displayText === "28") { displayText = "2"; } else if (displayText === "25") { displayText = "3"; } else if (displayText === "26") { displayText = "4"; } else if (displayText === "29") { displayText = "5"; } else if (displayText === "30") { displayText = "6"; } numberLabel.setText(displayText); numberLabel.visible = true; break; } } // Add arena to specified cells (excluding those with '-b' suffix) var arenaNumbers = [27, 165, 209, 89, 133, 177, 221, 309, 353, 429, 473, 569, 41, 85]; // Add arena2 to specified cells (excluding those with '-b' suffix) var arena2Numbers = [187, 231, 451, 495, 591, 111, 155, 199, 243, 331, 375, 63, 107]; var displayText = data.sequentialNumber ? data.sequentialNumber.toString() : "-"; var hasBSuffix = false; // Check if this cell would have '-b' suffix if (data.type === 0 || data.type === 2) { // path cells var isBorderCell = data.x === 0 || data.x === 23 || data.y === 4; hasBSuffix = isBorderCell && displayText !== "-"; } else if (data.type === 1) { // wall cells (all have '-b') hasBSuffix = displayText !== "-"; } else if (data.type === 3) { // goal cells var isBorderCell = data.x === 0 || data.x === 23 || data.y === 4; hasBSuffix = isBorderCell && displayText !== "-"; } if (data.sequentialNumber && arenaNumbers.indexOf(data.sequentialNumber) !== -1 && !hasBSuffix) { var arenaGraphics = self.attachAsset('arena', { anchorX: 0.5, anchorY: 0.5 }); arenaGraphics.alpha = 1.0; } if (data.sequentialNumber && arena2Numbers.indexOf(data.sequentialNumber) !== -1 && !hasBSuffix) { var arena2Graphics = self.attachAsset('arena2', { anchorX: 0.5, anchorY: 0.5 }); arena2Graphics.alpha = 1.0; } // Add arena2 to cell 1 (formerly 27-b) (only cells with -b suffix) if (data.sequentialNumber === 27 && hasBSuffix) { var arena2Graphics = self.attachAsset('arena2', { anchorX: 0.5, anchorY: 0.5 }); arena2Graphics.alpha = 1.0; } // Add arena4 to cell 28-b (only cells with -b suffix) if (data.sequentialNumber === 28 && hasBSuffix) { var arena4Graphics = self.attachAsset('arena4', { anchorX: 0.5, anchorY: 0.5 }); arena4Graphics.alpha = 1.0; } // Add arena3 to specified cells (excluding those with '-b' suffix) var arena3Numbers = [130, 128, 126, 124, 122, 254, 256, 258, 260, 262, 264, 266, 268, 270, 272, 228, 184, 140, 96, 52, 50, 48, 46, 398, 396, 394, 392, 390, 388, 386, 518, 520, 522, 524, 526, 606, 42, 86, 166, 210, 90, 134, 178, 222, 254, 430, 474, 570, 310, 354]; if (data.sequentialNumber && arena3Numbers.indexOf(data.sequentialNumber) !== -1 && !hasBSuffix) { var arena3Graphics = self.attachAsset('arena3', { anchorX: 0.5, anchorY: 0.5 }); arena3Graphics.alpha = 1.0; } // Add arena4 to specified cells (excluding those with '-b' suffix) var arena4Numbers = [64, 408, 232, 188, 112, 156, 200, 244, 376, 452, 592, 152, 150, 148, 146, 144, 276, 278, 280, 282, 284, 286, 288, 290, 292, 294, 250, 206, 162, 118, 74, 72, 70, 68, 420, 418, 416, 414, 412, 410, 540, 542, 544, 546, 548, 108, 496, 332]; if (data.sequentialNumber && arena4Numbers.indexOf(data.sequentialNumber) !== -1 && !hasBSuffix) { var arena4Graphics = self.attachAsset('arena4', { anchorX: 0.5, anchorY: 0.5 }); arena4Graphics.alpha = 1.0; } // Add arena to specified cells (excluding those with '-b' suffix) var arenaNumbers = [121, 123, 125, 127, 129, 253, 255, 257, 259, 261, 263, 267, 269, 271, 265, 45, 47, 49, 51, 95, 139, 183, 227, 387, 385, 389, 391, 393, 395, 397, 517, 519, 521, 523, 525, 695, 605]; if (data.sequentialNumber && arenaNumbers.indexOf(data.sequentialNumber) !== -1 && !hasBSuffix) { var arenaGraphics = self.attachAsset('arena', { anchorX: 0.5, anchorY: 0.5 }); arenaGraphics.alpha = 1.0; } // Add arena2 to specified cells (excluding those with '-b' suffix) var arena2Numbers = [151, 149, 147, 145, 143, 275, 277, 279, 281, 283, 285, 287, 289, 291, 293, 249, 205, 161, 117, 73, 71, 69, 67, 419, 417, 415, 413, 411, 409, 407, 539, 541, 543, 545, 547]; if (data.sequentialNumber && arena2Numbers.indexOf(data.sequentialNumber) !== -1 && !hasBSuffix) { var arena2Graphics = self.attachAsset('arena2', { anchorX: 0.5, anchorY: 0.5 }); arena2Graphics.alpha = 1.0; } // Sequential numbering is now handled above for each cell type }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 2; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 3; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'normal': default: // Normal enemy uses default values break; } // Wave-based speed multiplier now applied in game update loop using stored wave number // Remove from constructor to prevent slowdown of existing enemies if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses (but still affected by wave multiplier) self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; self.poisonSlowed = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Note: poisonSlowed remains true permanently, speed is not reset // Only reset tint if not slowed and not permanently poison slowed if (!self.slowed && !self.poisonSlowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Combine poison blue (0x0066FF) and slow (0x9900FF) colors enemyGraphics.tint = 0x0033DD; } else if (self.poisoned) { // Blue tint for poisoned enemies enemyGraphics.tint = 0x0066FF; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } // Enemy rotation removed to prevent assets from rotating when turning corners healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var FlowerBullet = Container.expand(function (startX, startY, direction, damage, speed) { var self = Container.call(this); self.direction = direction; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; self.startX = startX; // Store starting position self.startY = startY; // Store starting position self.sourceRange = null; // Will be set by the tower var bulletGraphics = self.attachAsset('proyectilflor', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { // Move in the specified direction self.x += self.direction.x * self.speed; self.y += self.direction.y * self.speed; // Check if bullet has traveled beyond source tower's range if (self.sourceRange) { var dx = self.x - self.startX; var dy = self.y - self.startY; var distanceFromOrigin = Math.sqrt(dx * dx + dy * dy); if (distanceFromOrigin > self.sourceRange) { self.destroy(); return; } } // Remove bullet if it goes off screen if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) { self.destroy(); } }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [], enemyEntered: [], enemyLeft: [] }; } } /* 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 > 11 - 3 && i <= 11 + 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; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { // Return cached result if pathfinding hasn't changed if (pathfindCache && !pathfindDirty) { return pathfindCache; } 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"); pathfindCache = true; pathfindDirty = false; 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 "); pathfindCache = true; pathfindDirty = false; return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); pathfindCache = true; pathfindDirty = false; return true; } } console.log("Speed", new Date().getTime() - before); // Cache the result and mark as clean pathfindCache = false; // No blocking found pathfindDirty = false; return pathfindCache; }; 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 enemy touches damage cells (603, 604, 605, 606, 607, 608) var damageCells = [603, 604, 605, 606, 607, 608]; var currentCell = grid.getCell(enemy.cellX, enemy.cellY); // Also check if enemy is moving towards or through these cells var nextCellX = Math.round(enemy.currentCellX); var nextCellY = Math.round(enemy.currentCellY); var nextCell = grid.getCell(nextCellX, nextCellY); // Check both current position and next position to prevent skipping var cellToCheck = currentCell; if (nextCell && nextCell.sequentialNumber && damageCells.indexOf(nextCell.sequentialNumber) !== -1) { cellToCheck = nextCell; } if (cellToCheck && cellToCheck.sequentialNumber && damageCells.indexOf(cellToCheck.sequentialNumber) !== -1) { // Apply damage based on enemy type: normal/flying = 1 damage, boss = 5 damage var damage; if (enemy.isBoss) { damage = 5; } else if (enemy.type === 'normal' || enemy.type === 'flying') { damage = 1; } else { // Other enemy types (fast, immune, swarm) also deal 1 damage unless they're bosses damage = 1; } lives = Math.max(0, lives - damage); updateUI(); // Show damage notification var damageText = enemy.isBoss ? "Boss hit! -" + damage + " lives!" : "Enemy hit! -" + damage + " life!"; var notification = game.addChild(new Notification(damageText)); notification.x = 2048 / 2; notification.y = grid.height - 100; // Remove enemy immediately // 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); } // Remove from enemies array var enemyIndex = enemies.indexOf(enemy); if (enemyIndex !== -1) { enemies.splice(enemyIndex, 1); } // Check for game over if (lives <= 0) { LK.showGameOver(); } return true; // Enemy was removed } // 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; // Enemy rotation removed to prevent assets from rotating when moving // Update enemy's position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // If enemy has now reached the entry area, update cell coordinates if (enemy.currentCellY >= 4) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Enemy rotation removed to prevent assets from rotating when turning // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Check if enemy reached cell '1' and should follow ruta1 if (cell.sequentialNumber === 27 && enemiesOnRuta1.indexOf(enemy) === -1) { // Cell '1' was originally cell 27 // Add enemy to ruta1 routing enemiesOnRuta1.push(enemy); enemy.ruta1Index = 0; // Start at first cell in ruta1 enemy.followingRuta1 = true; } // Check if enemy reached cell '2' and should follow ruta2 if (cell.sequentialNumber === 28 && enemiesOnRuta2.indexOf(enemy) === -1) { // Cell '2' was originally cell 28 // Add enemy to ruta2 routing enemiesOnRuta2.push(enemy); enemy.ruta2Index = 0; // Start at first cell in ruta2 enemy.followingRuta2 = true; } // Check if enemy reached cell '3' and should follow ruta3 if (cell.sequentialNumber === 25 && enemiesOnRuta3.indexOf(enemy) === -1) { // Cell '3' was originally cell 25 // Add enemy to ruta3 routing enemiesOnRuta3.push(enemy); enemy.ruta3Index = 0; // Start at first cell in ruta3 enemy.followingRuta3 = true; } // Check if enemy reached cell '4' and should follow ruta4 if (cell.sequentialNumber === 26 && enemiesOnRuta4.indexOf(enemy) === -1) { // Cell '4' was originally cell 26 // Add enemy to ruta4 routing enemiesOnRuta4.push(enemy); enemy.ruta4Index = 0; // Start at first cell in ruta4 enemy.followingRuta4 = true; } // Check if enemy reached cell '5' and should follow ruta5 if (cell.sequentialNumber === 29 && enemiesOnRuta5.indexOf(enemy) === -1) { // Cell '5' was originally cell 29 // Add enemy to ruta5 routing enemiesOnRuta5.push(enemy); enemy.ruta5Index = 0; // Start at first cell in ruta5 enemy.followingRuta5 = true; } // Check if enemy reached cell '6' and should follow ruta6 if (cell.sequentialNumber === 30 && enemiesOnRuta6.indexOf(enemy) === -1) { // Cell '6' was originally cell 30 // Add enemy to ruta6 routing enemiesOnRuta6.push(enemy); enemy.ruta6Index = 0; // Start at first cell in ruta6 enemy.followingRuta6 = true; } // Handle ruta1 routing if (enemy.followingRuta1 && enemiesOnRuta1.indexOf(enemy) !== -1) { if (enemy.ruta1Index < ruta1.length) { // Find the target cell based on ruta1 var targetCellNumber = ruta1[enemy.ruta1Index]; var targetCell = null; // Find cell with matching sequential number (excluding -b suffix) for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var checkCell = grid.cells[i][j]; if (checkCell && checkCell.sequentialNumber === targetCellNumber) { // Check if this cell would have -b suffix var hasBSuffix = false; if (checkCell.type === 0 || checkCell.type === 2) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } else if (checkCell.type === 1) { hasBSuffix = true; } else if (checkCell.type === 3) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } // Only use cells without -b suffix for ruta1 if (!hasBSuffix) { targetCell = checkCell; break; } } } if (targetCell) break; } if (targetCell) { enemy.currentTarget = targetCell; // Check if enemy reached this ruta1 target if (enemy.cellX === targetCell.x && enemy.cellY === targetCell.y) { enemy.ruta1Index++; // Move to next cell in ruta1 if (enemy.ruta1Index >= ruta1.length) { // Completed ruta1, remove from tracking and resume normal pathfinding var enemyIndex = enemiesOnRuta1.indexOf(enemy); if (enemyIndex !== -1) { enemiesOnRuta1.splice(enemyIndex, 1); } enemy.followingRuta1 = false; enemy.ruta1Index = undefined; enemy.currentTarget = undefined; // Let normal pathfinding take over } } } } } else if (enemy.followingRuta2 && enemiesOnRuta2.indexOf(enemy) !== -1) { // Handle ruta2 routing if (enemy.ruta2Index < ruta2.length) { // Find the target cell based on ruta2 var targetCellNumber = ruta2[enemy.ruta2Index]; var targetCell = null; // Find cell with matching sequential number (excluding -b suffix) for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var checkCell = grid.cells[i][j]; if (checkCell && checkCell.sequentialNumber === targetCellNumber) { // Check if this cell would have -b suffix var hasBSuffix = false; if (checkCell.type === 0 || checkCell.type === 2) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } else if (checkCell.type === 1) { hasBSuffix = true; } else if (checkCell.type === 3) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } // Only use cells without -b suffix for ruta2 if (!hasBSuffix) { targetCell = checkCell; break; } } } if (targetCell) break; } if (targetCell) { enemy.currentTarget = targetCell; // Check if enemy reached this ruta2 target if (enemy.cellX === targetCell.x && enemy.cellY === targetCell.y) { enemy.ruta2Index++; // Move to next cell in ruta2 if (enemy.ruta2Index >= ruta2.length) { // Completed ruta2, remove from tracking and resume normal pathfinding var enemyIndex = enemiesOnRuta2.indexOf(enemy); if (enemyIndex !== -1) { enemiesOnRuta2.splice(enemyIndex, 1); } enemy.followingRuta2 = false; enemy.ruta2Index = undefined; enemy.currentTarget = undefined; // Let normal pathfinding take over } } } } } else if (enemy.followingRuta3 && enemiesOnRuta3.indexOf(enemy) !== -1) { // Handle ruta3 routing if (enemy.ruta3Index < ruta3.length) { // Find the target cell based on ruta3 var targetCellNumber = ruta3[enemy.ruta3Index]; var targetCell = null; // Find cell with matching sequential number (excluding -b suffix) for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var checkCell = grid.cells[i][j]; if (checkCell && checkCell.sequentialNumber === targetCellNumber) { // Check if this cell would have -b suffix var hasBSuffix = false; if (checkCell.type === 0 || checkCell.type === 2) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } else if (checkCell.type === 1) { hasBSuffix = true; } else if (checkCell.type === 3) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } // Only use cells without -b suffix for ruta3 if (!hasBSuffix) { targetCell = checkCell; break; } } } if (targetCell) break; } if (targetCell) { enemy.currentTarget = targetCell; // Check if enemy reached this ruta3 target if (enemy.cellX === targetCell.x && enemy.cellY === targetCell.y) { enemy.ruta3Index++; // Move to next cell in ruta3 if (enemy.ruta3Index >= ruta3.length) { // Completed ruta3, remove from tracking and resume normal pathfinding var enemyIndex = enemiesOnRuta3.indexOf(enemy); if (enemyIndex !== -1) { enemiesOnRuta3.splice(enemyIndex, 1); } enemy.followingRuta3 = false; enemy.ruta3Index = undefined; enemy.currentTarget = undefined; // Let normal pathfinding take over } } } } } else if (enemy.followingRuta4 && enemiesOnRuta4.indexOf(enemy) !== -1) { // Handle ruta4 routing if (enemy.ruta4Index < ruta4.length) { // Find the target cell based on ruta4 var targetCellNumber = ruta4[enemy.ruta4Index]; var targetCell = null; // Find cell with matching sequential number (excluding -b suffix) for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var checkCell = grid.cells[i][j]; if (checkCell && checkCell.sequentialNumber === targetCellNumber) { // Check if this cell would have -b suffix var hasBSuffix = false; if (checkCell.type === 0 || checkCell.type === 2) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } else if (checkCell.type === 1) { hasBSuffix = true; } else if (checkCell.type === 3) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } // Only use cells without -b suffix for ruta4 if (!hasBSuffix) { targetCell = checkCell; break; } } } if (targetCell) break; } if (targetCell) { enemy.currentTarget = targetCell; // Check if enemy reached this ruta4 target if (enemy.cellX === targetCell.x && enemy.cellY === targetCell.y) { enemy.ruta4Index++; // Move to next cell in ruta4 if (enemy.ruta4Index >= ruta4.length) { // Completed ruta4, remove from tracking and resume normal pathfinding var enemyIndex = enemiesOnRuta4.indexOf(enemy); if (enemyIndex !== -1) { enemiesOnRuta4.splice(enemyIndex, 1); } enemy.followingRuta4 = false; enemy.ruta4Index = undefined; enemy.currentTarget = undefined; // Let normal pathfinding take over } } } } } else if (enemy.followingRuta5 && enemiesOnRuta5.indexOf(enemy) !== -1) { // Handle ruta5 routing if (enemy.ruta5Index < ruta5.length) { // Find the target cell based on ruta5 var targetCellNumber = ruta5[enemy.ruta5Index]; var targetCell = null; // Find cell with matching sequential number (excluding -b suffix) for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var checkCell = grid.cells[i][j]; if (checkCell && checkCell.sequentialNumber === targetCellNumber) { // Check if this cell would have -b suffix var hasBSuffix = false; if (checkCell.type === 0 || checkCell.type === 2) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } else if (checkCell.type === 1) { hasBSuffix = true; } else if (checkCell.type === 3) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } // Only use cells without -b suffix for ruta5 if (!hasBSuffix) { targetCell = checkCell; break; } } } if (targetCell) break; } if (targetCell) { enemy.currentTarget = targetCell; // Check if enemy reached this ruta5 target if (enemy.cellX === targetCell.x && enemy.cellY === targetCell.y) { enemy.ruta5Index++; // Move to next cell in ruta5 if (enemy.ruta5Index >= ruta5.length) { // Completed ruta5, remove from tracking and resume normal pathfinding var enemyIndex = enemiesOnRuta5.indexOf(enemy); if (enemyIndex !== -1) { enemiesOnRuta5.splice(enemyIndex, 1); } enemy.followingRuta5 = false; enemy.ruta5Index = undefined; enemy.currentTarget = undefined; // Let normal pathfinding take over } } } } } else if (enemy.followingRuta6 && enemiesOnRuta6.indexOf(enemy) !== -1) { // Handle ruta6 routing if (enemy.ruta6Index < ruta6.length) { // Find the target cell based on ruta6 var targetCellNumber = ruta6[enemy.ruta6Index]; var targetCell = null; // Find cell with matching sequential number (excluding -b suffix) for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var checkCell = grid.cells[i][j]; if (checkCell && checkCell.sequentialNumber === targetCellNumber) { // Check if this cell would have -b suffix var hasBSuffix = false; if (checkCell.type === 0 || checkCell.type === 2) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } else if (checkCell.type === 1) { hasBSuffix = true; } else if (checkCell.type === 3) { var isBorderCell = checkCell.x === 0 || checkCell.x === 23 || checkCell.y === 4; hasBSuffix = isBorderCell; } // Only use cells without -b suffix for ruta6 if (!hasBSuffix) { targetCell = checkCell; break; } } } if (targetCell) break; } if (targetCell) { enemy.currentTarget = targetCell; // Check if enemy reached this ruta6 target if (enemy.cellX === targetCell.x && enemy.cellY === targetCell.y) { enemy.ruta6Index++; // Move to next cell in ruta6 if (enemy.ruta6Index >= ruta6.length) { // Completed ruta6, remove from tracking and resume normal pathfinding var enemyIndex = enemiesOnRuta6.indexOf(enemy); if (enemyIndex !== -1) { enemiesOnRuta6.splice(enemyIndex, 1); } enemy.followingRuta6 = false; enemy.ruta6Index = undefined; enemy.currentTarget = undefined; // Let normal pathfinding take over } } } } } else { // Handle normal pathfinding enemies if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } } if (enemy.currentTarget) { if (!enemy.followingRuta1 && !enemy.followingRuta2 && !enemy.followingRuta3 && !enemy.followingRuta4 && !enemy.followingRuta5 && !enemy.followingRuta6 && 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) { // Move to exact target position to prevent getting stuck enemy.currentCellX = enemy.currentTarget.x; enemy.currentCellY = enemy.currentTarget.y; enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); var newX = enemy.currentCellX + Math.cos(angle) * enemy.speed; var newY = enemy.currentCellY + Math.sin(angle) * enemy.speed; // Ensure movement doesn't get stuck - if we're not making progress, jump to target if (!enemy.lastMovementCheck) enemy.lastMovementCheck = { x: enemy.currentCellX, y: enemy.currentCellY, ticks: 0 }; var movementDist = Math.sqrt((newX - enemy.lastMovementCheck.x) * (newX - enemy.lastMovementCheck.x) + (newY - enemy.lastMovementCheck.y) * (newY - enemy.lastMovementCheck.y)); if (movementDist < 0.01) { enemy.lastMovementCheck.ticks++; if (enemy.lastMovementCheck.ticks > 30) { // If stuck for 30 frames // Force movement towards target enemy.currentCellX = enemy.currentTarget.x; enemy.currentCellY = enemy.currentTarget.y; enemy.lastMovementCheck.ticks = 0; } } else { enemy.lastMovementCheck = { x: newX, y: newY, ticks: 0 }; } enemy.currentCellX = newX; enemy.currentCellY = newY; } // Track enemy movement for event-driven tower targeting var newCellX = Math.round(enemy.currentCellX); var newCellY = Math.round(enemy.currentCellY); var hasMovedToNewCell = enemy.lastTrackedCellX !== newCellX || enemy.lastTrackedCellY !== newCellY; if (hasMovedToNewCell) { // Enemy left previous cell - notify towers if (enemy.lastTrackedCellX !== undefined && enemy.lastTrackedCellY !== undefined) { var oldCell = self.getCell(enemy.lastTrackedCellX, enemy.lastTrackedCellY); if (oldCell && oldCell.enemyLeft) { oldCell.enemyLeft.push(enemy); } } // Enemy entered new cell - notify towers var newCell = self.getCell(newCellX, newCellY); if (newCell) { if (!newCell.enemyEntered) newCell.enemyEntered = []; newCell.enemyEntered.push(enemy); } // Update tracking enemy.lastTrackedCellX = newCellX; enemy.lastTrackedCellY = newCellY; } // Ensure cellX and cellY are always synchronized with current position enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Increase size of base for easier touch // Use different tower assets for different tower types var towerAssetName = 'tower'; // default tower asset switch (self.towerType) { case 'rapid': towerAssetName = 'tower_rapid'; break; case 'sniper': towerAssetName = 'tower_sniper'; break; case 'splash': towerAssetName = 'tower_splash'; break; case 'slow': towerAssetName = 'tower_slow'; break; case 'poison': towerAssetName = 'tower_poison'; break; default: towerAssetName = 'tower_default'; } var baseGraphics = self.attachAsset(towerAssetName, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); // Remove tints to show natural asset colors - tints only used during tower preview dragging var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -85 + 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -85; // Position higher above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 34 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 30 + 12; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; self.canFire = true; // Allow firing by default self.lastFired = 0; // Initialize lastFired to 0 // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case 'sniper': // Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost if (self.level === self.maxLevel) { return 7.6 * CELL_SIZE; // Level 6 range is 7.6 blocks } else if (self.level === 5) { return 7.5 * CELL_SIZE; // Level 5 range is 7.5 blocks } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case 'splash': // Splash: base 4, +0.4 per level (max ~8 blocks at max level) - doubled from original return (4 + (self.level - 1) * 0.4) * CELL_SIZE; case 'rapid': // Rapid: base 2.5, +0.5 per level return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'slow': // Slow: base 100, +0.5 per level return (100 + (self.level - 1) * 0.5) * CELL_SIZE; case 'poison': // Poison: base 3.2, +0.5 per level return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; default: // Default: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = self.id === 'default' ? 240 : 120; // Default tower fires twice as slow self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.lastUpgradeTime = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 30; self.damage = 5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'sniper': self.fireRate = 90; self.damage = 20; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'splash': self.fireRate = 75; self.damage = 15; self.range = 4 * CELL_SIZE; self.bulletSpeed = 4; break; case 'slow': self.fireRate = 200; self.damage = 8; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'poison': self.fireRate = 70; self.damage = 12; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 5; break; } // Removed baseGraphics creation - towers now only show the tower asset without plant base var gunContainer = new Container(); self.addChild(gunContainer); var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = CELL_SIZE * 2 / (maxDots + 1); var dotSize = CELL_SIZE / 6; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } // Use different tower assets for different tower types var towerAssetName = 'tower'; // default tower asset switch (self.id) { case 'rapid': towerAssetName = 'tower_rapid'; break; case 'sniper': towerAssetName = 'tower_sniper'; break; case 'splash': towerAssetName = 'tower_splash'; break; case 'slow': towerAssetName = 'tower_slow'; break; case 'poison': towerAssetName = 'tower_poison'; break; default: towerAssetName = 'tower_default'; } var gunGraphics = gunContainer.attachAsset(towerAssetName, { anchorX: 0.5, anchorY: 0.5 }); self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { switch (self.id) { case 'rapid': towerLevelIndicator.tint = 0x00AAFF; break; case 'sniper': towerLevelIndicator.tint = 0xFF5500; break; case 'splash': towerLevelIndicator.tint = 0x33CC00; break; case 'slow': towerLevelIndicator.tint = 0x9900FF; break; case 'poison': towerLevelIndicator.tint = 0x00FFAA; break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var rangeRadiusSquared = rangeRadius * rangeRadius; // Cache squared radius var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.max(0, Math.floor(centerX - rangeRadius - 0.5)); var maxI = Math.min(23, Math.ceil(centerX + rangeRadius + 0.5)); var minJ = Math.max(0, Math.floor(centerY - rangeRadius - 0.5)); var maxJ = Math.min(34, 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 <= rangeRadiusSquared) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); // Initialize enemy tracking arrays for event-driven targeting if (!cell.enemyEntered) cell.enemyEntered = []; if (!cell.enemyLeft) cell.enemyLeft = []; } } } } // Only render debug every few frames to reduce overhead if (frameCounter % 5 === 0) 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 () { // Prevent rapid upgrades to avoid visual bugs if (self.lastUpgradeTime && LK.ticks - self.lastUpgradeTime < 30) { return false; } if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Special case for default tower level 5 upgrade if (self.id === 'default' && self.level === 4) { upgradeCost = 35; // Level 4→5 upgrade costs 35 } else if (self.id === 'rapid' && self.level === 3) { upgradeCost = 45; // Level 3→4 upgrade costs 45 } else if (self.id === 'rapid' && self.level === 4) { upgradeCost = 60; // Level 4→5 upgrade costs 60 } else if (self.id === 'rapid' && self.level === 5) { upgradeCost = 75; // Level 5→6 upgrade costs 75 } else if (self.id === 'sniper' && self.level === 3) { upgradeCost = 75; // Level 3→4 upgrade costs 75 } else 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) { self.lastUpgradeTime = LK.ticks; setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rapid') { if (self.level === 2) { // Level 2: 2.0/s fire rate = 30 ticks between shots, 10 damage self.fireRate = 30; // 2.0 shots per second (60 FPS / 30 = 2.0) self.damage = 10; self.bulletSpeed = 7 + self.level * 0.7; } else if (self.level === 3) { // Level 3: 2.5/s fire rate = 24 ticks between shots, 10 damage self.fireRate = 24; // 2.5 shots per second (60 FPS / 24 = 2.5) self.damage = 10; self.bulletSpeed = 7 + self.level * 0.7; } else if (self.level === 4) { // Level 4: 2.5/s fire rate = 24 ticks between shots, 15 damage self.fireRate = 24; // 2.5 shots per second (60 FPS / 24 = 2.5) self.damage = 15; self.bulletSpeed = 7 + self.level * 0.7; } else if (self.level === 5) { // Level 5: 2.5/s fire rate = 24 ticks between shots, 20 damage self.fireRate = 24; // 2.5 shots per second (60 FPS / 24 = 2.5) self.damage = 20; self.bulletSpeed = 7 + self.level * 0.7; } else if (self.level === 6) { // Level 6: 2.5/s fire rate = 24 ticks between shots, 25 damage self.fireRate = 24; // 2.5 shots per second (60 FPS / 24 = 2.5) self.damage = 25; self.bulletSpeed = 7 + self.level * 0.7; } else if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect self.damage = 5 + self.level * 10; // double the effect self.bulletSpeed = 7 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades self.damage = 5 + self.level * 3; self.bulletSpeed = 7 + self.level * 0.7; } } else if (self.id === 'default') { // Default tower uses the slower fire rate (240 base, double the original 120) // Special upgrade cost for level 6 if (self.level === 5) { // Level 5 to 6 upgrade cost is fixed at 50 upgradeCost = 50; } if (self.level === self.maxLevel) { // Level 6: 0.5/s fire rate = 120 ticks between shots, damage = 35 self.fireRate = 120; // 0.5 shots per second (60 FPS / 120 = 0.5) self.damage = 35; // Level 6 damage self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(40, 240 - self.level * 16); // Starting from 240 (double slower) self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } else if (self.id === 'sniper') { // Sniper tower has special fire rate handling for level 2 if (self.level === 2) { // Level 2: 0.9/s fire rate = 67 ticks between shots, 20 damage self.fireRate = 67; // 0.9 shots per second (60 FPS / 67 ≈ 0.9) self.damage = 20; self.bulletSpeed = 25 + self.level * 0.5; } else if (self.level === 3) { // Level 3: 1.2/s fire rate = 50 ticks between shots, 25 damage self.fireRate = 50; // 1.2 shots per second (60 FPS / 50 = 1.2) self.damage = 25; self.bulletSpeed = 25 + self.level * 0.5; } else if (self.level === 4) { // Level 4: 1.5/s fire rate = 40 ticks between shots, 25 damage self.fireRate = 40; // 1.5 shots per second (60 FPS / 40 = 1.5) self.damage = 25; self.bulletSpeed = 25 + self.level * 0.5; } else if (self.level === 5) { // Level 5: 1.5/s fire rate = 40 ticks between shots, 30 damage self.fireRate = 40; // 1.5 shots per second (60 FPS / 40 = 1.5) self.damage = 30; self.bulletSpeed = 25 + self.level * 0.5; } else if (self.level === 6) { // Level 6: 1.7/s fire rate = 35 ticks between shots, 30 damage self.fireRate = 35; // 1.7 shots per second (60 FPS / 35 ≈ 1.7) self.damage = 30; self.bulletSpeed = 25 + self.level * 0.5; } else if (self.level === self.maxLevel) { // Extra powerful last upgrade for sniper tower (double the effect) self.fireRate = Math.max(5, 90 - self.level * 24); // double the effect self.damage = 20 + self.level * 20; // double the effect self.bulletSpeed = 25 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 90 - self.level * 8); self.damage = 20 + self.level * 5; self.bulletSpeed = 25 + self.level * 0.5; } } else if (self.id === 'splash') { // Splash tower has special fire rate handling for level 2 if (self.level === 2) { // Level 2: 0.9/s fire rate = 67 ticks between shots self.fireRate = 67; // 0.9 shots per second (60 FPS / 67 ≈ 0.9) self.damage = 15 + self.level * 5; self.bulletSpeed = 4 + self.level * 0.5; } else if (self.level === self.maxLevel) { // Level 6: 1.8/s fire rate = 33 ticks between shots, damage = 50 self.fireRate = 33; // 1.8 shots per second (60 FPS / 33 ≈ 1.8) self.damage = 50; // Level 6 damage self.bulletSpeed = 4 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 75 - self.level * 8); self.damage = 15 + self.level * 5; self.bulletSpeed = 4 + self.level * 0.5; } } else if (self.id === 'slow') { // Slow tower has special fire rate handling if (self.level === 2) { // Level 2: 0.4/s fire rate = 150 ticks between shots self.fireRate = 150; // 0.4 shots per second (60 FPS / 150 = 0.4) self.damage = 8 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } else if (self.level === 3) { // Level 3: 0.5/s fire rate = 120 ticks between shots self.fireRate = 120; // 0.5 shots per second (60 FPS / 120 = 0.5) self.damage = 8 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } else if (self.level === 4) { // Level 4: 0.6/s fire rate = 100 ticks between shots self.fireRate = 100; // 0.6 shots per second (60 FPS / 100 = 0.6) self.damage = 8 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } else if (self.level === 5) { // Level 5: 0.7/s fire rate = 86 ticks between shots self.fireRate = 86; // 0.7 shots per second (60 FPS / 86 ≈ 0.7) self.damage = 8 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } else if (self.level === 6) { // Level 6: 0.8/s fire rate = 75 ticks between shots self.fireRate = 75; // 0.8 shots per second (60 FPS / 75 = 0.8) self.damage = 8 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } else if (self.level === self.maxLevel) { // Extra powerful last upgrade for slow tower (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 8 + 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 = 8 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } else if (self.id === 'poison') { // Poison tower upgrades if (self.level === 6) { // Level 6: 2.2/s fire rate = 27 ticks between shots, specific damage value of 44 self.fireRate = 27; // 2.2 shots per second (60 FPS / 27 ≈ 2.2) self.damage = 44; // Level 6 specific damage self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else if (self.level === self.maxLevel) { // Extra powerful last upgrade for poison tower (double the effect) self.fireRate = Math.max(5, 70 - self.level * 24); // double the effect self.damage = 12 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 70 - self.level * 8); self.damage = 12 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; // Stop any existing tweens on this level dot to prevent conflicts tween.stop(levelDot, { scaleX: true, scaleY: true }); // Reset scale before animating levelDot.scaleX = 1; levelDot.scaleY = 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 closestScore = Infinity; var rangeSquared = self.getRange() * self.getRange(); // Quick distance pre-filter to reduce expensive calculations for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; // Quick Manhattan distance check first (cheaper than squared distance) if (Math.abs(dx) + Math.abs(dy) > self.getRange() * 1.4) continue; var distance = dx * dx + dy * dy; // Use squared distance to avoid sqrt // Check if enemy is in range (compare squared distances) if (distance <= rangeSquared) { // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { // Check if we need to update target due to enemy movement events if (!self.needsTargetUpdate) self.needsTargetUpdate = false; // Check for enemy entry/exit events in our range cells for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; if (cell.enemyEntered && cell.enemyEntered.length > 0) { self.needsTargetUpdate = true; cell.enemyEntered = []; // Clear the event list } if (cell.enemyLeft && cell.enemyLeft.length > 0) { self.needsTargetUpdate = true; cell.enemyLeft = []; // Clear the event list } } // Only find targets when needed (enemy events or no current target or ready to fire) if (self.needsTargetUpdate || !self.targetEnemy || LK.ticks - self.lastFired >= self.fireRate) { self.targetEnemy = self.findTarget(); self.needsTargetUpdate = false; } if (self.targetEnemy && self.canFire && LK.ticks - self.lastFired >= self.fireRate) { // Skip rotation for default tower and slow tower since they fire in all directions if (self.id !== 'default' && self.id !== 'slow') { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); gunContainer.rotation = angle; // Rapid tower no longer changes asset based on firing direction // Poison tower no longer changes asset based on firing direction } // Fire at target if conditions are met if (self.targetEnemy && self.canFire && LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } else if (self.id === 'rapid') { // When rapid tower stops firing, keep current rotation (no reset) } else if (self.id === 'poison') { // When poison tower stops firing, keep current rotation (no reset) } else if (self.id === 'slow') { // Slow tower never rotates or changes, do nothing } }; self.down = function (x, y, obj) { var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } // Removed selectedTower clearing and grid render debug to prevent map darkening return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } // Only show range circle for non-slow towers if (self.id !== 'slow') { var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; } var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); }; 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.id === 'default' && self.targetEnemy) { // Default tower shoots in 8 directions (visual only) var directions = [{ x: 0, y: -1 }, // up { x: 0, y: 1 }, // down { x: -1, y: 0 }, // left { x: 1, y: 0 }, // right { x: 1, y: -1 }, // up+right { x: -1, y: -1 }, // up+left { x: 1, y: 1 }, // down+right { x: -1, y: 1 } // down+left ]; var spawnDistance = 40; // Distance from tower center to spawn bullets for (var dir = 0; dir < directions.length; dir++) { var direction = directions[dir]; // Calculate spawn position based on the direction the bullet will travel var angle = Math.atan2(direction.y, direction.x); var bulletX = self.x + Math.cos(angle) * spawnDistance; var bulletY = self.y + Math.sin(angle) * spawnDistance; var bullet = new FlowerBullet(bulletX, bulletY, direction, self.damage, self.bulletSpeed); bullet.type = self.id; bullet.sourceRange = self.getRange(); // Pass the tower's range to the bullet game.addChild(bullet); bullets.push(bullet); } // Deal damage to up to 10 enemies in range (area damage) var enemiesInRange = []; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.getRange()) { enemiesInRange.push({ enemy: enemy, distance: distance }); } } // Sort enemies by distance and take only the closest 10 enemiesInRange.sort(function (a, b) { return a.distance - b.distance; }); var maxEnemies = Math.min(10, enemiesInRange.length); for (var i = 0; i < maxEnemies; i++) { var enemy = enemiesInRange[i].enemy; // Apply damage to enemy enemy.health -= self.damage; if (enemy.health <= 0) { enemy.health = 0; } else { enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; } } // --- Fire animation for default tower --- // Default tower gets a scaling animation instead of recoil tween.stop(self, { scaleX: true, scaleY: true }); // Animate scale down then back up tween(self, { scaleX: 0.8, scaleY: 0.8 }, { duration: 80, easing: tween.easeOut, onFinish: function onFinish() { // Animate return to original size tween(self, { scaleX: 1.0, scaleY: 1.0 }, { duration: 120, easing: tween.easeOut }); } }); } else if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; } // For poison tower, pass level for scaling area effect if (self.id === 'poison') { bullet.sourceTowerLevel = self.level; } // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0xFFFF00; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'sniper': // Remove the default bullet graphics and replace with boladefuego asset bullet.removeChild(bullet.children[0]); var boladeFireGraphics = bullet.attachAsset('boladefuego', { anchorX: 0.5, anchorY: 0.5 }); boladeFireGraphics.width = 35; boladeFireGraphics.height = 35; break; case 'splash': // Remove the default bullet graphics and replace with bola asset bullet.removeChild(bullet.children[0]); var bolaGraphics = bullet.attachAsset('bola', { anchorX: 0.5, anchorY: 0.5 }); bolaGraphics.width = 40; bolaGraphics.height = 40; break; case 'slow': // Remove the default bullet graphics and replace with boladefuego asset bullet.removeChild(bullet.children[0]); var boladeFireGraphics = bullet.attachAsset('boladefuego', { anchorX: 0.5, anchorY: 0.5 }); boladeFireGraphics.width = 35; boladeFireGraphics.height = 35; break; case 'poison': // Remove the default bullet graphics and replace with boladehielo asset bullet.removeChild(bullet.children[0]); var boladeHieloGraphics = bullet.attachAsset('boladehielo', { anchorX: 0.5, anchorY: 0.5 }); boladeHieloGraphics.width = 35; boladeHieloGraphics.height = 35; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; } } } pathfindDirty = true; // Mark pathfind cache as dirty 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; // Use different preview assets for different tower types var previewAssetName = 'tower'; // default preview asset switch (self.towerType) { case 'rapid': previewAssetName = 'tower_rapid'; break; case 'sniper': previewAssetName = 'tower_sniper'; break; case 'splash': previewAssetName = 'tower_splash'; break; case 'slow': previewAssetName = 'tower_slow'; break; case 'poison': previewAssetName = 'tower_poison'; break; default: previewAssetName = 'tower'; } var previewGraphics = self.attachAsset(previewAssetName, { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; // Remove old preview graphics and create new one with correct asset if (previewGraphics && previewGraphics.parent) { self.removeChild(previewGraphics); } // Use different preview assets for different tower types var previewAssetName = 'tower'; // default preview asset switch (self.towerType) { case 'rapid': previewAssetName = 'tower_rapid'; break; case 'sniper': previewAssetName = 'tower_sniper'; break; case 'splash': previewAssetName = 'tower_splash'; break; case 'slow': previewAssetName = 'tower_slow'; break; case 'poison': previewAssetName = 'tower_poison'; break; case 'default': previewAssetName = 'tower_default'; break; default: previewAssetName = 'tower_default'; } previewGraphics = self.attachAsset(previewAssetName, { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; // For specific tower types, also show the tower asset if (self.towerType === 'default') { var towerGraphics = self.attachAsset('tower_default', { anchorX: 0.5, anchorY: 0.5 }); towerGraphics.width = CELL_SIZE * 2; towerGraphics.height = CELL_SIZE * 2; } else if (self.towerType === 'rapid') { var towerGraphics = self.attachAsset('tower_rapid', { anchorX: 0.5, anchorY: 0.5 }); towerGraphics.width = CELL_SIZE * 2; towerGraphics.height = CELL_SIZE * 2; } else if (self.towerType === 'sniper') { var towerGraphics = self.attachAsset('tower_sniper', { anchorX: 0.5, anchorY: 0.5 }); towerGraphics.width = CELL_SIZE * 2; towerGraphics.height = CELL_SIZE * 2; } else if (self.towerType === 'splash') { var towerGraphics = self.attachAsset('tower_splash', { anchorX: 0.5, anchorY: 0.5 }); towerGraphics.width = CELL_SIZE * 2; towerGraphics.height = CELL_SIZE * 2; } else if (self.towerType === 'slow') { var towerGraphics = self.attachAsset('tower_slow', { anchorX: 0.5, anchorY: 0.5 }); towerGraphics.width = CELL_SIZE * 2; towerGraphics.height = CELL_SIZE * 2; } else if (self.towerType === 'poison') { var towerGraphics = self.attachAsset('tower_poison', { anchorX: 0.5, anchorY: 0.5 }); towerGraphics.width = CELL_SIZE * 2; towerGraphics.height = CELL_SIZE * 2; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) { validGridPlacement = false; } else { for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (!cell || cell.type !== 0) { validGridPlacement = false; break; } // Check if this cell's sequential number is in the excluded list if (cell.sequentialNumber && excludedTowerCells.indexOf(cell.sequentialNumber) !== -1) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) { continue; } // Only check non-flying enemies, flying enemies can pass over towers if (!enemy.isFlying) { if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) { self.blockedByEnemy = true; break; } } } } } self.canPlace = validGridPlacement && !self.blockedByEnemy; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; self.checkPlacement(); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); // Create damage text based on tower type var damageText; if (self.tower.id === 'slow') { // For slow towers, show percentage damage var damagePercentages = [0.1, 0.12, 0.14, 0.16, 0.18, 0.2]; var idx = Math.max(0, Math.min(5, self.tower.level - 1)); var damagePercentage = damagePercentages[idx]; damageText = (damagePercentage * 100).toFixed(0) + '% HP'; } else if (self.tower.id === 'poison' && self.tower.level === 6) { // For poison tower level 6, show specific damage value damageText = '44'; } else { // For other towers, show regular damage damageText = self.tower.damage.toString(); } // Calculate fire rate display - handle special cases for slow tower var fireRateDisplay; if (self.tower.id === 'slow' && self.tower.level === 2) { fireRateDisplay = '0.4'; // Special case for slow tower level 2 } else if (self.tower.id === 'slow' && self.tower.level === 3) { fireRateDisplay = '0.5'; // Special case for slow tower level 3 } else if (self.tower.id === 'slow' && self.tower.level === 4) { fireRateDisplay = '0.6'; // Special case for slow tower level 4 } else if (self.tower.id === 'slow' && self.tower.level === 5) { fireRateDisplay = '0.7'; // Special case for slow tower level 5 } else if (self.tower.id === 'slow' && self.tower.level === 6) { fireRateDisplay = '0.8'; // Special case for slow tower level 6 } else if (self.tower.id === 'rapid' && self.tower.level === 2) { fireRateDisplay = '2.0'; // Special case for rapid tower level 2 } else if (self.tower.id === 'rapid' && self.tower.level === 3) { fireRateDisplay = '2.5'; // Special case for rapid tower level 3 } else if (self.tower.id === 'rapid' && self.tower.level === 4) { fireRateDisplay = '2.5'; // Special case for rapid tower level 4 } else if (self.tower.id === 'rapid' && self.tower.level === 5) { fireRateDisplay = '2.5'; // Special case for rapid tower level 5 } else if (self.tower.id === 'rapid' && self.tower.level === 6) { fireRateDisplay = '2.5'; // Special case for rapid tower level 6 } else if (self.tower.id === 'sniper' && self.tower.level === 2) { fireRateDisplay = '0.9'; // Special case for sniper tower level 2 } else if (self.tower.id === 'sniper' && self.tower.level === 3) { fireRateDisplay = '1.2'; // Special case for sniper tower level 3 } else if (self.tower.id === 'sniper' && self.tower.level === 4) { fireRateDisplay = '1.5'; // Special case for sniper tower level 4 } else if (self.tower.id === 'sniper' && self.tower.level === 5) { fireRateDisplay = '1.5'; // Special case for sniper tower level 5 } else if (self.tower.id === 'sniper' && self.tower.level === 6) { fireRateDisplay = '1.7'; // Special case for sniper tower level 6 } else if (self.tower.id === 'splash' && self.tower.level === 2) { fireRateDisplay = '0.9'; // Special case for splash tower level 2 } else if (self.tower.id === 'splash' && self.tower.level === 6) { fireRateDisplay = '1.8'; // Special case for splash tower level 6 } else { fireRateDisplay = (60 / self.tower.fireRate).toFixed(1); } var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + damageText + '\nFire Rate: ' + fireRateDisplay + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.id === 'default' && self.tower.level === 4) { // Special case: default tower level 4→5 upgrade costs 35 upgradeCost = 35; } else if (self.tower.id === 'default' && self.tower.level === 5) { // Special case: default tower level 5→6 upgrade costs 50 upgradeCost = 50; } else if (self.tower.id === 'rapid' && self.tower.level === 3) { // Special case: rapid tower level 3→4 upgrade costs 45 upgradeCost = 45; } else if (self.tower.id === 'rapid' && self.tower.level === 4) { // Special case: rapid tower level 4→5 upgrade costs 60 upgradeCost = 60; } else if (self.tower.id === 'rapid' && self.tower.level === 5) { // Special case: rapid tower level 5→6 upgrade costs 75 upgradeCost = 75; } else if (self.tower.id === 'sniper' && self.tower.level === 4) { // Special case: sniper tower level 4→5 upgrade costs 100 upgradeCost = 100; } else if (self.tower.id === 'sniper' && self.tower.level === 3) { // Special case: sniper tower level 3→4 upgrade costs 75 upgradeCost = 75; } else if (self.tower.id === 'sniper' && self.tower.level === 4) { // Special case: sniper tower level 4→5 upgrade costs 100 upgradeCost = 100; } else if (self.tower.id === 'sniper' && self.tower.level === 5) { // Special case: sniper tower level 5→6 upgrade costs 125 upgradeCost = 125; } else if (self.tower.id === 'sniper' && self.tower.level === 5) { // Special case: sniper tower level 5→6 upgrade costs 125 upgradeCost = 125; } else if (self.tower.id === 'splash' && self.tower.level === 3) { // Special case: splash tower level 3→4 upgrade costs 95 upgradeCost = 95; } else if (self.tower.id === 'splash' && self.tower.level === 4) { // Special case: splash tower level 4→5 upgrade costs 125 upgradeCost = 125; } else if (self.tower.id === 'splash' && self.tower.level === 5) { // Special case: splash tower level 5→6 upgrade costs 155 upgradeCost = 155; } else if (self.tower.id === 'splash' && self.tower.level === 4) { // Special case: splash tower level 4→5 upgrade costs 125 upgradeCost = 125; } else if (self.tower.id === 'slow' && self.tower.level === 1) { // Special case: slow tower level 1→2 upgrade costs 45 upgradeCost = 45; } else if (self.tower.id === 'slow' && self.tower.level === 2) { // Special case: slow tower level 2→3 upgrade costs 90 upgradeCost = 90; } else if (self.tower.id === 'slow' && self.tower.level === 3) { // Special case: slow tower level 3→4 upgrade costs 135 upgradeCost = 135; } else if (self.tower.id === 'slow' && self.tower.level === 4) { // Special case: slow tower level 4→5 upgrade costs 180 upgradeCost = 180; } else if (self.tower.id === 'slow' && self.tower.level === 5) { // Special case: slow tower level 5→6 upgrade costs 200 upgradeCost = 200; } else if (self.tower.id === 'poison' && self.tower.level === 1) { // Special case: poison tower level 1→2 upgrade costs 55 upgradeCost = 55; } else if (self.tower.id === 'poison' && self.tower.level === 2) { // Special case: poison tower level 2→3 upgrade costs 105 upgradeCost = 105; } else if (self.tower.id === 'poison' && self.tower.level === 3) { // Special case: poison tower level 3→4 upgrade costs 160 upgradeCost = 160; } else if (self.tower.id === 'poison' && self.tower.level === 4) { // Special case: poison tower level 4→5 upgrade costs 215 upgradeCost = 215; } else if (self.tower.id === 'poison' && self.tower.level === 5) { // Special case: poison tower level 5→6 upgrade costs 250 upgradeCost = 250; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment, self.tower); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.id === 'default' && self.tower.level === 4) { // Special case: default tower level 4→5 upgrade costs 35 upgradeCost = 35; } else if (self.tower.id === 'default' && self.tower.level === 5) { // Special case: default tower level 5→6 upgrade costs 50 upgradeCost = 50; } else if (self.tower.id === 'rapid' && self.tower.level === 3) { // Special case: rapid tower level 3→4 upgrade costs 45 upgradeCost = 45; } else if (self.tower.id === 'rapid' && self.tower.level === 4) { // Special case: rapid tower level 4→5 upgrade costs 60 upgradeCost = 60; } else if (self.tower.id === 'rapid' && self.tower.level === 5) { // Special case: rapid tower level 5→6 upgrade costs 75 upgradeCost = 75; } else if (self.tower.id === 'sniper' && self.tower.level === 3) { // Special case: sniper tower level 3→4 upgrade costs 75 upgradeCost = 75; } else if (self.tower.id === 'sniper' && self.tower.level === 4) { // Special case: sniper tower level 4→5 upgrade costs 100 upgradeCost = 100; } else if (self.tower.id === 'sniper' && self.tower.level === 5) { // Special case: sniper tower level 5→6 upgrade costs 125 upgradeCost = 125; } else if (self.tower.id === 'splash' && self.tower.level === 3) { // Special case: splash tower level 3→4 upgrade costs 95 upgradeCost = 95; } else if (self.tower.id === 'splash' && self.tower.level === 4) { // Special case: splash tower level 4→5 upgrade costs 125 upgradeCost = 125; } else if (self.tower.id === 'splash' && self.tower.level === 5) { // Special case: splash tower level 5→6 upgrade costs 155 upgradeCost = 155; } else if (self.tower.id === 'slow' && self.tower.level === 1) { // Special case: slow tower level 1→2 upgrade costs 45 upgradeCost = 45; } else if (self.tower.id === 'slow' && self.tower.level === 2) { // Special case: slow tower level 2→3 upgrade costs 90 upgradeCost = 90; } else if (self.tower.id === 'slow' && self.tower.level === 3) { // Special case: slow tower level 3→4 upgrade costs 135 upgradeCost = 135; } else if (self.tower.id === 'slow' && self.tower.level === 4) { // Special case: slow tower level 4→5 upgrade costs 180 upgradeCost = 180; } else if (self.tower.id === 'slow' && self.tower.level === 5) { // Special case: slow tower level 5→6 upgrade costs 200 upgradeCost = 200; } else if (self.tower.id === 'poison' && self.tower.level === 1) { // Special case: poison tower level 1→2 upgrade costs 55 upgradeCost = 55; } else if (self.tower.id === 'poison' && self.tower.level === 2) { // Special case: poison tower level 2→3 upgrade costs 105 upgradeCost = 105; } else if (self.tower.id === 'poison' && self.tower.level === 3) { // Special case: poison tower level 3→4 upgrade costs 160 upgradeCost = 160; } else if (self.tower.id === 'poison' && self.tower.level === 4) { // Special case: poison tower level 4→5 upgrade costs 215 upgradeCost = 215; } else if (self.tower.id === 'poison' && self.tower.level === 5) { // Special case: poison tower level 5→6 upgrade costs 250 upgradeCost = 250; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } // Update damage text based on tower type var damageText; if (self.tower.id === 'slow') { // For slow towers, show percentage damage var damagePercentages = [0.1, 0.12, 0.14, 0.16, 0.18, 0.2]; var idx = Math.max(0, Math.min(5, self.tower.level - 1)); var damagePercentage = damagePercentages[idx]; damageText = (damagePercentage * 100).toFixed(0) + '% HP'; } else if (self.tower.id === 'poison' && self.tower.level === 6) { // For poison tower level 6, show specific damage value damageText = '44'; } else { // For other towers, show regular damage damageText = self.tower.damage.toString(); } // Calculate fire rate display - handle special cases for slow tower var fireRateDisplay; if (self.tower.id === 'slow' && self.tower.level === 2) { fireRateDisplay = '0.4'; // Special case for slow tower level 2 } else if (self.tower.id === 'slow' && self.tower.level === 3) { fireRateDisplay = '0.5'; // Special case for slow tower level 3 } else if (self.tower.id === 'slow' && self.tower.level === 4) { fireRateDisplay = '0.6'; // Special case for slow tower level 4 } else if (self.tower.id === 'slow' && self.tower.level === 5) { fireRateDisplay = '0.7'; // Special case for slow tower level 5 } else if (self.tower.id === 'slow' && self.tower.level === 6) { fireRateDisplay = '0.8'; // Special case for slow tower level 6 } else if (self.tower.id === 'rapid' && self.tower.level === 2) { fireRateDisplay = '2.0'; // Special case for rapid tower level 2 } else if (self.tower.id === 'rapid' && self.tower.level === 3) { fireRateDisplay = '2.5'; // Special case for rapid tower level 3 } else if (self.tower.id === 'rapid' && self.tower.level === 4) { fireRateDisplay = '2.5'; // Special case for rapid tower level 4 } else if (self.tower.id === 'rapid' && self.tower.level === 5) { fireRateDisplay = '2.5'; // Special case for rapid tower level 5 } else if (self.tower.id === 'rapid' && self.tower.level === 6) { fireRateDisplay = '2.5'; // Special case for rapid tower level 6 } else if (self.tower.id === 'sniper' && self.tower.level === 2) { fireRateDisplay = '0.9'; // Special case for sniper tower level 2 } else if (self.tower.id === 'sniper' && self.tower.level === 3) { fireRateDisplay = '1.2'; // Special case for sniper tower level 3 } else if (self.tower.id === 'sniper' && self.tower.level === 4) { fireRateDisplay = '1.5'; // Special case for sniper tower level 4 } else if (self.tower.id === 'sniper' && self.tower.level === 5) { fireRateDisplay = '1.5'; // Special case for sniper tower level 5 } else if (self.tower.id === 'sniper' && self.tower.level === 6) { fireRateDisplay = '1.7'; // Special case for sniper tower level 6 } else if (self.tower.id === 'splash' && self.tower.level === 2) { fireRateDisplay = '0.9'; // Special case for splash tower level 2 } else if (self.tower.id === 'splash' && self.tower.level === 6) { fireRateDisplay = '1.8'; // Special case for splash tower level 6 } else { fireRateDisplay = (60 / self.tower.fireRate).toFixed(1); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + damageText + '\nFire Rate: ' + fireRateDisplay + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment, self.tower); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment, self.tower); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } // Removed selectedTower clearing to prevent map darkening var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); pathfindDirty = true; // Mark pathfind cache as dirty grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.id === 'default' && self.tower.level === 4) { // Special case: default tower level 4→5 upgrade costs 35 currentUpgradeCost = 35; } else if (self.tower.id === 'default' && self.tower.level === 5) { // Special case: default tower level 5→6 upgrade costs 50 currentUpgradeCost = 50; } else if (self.tower.id === 'rapid' && self.tower.level === 3) { // Special case: rapid tower level 3→4 upgrade costs 45 currentUpgradeCost = 45; } else if (self.tower.id === 'rapid' && self.tower.level === 4) { // Special case: rapid tower level 4→5 upgrade costs 60 currentUpgradeCost = 60; } else if (self.tower.id === 'rapid' && self.tower.level === 5) { // Special case: rapid tower level 5→6 upgrade costs 75 currentUpgradeCost = 75; } else if (self.tower.id === 'sniper' && self.tower.level === 3) { // Special case: sniper tower level 3→4 upgrade costs 75 currentUpgradeCost = 75; } else if (self.tower.id === 'sniper' && self.tower.level === 4) { // Special case: sniper tower level 4→5 upgrade costs 100 currentUpgradeCost = 100; } else if (self.tower.id === 'sniper' && self.tower.level === 5) { // Special case: sniper tower level 5→6 upgrade costs 125 currentUpgradeCost = 125; } else if (self.tower.id === 'splash' && self.tower.level === 3) { // Special case: splash tower level 3→4 upgrade costs 95 currentUpgradeCost = 95; } else if (self.tower.id === 'splash' && self.tower.level === 4) { // Special case: splash tower level 4→5 upgrade costs 125 currentUpgradeCost = 125; } else if (self.tower.id === 'splash' && self.tower.level === 5) { // Special case: splash tower level 5→6 upgrade costs 155 currentUpgradeCost = 155; } else if (self.tower.id === 'slow' && self.tower.level === 1) { // Special case: slow tower level 1→2 upgrade costs 45 currentUpgradeCost = 45; } else if (self.tower.id === 'slow' && self.tower.level === 2) { // Special case: slow tower level 2→3 upgrade costs 90 currentUpgradeCost = 90; } else if (self.tower.id === 'slow' && self.tower.level === 3) { // Special case: slow tower level 3→4 upgrade costs 135 currentUpgradeCost = 135; } else if (self.tower.id === 'slow' && self.tower.level === 4) { // Special case: slow tower level 4→5 upgrade costs 180 currentUpgradeCost = 180; } else if (self.tower.id === 'slow' && self.tower.level === 5) { // Special case: slow tower level 5→6 upgrade costs 200 currentUpgradeCost = 200; } else if (self.tower.id === 'poison' && self.tower.level === 1) { // Special case: poison tower level 1→2 upgrade costs 55 currentUpgradeCost = 55; } else if (self.tower.id === 'poison' && self.tower.level === 2) { // Special case: poison tower level 2→3 upgrade costs 105 currentUpgradeCost = 105; } else if (self.tower.id === 'poison' && self.tower.level === 3) { // Special case: poison tower level 3→4 upgrade costs 160 currentUpgradeCost = 160; } else if (self.tower.id === 'poison' && self.tower.level === 4) { // Special case: poison tower level 4→5 upgrade costs 215 currentUpgradeCost = 215; } else if (self.tower.id === 'poison' && self.tower.level === 5) { // Special case: poison tower level 5→6 upgrade costs 250 currentUpgradeCost = 250; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0xFFFF00; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Boss Normal"; break; case 'fast': block.tint = 0x00AAFF; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xAA0000; waveType = "Boss Immune"; break; case 'flying': block.tint = 0xFFFF00; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the color of the wave type switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; break; case 'fast': block.tint = 0x00AAFF; break; case 'immune': block.tint = 0xAA0000; break; case 'flying': block.tint = 0xFFFF00; break; default: block.tint = 0xFF0000; break; } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a crown or some indicator to the wave marker for boss waves var bossIndicator = marker.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); bossIndicator.width = 30; bossIndicator.height = 30; bossIndicator.tint = 0xFFD700; // Gold color bossIndicator.y = -block.height / 2 - 15; // Change the wave type text to indicate boss waveType = "BOSS"; } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2(waveType, { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (every 10th wave) if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; for (var i = 0; i < totalWaves + 1; i++) { var marker = self.waveMarkers[i]; if (i === 0) { continue; } var block = marker.children[0]; if (i - 1 < currentWave) { block.alpha = .5; } } 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 isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } // Performance tracking variables var frameCounter = 0; var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var pathfindCache = null; var pathfindDirty = false; var enemies = []; var towers = []; var bullets = []; var enemyGrid = {}; // Spatial partitioning for enemy collision detection var SPATIAL_GRID_SIZE = CELL_SIZE * 2; // Size of spatial grid cells var defenses = []; var bulletPool = []; // Pool for reusing bullet objects var effectPool = []; // Pool for reusing effect objects var selectedTower = null; var gold = 99999; var lives = 20; var score = 1000; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave // Define ruta1 route for enemies that reach cell '1' var ruta1 = [129, 121, 275, 294, 52, 45, 397, 385, 539, 547, 605]; // Route cells (excluding those with -b suffix) var enemiesOnRuta1 = []; // Track enemies following ruta1 // Define ruta2 route for enemies that reach cell '2' var ruta2 = [152, 144, 254, 271, 73, 68, 420, 408, 518, 526]; // Route cells (excluding those with -b suffix) var enemiesOnRuta2 = []; // Track enemies following ruta2 // Define ruta3 route for enemies that reach cell '3' var ruta3 = [41, 129, 121, 275, 294, 52, 45, 397, 385, 539, 547, 605]; // Route cells (excluding those with -b suffix) var enemiesOnRuta3 = []; // Track enemies following ruta3 // Define ruta4 route for enemies that reach cell '4' var ruta4 = [41, 129, 121, 275, 294, 52, 45, 397, 385, 539, 547, 605]; // Route cells (excluding those with -b suffix) var enemiesOnRuta4 = []; // Track enemies following ruta4 // Define ruta5 route for enemies that reach cell '5' var ruta5 = [42, 152, 144, 254, 271, 73, 68, 420, 408, 518, 526]; // Route cells (excluding those with -b suffix) var enemiesOnRuta5 = []; // Track enemies following ruta5 // Define ruta6 route for enemies that reach cell '6' var ruta6 = [42, 152, 144, 254, 271, 73, 68, 420, 408, 518, 526]; // Route cells (excluding those with -b suffix) var enemiesOnRuta6 = []; // Track enemies following ruta6 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); livesText.visible = false; var scoreAsset = LK.getAsset('score', { anchorX: 0, anchorY: 0.5, scaleX: 2.0, scaleY: 2.0 }); // Container to hold all digit assets var scoreDigitsContainer = new Container(); var scoreDigits = []; // Array to hold individual digit assets var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(scoreAsset); LK.gui.top.addChild(scoreDigitsContainer); goldText.x = -spacing; goldText.y = topMargin; scoreAsset.x = spacing - 150; scoreAsset.y = topMargin; scoreDigitsContainer.x = spacing + 150; scoreDigitsContainer.y = topMargin; function getSpatialKey(x, y) { var gridX = Math.floor(x / SPATIAL_GRID_SIZE); var gridY = Math.floor(y / SPATIAL_GRID_SIZE); return gridX + "," + gridY; } function addEnemyToSpatialGrid(enemy) { var key = getSpatialKey(enemy.x, enemy.y); if (!enemyGrid[key]) enemyGrid[key] = []; enemyGrid[key].push(enemy); enemy._spatialKey = key; } function removeEnemyFromSpatialGrid(enemy) { if (enemy._spatialKey && enemyGrid[enemy._spatialKey]) { var index = enemyGrid[enemy._spatialKey].indexOf(enemy); if (index !== -1) { enemyGrid[enemy._spatialKey].splice(index, 1); } if (enemyGrid[enemy._spatialKey].length === 0) { delete enemyGrid[enemy._spatialKey]; } } } function updateScoreDigits() { // Clear existing digit assets for (var i = 0; i < scoreDigits.length; i++) { scoreDigitsContainer.removeChild(scoreDigits[i]); } scoreDigits = []; // Convert score to string to get individual digits var scoreString = score.toString(); var digitWidth = 40; // Width of each digit asset var totalWidth = scoreString.length * digitWidth; var startX = -totalWidth / 2; // Center the digits // Create asset for each digit for (var i = 0; i < scoreString.length; i++) { var digitChar = scoreString.charAt(i); var digitAsset = LK.getAsset(digitChar, { anchorX: 0.5, anchorY: 0.5 }); digitAsset.x = startX + i * digitWidth + digitWidth / 2; digitAsset.y = 0; scoreDigitsContainer.addChild(digitAsset); scoreDigits.push(digitAsset); } } function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); updateScoreDigits(); updateHealthBar(); // Update health bar when UI updates } // Cached UI update to reduce expensive text operations var lastUIUpdate = 0; function updateUICached() { if (LK.ticks - lastUIUpdate > 10) { // Update UI every 10 frames instead of every frame updateUI(); lastUIUpdate = LK.ticks; } } // Initialize score digits display at game start updateScoreDigits(); 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(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; var dragClone = null; function wouldBlockPath(gridX, gridY) { var cells = []; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } pathfindDirty = true; // Force pathfind recalculation var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } pathfindDirty = true; // Force pathfind recalculation grid.pathFind(); // Only render debug every few frames to reduce overhead if (frameCounter % 3 === 0) grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 5; switch (towerType) { case 'rapid': cost = 15; break; case 'sniper': cost = 25; break; case 'splash': cost = 35; break; case 'slow': cost = 45; break; case 'poison': cost = 55; break; } return cost; } function getTowerSellValue(totalValue, tower) { // Special sell values for default tower based on level if (tower && tower.id === 'default') { switch (tower.level) { case 1: return 5; case 2: return 7; case 3: return 14; case 4: return 28; case 5: return 53; case 6: return 88; default: return Math.floor(totalValue * 0.6); } } // Special sell values for rapid tower based on level if (tower && tower.id === 'rapid') { switch (tower.level) { case 1: return 15; case 2: return 21; case 3: return 42; case 4: return 74; case 5: return 116; case 6: return 168; default: return Math.floor(totalValue * 0.6); } } // Special sell values for sniper tower based on level if (tower && tower.id === 'sniper') { switch (tower.level) { case 1: return 25; case 2: return 35; case 3: return 70; case 4: return 123; case 5: return 193; case 6: return 280; default: return Math.floor(totalValue * 0.6); } } // Special sell values for splash tower based on level if (tower && tower.id === 'splash') { switch (tower.level) { case 1: return 35; case 2: return 49; case 3: return 98; case 4: return 165; case 5: return 252; case 6: return 360; default: return Math.floor(totalValue * 0.6); } } // Special sell values for slow tower based on level if (tower && tower.id === 'slow') { switch (tower.level) { case 1: return 45; case 2: return 63; case 3: return 126; case 4: return 220; case 5: return 347; case 6: return 487; default: return Math.floor(totalValue * 0.6); } } // Special sell values for poison tower based on level if (tower && tower.id === 'poison') { switch (tower.level) { case 1: return 55; case 2: return 77; case 3: return 150; case 4: return 263; case 5: return 413; case 6: return 588; default: return Math.floor(totalValue * 0.6); } } return Math.floor(totalValue * 0.6); } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); pathfindDirty = true; // Mark pathfind cache as dirty grid.pathFind(); // Only render debug every few frames to reduce overhead if (frameCounter % 3 === 0) grid.renderDebug(); return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } // List of cell numbers where towers cannot be placed var excludedTowerCells = [1, 2, 41, 42, 63, 64, 85, 86, 107, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 165, 166, 187, 188, 209, 210, 231, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 51, 52, 73, 74, 95, 96, 117, 118, 139, 140, 161, 162, 183, 184, 205, 206, 227, 228, 249, 250, 45, 46, 47, 48, 49, 50, 67, 68, 69, 70, 71, 72, 89, 90, 111, 112, 133, 134, 155, 156, 177, 178, 199, 200, 221, 222, 243, 244, 309, 310, 331, 332, 353, 354, 375, 376, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 429, 430, 451, 452, 473, 474, 495, 496, 517, 518, 539, 540, 519, 520, 521, 522, 523, 524, 525, 526, 541, 542, 543, 544, 545, 546, 547, 548, 569, 570, 591, 592, 605, 606]; game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { // Create a clone of the tower for dragging dragClone = new Tower(tower.towerType); dragClone.x = x; dragClone.y = y - CELL_SIZE * 1.5; dragClone.alpha = 0.7; // Make it semi-transparent to show it's being dragged dragClone.canFire = false; // Prevent drag clones from firing game.addChild(dragClone); // Hide the tower preview to avoid overlapping with dragClone towerPreview.visible = false; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); // Apply the same offset as in move handler to ensure consistency when starting drag towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); break; } } }; game.move = function (x, y, obj) { if (isDragging) { // Update dragClone position if (dragClone) { dragClone.x = x; dragClone.y = y - CELL_SIZE * 1.5; } // Update towerPreview position for placement validation but keep it hidden towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } }; game.up = function (x, y, obj) { var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } // Removed selectedTower clearing and grid render debug to prevent map darkening } } if (isDragging) { isDragging = false; // Destroy the drag clone if (dragClone) { dragClone.destroy(); dragClone = null; } // Temporarily show towerPreview for placement validation towerPreview.visible = true; if (towerPreview.canPlace) { if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } // Hide towerPreview again after placement attempt towerPreview.visible = false; if (isDragging) { var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } } }; var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2; waveIndicator.y = 2732 - 80; game.addChild(waveIndicator); var nestWaveButton = LK.getAsset('nestwave', { anchorX: 0.5, anchorY: 0.5 }); nestWaveButton.x = 2048 - 312.5; // Move right (was 412.5, now 312.5 - moved 100px right) nestWaveButton.y = 2732 - 37.5 + 50; // Move down (was -50 offset, now +50 offset - moved 100px down) nestWaveButton.enabled = false; nestWaveButton.visible = true; nestWaveButton.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { nestWaveButton.enabled = true; nestWaveButton.visible = true; tween(nestWaveButton, { alpha: 1 }, { duration: 300, easing: tween.easeOut }); } else { nestWaveButton.enabled = false; nestWaveButton.visible = true; tween(nestWaveButton, { alpha: 1 }, { duration: 300, easing: tween.easeOut }); } }; // nestwave asset is now non-clickable - down method removed game.addChild(nestWaveButton); var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; var sourceTowers = []; var towerSpacing = 300; // Increase spacing for larger towers var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 60; // Moved towers up by 10 pixels (from -50 to -60) for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; // Position pared1 asset at bottom left var pared1Asset = LK.getAsset('pared1', { anchorX: 0, anchorY: 1 }); pared1Asset.x = 0; pared1Asset.y = 2732; game.addChild(pared1Asset); // Position pared3 asset at top center var pared3Asset = LK.getAsset('pared3', { anchorX: 0.5, anchorY: 0 }); pared3Asset.x = 2048 / 2; // Center horizontally pared3Asset.y = 0; // Top of screen game.addChild(pared3Asset); // Position pared2 asset at bottom right and make it act as nextwave button // Create health bar at bottom left var healthBarOutlineUI = LK.getAsset('healthBarOutline', { anchorX: 0, anchorY: 1 }); healthBarOutlineUI.x = 100; // 100px from left edge healthBarOutlineUI.y = 2732 - 50; // 50px from bottom healthBarOutlineUI.width = 450; // Make it much wider for better visibility healthBarOutlineUI.height = 65; // Make it much taller game.addChild(healthBarOutlineUI); var healthBarBGUI = LK.getAsset('healthBar', { anchorX: 0, anchorY: 1 }); healthBarBGUI.x = 102; // Slight offset from outline healthBarBGUI.y = 2732 - 52; // Slight offset from outline healthBarBGUI.width = 446; // Slightly smaller than outline healthBarBGUI.height = 61; // Slightly smaller than outline healthBarBGUI.tint = 0xff0000; // Red background game.addChild(healthBarBGUI); var healthBarFillUI = LK.getAsset('healthBar', { anchorX: 0, anchorY: 1 }); healthBarFillUI.x = 102; // Same as background healthBarFillUI.y = 2732 - 52; // Same as background healthBarFillUI.width = 446; // Full width initially healthBarFillUI.height = 61; // Same as background healthBarFillUI.tint = 0x00ff00; // Green fill game.addChild(healthBarFillUI); // Add HP asset above the health bar var hpAsset = LK.getAsset('hp', { anchorX: 0.5, anchorY: 1 }); hpAsset.x = 325; // Center of health bar (102 + 446/2) hpAsset.y = 2732 - 110; // Above the health bar, moved down 20 pixels game.addChild(hpAsset); // Add HP text above the health bar var hpText = new Text2(lives + '/20', { size: 50, fill: 0xFFFFFF, weight: 800 }); hpText.anchor.set(0.5, 1); // Center horizontally, bottom anchor hpText.x = 325; // Center of health bar (102 + 446/2) hpText.y = 2732 - 52; // Above the health bar, moved up by 10 pixels (was -42, now -52) game.addChild(hpText); // Function to update health bar function updateHealthBar() { var healthPercentage = lives / 20; // Assuming max lives is 20 healthBarFillUI.width = 446 * healthPercentage; // Update HP text if (hpText) { hpText.setText(lives + '/20'); } } // Initialize health bar display after UI elements are created updateHealthBar(); var pared2Asset = LK.getAsset('pared2', { anchorX: 1, anchorY: 1 }); pared2Asset.x = 2048 - 500; // Move to the right (moved 50px right from previous position) pared2Asset.y = 2732 - 10; // Move down a few pixels (was -20, now -10) pared2Asset.enabled = false; pared2Asset.visible = false; pared2Asset.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { pared2Asset.enabled = true; pared2Asset.visible = true; pared2Asset.alpha = 1; } else if (waveIndicator && waveIndicator.gameStarted) { pared2Asset.enabled = false; pared2Asset.visible = true; pared2Asset.alpha = 0.7; } else { pared2Asset.enabled = false; pared2Asset.visible = false; pared2Asset.alpha = 0.7; } }; pared2Asset.down = function () { if (!pared2Asset.enabled) { return; } // Add button press animation - shrink and return to original size tween(pared2Asset, { scaleX: 0.9, scaleY: 0.9 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(pared2Asset, { scaleX: 1.0, scaleY: 1.0 }, { duration: 150, easing: tween.easeOut }); } }); if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; game.addChild(pared2Asset); // Tutorial menu variables var showingTutorialMenu = true; var tutorialMenu = null; // Create tutorial menu function createTutorialMenu() { tutorialMenu = new Container(); tutorialMenu.x = 2048 / 2; tutorialMenu.y = 2732 / 2; // Semi-transparent background var overlay = tutorialMenu.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); overlay.width = 800; overlay.height = 400; overlay.tint = 0x000000; overlay.alpha = 0.8; // Title text var titleText = new Text2("Do you need a tutorial?", { size: 70, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -80; tutorialMenu.addChild(titleText); // Yes button var yesButton = new Container(); var yesButtonBg = yesButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); yesButtonBg.width = 200; yesButtonBg.height = 80; yesButtonBg.tint = 0x00AA00; var yesText = new Text2("Yes", { size: 50, fill: 0xFFFFFF, weight: 800 }); yesText.anchor.set(0.5, 0.5); yesButton.addChild(yesText); yesButton.x = -120; yesButton.y = 40; tutorialMenu.addChild(yesButton); // No button var noButton = new Container(); var noButtonBg = noButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); noButtonBg.width = 200; noButtonBg.height = 80; noButtonBg.tint = 0xAA0000; var noText = new Text2("No", { size: 50, fill: 0xFFFFFF, weight: 800 }); noText.anchor.set(0.5, 0.5); noButton.addChild(noText); noButton.x = 120; noButton.y = 40; tutorialMenu.addChild(noButton); // Button handlers yesButton.down = function () { // Hide all game elements game.visible = false; LK.gui.visible = false; // Create pergamino background var pergaminoBackground = LK.getAsset('pergamino', { anchorX: 0.5, anchorY: 0.5 }); pergaminoBackground.x = 2048 / 2; pergaminoBackground.y = 2732 / 2; pergaminoBackground.width = 2048; pergaminoBackground.height = 2732; // Add pergamino to a new container that bypasses the game container var tutorialContainer = new Container(); tutorialContainer.addChild(pergaminoBackground); // Add bold white 'TROOPS' text at the top center var troopTitleText = new Text2("TROOPS", { size: 120, fill: 0xFFFFFF, weight: 800 }); troopTitleText.anchor.set(0.5, 0.5); troopTitleText.x = 2048 / 2; troopTitleText.y = 200; // Positioned at the top, moved down tutorialContainer.addChild(troopTitleText); // Add bold white 'ENEMIES' text matching 'TROOPS' text style and position var enemiesTitleText = new Text2("ENEMIES", { size: 120, fill: 0xFFFFFF, weight: 800 }); enemiesTitleText.anchor.set(0.5, 0.5); enemiesTitleText.x = 2048 / 2; enemiesTitleText.y = 200; // Same position as TROOPS text enemiesTitleText.visible = false; // Hide ENEMIES text initially tutorialContainer.addChild(enemiesTitleText); // Create tutorial section with tower assets var tutorialSection = new Container(); tutorialSection.x = 2048 / 2; tutorialSection.y = 300; // Start from top area // Tower types in the specified order var towerAssets = ['tower_default', 'tower_rapid', 'tower_sniper', 'tower_splash', 'tower_slow', 'tower_poison']; var towerSpacing = 300; // Reduced vertical spacing between towers // Create and position tower assets vertically for (var i = 0; i < towerAssets.length; i++) { var towerAsset = LK.getAsset(towerAssets[i], { anchorX: 0.5, anchorY: 0.5 }); towerAsset.x = -400; // Moved to the left towerAsset.y = 250 + i * towerSpacing; // Positioned vertically below title, moved down by 100px towerAsset.width = 250; // Scale to larger size towerAsset.height = 250; tutorialSection.addChild(towerAsset); } // Position enemy assets to match tower assets size and position var enemyAssets = ['enemy', 'enemy_immune', 'enemy_fast', 'enemy_swarm', 'enemy_flying']; var enemySpacing = 300; // Match tower spacing for (var i = 0; i < enemyAssets.length; i++) { var enemyAsset = LK.getAsset(enemyAssets[i], { anchorX: 0.5, anchorY: 0.5 }); // Position enemy assets to match corresponding tower positions if (i === 0) { // enemy matches default tower enemyAsset.x = -400; // Same as default tower enemyAsset.y = 250; // Same as default tower enemyAsset.width = 250; // Same as default tower enemyAsset.height = 250; // Same as default tower enemyAsset.scaleY = 1.3; // Stretch vertically } else if (i === 1) { // enemy_immune matches rapid tower enemyAsset.x = -400; // Same as rapid tower enemyAsset.y = 250 + 1 * enemySpacing; // Same as rapid tower enemyAsset.width = 250; // Same as rapid tower enemyAsset.height = 250; // Same as rapid tower } else if (i === 2) { // enemy_fast matches sniper tower enemyAsset.x = -400; // Same as sniper tower enemyAsset.y = 250 + 2 * enemySpacing; // Same as sniper tower enemyAsset.width = 250; // Same as sniper tower enemyAsset.height = 250; // Same as sniper tower } else if (i === 3) { // enemy_swarm matches splash tower enemyAsset.x = -400; // Same as splash tower enemyAsset.y = 250 + 3 * enemySpacing; // Same as splash tower enemyAsset.width = 125; // Half the size of splash tower enemyAsset.height = 125; // Half the size of splash tower enemyAsset.scaleY = 1.3; // Stretch vertically } else if (i === 4) { // enemy_flying matches slow tower enemyAsset.x = -400; // Same as slow tower enemyAsset.y = 250 + 4 * enemySpacing; // Same as slow tower enemyAsset.width = 250; // Same as slow tower enemyAsset.height = 250; // Same as slow tower } enemyAsset.visible = false; // Hide enemy assets initially tutorialSection.addChild(enemyAsset); // Add description text for the default tower (Pink Ivy) if (i === 0) { // First tower is tower_default var towerNameText = new Text2("Pink Ivy", { size: 60, fill: 0xFFFFFF, weight: 800 }); towerNameText.anchor.set(0, 0.5); towerNameText.x = -200; // To the right of the tower asset towerNameText.y = 250 + i * towerSpacing - 50; // Moved higher up tutorialSection.addChild(towerNameText); var towerDescText = new Text2("Fires a projectile in each of the\n8 directions.", { size: 55, fill: 0xFFFFFF, weight: 400 }); towerDescText.anchor.set(0, 0.5); towerDescText.x = -200; // To the right of the tower asset towerDescText.y = 250 + i * towerSpacing + 30; // Slightly below center tutorialSection.addChild(towerDescText); } // Add description text for the rapid tower (Wizard) if (i === 1) { // Second tower is tower_rapid var wizardNameText = new Text2("Wizard", { size: 60, fill: 0xFFFFFF, weight: 800 }); wizardNameText.anchor.set(0, 0.5); wizardNameText.x = -200; // To the right of the tower asset wizardNameText.y = 250 + i * towerSpacing - 50; // Moved higher up tutorialSection.addChild(wizardNameText); var wizardDescText = new Text2("Deals low damage and has short range,\nbut fires very quickly.", { size: 55, fill: 0xFFFFFF, weight: 400 }); wizardDescText.anchor.set(0, 0.5); wizardDescText.x = -200; // To the right of the tower asset wizardDescText.y = 250 + i * towerSpacing + 30; // Slightly below center tutorialSection.addChild(wizardDescText); } // Add description text for the sniper tower (Pyromancer) if (i === 2) { // Third tower is tower_sniper var pyromancerNameText = new Text2("Pyromancer", { size: 60, fill: 0xFFFFFF, weight: 800 }); pyromancerNameText.anchor.set(0, 0.5); pyromancerNameText.x = -200; // To the right of the tower asset pyromancerNameText.y = 250 + i * towerSpacing - 50; // Moved higher up tutorialSection.addChild(pyromancerNameText); var pyromancerDescText = new Text2("Deals high damage and has long range,\nbut fires more slowly.", { size: 55, fill: 0xFFFFFF, weight: 400 }); pyromancerDescText.anchor.set(0, 0.5); pyromancerDescText.x = -200; // To the right of the tower asset pyromancerDescText.y = 250 + i * towerSpacing + 30; // Slightly below center tutorialSection.addChild(pyromancerDescText); } // Add description text for the splash tower (Catapult) if (i === 3) { // Fourth tower is tower_splash var catapultNameText = new Text2("Catapult", { size: 60, fill: 0xFFFFFF, weight: 800 }); catapultNameText.anchor.set(0, 0.5); catapultNameText.x = -200; // To the right of the tower asset catapultNameText.y = 250 + i * towerSpacing - 50; // Moved higher up tutorialSection.addChild(catapultNameText); var catapultDescText = new Text2("Launches a stone ball that deals damage\nin a 2x2 tile area.", { size: 55, fill: 0xFFFFFF, weight: 400 }); catapultDescText.anchor.set(0, 0.5); catapultDescText.x = -200; // To the right of the tower asset catapultDescText.y = 250 + i * towerSpacing + 30; // Slightly below center tutorialSection.addChild(catapultDescText); } // Add description text for the slow tower (Inferno Tower) if (i === 4) { // Fifth tower is tower_slow var infernoNameText = new Text2("Inferno Tower", { size: 60, fill: 0xFFFFFF, weight: 800 }); infernoNameText.anchor.set(0, 0.5); infernoNameText.x = -200; // To the right of the tower asset infernoNameText.y = 250 + i * towerSpacing - 70; // Moved higher up by 20 pixels tutorialSection.addChild(infernoNameText); var infernoDescText = new Text2("Can reach any part of the map and\nattacks by draininga % of health,\nperfect for enemies whit high HP.", { size: 55, fill: 0xFFFFFF, weight: 400 }); infernoDescText.anchor.set(0, 0.5); infernoDescText.x = -200; // To the right of the tower asset infernoDescText.y = 250 + i * towerSpacing + 30; // Slightly below center tutorialSection.addChild(infernoDescText); } // Add description text for the poison tower (Frost Wizard) if (i === 5) { // Sixth tower is tower_poison var frostWizardNameText = new Text2("Frost Wizard", { size: 60, fill: 0xFFFFFF, weight: 800 }); frostWizardNameText.anchor.set(0, 0.5); frostWizardNameText.x = -200; // To the right of the tower asset frostWizardNameText.y = 250 + i * towerSpacing - 50; // Moved higher up tutorialSection.addChild(frostWizardNameText); var frostWizardDescText = new Text2("Freezes enemies and deals cold damage.", { size: 55, fill: 0xFFFFFF, weight: 400 }); frostWizardDescText.anchor.set(0, 0.5); frostWizardDescText.x = -200; // To the right of the tower asset frostWizardDescText.y = 250 + i * towerSpacing + 30; // Slightly below center tutorialSection.addChild(frostWizardDescText); } } // Add 'finish' asset at bottom right of tutorial area (same position as next) var finishAsset = LK.getAsset('finish', { anchorX: 1, anchorY: 1 }); finishAsset.x = 2048 - 250; // Bottom right position, moved further left finishAsset.y = 2732 - 150; // Bottom right position, moved up // Add 'next' asset at bottom right of tutorial area var nextAsset = LK.getAsset('next', { anchorX: 1, anchorY: 1 }); nextAsset.x = 2048 - 250; // Bottom right position, moved further left nextAsset.y = 2732 - 150; // Bottom right position, moved up // Add 'back' asset at bottom left of tutorial area var backAsset = LK.getAsset('back', { anchorX: 0, anchorY: 1 }); backAsset.x = 250; // Bottom left position backAsset.y = 2732 - 150; // Bottom left position, moved up to match next button height backAsset.visible = false; // Initially hidden // Add down event handler for back button with animation backAsset.down = function () { // Animate shrink effect tween(backAsset, { scaleX: 0.85, scaleY: 0.85 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { // Animate return to original size tween(backAsset, { scaleX: 1.0, scaleY: 1.0 }, { duration: 200, easing: tween.easeOut }); } }); // Show all tutorial assets and their texts again for (var i = 0; i < towerAssets.length; i++) { // Show the tower asset if (tutorialSection.children[i * 3]) { tutorialSection.children[i * 3].visible = true; } // Show the tower name text (if exists) if (tutorialSection.children[i * 3 + 1]) { tutorialSection.children[i * 3 + 1].visible = true; } // Show the tower description text (if exists) if (tutorialSection.children[i * 3 + 2]) { tutorialSection.children[i * 3 + 2].visible = true; } } // Also show the 'troops' text troopTitleText.visible = true; // Hide enemy assets for (var i = 0; i < enemyAssets.length; i++) { var enemyAsset = tutorialSection.children[towerAssets.length * 3 + i]; if (enemyAsset) { enemyAsset.visible = false; } } // Hide ENEMIES text enemiesTitleText.visible = false; // Hide the back button after showing everything backAsset.visible = false; // Show the next button again nextAsset.visible = true; }; // Add down event handler for next button with animation nextAsset.down = function () { // Animate shrink effect tween(nextAsset, { scaleX: 0.85, scaleY: 0.85 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { // Animate return to original size tween(nextAsset, { scaleX: 1.0, scaleY: 1.0 }, { duration: 200, easing: tween.easeOut }); } }); // Hide tutorial assets and their texts for (var i = 0; i < towerAssets.length; i++) { // Hide the tower asset if (tutorialSection.children[i * 3]) { tutorialSection.children[i * 3].visible = false; } // Hide the tower name text (if exists) if (tutorialSection.children[i * 3 + 1]) { tutorialSection.children[i * 3 + 1].visible = false; } // Hide the tower description text (if exists) if (tutorialSection.children[i * 3 + 2]) { tutorialSection.children[i * 3 + 2].visible = false; } } // Also hide the 'troops' text troopTitleText.visible = false; // Show enemy assets for (var i = 0; i < enemyAssets.length; i++) { var enemyAsset = tutorialSection.children[towerAssets.length * 3 + i]; if (enemyAsset) { enemyAsset.visible = true; } } // Show ENEMIES text enemiesTitleText.visible = true; // Show the back button after hiding everything backAsset.visible = true; // Hide the next button nextAsset.visible = false; }; tutorialContainer.addChild(finishAsset); tutorialContainer.addChild(nextAsset); tutorialContainer.addChild(backAsset); tutorialContainer.addChild(tutorialSection); LK.stage.addChild(tutorialContainer); closeTutorialMenu(); }; noButton.down = function () { closeTutorialMenu(); }; game.addChild(tutorialMenu); } function closeTutorialMenu() { if (tutorialMenu) { showingTutorialMenu = false; tween(tutorialMenu, { alpha: 0, scaleX: 0.8, scaleY: 0.8 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { tutorialMenu.destroy(); tutorialMenu = null; } }); } } // Show tutorial menu on game start createTutorialMenu(); game.update = function () { // Skip game updates while tutorial menu is showing if (showingTutorialMenu) { return; } frameCounter++; 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 = 24; var midPoint = Math.floor(gridWidth / 2); // 12 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading enemy.cellX = spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; } } // Process enemies in batches to spread CPU load - increased batch size var enemiesPerFrame = Math.max(8, Math.ceil(enemies.length / 2)); var startIndex = frameCounter * enemiesPerFrame % enemies.length; var endIndex = Math.min(startIndex + enemiesPerFrame, enemies.length); if (startIndex >= enemies.length) startIndex = 0; for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; // Apply wave-based speed multiplier using enemy's original wave number // Speed increases progressively from wave 1 to wave 50 // Formula: 1 + (enemyWave - 1) / (totalWaves - 1) * 1.5 // This gives 1x speed at wave 1 and 2.5x speed at wave 50 if (!enemy.baseSpeed) { enemy.baseSpeed = enemy.speed; // Store original speed before any multipliers } var waveSpeedMultiplier = 1 + Math.min((enemy.waveNumber - 1) / (totalWaves - 1) * 1.5, 1.5); enemy.speed = enemy.baseSpeed * waveSpeedMultiplier; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Boss enemies give more gold and score var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } // Update UI less frequently for performance if (frameCounter % 3 === 0) updateUICached(); // 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; } // Only update enemy position if it's in current processing batch or critical (low health/boss) if (a >= startIndex && a < endIndex || enemy.health < enemy.maxHealth * 0.2 || enemy.isBoss) { 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); // Apply damage based on enemy type: normal/flying = 1 damage, boss = 5 damage var damage; if (enemy.isBoss) { damage = 5; } else if (enemy.type === 'normal' || enemy.type === 'flying') { damage = 1; } else { // Other enemy types (fast, immune, swarm) also deal 1 damage unless they're bosses damage = 1; } lives = Math.max(0, lives - damage); updateUICached(); if (lives <= 0) { LK.showGameOver(); } } } } // Clean up bullets every few frames to reduce CPU load if (frameCounter % 2 === 0) { 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
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
mago pixel art In-Game asset. 2d. High contrast. No shadows
bola de hielo pixel art. In-Game asset. 2d. High contrast. No shadows
dragón volando pixel art. In-Game asset. 2d. High contrast. No shadows vista desde arriba
botón con el texto 'next wave' pixel art. que tenga el color verde In-Game asset. 2d. High contrast. No shadows
pergamino para usar de fondo. In-Game asset. 2d. High contrast. No shadows
rata pixel art. desde arriba In-Game asset. 2d. High contrast. No shadows