User prompt
Make the upgrade button in the menu twice as tall
User prompt
Update these lines so they use variabels from the element itself
Code edit (1 edits merged)
Please save this source code
User prompt
If you release the mouse while hovering over the label text on the upgrade button it seems to hide the upgrade dialogue. Make sure that does not happen.
Code edit (4 edits merged)
Please save this source code
User prompt
Swap the position of sell and upgrade buttons
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'obj.event.stopPropagation = true;' Line Number: 1000
User prompt
When releasing the upgrade button the upgrade dialogue seems to hide itself. It should not. Overall you should not hide the dialogue when anything inside of it is clicked. Except the close button.
User prompt
When pressing the upgrade button the upgrade dialogue seems to hide itself. It should not
Code edit (1 edits merged)
Please save this source code
User prompt
The dialogue still closes if you press the bottom of the buy button. Make sure the hight is the full with of the content of the dialogue. Also scale up the upgrade info text by 30% at the same time.
User prompt
When pressing the upgrade button the dialogue seems to hide itself. Debug this and fix it. I think it happens because the dialogue background is not as high as the button set. (Only happens if you press the bottom of the upgrade button) at the same time make the upgrade button 30% taller. Note the sell button should keep it's height,
User prompt
When pressing the upgrade button the dialogue seems to hide itself. Debug this and fix it.
User prompt
Remove the + from the sell button. Also make upgrade the bottom option with sell being the top option of the two.
User prompt
The sell value shown in the upgrade dialogue does not update live when you upgrade your tower. It should
User prompt
The elements in the powerup dialogue now overlaps, make it taller so all elements fit.
User prompt
Also add the option to sell towers. Sell value should be 60% of the total value of the tower and it's upgrades
User prompt
Make the upgrade dialogue slightly transparent, to allow you to see the game below it
User prompt
The range indicator seems to disappear when you upgrade a tower. It should show anytime the tower is selected
User prompt
The range indicator seems to disappear when you upgrade a tower. It should show anytime the tower is selected
Code edit (1 edits merged)
Please save this source code
User prompt
In the upgrade dialogue, the button should be the full width of the dialogue. It should also take up the entire bottom of the dialogue with no bottom margin
User prompt
The range indicator on the towers when showing upgrading, should appear below the tower and the upgrade dialog
User prompt
Move down the source towers a bit, they overlap the the game itself now
User prompt
When a tower is selected for upgrading, also show the towers current range the same way as we do on the preview tower
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Enemy class // Bullet class var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); // Bullet properties self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; // Visual representation of bullet var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); // Update method - called every frame self.update = function () { // If target is gone, destroy this bullet if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } // Move toward target var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // If we hit the target if (distance < self.speed) { // Deal damage to enemy self.targetEnemy.health -= self.damage; // Update health bar visual if (self.targetEnemy.health <= 0) { // Enemy is defeated self.targetEnemy.health = 0; } else { // Update health bar width based on remaining health percentage self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Remove this bullet self.destroy(); } else { // Move toward target var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); // DebugCell class var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; // Add a debug arrow to the debug cells var debugArrows = []; // Add a number label to the debug cells var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); cellGraphics.tint = 0x880000; return; } numberLabel.visible = true; var tint = Math.floor(data.score / maxScore * 0x88); cellGraphics.tint = 0x88 - tint << 8 | tint; while (debugArrows.length > data.targets.length) { self.removeChild(debugArrows.pop()); } for (var a = 0; a < data.targets.length; a++) { var destination = data.targets[a]; var ox = destination.x - data.x; var oy = destination.y - data.y; var angle = Math.atan2(oy, ox); if (!debugArrows[a]) { debugArrows[a] = LK.getAsset('arrow', { anchorX: -.5, anchorY: 0.5 }); debugArrows[a].alpha = .5; self.addChildAt(debugArrows[a], 1); // debugArrows = } debugArrows[a].rotation = angle; // debugArrow.rotation = angle; } break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0x008800; numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); }; }); var Enemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); self.speed = .05; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; // Starting max health self.health = self.maxHealth; // Current health self.bulletsTargetingThis = []; // Track bullets targeting this enemy // Add health bar to the enemy var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; // Position the health bar above the enemy healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; }); //Class for the gr0x412e2eles all logic related to grid movement and interaction 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: [] // Cache for towers in range }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { // Set the type to 1 (wall) if the cell is on the edge of the grid, otherwise set it to 0 (ground) var cell = self.cells[i][j]; var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; if (i > 11 - 3 && i <= 11 + 3) { if (j === 0) { cellType = 2; self.spawns.push(cell); } else if (j <= 4) { cellType = 0; } else if (j === gridHeight - 1) { cellType = 3; self.goals.push(cell); } else if (j >= gridHeight - 4) { cellType = 0; } } cell.type = cellType; cell.x = i; cell.y = j; /*if (cell.type == 0) { if (Math.random() > .5) { cell.type = 1; } }*/ //Cache neighbours for speed cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { 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; // Check for diagonal movement and ensure both adjacent cells are not walls 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]; 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"); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked"); 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.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { //Allows enemies to change direction right away. 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; }; }); // Notification class 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; // 2 seconds at 60FPS self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); // SourceTower class var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Create the tower base var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); // Customize visuals based on tower type switch (self.towerType) { case 'rapid': baseGraphics.tint = 0x00AAFF; // Blue break; case 'sniper': baseGraphics.tint = 0xFF5500; // Orange break; case 'splash': baseGraphics.tint = 0x33CC00; // Bright Green break; case 'slow': baseGraphics.tint = 0x9900FF; // Deep Purple break; case 'poison': baseGraphics.tint = 0x00FFAA; // Aqua break; default: baseGraphics.tint = 0xAAAAAA; // Gray } // Calculate cost based on tower type var towerCost = 5; switch (self.towerType) { case 'rapid': towerCost = 15; // 10 gold more than default break; case 'sniper': towerCost = 25; // 10 gold more than rapid break; case 'splash': towerCost = 35; // 10 gold more than sniper break; case 'slow': towerCost = 45; // 10 gold more than splash break; case 'poison': towerCost = 55; // 10 gold more than slow break; } // Add a label to indicate tower cost var costLabel = new Text2('Cost: ' + towerCost, { size: 40, fill: 0xFFFFFF, weight: 800 }); costLabel.anchor.set(0.5, 0); costLabel.y = baseGraphics.height / 2 + 5; self.addChild(costLabel); return self; }); // Tower class that can be placed on the grid var Tower = Container.expand(function (id) { var self = Container.call(this); // Tower attributes self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // initial range (3 grid cells) self.cellsInRange = []; // Cache for cells in range self.fireRate = 60; // frames between shots self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; // Set tower attributes based on type switch (self.id) { case 'rapid': self.fireRate = 30; // Faster firing rate self.damage = 5; // Lower damage self.range = 2.5 * CELL_SIZE; // Slightly shorter range self.bulletSpeed = 7; // Faster bullets break; case 'sniper': self.fireRate = 90; // Slower firing rate self.damage = 25; // Higher damage self.range = 5 * CELL_SIZE; // Longer range self.bulletSpeed = 8; // Fast bullets break; case 'splash': self.fireRate = 75; // Medium firing rate self.damage = 15; // Medium damage self.range = 3 * CELL_SIZE; // Medium range self.bulletSpeed = 4; // Slower bullets break; case 'slow': self.fireRate = 50; // Medium firing rate self.damage = 8; // Lower damage self.range = 3.5 * CELL_SIZE; // Medium-high range self.bulletSpeed = 5; // Medium bullets break; case 'poison': self.fireRate = 70; // Medium firing rate self.damage = 12; // Medium damage self.range = 3.2 * CELL_SIZE; // Medium range self.bulletSpeed = 5; // Medium bullets break; } // Create the tower base (a square) var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); // Set tower color based on type switch (self.id) { case 'rapid': baseGraphics.tint = 0x00AAFF; // Blue break; case 'sniper': baseGraphics.tint = 0xFF5500; // Orange break; case 'splash': baseGraphics.tint = 0x33CC00; // Bright Green break; case 'slow': baseGraphics.tint = 0x9900FF; // Deep Purple break; case 'poison': baseGraphics.tint = 0x00FFAA; // Aqua break; default: baseGraphics.tint = 0xAAAAAA; // Gray } // Level indicators (dots at bottom of tower) 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(); // Create black outline circle behind the indicator var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; // Slightly larger for outline outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; // Black outline // Add the colored indicator on top var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; // Gray dots by default // Position dot at bottom of tower dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } // Create the gun turret that will rotate var gunContainer = new Container(); self.addChild(gunContainer); var gunGraphics = gunContainer.attachAsset('defense', { anchorX: 0.5, anchorY: 0.5 }); //gunGraphics.y = -CELL_SIZE * 0.3; // Position the gun slightly up from center // Update the level indicators self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; // Index 1 is the colored indicator (index 0 is the black outline) // Light up dots up to current level with white color if (i < self.level) { // All upgraded levels should be white towerLevelIndicator.tint = 0xFFFFFF; // White for active levels } else { // Levels not yet unlocked should match the tower's background color switch (self.id) { case 'rapid': towerLevelIndicator.tint = 0x00AAFF; // Blue break; case 'sniper': towerLevelIndicator.tint = 0xFF5500; // Orange break; case 'splash': towerLevelIndicator.tint = 0x33CC00; // Bright Green break; case 'slow': towerLevelIndicator.tint = 0x9900FF; // Deep Purple break; case 'poison': towerLevelIndicator.tint = 0x00FFAA; // Aqua break; default: towerLevelIndicator.tint = 0xAAAAAA; // Gray for default } } } }; // Initialize level indicators self.updateLevelIndicators(); // Method to upgrade the tower self.upgrade = function () { if (self.level < self.maxLevel) { // Calculate upgrade cost based on level var upgradeCost = 10 + self.level * 5; // Check if player has enough gold if (gold >= upgradeCost) { gold -= upgradeCost; self.level++; // Increase stats based on level self.range = (3 + self.level * 0.5) * CELL_SIZE; self.fireRate = Math.max(20, 60 - self.level * 8); // Faster firing rate self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; // Update level indicators with animation self.updateLevelIndicators(); // Add a flash animation to the newly activated level indicator if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; // Get the newly activated indicator // Flash the indicator by scaling it up and back 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 }); } }); } // Update UI to reflect gold change updateUI(); return true; } else { // Show notification for not enough gold var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; // Method to find the nearest enemy within range self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; // Use pathfinding score to determine proximity to goal for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; for (var j = 0; j < enemies.length; j++) { var enemy = enemies[j]; if (enemy.cellX === cell.x && enemy.cellY === cell.y) { // Check if this enemy is closer to the goal based on pathfinding score if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } // Reset targetEnemy if no valid target is found if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; // Update method self.update = function () { // Always find the target every frame to ensure state consistency self.targetEnemy = self.findTarget(); // Rotate gun toward target if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); // Rotate the gun container gunContainer.rotation = angle; // Fire at target if cooldown is complete if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } }; // Add click event handler to open upgrade menu self.down = function (x, y, obj) { // Check if there's already an upgrade menu open var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); // Check if this tower already has an upgrade menu open var hasOwnMenu = false; var rangeCircle = null; // Find this tower's range indicator if it exists 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 this tower already has a menu open, close it and return if (hasOwnMenu) { // Find and destroy this tower's upgrade menu for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { existingMenus[i].destroy(); } } // Remove range circle if it exists if (rangeCircle) { game.removeChild(rangeCircle); } return; } // Remove any existing menus and range circles 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]); } } // Create new upgrade menu var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); // Position menu above the tower with increased distance to account for larger size upgradeMenu.x = self.x; upgradeMenu.y = self.y - 225; // Increased from 150 to 225 (50% larger) // Add a small animation upgradeMenu.scale.set(0.1); tween(upgradeMenu, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.elasticOut }); // Show the tower's range circle var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; // Mark this as a tower range indicator rangeIndicator.tower = self; // Reference to the tower it belongs to game.addChild(rangeIndicator); // Position at tower center rangeIndicator.x = self.x; rangeIndicator.y = self.y; // Create a range circle with the tower's current range var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.range * 2; // Diameter is twice the radius rangeGraphics.alpha = 0.3; // Make it semi-transparent }; // Check if enemy is in range 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.range; }; // Fire at the current target self.fire = function () { if (self.targetEnemy) { // Verify the target is alive and calculate damage including future damage var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } // Only shoot if enemy would still be alive after all current bullets hit if (self.targetEnemy.health > potentialDamage) { // Calculate bullet starting position var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; // Create a new bullet var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); game.addChild(bullet); // Add bullet to global bullets array and to enemy's tracking array bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); } } }; // Place tower on the grid self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; // Adjust by half a cell size to align with grid cell centers self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; // Mark these grid cells as occupied 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; // Mark as wall/occupied } } } // Calculate cells in range self.cellsInRange = []; for (var i = -Math.floor(self.range / CELL_SIZE); i <= Math.floor(self.range / CELL_SIZE); i++) { for (var j = -Math.floor(self.range / CELL_SIZE); j <= Math.floor(self.range / CELL_SIZE); j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } }; return self; }); // TowerPreview class for showing where towers can be placed var TowerPreview = Container.expand(function () { var self = Container.call(this); // Define tower range (in grid cells) var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; // Tower type being dragged self.towerType = 'default'; // Create the range indicator (circle) var rangeIndicator = new Container(); self.addChild(rangeIndicator); // Since we don't have direct access to Graphics, // we'll create a white semi-transparent circle shape asset var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; // Make it semi-transparent // Create the preview square (2x2 grid cells) var previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); // Set the size to cover 2x2 grid cells previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; // Initial state self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; // Track if placement is blocked by enemy // Update appearance based on tower type self.updateAppearance = function () { // Update range size based on tower type switch (self.towerType) { case 'rapid': rangeGraphics.width = rangeGraphics.height = 2.5 * CELL_SIZE * 2; previewGraphics.tint = 0x00AAFF; // Blue break; case 'sniper': rangeGraphics.width = rangeGraphics.height = 5 * CELL_SIZE * 2; previewGraphics.tint = 0xFF5500; // Orange break; case 'splash': rangeGraphics.width = rangeGraphics.height = 3 * CELL_SIZE * 2; previewGraphics.tint = 0x33CC00; // Bright Green break; case 'slow': rangeGraphics.width = rangeGraphics.height = 3.5 * CELL_SIZE * 2; previewGraphics.tint = 0x9900FF; // Deep Purple break; case 'poison': rangeGraphics.width = rangeGraphics.height = 3.2 * CELL_SIZE * 2; previewGraphics.tint = 0x00FFAA; // Aqua break; default: rangeGraphics.width = rangeGraphics.height = 3 * CELL_SIZE * 2; previewGraphics.tint = 0xAAAAAA; // Gray } // Reset to original preview color based on placement possibility if (!self.canPlace) { previewGraphics.tint = 0xFF0000; // Red if can't place } }; // Update the preview color based on placement possibility self.updatePlacementStatus = function () { // First check if placement is valid from a grid perspective var validGridPlacement = true; // Check if we're in the valid placement area (main square of the board) if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) { validGridPlacement = false; } else { // Check each of the 2x2 cells for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); // Can't place if any cell is undefined or not of type 0 (empty ground) if (!cell || cell.type !== 0) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } // Now check if any enemy is occupying the space where the tower would be placed // or if any cell is the current target of an enemy self.blockedByEnemy = false; if (validGridPlacement) { // Check for enemies occupying cells for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Check if enemy is on this cell if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) { self.blockedByEnemy = true; break; } // Check if enemy's current target is one of these cells 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; } } } } // Set final placement status self.canPlace = validGridPlacement && !self.blockedByEnemy; // Update appearance based on placement and tower type self.updateAppearance(); }; // Check if the tower can be placed at the current position self.checkPlacement = function () { self.updatePlacementStatus(); }; // Snap the preview to the grid self.snapToGrid = function (x, y) { // Convert screen coordinates to grid coordinates // Offset by the grid's position var gridPosX = x - grid.x; var gridPosY = y - grid.y; // Convert to grid coordinates and round down to get the top-left cell self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); // Position the preview at the intersection of the four cells // Adding CELL_SIZE to gridX and gridY gives us the center point // Adjust by half a cell size to align with grid cell centers self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; // Center of the 2x2 grid self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; // Center of the 2x2 grid // Check if we can place the tower here self.checkPlacement(); }; return self; }); // UpgradeMenu class var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; // Create background var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 450; // 50% larger: 300 * 1.5 menuBackground.height = 300; // 50% larger: 200 * 1.5 menuBackground.tint = 0x444444; // Create upgrade button var upgradeButton = new Container(); self.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; // 50% larger: 200 * 1.5 buttonBackground.height = 90; // 50% larger: 60 * 1.5 buttonBackground.tint = 0x00AA00; // Calculate upgrade cost var upgradeCost = 10 + self.tower.level * 5; // Create button text var buttonText = new Text2('Upgrade: ' + upgradeCost + ' gold', { size: 45, // 50% larger: 30 * 1.5 fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); // Tower stats 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: 36, // 50% larger: 24 * 1.5 fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0.5, 0); statsText.y = -120; // 50% larger: -80 * 1.5 self.addChild(statsText); // Close button var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 60; // 50% larger: 40 * 1.5 closeBackground.height = 60; // 50% larger: 40 * 1.5 closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 45, // 50% larger: 30 * 1.5 fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 38; // 50% larger: 25 * 1.5 = 37.5, rounded to 38 closeButton.y = -menuBackground.height / 2 + 38; // 50% larger: 25 * 1.5 = 37.5, rounded to 38 // Position upgrade button upgradeButton.y = 60; // 50% larger: 40 * 1.5 // Event handlers upgradeButton.down = function (x, y, obj) { if (self.tower.upgrade()) { // Update stats text after upgrade upgradeCost = 10 + self.tower.level * 5; 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'); // If tower is max level, disable button if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } // Update range indicator if it exists for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { var rangeCircle = game.children[i]; var rangeGraphics = rangeCircle.children[0]; // Update the range circle to the new tower range rangeGraphics.width = rangeGraphics.height = self.tower.range * 2; break; } } // Add a nice animation effect 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 }); } }); } }; closeButton.down = function (x, y, obj) { self.destroy(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 //Init game with black background }); /**** * Game Code ****/ // Define cell size as a global constant // Import tween plugin for animations var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; // Array to store active bullets var defenses = []; // Game resources and scores var gold = 80; var lives = 20; var score = 0; // Source tower that players drag from to create new towers var sourceTower = null; // Create UI elements for resources var goldText = new Text2('Gold: ' + gold, { size: 40, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 40, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 40, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); // Position UI elements at the top of the screen // Avoid top left corner (pause button area) // Center the UI elements with proper spacing var topMargin = 60; var centerX = 2048 / 2; var spacing = 300; // Reduced spacing between elements // Position the elements centered on the screen // First, attach to the top center container LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); // Position lives text in the center livesText.x = 0; // Center position (relative to top center) livesText.y = topMargin; // Position gold text to the left of lives goldText.x = -spacing; goldText.y = topMargin; // Position score text to the right of lives scoreText.x = spacing; scoreText.y = topMargin; // Helper function to update UI function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); } // Create separate layers for debug tiles, towers, and enemies var debugLayer = new Container(); var towerLayer = new Container(); var enemyLayer = new Container(); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 250 - CELL_SIZE * 4; grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); // Add layers to the game in the correct order game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; // Create tower preview var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; // Handle drag events for tower placement var isDragging = false; // Check if placing a tower would block the path function wouldBlockPath(gridX, gridY) { // Temporarily mark cells as walls 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; // Mark as wall } } } // Check if path is still valid var blocked = grid.pathFind(); // Restore original cell types for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } // Re-find path with original cells grid.pathFind(); grid.renderDebug(); return blocked; } // Place a tower at the grid position function placeTower(gridX, gridY, towerType) { // Tower cost based on tower type var towerCost = 5; switch (towerType) { case 'rapid': towerCost = 15; break; case 'sniper': towerCost = 25; break; case 'splash': towerCost = 35; break; case 'slow': towerCost = 45; break; case 'poison': towerCost = 55; break; } // Check if player has enough gold if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); // Deduct gold gold -= towerCost; updateUI(); // Update grid pathing grid.pathFind(); grid.renderDebug(); return true; } else { // Not enough gold var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { // Check if click is on any source tower for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { towerPreview.visible = true; isDragging = true; // Store which tower type we're dragging towerPreview.towerType = tower.towerType; // Update preview appearance based on tower type towerPreview.updateAppearance(); towerPreview.snapToGrid(x, y); break; } } }; game.move = function (x, y, obj) { if (isDragging) { towerPreview.snapToGrid(x, y); } }; game.up = function (x, y, obj) { // Track if we clicked on a tower to show the upgrade menu var clickedOnTower = false; // Check if click was on a tower for (var i = 0; i < towers.length; i++) { var tower = towers[i]; // Calculate tower bounds 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; // Check if click is inside tower bounds if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } // Check for clicking outside upgrade menu var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); // If there are upgrade menus open, check if we clicked outside if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; // Check if the click is inside any upgrade menu for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; // Calculate menu bounds var menuWidth = 450; // From UpgradeMenu width var menuHeight = 300; // From UpgradeMenu height var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; // Check if click is inside menu bounds if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } // If clicked outside all menus, close them if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } // Also remove any range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } } } if (isDragging) { isDragging = false; // Check placement possibilities if (towerPreview.canPlace) { // Check if placing here would block the path if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) { // Place the tower (placeTower will handle gold check internally) placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { // Show notification that we can't place here var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (towerPreview.blockedByEnemy) { // Show notification that an enemy is in the way 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) { // Generic notification for other reasons var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; // Close any upgrade menus when placing towers if (isDragging) { var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } } }; // Create a row of source towers var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; var sourceTowers = []; var towerSpacing = 170; var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 5; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } // Set sourceTower to null as we'll use the array instead sourceTower = null; game.update = function () { // Spawn enemies periodically if (++offset % 100 == 0) { var target = grid.spawns[Math.random() * grid.spawns.length >> 0]; var enemy = enemyLayer.addChild(new Enemy()); enemy.currentCellX = enemy.cellX = target.x; enemy.currentCellY = enemy.cellY = target.y; enemies.push(enemy); } // Update enemies for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; // Check if enemy is defeated if (enemy.health <= 0) { // Clear bullet targeting references for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; // This will cause bullet to destroy itself on next update } // Award gold and score for defeating enemy gold += 1; score += 5; updateUI(); // Remove enemy enemyLayer.removeChild(enemy); enemies.splice(a, 1); continue; } // Update enemy movement if (grid.updateEnemy(enemy)) { // Enemy reached goal enemyLayer.removeChild(enemy); enemies.splice(a, 1); // Reduce lives when enemy reaches goal lives = Math.max(0, lives - 1); updateUI(); // Check if game over (no lives left) if (lives <= 0) { LK.showGameOver(); } } } // Update towers for (var i = 0; i < towers.length; i++) { towers[i].update(); } // Update bullets for (var i = bullets.length - 1; i >= 0; i--) { bullets[i].update(); // Remove destroyed bullets if (!bullets[i].parent) { // If this bullet has a target, remove the bullet from the target's tracking array if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } // Ensure bullet is removed from global bullets array bullets.splice(i, 1); } } // Update tower preview placement status on every frame // This ensures the placement status updates in real-time as enemies move if (towerPreview.visible) { towerPreview.checkPlacement(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Enemy class
// Bullet class
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
// Bullet properties
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
// Visual representation of bullet
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
// Update method - called every frame
self.update = function () {
// If target is gone, destroy this bullet
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
// Move toward target
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If we hit the target
if (distance < self.speed) {
// Deal damage to enemy
self.targetEnemy.health -= self.damage;
// Update health bar visual
if (self.targetEnemy.health <= 0) {
// Enemy is defeated
self.targetEnemy.health = 0;
} else {
// Update health bar width based on remaining health percentage
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Remove this bullet
self.destroy();
} else {
// Move toward target
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
// DebugCell class
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
// Add a debug arrow to the debug cells
var debugArrows = [];
// Add a number label to the debug cells
var numberLabel = new Text2('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.setText("-");
cellGraphics.tint = 0x880000;
return;
}
numberLabel.visible = true;
var tint = Math.floor(data.score / maxScore * 0x88);
cellGraphics.tint = 0x88 - tint << 8 | tint;
while (debugArrows.length > data.targets.length) {
self.removeChild(debugArrows.pop());
}
for (var a = 0; a < data.targets.length; a++) {
var destination = data.targets[a];
var ox = destination.x - data.x;
var oy = destination.y - data.y;
var angle = Math.atan2(oy, ox);
if (!debugArrows[a]) {
debugArrows[a] = LK.getAsset('arrow', {
anchorX: -.5,
anchorY: 0.5
});
debugArrows[a].alpha = .5;
self.addChildAt(debugArrows[a], 1);
// debugArrows =
}
debugArrows[a].rotation = angle;
// debugArrow.rotation = angle;
}
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.tint = 0x008800;
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 1000) / 10);
};
});
var Enemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = .05;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100; // Starting max health
self.health = self.maxHealth; // Current health
self.bulletsTargetingThis = []; // Track bullets targeting this enemy
// Add health bar to the enemy
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; // Position the health bar above the enemy
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
});
//Class for the gr0x412e2eles all logic related to grid movement and interaction
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: [] // Cache for towers in range
};
}
}
/*
Cell Types
0: Transparent floor
1: Wall
2: Spawn
3: Goal
*/
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
// Set the type to 1 (wall) if the cell is on the edge of the grid, otherwise set it to 0 (ground)
var cell = self.cells[i][j];
var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0;
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = 0;
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = 0;
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
/*if (cell.type == 0) {
if (Math.random() > .5) {
cell.type = 1;
}
}*/
//Cache neighbours for speed
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
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;
// Check for diagonal movement and ensure both adjacent cells are not walls
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];
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");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked");
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.currentTarget) {
enemy.currentTarget = cell.targets[0];
}
if (enemy.currentTarget) {
//Allows enemies to change direction right away.
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;
};
});
// Notification class
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; // 2 seconds at 60FPS
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
// SourceTower class
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'default';
// Create the tower base
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
// Customize visuals based on tower type
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF; // Blue
break;
case 'sniper':
baseGraphics.tint = 0xFF5500; // Orange
break;
case 'splash':
baseGraphics.tint = 0x33CC00; // Bright Green
break;
case 'slow':
baseGraphics.tint = 0x9900FF; // Deep Purple
break;
case 'poison':
baseGraphics.tint = 0x00FFAA; // Aqua
break;
default:
baseGraphics.tint = 0xAAAAAA;
// Gray
}
// Calculate cost based on tower type
var towerCost = 5;
switch (self.towerType) {
case 'rapid':
towerCost = 15; // 10 gold more than default
break;
case 'sniper':
towerCost = 25; // 10 gold more than rapid
break;
case 'splash':
towerCost = 35; // 10 gold more than sniper
break;
case 'slow':
towerCost = 45; // 10 gold more than splash
break;
case 'poison':
towerCost = 55; // 10 gold more than slow
break;
}
// Add a label to indicate tower cost
var costLabel = new Text2('Cost: ' + towerCost, {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
costLabel.anchor.set(0.5, 0);
costLabel.y = baseGraphics.height / 2 + 5;
self.addChild(costLabel);
return self;
});
// Tower class that can be placed on the grid
var Tower = Container.expand(function (id) {
var self = Container.call(this);
// Tower attributes
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE; // initial range (3 grid cells)
self.cellsInRange = []; // Cache for cells in range
self.fireRate = 60; // frames between shots
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
// Set tower attributes based on type
switch (self.id) {
case 'rapid':
self.fireRate = 30; // Faster firing rate
self.damage = 5; // Lower damage
self.range = 2.5 * CELL_SIZE; // Slightly shorter range
self.bulletSpeed = 7; // Faster bullets
break;
case 'sniper':
self.fireRate = 90; // Slower firing rate
self.damage = 25; // Higher damage
self.range = 5 * CELL_SIZE; // Longer range
self.bulletSpeed = 8; // Fast bullets
break;
case 'splash':
self.fireRate = 75; // Medium firing rate
self.damage = 15; // Medium damage
self.range = 3 * CELL_SIZE; // Medium range
self.bulletSpeed = 4; // Slower bullets
break;
case 'slow':
self.fireRate = 50; // Medium firing rate
self.damage = 8; // Lower damage
self.range = 3.5 * CELL_SIZE; // Medium-high range
self.bulletSpeed = 5; // Medium bullets
break;
case 'poison':
self.fireRate = 70; // Medium firing rate
self.damage = 12; // Medium damage
self.range = 3.2 * CELL_SIZE; // Medium range
self.bulletSpeed = 5; // Medium bullets
break;
}
// Create the tower base (a square)
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
// Set tower color based on type
switch (self.id) {
case 'rapid':
baseGraphics.tint = 0x00AAFF; // Blue
break;
case 'sniper':
baseGraphics.tint = 0xFF5500; // Orange
break;
case 'splash':
baseGraphics.tint = 0x33CC00; // Bright Green
break;
case 'slow':
baseGraphics.tint = 0x9900FF; // Deep Purple
break;
case 'poison':
baseGraphics.tint = 0x00FFAA; // Aqua
break;
default:
baseGraphics.tint = 0xAAAAAA;
// Gray
}
// Level indicators (dots at bottom of tower)
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();
// Create black outline circle behind the indicator
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4; // Slightly larger for outline
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000; // Black outline
// Add the colored indicator on top
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC; // Gray dots by default
// Position dot at bottom of tower
dot.x = -CELL_SIZE + dotSpacing * (i + 1);
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
// Create the gun turret that will rotate
var gunContainer = new Container();
self.addChild(gunContainer);
var gunGraphics = gunContainer.attachAsset('defense', {
anchorX: 0.5,
anchorY: 0.5
});
//gunGraphics.y = -CELL_SIZE * 0.3; // Position the gun slightly up from center
// Update the level indicators
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1]; // Index 1 is the colored indicator (index 0 is the black outline)
// Light up dots up to current level with white color
if (i < self.level) {
// All upgraded levels should be white
towerLevelIndicator.tint = 0xFFFFFF; // White for active levels
} else {
// Levels not yet unlocked should match the tower's background color
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF; // Blue
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500; // Orange
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00; // Bright Green
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF; // Deep Purple
break;
case 'poison':
towerLevelIndicator.tint = 0x00FFAA; // Aqua
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
// Gray for default
}
}
}
};
// Initialize level indicators
self.updateLevelIndicators();
// Method to upgrade the tower
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Calculate upgrade cost based on level
var upgradeCost = 10 + self.level * 5;
// Check if player has enough gold
if (gold >= upgradeCost) {
gold -= upgradeCost;
self.level++;
// Increase stats based on level
self.range = (3 + self.level * 0.5) * CELL_SIZE;
self.fireRate = Math.max(20, 60 - self.level * 8); // Faster firing rate
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
// Update level indicators with animation
self.updateLevelIndicators();
// Add a flash animation to the newly activated level indicator
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1]; // Get the newly activated indicator
// Flash the indicator by scaling it up and back
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
});
}
});
}
// Update UI to reflect gold change
updateUI();
return true;
} else {
// Show notification for not enough gold
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
// Method to find the nearest enemy within range
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity; // Use pathfinding score to determine proximity to goal
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
for (var j = 0; j < enemies.length; j++) {
var enemy = enemies[j];
if (enemy.cellX === cell.x && enemy.cellY === cell.y) {
// Check if this enemy is closer to the goal based on pathfinding score
if (cell.score < closestScore) {
closestScore = cell.score;
closestEnemy = enemy;
}
}
}
}
// Reset targetEnemy if no valid target is found
if (!closestEnemy) {
self.targetEnemy = null;
}
return closestEnemy;
};
// Update method
self.update = function () {
// Always find the target every frame to ensure state consistency
self.targetEnemy = self.findTarget();
// Rotate gun toward target
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
// Rotate the gun container
gunContainer.rotation = angle;
// Fire at target if cooldown is complete
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
// Add click event handler to open upgrade menu
self.down = function (x, y, obj) {
// Check if there's already an upgrade menu open
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
// Check if this tower already has an upgrade menu open
var hasOwnMenu = false;
var rangeCircle = null;
// Find this tower's range indicator if it exists
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 this tower already has a menu open, close it and return
if (hasOwnMenu) {
// Find and destroy this tower's upgrade menu
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
existingMenus[i].destroy();
}
}
// Remove range circle if it exists
if (rangeCircle) {
game.removeChild(rangeCircle);
}
return;
}
// Remove any existing menus and range circles
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]);
}
}
// Create new upgrade menu
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
// Position menu above the tower with increased distance to account for larger size
upgradeMenu.x = self.x;
upgradeMenu.y = self.y - 225; // Increased from 150 to 225 (50% larger)
// Add a small animation
upgradeMenu.scale.set(0.1);
tween(upgradeMenu, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.elasticOut
});
// Show the tower's range circle
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true; // Mark this as a tower range indicator
rangeIndicator.tower = self; // Reference to the tower it belongs to
game.addChild(rangeIndicator);
// Position at tower center
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
// Create a range circle with the tower's current range
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.range * 2; // Diameter is twice the radius
rangeGraphics.alpha = 0.3; // Make it semi-transparent
};
// Check if enemy is in range
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.range;
};
// Fire at the current target
self.fire = function () {
if (self.targetEnemy) {
// Verify the target is alive and calculate damage including future damage
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
// Only shoot if enemy would still be alive after all current bullets hit
if (self.targetEnemy.health > potentialDamage) {
// Calculate bullet starting position
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
// Create a new bullet
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
game.addChild(bullet);
// Add bullet to global bullets array and to enemy's tracking array
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
}
}
};
// Place tower on the grid
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
// Adjust by half a cell size to align with grid cell centers
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
// Mark these grid cells as occupied
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; // Mark as wall/occupied
}
}
}
// Calculate cells in range
self.cellsInRange = [];
for (var i = -Math.floor(self.range / CELL_SIZE); i <= Math.floor(self.range / CELL_SIZE); i++) {
for (var j = -Math.floor(self.range / CELL_SIZE); j <= Math.floor(self.range / CELL_SIZE); j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
};
return self;
});
// TowerPreview class for showing where towers can be placed
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
// Define tower range (in grid cells)
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
// Tower type being dragged
self.towerType = 'default';
// Create the range indicator (circle)
var rangeIndicator = new Container();
self.addChild(rangeIndicator);
// Since we don't have direct access to Graphics,
// we'll create a white semi-transparent circle shape asset
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3; // Make it semi-transparent
// Create the preview square (2x2 grid cells)
var previewGraphics = self.attachAsset('towerpreview', {
anchorX: 0.5,
anchorY: 0.5
});
// Set the size to cover 2x2 grid cells
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Initial state
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false; // Track if placement is blocked by enemy
// Update appearance based on tower type
self.updateAppearance = function () {
// Update range size based on tower type
switch (self.towerType) {
case 'rapid':
rangeGraphics.width = rangeGraphics.height = 2.5 * CELL_SIZE * 2;
previewGraphics.tint = 0x00AAFF; // Blue
break;
case 'sniper':
rangeGraphics.width = rangeGraphics.height = 5 * CELL_SIZE * 2;
previewGraphics.tint = 0xFF5500; // Orange
break;
case 'splash':
rangeGraphics.width = rangeGraphics.height = 3 * CELL_SIZE * 2;
previewGraphics.tint = 0x33CC00; // Bright Green
break;
case 'slow':
rangeGraphics.width = rangeGraphics.height = 3.5 * CELL_SIZE * 2;
previewGraphics.tint = 0x9900FF; // Deep Purple
break;
case 'poison':
rangeGraphics.width = rangeGraphics.height = 3.2 * CELL_SIZE * 2;
previewGraphics.tint = 0x00FFAA; // Aqua
break;
default:
rangeGraphics.width = rangeGraphics.height = 3 * CELL_SIZE * 2;
previewGraphics.tint = 0xAAAAAA;
// Gray
}
// Reset to original preview color based on placement possibility
if (!self.canPlace) {
previewGraphics.tint = 0xFF0000; // Red if can't place
}
};
// Update the preview color based on placement possibility
self.updatePlacementStatus = function () {
// First check if placement is valid from a grid perspective
var validGridPlacement = true;
// Check if we're in the valid placement area (main square of the board)
if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
// Check each of the 2x2 cells
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
// Can't place if any cell is undefined or not of type 0 (empty ground)
if (!cell || cell.type !== 0) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
// Now check if any enemy is occupying the space where the tower would be placed
// or if any cell is the current target of an enemy
self.blockedByEnemy = false;
if (validGridPlacement) {
// Check for enemies occupying cells
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Check if enemy is on this cell
if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
// Check if enemy's current target is one of these cells
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;
}
}
}
}
// Set final placement status
self.canPlace = validGridPlacement && !self.blockedByEnemy;
// Update appearance based on placement and tower type
self.updateAppearance();
};
// Check if the tower can be placed at the current position
self.checkPlacement = function () {
self.updatePlacementStatus();
};
// Snap the preview to the grid
self.snapToGrid = function (x, y) {
// Convert screen coordinates to grid coordinates
// Offset by the grid's position
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
// Convert to grid coordinates and round down to get the top-left cell
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
// Position the preview at the intersection of the four cells
// Adding CELL_SIZE to gridX and gridY gives us the center point
// Adjust by half a cell size to align with grid cell centers
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; // Center of the 2x2 grid
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; // Center of the 2x2 grid
// Check if we can place the tower here
self.checkPlacement();
};
return self;
});
// UpgradeMenu class
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
// Create background
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 450; // 50% larger: 300 * 1.5
menuBackground.height = 300; // 50% larger: 200 * 1.5
menuBackground.tint = 0x444444;
// Create upgrade button
var upgradeButton = new Container();
self.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300; // 50% larger: 200 * 1.5
buttonBackground.height = 90; // 50% larger: 60 * 1.5
buttonBackground.tint = 0x00AA00;
// Calculate upgrade cost
var upgradeCost = 10 + self.tower.level * 5;
// Create button text
var buttonText = new Text2('Upgrade: ' + upgradeCost + ' gold', {
size: 45,
// 50% larger: 30 * 1.5
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
// Tower stats
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: 36,
// 50% larger: 24 * 1.5
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0.5, 0);
statsText.y = -120; // 50% larger: -80 * 1.5
self.addChild(statsText);
// Close button
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 60; // 50% larger: 40 * 1.5
closeBackground.height = 60; // 50% larger: 40 * 1.5
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 45,
// 50% larger: 30 * 1.5
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 38; // 50% larger: 25 * 1.5 = 37.5, rounded to 38
closeButton.y = -menuBackground.height / 2 + 38; // 50% larger: 25 * 1.5 = 37.5, rounded to 38
// Position upgrade button
upgradeButton.y = 60; // 50% larger: 40 * 1.5
// Event handlers
upgradeButton.down = function (x, y, obj) {
if (self.tower.upgrade()) {
// Update stats text after upgrade
upgradeCost = 10 + self.tower.level * 5;
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');
// If tower is max level, disable button
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
// Update range indicator if it exists
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
var rangeCircle = game.children[i];
var rangeGraphics = rangeCircle.children[0];
// Update the range circle to the new tower range
rangeGraphics.width = rangeGraphics.height = self.tower.range * 2;
break;
}
}
// Add a nice animation effect
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
});
}
});
}
};
closeButton.down = function (x, y, obj) {
self.destroy();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333 //Init game with black background
});
/****
* Game Code
****/
// Define cell size as a global constant
// Import tween plugin for animations
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = []; // Array to store active bullets
var defenses = [];
// Game resources and scores
var gold = 80;
var lives = 20;
var score = 0;
// Source tower that players drag from to create new towers
var sourceTower = null;
// Create UI elements for resources
var goldText = new Text2('Gold: ' + gold, {
size: 40,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 40,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('Score: ' + score, {
size: 40,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
// Position UI elements at the top of the screen
// Avoid top left corner (pause button area)
// Center the UI elements with proper spacing
var topMargin = 60;
var centerX = 2048 / 2;
var spacing = 300; // Reduced spacing between elements
// Position the elements centered on the screen
// First, attach to the top center container
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(livesText);
LK.gui.top.addChild(scoreText);
// Position lives text in the center
livesText.x = 0; // Center position (relative to top center)
livesText.y = topMargin;
// Position gold text to the left of lives
goldText.x = -spacing;
goldText.y = topMargin;
// Position score text to the right of lives
scoreText.x = spacing;
scoreText.y = topMargin;
// Helper function to update UI
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
}
// Create separate layers for debug tiles, towers, and enemies
var debugLayer = new Container();
var towerLayer = new Container();
var enemyLayer = new Container();
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 250 - CELL_SIZE * 4;
grid.pathFind();
grid.renderDebug();
debugLayer.addChild(grid);
// Add layers to the game in the correct order
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
// Create tower preview
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
// Handle drag events for tower placement
var isDragging = false;
// Check if placing a tower would block the path
function wouldBlockPath(gridX, gridY) {
// Temporarily mark cells as walls
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; // Mark as wall
}
}
}
// Check if path is still valid
var blocked = grid.pathFind();
// Restore original cell types
for (var i = 0; i < cells.length; i++) {
cells[i].cell.type = cells[i].originalType;
}
// Re-find path with original cells
grid.pathFind();
grid.renderDebug();
return blocked;
}
// Place a tower at the grid position
function placeTower(gridX, gridY, towerType) {
// Tower cost based on tower type
var towerCost = 5;
switch (towerType) {
case 'rapid':
towerCost = 15;
break;
case 'sniper':
towerCost = 25;
break;
case 'splash':
towerCost = 35;
break;
case 'slow':
towerCost = 45;
break;
case 'poison':
towerCost = 55;
break;
}
// Check if player has enough gold
if (gold >= towerCost) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
// Deduct gold
gold -= towerCost;
updateUI();
// Update grid pathing
grid.pathFind();
grid.renderDebug();
return true;
} else {
// Not enough gold
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
// Check if click is on any source tower
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
towerPreview.visible = true;
isDragging = true;
// Store which tower type we're dragging
towerPreview.towerType = tower.towerType;
// Update preview appearance based on tower type
towerPreview.updateAppearance();
towerPreview.snapToGrid(x, y);
break;
}
}
};
game.move = function (x, y, obj) {
if (isDragging) {
towerPreview.snapToGrid(x, y);
}
};
game.up = function (x, y, obj) {
// Track if we clicked on a tower to show the upgrade menu
var clickedOnTower = false;
// Check if click was on a tower
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Calculate tower bounds
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;
// Check if click is inside tower bounds
if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) {
clickedOnTower = true;
break;
}
}
// Check for clicking outside upgrade menu
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
// If there are upgrade menus open, check if we clicked outside
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
// Check if the click is inside any upgrade menu
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
// Calculate menu bounds
var menuWidth = 450; // From UpgradeMenu width
var menuHeight = 300; // From UpgradeMenu height
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
// Check if click is inside menu bounds
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
// If clicked outside all menus, close them
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
// Also remove any range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
}
}
if (isDragging) {
isDragging = false;
// Check placement possibilities
if (towerPreview.canPlace) {
// Check if placing here would block the path
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
// Place the tower (placeTower will handle gold check internally)
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
// Show notification that we can't place here
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.blockedByEnemy) {
// Show notification that an enemy is in the way
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) {
// Generic notification for other reasons
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
// Close any upgrade menus when placing towers
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
}
};
// Create a row of source towers
var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
var sourceTowers = [];
var towerSpacing = 170;
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 5;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
// Set sourceTower to null as we'll use the array instead
sourceTower = null;
game.update = function () {
// Spawn enemies periodically
if (++offset % 100 == 0) {
var target = grid.spawns[Math.random() * grid.spawns.length >> 0];
var enemy = enemyLayer.addChild(new Enemy());
enemy.currentCellX = enemy.cellX = target.x;
enemy.currentCellY = enemy.cellY = target.y;
enemies.push(enemy);
}
// Update enemies
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
// Check if enemy is defeated
if (enemy.health <= 0) {
// Clear bullet targeting references
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null; // This will cause bullet to destroy itself on next update
}
// Award gold and score for defeating enemy
gold += 1;
score += 5;
updateUI();
// Remove enemy
enemyLayer.removeChild(enemy);
enemies.splice(a, 1);
continue;
}
// Update enemy movement
if (grid.updateEnemy(enemy)) {
// Enemy reached goal
enemyLayer.removeChild(enemy);
enemies.splice(a, 1);
// Reduce lives when enemy reaches goal
lives = Math.max(0, lives - 1);
updateUI();
// Check if game over (no lives left)
if (lives <= 0) {
LK.showGameOver();
}
}
}
// Update towers
for (var i = 0; i < towers.length; i++) {
towers[i].update();
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; i--) {
bullets[i].update();
// Remove destroyed bullets
if (!bullets[i].parent) {
// If this bullet has a target, remove the bullet from the target's tracking array
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
// Ensure bullet is removed from global bullets array
bullets.splice(i, 1);
}
}
// Update tower preview placement status on every frame
// This ensures the placement status updates in real-time as enemies move
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
};
White circle with two eyes, seen from above.. In-Game asset. 2d. High contrast. No shadows
White simple circular enemy seen from above, black outline. Black eyes, with a single shield in-font of it. Black and white only. Blue background.
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows