Code edit (19 edits merged)
Please save this source code
User prompt
``` var turretGraphics = gunContainer.children[0]; var gunGraphics = gunContainer.children[1]; var prismGraphics = gunContainer.children[2]; ``` is used multiple times; factorize properly by using `self.`
Code edit (2 edits merged)
Please save this source code
User prompt
Please fix the bug: 'UnitManager is not defined' in or related to this line: 'unitManager = new UnitManager();' Line Number: 3256
Code edit (1 edits merged)
Please save this source code
Code edit (4 edits merged)
Please save this source code
User prompt
tint prism & turret with teamColors in activateGeneratorMode & activateDefenseMode
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
User prompt
Now, make towers in Defense mode (!isProducing) fire bullets (pratically bullet asset is 15x5px horizontal laser beam) on enmies in range. Handle turret & bullet rotation properly Analayze properly current tower and units logic to make a proper integration without breaking existing features. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (1 edits merged)
Please save this source code
User prompt
no enemy Units are visible. please check again properly
User prompt
add a new function 'tempInitEnemies()' that will be used to spwan enemy Units during early development phases; it will just spawn 10 enemy 1 Units (Unit class, not the old Enemy class) around the enemy HQ
User prompt
add a new function 'tempInitEnemies()' that will be used to spwan enemies during early development phases; it will just spawn 10 enemy 1 units around the enemy HQ
Code edit (4 edits merged)
Please save this source code
User prompt
in BackgroundManager, in createParallaxEffect, in animateBackground, also animate alpha in loop from 0.5 to 1 for index > 0 ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (6 edits merged)
Please save this source code
User prompt
current background animation is missing the logic that reset the background position: this causes the animation to work at first, then the background_1 is no more visible then it appears again from the right
User prompt
for each paralax layer, to create a continuous effect, use the same background with a scaleX =-1
User prompt
- add background_3 to the paralax anim
User prompt
Use background Manager to make a paralax effect with background_1 & background_2
Code edit (1 edits merged)
Please save this source code
Code edit (21 edits merged)
Please save this source code
User prompt
rename asset 'headquartersBase' to 'headquarterBase'
Code edit (1 edits merged)
Please save this source code
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var BackgroundManager = Container.expand(function () { var self = Container.call(this); self.backgrounds = []; self.currentBackground = null; self.animationActive = false; // Method to add a background layer self.addBackground = function (assetId, config) { config = config || {}; var background = new Container(); var bgGraphics = background.attachAsset(assetId, { anchorX: config.anchorX || 0.5, anchorY: config.anchorY || 0.5, x: config.x || 0, y: config.y || 0, scaleX: config.scaleX || 1, scaleY: config.scaleY || 1, alpha: config.alpha !== undefined ? config.alpha : 1, tint: config.tint || 0xFFFFFF }); background.config = config; background.graphics = bgGraphics; background.assetId = assetId; self.addChild(background); self.backgrounds.push(background); return background; }; // Method to remove a background self.removeBackground = function (background) { var index = self.backgrounds.indexOf(background); if (index !== -1) { self.backgrounds.splice(index, 1); self.removeChild(background); } }; // Method to animate a background self.animateBackground = function (background, properties, config) { if (!background || !background.graphics) { return; } config = config || {}; var duration = config.duration || 1000; var easing = config.easing || tween.linear; var loop = config.loop || false; var _onFinish = config.onFinish; // Create the animation var _animateFunc = function animateFunc() { tween(background.graphics, properties, { duration: duration, easing: easing, onFinish: function onFinish() { if (loop && self.animationActive) { // Reset properties for looping if (config.resetOnLoop) { for (var prop in config.resetOnLoop) { background.graphics[prop] = config.resetOnLoop[prop]; } } _animateFunc(); } else if (_onFinish) { _onFinish(); } } }); }; self.animationActive = true; _animateFunc(); }; // Method to stop all animations self.stopAnimations = function () { self.animationActive = false; for (var i = 0; i < self.backgrounds.length; i++) { if (self.backgrounds[i].graphics) { tween.stop(self.backgrounds[i].graphics); } } }; // Method to fade between backgrounds self.fadeToBackground = function (newBackground, duration, onComplete) { duration = duration || 1000; if (self.currentBackground && self.currentBackground !== newBackground) { // Fade out current background tween(self.currentBackground.graphics, { alpha: 0 }, { duration: duration / 2, easing: tween.easeInOut, onFinish: function onFinish() { self.currentBackground.visible = false; } }); } // Fade in new background newBackground.visible = true; newBackground.graphics.alpha = 0; tween(newBackground.graphics, { alpha: 1 }, { duration: duration / 2, easing: tween.easeInOut, onFinish: function onFinish() { self.currentBackground = newBackground; if (onComplete) { onComplete(); } } }); }; // Method for parallax scrolling effect self.createParallaxEffect = function (backgrounds, baseSpeed) { baseSpeed = baseSpeed || 1; for (var i = 0; i < backgrounds.length; i++) { (function (index) { var bg1 = backgrounds[index]; var speed = baseSpeed * (index + 1) * 0.5; // Each layer moves at different speed var bg2 = self.addBackground(bg1.assetId, { anchorX: bg1.config.anchorX || 0.5, anchorY: bg1.config.anchorY || 0.5, x: bg1.graphics.x + bg1.graphics.width, y: bg1.config.y || 0, scaleX: -1, scaleY: bg1.config.scaleY || 1, alpha: bg1.config.alpha !== undefined ? bg1.config.alpha : 1, tint: bg1.config.tint || 0xFFFFFF }); var moveDistance = bg1.graphics.width; var duration = 40000 / speed; var _animate = function animate(bg, initialX) { // Always animate from current position to current - moveDistance self.animateBackground(bg, { x: bg.graphics.x - moveDistance }, { duration: duration, easing: tween.linear, onFinish: function onFinish() { if (self.animationActive) { // If the background has fully moved out of view to the left, reset to the right if (bg.graphics.x <= -moveDistance / 2) { bg.graphics.x = bg.graphics.x + moveDistance * 2; } _animate(bg, initialX); } } }); }; _animate(bg1, bg1.graphics.x); _animate(bg2, bg2.graphics.x); })(i); } }; return self; }); var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); 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 distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } // 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 * 1.5; 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 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 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') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual poison effect var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Apply poison effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } } 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); var cellGraphics = null; // Only create and attach cellGraphics when in debug mode if (isDebug) { cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5, alpha: 1 }); cellGraphics.tint = Math.random() * 0xffffff; } var debugArrows = []; var numberLabel = new Text2("-", { size: 20, fill: 0xFFFFFF }); 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) { // Hide cell gradient when not in debug mode if (!isDebug) { numberLabel.visible = false; numberLabel.alpha = 0; // Also set alpha to 0 to ensure it's completely invisible numberLabel.setText(""); // Clear text content when not in debug mode // cellGraphics doesn't exist when not in debug mode, so no need to hide it // Show wall asset for walls, hide cellGraphics for other types if (data.type === 1 && !data.isVisuallyEmptyTower) { // Remove existing wall if any if (self.wallInstance) { self.removeChild(self.wallInstance); self.wallInstance = null; } // Create and add wall asset self.wallInstance = new Wall(); self.addChild(self.wallInstance); } else { // Remove wall if switching away from wall type if (self.wallInstance) { self.removeChild(self.wallInstance); self.wallInstance = null; } } return; } // Show cells in debug mode if (cellGraphics) { cellGraphics.visible = true; cellGraphics.alpha = 1; } switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); if (cellGraphics) { cellGraphics.tint = 0x880000; } return; } var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; if (cellGraphics) { cellGraphics.tint = 0x0088ff; } } else { if (cellGraphics) { cellGraphics.tint = 0x88 - tint << 8 | tint; } } break; } case 1: { self.removeArrows(); if (cellGraphics) { cellGraphics.tint = 0xaaaaaa; } numberLabel.visible = false; break; } case 3: { self.removeArrows(); if (cellGraphics) { cellGraphics.tint = 0x008800; } numberLabel.visible = false; break; } case 4: { self.removeArrows(); if (cellGraphics) { cellGraphics.tint = 0x888888; } numberLabel.visible = false; break; } } }; }); // 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 = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; 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 = 1; 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 // Normal enemy uses default values 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 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; } // Health bar graphics removed - health system remains intact self.update = function () { if (self.health <= 0) { self.health = 0; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } // Health bar positioning removed }; 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: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal 4: Empty Tower 9: Wall (from map) */ // Use first map from MAPS array var currentMap = MAPS[0]; var mapHeight = currentMap.length; var mapWidth = currentMap[0].length; for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = 0; // Default to empty // Check if coordinates are within map bounds if (j < mapHeight && i < mapWidth) { var mapValue = currentMap[j][i]; // Note: map is [row][col], grid is [col][row] // Convert map values to cell types switch (mapValue) { case 0: // empty cell cellType = 0; break; case 1: // player base cellType = 3; // Goal self.goals.push(cell); break; case 22: cell.orientation = Math.PI; // fallthrough case 2: // enemy base cellType = 2; // Spawn self.spawns.push(cell); break; case 3: // empty tower cellType = 4; break; case 9: // wall cellType = 1; break; default: cellType = 0; } } else { // Outside map bounds, default to wall cellType = 1; } 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 >= 0 && j <= gridHeight - 5) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = (j + mapGridOffset) * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 1 && node.left.type != 1) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.right.type != 1) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.right.type != 1) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.left.type != 1) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < mapGridOffset) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= mapGridOffset; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed; // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) var angle = Math.PI / 2; if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update enemy's position enemy.x = grid.x + 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 >= mapGridOffset) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { if (cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + 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 SelectionCircle = Container.expand(function () { var self = Container.call(this); self.startX = 0; self.startY = 0; self.currentRadius = 0; var circleGraphics = self.attachAsset('circleSelector', { anchorX: 0.5, anchorY: 0.5 }); circleGraphics.alpha = 0.3; circleGraphics.tint = 0x00FF00; circleGraphics.width = 1; circleGraphics.height = 1; self.updateCircle = function (endX, endY) { var dx = endX - self.startX; var dy = endY - self.startY; self.currentRadius = Math.sqrt(dx * dx + dy * dy); circleGraphics.width = self.currentRadius * 2; circleGraphics.height = self.currentRadius * 2; }; self.setPosition = function (x, y) { self.startX = x; self.startY = y; self.x = x; self.y = y; }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Increase size of base for easier touch var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); baseGraphics.tint = 0xAAAAAA; 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 = -20 + 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 = -20; // Position 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 = 24 + 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 = 20 + 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 (headquarters, orientation, playerIndex) { var self = Container.call(this); self.isHeadquarters = headquarters || false; self.baseOrientation = orientation; self.playerIndex = playerIndex !== undefined ? playerIndex : 0; self.isActivated = self.isHeadquarters; self.activationThreshold = 10; self.currentShares = 0; self.capturingPlayer = -1; self.activationGauge = null; self.id = self.isHeadquarters ? 'headquarters' : 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.isProducing = false; self.range = 3 * CELL_SIZE; self.health = isDebug ? self.isHeadquarters ? 4 : 2 : self.isHeadquarters ? 30 : 10; self.maxHealth = isDebug ? self.isHeadquarters ? 4 : 2 : self.isHeadquarters ? 30 : 10; // 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 12 * CELL_SIZE; // Significantly increased range for max level } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case 'splash': // Splash: base 2, +0.2 per level (max ~4 blocks at max level) return (2 + (self.level - 1) * 0.2) * 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 3.5, +0.5 per level return (3.5 + (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 = 60; self.bulletSpeed = 5; self.damage = 1; self.lastFired = 0; self.targetEnemy = null; self.fireRate = 60; self.damage = 1; self.range = 3 * CELL_SIZE; self.bulletSpeed = 5; // Create appropriate graphics based on tower type var baseGraphics; if (self.isHeadquarters) { baseGraphics = self.attachAsset('headquarterBase', { anchorX: 0.5, anchorY: 0.25, width: 150, height: 150 }); baseGraphics.rotation = orientation || 0; } else { baseGraphics = self.attachAsset('towerBase', { anchorX: 0.5, anchorY: 0.5 }); } switch (self.id) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'sniper': baseGraphics.tint = 0xFF5500; break; case 'splash': baseGraphics.tint = 0x33CC00; break; case 'slow': baseGraphics.tint = 0x9900FF; break; case 'poison': baseGraphics.tint = 0x00FFAA; break; default: baseGraphics.tint = 0xAAAAAA; } var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (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 * 2 + dotSpacing * (i + 1); dot.y = CELL_SIZE * 1.3; //0.7 // Hide level indicators for enemy towers (playerIndex != 0) if (self.playerIndex !== 0) { dot.visible = false; } self.addChild(dot); levelIndicators.push(dot); } var activationGaugeContainer = new Container(); self.addChild(activationGaugeContainer); activationGaugeContainer.y = CELL_SIZE * 0.9; self.activationGauge = activationGaugeContainer; self.activationGauge.visible = false; var gunContainer = new Container(); self.addChild(gunContainer); var gunGraphics; var prismGraphics; if (self.isHeadquarters) { gunGraphics = gunContainer.attachAsset('headquarterTurret', { anchorX: 0.5, anchorY: 0.5, width: 125, height: 125 }); prismGraphics = gunContainer.attachAsset('prism', { anchorX: 0.5, anchorY: 0.5, width: 100, height: 100 }); } else { gunGraphics = gunContainer.attachAsset('turret', { anchorX: 0.5, anchorY: 0.5, width: 50, height: 65 }); prismGraphics = gunContainer.attachAsset('prism', { anchorX: 0.5, anchorY: 0.5, width: 50, height: 50 }); } prismGraphics.visible = self.isProducing; gunGraphics.visible = !self.isProducing; //gunGraphicsOffsetRatio = 0.33 * Math.abs(gunGraphics.height - gunGraphics.width) / gunGraphics.height; gunGraphicsOffsetRatio = self.isHeadquarters ? 0.15 : -0.15; if (!self.isProducing) { if (orientation) { gunGraphics.y = -gunGraphics.height * gunGraphicsOffsetRatio; gunGraphics.rotation = orientation; } else { gunGraphics.y = gunGraphics.height * gunGraphicsOffsetRatio; } } // Tint turret graphics based on player if (self.playerIndex === 0) { gunGraphics.tint = teamColors[0]; // Player color prismGraphics.tint = teamColors[0]; } else if (self.playerIndex === 1) { gunGraphics.tint = teamColors[1]; // Enemy color prismGraphics.tint = teamColors[1]; } else { gunGraphics.tint = 0xAAAAAA; // Gray for neutral prismGraphics.tint = 0xAAAAAA; } 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.updateActivation = function (fromPlayerIndex) { if (self.isActivated && self.playerIndex === fromPlayerIndex) { return; } if (self.capturingPlayer === -1) { self.capturingPlayer = fromPlayerIndex; } if (self.capturingPlayer === fromPlayerIndex) { self.currentShares++; } else { self.currentShares--; } if (self.currentShares <= 0) { self.currentShares = 0; if (self.isActivated) { self.deactivate(); } self.capturingPlayer = -1; } else if (self.currentShares >= self.activationThreshold) { self.currentShares = self.activationThreshold; if (!self.isActivated) { self.activate(self.capturingPlayer); } } self.updateActivationGaugeVisuals(); }; self.activate = function (newPlayerIndex) { self.isActivated = true; self.playerIndex = newPlayerIndex; gunContainer.visible = true; var baseGraphics = self.children[0]; var gunGraphics = gunContainer.children[0]; var prismGraphics = gunContainer.children[1]; if (self.playerIndex == 0) { baseGraphics.tint = teamColors[0]; gunGraphics.tint = teamColors[0]; prismGraphics.tint = teamColors[0]; } else if (self.playerIndex == 1) { baseGraphics.tint = teamColors[1]; gunGraphics.tint = teamColors[1]; prismGraphics.tint = teamColors[1]; } if (self.activationGauge) { self.activationGauge.visible = false; } }; self.deactivate = function () { self.isActivated = false; self.playerIndex = -1; gunContainer.visible = false; var baseGraphics = self.children[0]; var gunGraphics = gunContainer.children[0]; var prismGraphics = gunContainer.children[1]; baseGraphics.tint = 0xAAAAAA; gunGraphics.tint = 0xAAAAAA; prismGraphics.tint = 0xAAAAAA; self.updateActivationGaugeVisuals(); }; self.updateActivationGaugeVisuals = function () { if (!self.activationGauge) { return; } self.activationGauge.y = 0; // Center the gauge on the tower self.activationGauge.removeChildren(); if (self.currentShares > 0 && !self.isActivated) { self.activationGauge.visible = true; var totalDots = 12; var radius = CELL_SIZE / 2.5; var dotsToShow = Math.ceil(self.currentShares / self.activationThreshold * totalDots); var playerColor = 0x0000FF; // Default/neutral color if (self.capturingPlayer === 0) { playerColor = teamColors[0]; // Player color } else if (self.capturingPlayer === 1) { playerColor = teamColors[1]; // Enemy color } for (var i = 0; i < totalDots; i++) { var angle = i / totalDots * Math.PI * 2 - Math.PI / 2; // Start from top var dot = self.activationGauge.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5, width: 12, height: 12 }); dot.x = Math.cos(angle) * radius; dot.y = Math.sin(angle) * radius; if (i < dotsToShow) { dot.tint = playerColor; dot.alpha = 1.0; } else { dot.tint = 0x666666; dot.alpha = 0.4; } } } else { self.activationGauge.visible = false; } }; self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rapid') { 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.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]; 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; 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); // Check if enemy is in range if (distance <= self.getRange()) { // 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 () { if (!self.isActivated) { return; } self.targetEnemy = self.findTarget(); if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); gunContainer.rotation = angle; if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { if (!self.isActivated) { return; } // Don't show upgrade menu for enemy towers if (self.playerIndex !== 0) { return; } 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); } selectedTower = null; grid.renderDebug(); 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]); } } selectedTower = self; 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 }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { 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; } // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0x00AAFF; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'sniper': bullet.children[0].tint = 0xFF5500; bullet.children[0].width = 15; bullet.children[0].height = 15; break; case 'splash': bullet.children[0].tint = 0x33CC00; bullet.children[0].width = 40; bullet.children[0].height = 40; break; case 'slow': bullet.children[0].tint = 0x9900FF; bullet.children[0].width = 35; bullet.children[0].height = 35; break; case 'poison': bullet.children[0].tint = 0x00FFAA; bullet.children[0].width = 35; bullet.children[0].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; } } } self.refreshCellsInRange(); }; self.changeTowerMode = function () { // Toggle the tower's producing mode self.isProducing = !self.isProducing; if (self.isProducing) { // New mode is "Generator" - call generator mode function self.activateGeneratorMode(); } else { // New mode is "Defense" - call defense mode function self.activateDefenseMode(); } }; self.animatePrism = function () { var prismGraphics = gunContainer.children[1]; // prism is second child // Continuous rotation animation tween(prismGraphics, { rotation: prismGraphics.rotation + Math.PI * 2 }, { duration: 8000, // 8 seconds for full rotation easing: tween.linear, onFinish: function onFinish() { // Restart rotation when complete self.animatePrism(); } }); // Tint variation animation - cycle through colors var tintColors = [0xFFFFFF, 0xFFE0E0, 0xE0FFFF, 0xE0FFE0, 0xFFFFE0, 0xF0E0FF]; var currentTintIndex = 0; function animateTint() { var nextIndex = (currentTintIndex + 1) % tintColors.length; tween(prismGraphics, { tint: tintColors[nextIndex] }, { duration: 2000, // 2 seconds per tint transition easing: tween.easeInOut, onFinish: function onFinish() { currentTintIndex = nextIndex; animateTint(); // Continue tint animation } }); } animateTint(); // Start tint animation }; self.activateGeneratorMode = function () { console.log("Tower activating generator mode at", self.gridX, self.gridY); // Update graphics visibility for generator mode var prismGraphics = gunContainer.children[1]; // prism is second child var gunGraphics = gunContainer.children[0]; // gun is first child prismGraphics.visible = true; gunGraphics.visible = false; if (self.baseOrientation) { prismGraphics.y = -prismGraphics.height * 0.33; } else { prismGraphics.y = prismGraphics.height * 0.33; } // Start prism animation self.animatePrism(); // Update button text in any open upgrade menu var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu && child.tower === self; }); for (var i = 0; i < upgradeMenus.length; i++) { if (upgradeMenus[i].updateButtonText) { upgradeMenus[i].updateButtonText('Defense mode'); } } }; self.activateDefenseMode = function () { // Update graphics visibility for defense mode var prismGraphics = gunContainer.children[1]; // prism is second child var gunGraphics = gunContainer.children[0]; // gun is first child prismGraphics.visible = false; gunGraphics.visible = true; if (self.baseOrientation) { gunGraphics.y = -gunGraphics.height * 0.15; gunGraphics.rotation = self.baseOrientation; } else { gunGraphics.y = gunGraphics.height * 0.15; } // Stop prism animations tween.stop(prismGraphics, { rotation: true, tint: true }); // Reset prism to default state prismGraphics.rotation = 0; prismGraphics.tint = 0xFFFFFF; // Update button text in any open upgrade menu var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu && child.tower === self; }); for (var i = 0; i < upgradeMenus.length; i++) { if (upgradeMenus[i].updateButtonText) { upgradeMenus[i].updateButtonText('Generator mode'); } } }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; self.hasEnoughGold = true; var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(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(false, undefined, 0); // Pass false for headquarters, undefined orientation, playerIndex 0 for player 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; previewGraphics.tint = 0xAAAAAA; if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; if (self.gridY <= mapGridOffset || self.gridY + 1 >= grid.cells[0].length - mapGridOffset) { 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; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < mapGridOffset) { 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 Unit = 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 = 1; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.health = self.maxHealth; self.enemyHQ = null; // Store reference to enemy HQ self.isIdle = false; // Track if unit is idle self.idleBaseX = 0; // Base position for idle animation self.idleBaseY = 0; // Base position for idle animation self.idleAnimationActive = false; // Track if idle animation is running self.isSelected = false; // Track if unit is selected self.selectionIndicator = null; // Visual indicator for selection self.targetCellX = null; // Target cell X coordinate self.targetCellY = null; // Target cell Y coordinate self.movementSpeed = 0.05; // Movement speed for units // Get appropriate asset for this unit type var assetId = 'unit'; if (self.type !== 'normal') { assetId = 'unit_' + self.type; } var unitGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Find enemy HQ automatically for (var i = 0; i < towers.length; i++) { if (towers[i].isHeadquarters && towers[i].playerIndex === 1) { self.enemyHQ = towers[i]; break; } } self.setSelected = function (selected) { self.isSelected = selected; if (selected && !self.selectionIndicator) { // Create selection indicator self.selectionIndicator = new Container(); var indicator = self.selectionIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = 50; indicator.height = 50; indicator.alpha = 0.5; indicator.tint = 0x00FF00; self.addChild(self.selectionIndicator); } else if (!selected && self.selectionIndicator) { // Remove selection indicator self.removeChild(self.selectionIndicator); self.selectionIndicator = null; } }; self.setTarget = function (cellX, cellY) { self.targetCellX = cellX; self.targetCellY = cellY; // Stop idle animation immediately self.isIdle = false; self.idleAnimationActive = false; // Stop any ongoing idle animation tweens tween.stop(self); // Check if target cell contains an enemy tower self.targetTower = null; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; // Check if this tower is an enemy tower (different playerIndex) if (tower.playerIndex !== self.ownerIndex) { // Check if tower occupies the target cell var towerGridX, towerGridY; var towerSize = tower.isHeadquarters ? 4 : 2; if (tower.isHeadquarters) { towerGridX = Math.round((tower.x - grid.x) / CELL_SIZE) - towerSize / 2; towerGridY = Math.round((tower.y - grid.y) / CELL_SIZE) - towerSize / 2 - mapGridOffset; } else { towerGridX = Math.floor((tower.x - grid.x) / CELL_SIZE); towerGridY = Math.floor((tower.y - grid.y) / CELL_SIZE) - mapGridOffset; } if (cellX >= towerGridX && cellX < towerGridX + towerSize && cellY >= towerGridY && cellY < towerGridY + towerSize) { self.targetTower = tower; break; } } } }; self.update = function () { if (self.health <= 0) { self.health = 0; } // Handle movement to target if (self.targetCellX !== null && self.targetCellY !== null) { var targetX = grid.x + self.targetCellX * CELL_SIZE + CELL_SIZE / 2; var targetY = grid.y + (self.targetCellY + mapGridOffset) * CELL_SIZE + CELL_SIZE / 2; // If targeting a tower, adjust target position to tower's actual position if (self.targetTower && self.targetTower.parent) { targetX = self.targetTower.x; targetY = self.targetTower.y; } var dx = targetX - self.x; var dy = targetY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if close enough to attack (within 1 cell distance) var attackRange = CELL_SIZE; if (self.targetTower && distance <= attackRange) { if (!self.targetTower.isHeadquarters && self.targetTower.updateActivation) { self.targetTower.updateActivation(self.ownerIndex); self.health = 0; return; } // Log unit reaching tower var isEnemyTower = self.targetTower.playerIndex !== self.ownerIndex; console.log("Unit reached tower. Enemy tower:", isEnemyTower, "Tower health:", self.targetTower.health); // Attack the tower self.targetTower.health -= 1; // Destroy unit after attacking self.health = 0; // Check if tower was destroyed if (self.targetTower.health <= 0) { // Remove tower from game var towerIndex = towers.indexOf(self.targetTower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } game.removeChild(self.targetTower); // Clear the cells occupied by the tower var towerGridX, towerGridY; var towerSize = self.targetTower.isHeadquarters ? 4 : 2; if (self.targetTower.isHeadquarters) { towerGridX = Math.round((self.targetTower.x - grid.x) / CELL_SIZE) - towerSize / 2; towerGridY = Math.round((self.targetTower.y - grid.y) / CELL_SIZE) - towerSize / 2; } else { towerGridX = Math.floor((self.targetTower.x - grid.x) / CELL_SIZE); towerGridY = Math.floor((self.targetTower.y - grid.y) / CELL_SIZE); } for (var i = 0; i < towerSize; i++) { for (var j = 0; j < towerSize; j++) { var cell = grid.getCell(towerGridX + i, towerGridY + j); if (cell) { cell.type = 0; } } } // Update pathfinding grid.pathFind(); grid.renderDebug(); } return; } if (distance > 2) { // Not at target yet // Move towards target var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.movementSpeed * CELL_SIZE; self.y += Math.sin(angle) * self.movementSpeed * CELL_SIZE; // Update current cell position self.currentCellX = (self.x - grid.x) / CELL_SIZE - 0.5; self.currentCellY = (self.y - grid.y) / CELL_SIZE - 0.5; self.cellX = Math.round(self.currentCellX); self.cellY = Math.round(self.currentCellY); } else { // Reached target self.targetCellX = null; self.targetCellY = null; self.targetTower = null; // Set as idle at new position self.isIdle = true; self.idleBaseX = targetX; self.idleBaseY = targetY; // Start idle animation at new position if (unitManager) { unitManager.startIdleAnimation(self); } } } }; return self; }); var UnitManager = Container.expand(function () { var self = Container.call(this); self.units = []; self.spawnInterval = 180; // Spawn units every 3 seconds (60 FPS * 3) self.lastSpawnTime = 0; self.maxUnits = 100; // Global maximum units limit self.spawnUnitsAroundTower = function (tower) { // Calculate actual grid coordinates from tower position var actualGridX = Math.floor((tower.x - grid.x) / CELL_SIZE); var actualGridY = Math.floor((tower.y - grid.y) / CELL_SIZE) - mapGridOffset; // For headquarters, adjust to get the top-left corner of the 2x2 area if (tower.isHeadquarters) { actualGridX -= 1; // Headquarters is centered at 1.5 cells from left edge actualGridY -= 1; // Adjust for centering } // Count existing units globally var totalUnits = self.units.length; // Don't spawn more if we've reached the global limit if (totalUnits >= self.maxUnits) { return; } // Find valid spawn positions around the tower, prioritizing closest empty cells var emptySpawnPositions = []; var unitOccupiedSpawnPositions = []; var towerSize = 2; // Towers occupy a 2x2 logical grid space var maxSearchRadius = 2; function checkCell(checkX, checkY) { var cell = grid.getCell(checkX, checkY); // Cell must be valid and of type 0 (empty floor) if (!cell || cell.type !== 0) { return; } // Cell must not be occupied by an enemy for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === checkX && enemies[e].cellY === checkY) { return; // Occupied by enemy, invalid } } // Check if occupied by a friendly unit var isOccupiedByUnit = false; for (var u = 0; u < self.units.length; u++) { if (self.units[u].cellX === checkX && self.units[u].cellY === checkY) { isOccupiedByUnit = true; break; } } var pos = { x: checkX, y: checkY }; if (isOccupiedByUnit) { unitOccupiedSpawnPositions.push(pos); } else { emptySpawnPositions.push(pos); } } // Search in expanding rings for the closest available spot for (var r = 1; r <= maxSearchRadius; r++) { // Top and bottom rows of the ring for (var dx = -r; dx < towerSize + r; dx++) { checkCell(actualGridX + dx, actualGridY - r); checkCell(actualGridX + dx, actualGridY + towerSize - 1 + r); } // Left and right columns of the ring (excluding corners) for (var dy = -r + 1; dy < towerSize + r - 1; dy++) { checkCell(actualGridX - r, actualGridY + dy); checkCell(actualGridX + towerSize - 1 + r, actualGridY + dy); } // If we found completely empty spots, we prioritize them and stop searching wider if (emptySpawnPositions.length > 0) { break; } } var spawnPositions = emptySpawnPositions; // If no empty positions were found, use positions occupied by other units as a fallback if (spawnPositions.length === 0) { spawnPositions = unitOccupiedSpawnPositions; } // Spawn a unit at a random valid position from the chosen list if (spawnPositions.length > 0) { var spawnPos = spawnPositions[Math.floor(Math.random() * spawnPositions.length)]; var unit = new Unit('normal'); unit.cellX = spawnPos.x; unit.cellY = spawnPos.y; unit.currentCellX = spawnPos.x; unit.currentCellY = spawnPos.y; // Calculate final position var finalX = grid.x + spawnPos.x * CELL_SIZE + CELL_SIZE / 2; var finalY = grid.y + (spawnPos.y + mapGridOffset) * CELL_SIZE + CELL_SIZE / 2; // Start unit at tower's center (prism position) unit.x = tower.x; unit.y = tower.y; unit.sourceTower = tower; // Track which tower spawned this unit unit.ownerIndex = tower.playerIndex; // Track owner player index enemyLayerTop.addChild(unit); self.units.push(unit); // Start with unit invisible and small unit.alpha = 0; unit.scaleX = 0.1; unit.scaleY = 0.1; // Tint unit based on owner (source tower's playerIndex) if (tower.playerIndex === 0) { // Player units unit.children[0].tint = teamColors[0]; } else if (tower.playerIndex === 1) { // Enemy units unit.children[0].tint = teamColors[1]; } else { // Other players - different colors var playerColors = [0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF]; unit.children[0].tint = playerColors[(tower.playerIndex - 2) % playerColors.length]; } // Animate unit emerging from prism center and moving to spawn position tween(unit, { scaleX: 1, scaleY: 1, alpha: 1, x: finalX, y: finalY }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { // Set unit as idle and save base position unit.isIdle = true; unit.idleBaseX = finalX; unit.idleBaseY = finalY; // Start idle animation self.startIdleAnimation(unit); } }); } }; // Method to start idle animation for a unit self.startIdleAnimation = function (unit) { if (!unit.isIdle || unit.idleAnimationActive) { return; } unit.idleAnimationActive = true; // Create a gentle floating animation var _animateFloat = function animateFloat() { if (!unit.isIdle || !unit.parent) { unit.idleAnimationActive = false; return; } // Random offset within a small radius var radius = 8; // Small movement radius var angle = Math.random() * Math.PI * 2; var offsetX = Math.cos(angle) * radius; var offsetY = Math.sin(angle) * radius; tween(unit, { x: unit.idleBaseX + offsetX, y: unit.idleBaseY + offsetY }, { duration: 800 + Math.random() * 400, // 0.8-1.2 seconds (faster animation) easing: tween.easeInOut, onFinish: function onFinish() { // Continue animation if still idle if (unit.isIdle && unit.parent) { _animateFloat(); } else { unit.idleAnimationActive = false; } } }); }; _animateFloat(); }; self.update = function () { // Check if it's time to spawn new units if (LK.ticks - self.lastSpawnTime >= self.spawnInterval) { self.lastSpawnTime = LK.ticks; // Find all towers in generator mode var generatorTowerCount = 0; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; if (tower.isProducing) { generatorTowerCount++; self.spawnUnitsAroundTower(tower); } } } // Update all units for (var i = self.units.length - 1; i >= 0; i--) { var unit = self.units[i]; // Remove dead units if (unit.health <= 0) { enemyLayerTop.removeChild(unit); self.units.splice(i, 1); continue; } // Check if source tower still exists and is in generator mode if (!unit.sourceTower || !unit.sourceTower.parent || !unit.sourceTower.isProducing) { // Stop idle animation unit.isIdle = false; // Stop any ongoing tweens tween.stop(unit); // Fade out and remove unit tween(unit, { alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: 300, easing: tween.easeIn, onFinish: function (u) { return function () { var idx = self.units.indexOf(u); if (idx !== -1) { self.units.splice(idx, 1); } enemyLayerTop.removeChild(u); }; }(unit) }); // Remove from array immediately to prevent multiple tweens self.units.splice(i, 1); } } }; 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); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/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.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(self.tower.isHeadquarters ? 'Generator mode' : 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); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); // Hide sell button for headquarters if (self.tower.isHeadquarters) { sellButton.visible = false; } 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.isHeadquarters) { self.tower.changeTowerMode(); return; } 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.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)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); 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); 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); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } game.removeChild(self.tower); 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); selectedTower = null; grid.renderDebug(); }; 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.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); } */ }; self.updateButtonText = function (newText) { buttonText.setText(newText); }; return self; }); var Wall = Container.expand(function () { var self = Container.call(this); var wallGraphics = self.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); wallGraphics.alpha = 0.5; // Start pulsing animation self.startPulseAnimation = function () { var _pulseAnimation2 = function _pulseAnimation() { // Pulse from 0.5 to 1 tween(wallGraphics, { alpha: 1 }, { duration: 4000, easing: tween.easeInOut, onFinish: function onFinish() { // Pulse back from 1 to 0.5 tween(wallGraphics, { alpha: 0.5 }, { duration: 4000, easing: tween.easeInOut, onFinish: function onFinish() { // Continue the loop if wall still exists if (self.parent) { _pulseAnimation2(); } } }); } }); }; _pulseAnimation2(); }; // Start the animation immediately self.startPulseAnimation(); 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 (isBossWave) { enemyType = 'normal'; waveType = "Boss Normal"; block.tint = 0xAAAAAA; enemyCount = 1; } 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 teamColors = [0x00AA00, 0xAA0000]; // Index 0: player color (green), Index 1: enemy color (red) var isHidingUpgradeMenu = false; var isSelectingUnits = false; var selectionStartX = 0; var selectionStartY = 0; var selectionCircle = null; var selectedUnits = []; var isCommandMode = false; // Track if we're in command mode after selecting units 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; } }); } var isDebug = false; var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var mapGridOffset = 4; // Global offset for Y-coordinate conversion var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; var unitManager = null; var gold = 80; var lives = 20; var score = 0; 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 // Map cell values: 0 = empty cell, 1 = player base, 2 = enemy base, 3 = empty tower, 9 = wall, 22 = enemy base (rotated) var MAPS = [ // Level 1: Basic tower defense layout (24x31) [[9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 22, 22, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9], // Row 0 - Enemy spawn area [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 5 - Main play area starts [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 6 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 7 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 8 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 9 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 10 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 11 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 12 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 13 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 14 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 15 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 16 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 17 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 18 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 19 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 20 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 21 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 22 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 23 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 24 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 25 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 26 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 27 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 28 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 29 - Main play area ends [9, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 30 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 31 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 32 [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], // Row 33 [9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9] // Row 34 - Player base area ]]; var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); } function setGold(value) { gold = value; updateUI(); } // Create background manager var backgroundManager = new BackgroundManager(); game.addChild(backgroundManager); // Add background layers for parallax effect var background1 = backgroundManager.addBackground('background_1', { x: 1024, y: 1366, scaleX: 1, scaleY: 1, alpha: 0.8, tint: 0xCCCCCC }); var background2 = backgroundManager.addBackground('background_2', { x: 1024, y: 1366, scaleX: 1, scaleY: 1, alpha: 1 }); var background3 = backgroundManager.addBackground('background_3', { x: 1024, y: 1366, scaleX: 1, scaleY: 1, alpha: 1 }); // Create a parallax scrolling effect with the three backgrounds backgroundManager.createParallaxEffect([background1, background2, background3], 1); var debugLayer = 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 * mapGridOffset; grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); game.addChild(debugLayer); // Create and add unit manager unitManager = new UnitManager(); game.addChild(unitManager); // Add headquarters towers for player and enemy bases function addHeadquarters() { // Find player base cells (type 3) and enemy base cells (type 2) var playerBaseCells = []; var enemyBaseCells = []; for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var cell = grid.cells[i][j]; if (cell.type === 3) { // Player base playerBaseCells.push(cell); } else if (cell.type === 2) { // Enemy base enemyBaseCells.push(cell); } } } // Add player headquarters (center of player base area) if (playerBaseCells.length > 0) { var playerOrientation = 0; for (var i = 0; i < playerBaseCells.length; i++) { if (playerBaseCells[i].orientation) { playerOrientation = playerBaseCells[i].orientation; break; } } var playerHQ = new Tower(true, playerOrientation, 0); // Pass true for headquarters, playerIndex 0 for player // Position at center of the 4x3 player base area (anchor at top-left of base area) var playerAnchorX = 10; // Top-left of 4x3 player base area var playerAnchorY = 31; // Top-left of 4x3 player base area // Position the headquarters asset at the center of the 4x3 area playerHQ.x = grid.x + (playerAnchorX + 1.5) * CELL_SIZE; // Center horizontally (2 cells from anchor) playerHQ.y = grid.y + (playerAnchorY + 2) * CELL_SIZE; // Center vertically (1.5 cells from anchor) // Tint the headquarters graphics playerHQ.children[0].tint = teamColors[0]; // Player color game.addChild(playerHQ); towers.push(playerHQ); // Add to towers array so UnitManager can find it } // Add enemy headquarters (center of enemy base area) if (enemyBaseCells.length > 0) { var enemyOrientation = 0; for (var i = 0; i < enemyBaseCells.length; i++) { if (enemyBaseCells[i].orientation) { enemyOrientation = enemyBaseCells[i].orientation; break; } } var enemyHQ = new Tower(true, enemyOrientation, 1); // Pass true for headquarters, playerIndex 1 for first enemy // Position at center of the 4x3 enemy base area (anchor at top-left of base area) var enemyAnchorX = 10; // Top-left of 4x3 enemy base area var enemyAnchorY = 0; // Top-left of 4x3 enemy base area // Position the headquarters asset at the center of the 4x3 area enemyHQ.x = grid.x + (enemyAnchorX + 1.5) * CELL_SIZE; // Center horizontally (2 cells from anchor) if (enemyOrientation !== 0) { // For a rotated HQ, place it at the bottom edge of its base area to make it visible and appear to be "entering" the map. enemyHQ.y = grid.y + (enemyAnchorY + 5) * CELL_SIZE; } else { enemyHQ.y = grid.y + (enemyAnchorY + 2) * CELL_SIZE; // Center vertically (1.5 cells from anchor) } // Tint the headquarters graphics enemyHQ.children[0].tint = teamColors[1]; // Enemy color game.addChild(enemyHQ); towers.push(enemyHQ); // Add to towers array } } function addEmptyTowers() { // Find empty tower cells (type 4) var emptyTowerCells = []; for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var cell = grid.cells[i][j]; if (cell.type === 4) { emptyTowerCells.push(cell); } } } // Add empty towers to the grid for (var i = 0; i < emptyTowerCells.length; i++) { var cell = emptyTowerCells[i]; var emptyTower = new Tower(false, 0, -1); // Not HQ, neutral // Position the tower on its cell emptyTower.x = grid.x + cell.x * CELL_SIZE + CELL_SIZE / 2; emptyTower.y = grid.y + (cell.y + mapGridOffset) * CELL_SIZE + CELL_SIZE / 2; // Customize appearance for an empty, 1x1 tower var baseGraphics = emptyTower.children[0]; baseGraphics.width = CELL_SIZE; baseGraphics.height = CELL_SIZE; baseGraphics.tint = 0xAAAAAA; // Neutral gray color // Hide the gun var gunContainer = emptyTower.children[emptyTower.children.length - 1]; gunContainer.visible = false; // Level indicators are already hidden due to playerIndex being -1 // Behaviors are disabled via the isActivated flag in the Tower class update/down methods. // Add to game world game.addChild(emptyTower); towers.push(emptyTower); // Mark the cell as an obstacle cell.type = 1; // Flag this cell to prevent the renderer from drawing a 'wall' asset on top cell.isVisuallyEmptyTower = true; } // If any towers were added, update pathfinding if (emptyTowerCells.length > 0) { grid.pathFind(); grid.renderDebug(); } } // Call the function to add headquarters addHeadquarters(); // Call the function to add empty towers addEmptyTowers(); var offset = 0; game.addChild(enemyLayer); var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; 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; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 5; cost = 5; return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(false, undefined, 0); // Pass false for headquarters, undefined orientation, playerIndex 0 for player tower.placeOnGrid(gridX, gridY); game.addChild(tower); towers.push(tower); setGold(gold - towerCost); grid.pathFind(); 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; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } // Check if clicking on a tower or source tower var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - CELL_SIZE; var towerRight = tower.x + CELL_SIZE; var towerTop = tower.y - CELL_SIZE; var towerBottom = tower.y + CELL_SIZE; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } // Check if clicking on source tower if (sourceTower) { var sourceLeft = sourceTower.x - sourceTower.width / 2; var sourceRight = sourceTower.x + sourceTower.width / 2; var sourceTop = sourceTower.y - sourceTower.height / 2; var sourceBottom = sourceTower.y + sourceTower.height / 2; if (x >= sourceLeft && x <= sourceRight && y >= sourceTop && y <= sourceBottom) { clickedOnTower = true; } } // Start unit selection if not clicking on a tower and not in command mode if (!clickedOnTower && !isDragging && !isCommandMode) { // Clean up any existing selection circle first if (selectionCircle) { game.removeChild(selectionCircle); selectionCircle = null; } isSelectingUnits = true; selectionStartX = x; selectionStartY = y; // Create selection circle selectionCircle = new SelectionCircle(); selectionCircle.setPosition(x, y); game.addChild(selectionCircle); } }; game.move = function (x, y, obj) { if (isDragging) { // Shift the y position upward by 1.5 tiles to show preview above finger towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } else if (isSelectingUnits && selectionCircle) { // Check if cursor went out of screen bounds (with some margin for edge cases) var screenMargin = 50; if (x < -screenMargin || x > 2048 + screenMargin || y < -screenMargin || y > 2732 + screenMargin) { // Cursor went out of bounds, cancel selection isSelectingUnits = false; if (selectionCircle) { game.removeChild(selectionCircle); selectionCircle = null; } } else { // Update selection circle normally selectionCircle.updateCircle(x, y); } } }; game.up = function (x, y, obj) { // Handle unit selection completion if (isSelectingUnits && selectionCircle) { isSelectingUnits = false; // Clear previous selection for (var i = 0; i < selectedUnits.length; i++) { selectedUnits[i].setSelected(false); } selectedUnits = []; // Check which units are within the selection circle var centerX = selectionCircle.startX; var centerY = selectionCircle.startY; var radius = selectionCircle.currentRadius; if (unitManager && unitManager.units) { for (var i = 0; i < unitManager.units.length; i++) { var unit = unitManager.units[i]; // Only select player units (ownerIndex === 0) if (unit.ownerIndex === 0) { var dx = unit.x - centerX; var dy = unit.y - centerY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= radius) { unit.setSelected(true); selectedUnits.push(unit); } } } } // Remove selection circle game.removeChild(selectionCircle); selectionCircle = null; // Show feedback if units were selected if (selectedUnits.length > 0) { var notification = game.addChild(new Notification(selectedUnits.length + " units selected")); notification.x = 2048 / 2; notification.y = grid.height - 250; isCommandMode = true; // Enter command mode } return; } // Handle command casting for selected units if (isCommandMode && selectedUnits.length > 0) { // Convert screen coordinates to grid coordinates var gridPosX = x - grid.x; var gridPosY = y - grid.y; var cellX = Math.floor(gridPosX / CELL_SIZE); var cellY = Math.floor(gridPosY / CELL_SIZE) - mapGridOffset; // Get the cell at the tapped position var targetCell = grid.getCell(cellX, cellY); // If tapped outside grid or on a wall, find closest empty cell if (!targetCell || targetCell.type === 1 || targetCell.type === 9) { // Find closest empty cell var closestCell = null; var closestDistance = Infinity; // Search in expanding rings from the tapped position var searchRadius = 1; var maxSearchRadius = 10; while (!closestCell && searchRadius <= maxSearchRadius) { for (var dx = -searchRadius; dx <= searchRadius; dx++) { for (var dy = -searchRadius; dy <= searchRadius; dy++) { // Only check cells on the perimeter of the search square if (Math.abs(dx) === searchRadius || Math.abs(dy) === searchRadius) { var checkX = cellX + dx; var checkY = cellY + dy; var checkCell = grid.getCell(checkX, checkY); if (checkCell && checkCell.type === 0) { var dist = dx * dx + dy * dy; if (dist < closestDistance) { closestDistance = dist; closestCell = checkCell; } } } } } searchRadius++; } targetCell = closestCell; } // Check if target cell contains an enemy tower var targetHasTower = false; var enemyTower = null; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; // Check if this is an enemy tower (playerIndex !== 0 for player units) if (tower.playerIndex !== 0) { var towerGridX, towerGridY; var towerSize = tower.isHeadquarters ? 4 : 2; if (tower.isHeadquarters) { towerGridX = Math.round((tower.x - grid.x) / CELL_SIZE) - towerSize / 2; towerGridY = Math.round((tower.y - grid.y) / CELL_SIZE) - towerSize / 2 - mapGridOffset; } else { towerGridX = Math.floor((tower.x - grid.x) / CELL_SIZE); towerGridY = Math.floor((tower.y - grid.y) / CELL_SIZE) - mapGridOffset; } // Check if the clicked cell is within the tower's area if (cellX >= towerGridX && cellX < towerGridX + towerSize && cellY >= towerGridY && cellY < towerGridY + towerSize) { targetHasTower = true; enemyTower = tower; break; } } } // If we found a valid target cell or an enemy tower if (targetCell && targetCell.type === 0 || targetHasTower) { // Send all selected units to the target for (var i = 0; i < selectedUnits.length; i++) { var unit = selectedUnits[i]; if (targetHasTower) { // Use the tower's grid position for targeting unit.setTarget(cellX, cellY); } else { unit.setTarget(targetCell.x, targetCell.y); } } // Show feedback var message; if (targetHasTower) { if (enemyTower.playerIndex === -1) { message = "Units capturing tower!"; } else { message = "Units attacking enemy tower!"; } } else { message = "Units moving to target"; } var notification = game.addChild(new Notification(message)); notification.x = 2048 / 2; notification.y = grid.height - 250; // Clear selection and exit command mode for (var i = 0; i < selectedUnits.length; i++) { selectedUnits[i].setSelected(false); } selectedUnits = []; isCommandMode = false; } else { // No valid target found var notification = game.addChild(new Notification("Invalid target location")); notification.x = 2048 / 2; notification.y = grid.height - 250; } return; } 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]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; 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 if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } 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 nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); sourceTower = null; enemiesToSpawn = 10; game.update = function () { // Update unit manager if (unitManager) { unitManager.update(); } 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) { enemyCount = 1; 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 = mapGridOffset; // Position at entry area boundary enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // 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; } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { LK.showGameOver(); } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
===================================================================
--- original.js
+++ change.js
@@ -131,24 +131,28 @@
tint: bg1.config.tint || 0xFFFFFF
});
var moveDistance = bg1.graphics.width;
var duration = 40000 / speed;
- var _animate = function animate(bg) {
+ var _animate = function animate(bg, initialX) {
+ // Always animate from current position to current - moveDistance
self.animateBackground(bg, {
x: bg.graphics.x - moveDistance
}, {
duration: duration,
easing: tween.linear,
onFinish: function onFinish() {
if (self.animationActive) {
- bg.graphics.x += moveDistance * 2;
- _animate(bg);
+ // If the background has fully moved out of view to the left, reset to the right
+ if (bg.graphics.x <= -moveDistance / 2) {
+ bg.graphics.x = bg.graphics.x + moveDistance * 2;
+ }
+ _animate(bg, initialX);
}
}
});
};
- _animate(bg1);
- _animate(bg2);
+ _animate(bg1, bg1.graphics.x);
+ _animate(bg2, bg2.graphics.x);
})(i);
}
};
return self;