Code edit (2 edits merged)
Please save this source code
User prompt
Enemies can currently outrun bullets. Find a clever way to solve this issue. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Our towers should be able to shoot at our enemies. The current tower type is a simple bullet type tower. So make it just shoot bullets at our enemies. Make sure that we only shoot at enemies that are still alive after having been hit by all bullets that have already been fired at said enemy
User prompt
Sometimes when we try to build a tower by releasing the screen, the tower fails to build because an enemy is in the way. In those cases please show a notification that it failed and why.
User prompt
Sometimes when we try to build a tower by releasing the screen, the tower fails to build because an enemy is in the way. In those cases please show a notification that it failed and why.
User prompt
When a tower fails to build because an enemy was in the way, show a notification informing the user of that it happened and why
User prompt
When a tower fails to build because an enemy was in the way, show a notification informing the user of that it happened and why
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'self.cells[i][j].towersInRange = []')' in or related to this line: 'self.cells[i][j].towersInRange = []; // Cache for towers in range' Line Number: 145
User prompt
Ok let's look into targeting from towers. Simply doing a full check of all towers vs all enemies is going to be slow when we have hundreds of each. Therefore each tower should only look for enemies who are currently occupying cells inside the towers reach. You might have to create some caches such that each cell knows which towers are inside it and each tower knows which cells are inside its range.
Code edit (3 edits merged)
Please save this source code
User prompt
currently the start and end gabs in the walls are 7 cells wide, reduce this to 6
Code edit (2 edits merged)
Please save this source code
User prompt
Tower preview based on if enemies are in the way does not seem to work. If I move my mouse a bit it updates correctly, but it does not seem to automatically update correctly when enemies move in and out of the preview area
User prompt
For the tower preview, factor in if the placement of the tower is blocked due to an enemy being where the tower would be placed. This should be live such that the state changes as enemies move. (E.g. update on each update on game)
User prompt
For the tower preview, factor in if the placement of the tower is blocked due to an enemy being where the tower would be placed. This should be live such that the state changes as enemies move.
User prompt
For the tower preview, factor in if the placement of the tower is blocked due to an enemy being where the tower would be placed. This should be live such that the state changes as enemies move.
User prompt
As we can't place a tower if an enemy is currently occupying part of where the tower would be placed. Please update the tower preview to also factor this in.
User prompt
Currently enemies can pathfinder trough a tight corner where two corners of towers touch. I don't think I like that. However I still want towers to be able to traverse diagonally across the board. Can you please update the pathfinding accordingly.
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'dotGraphics.tint = 0xFFFF00; // Yellow for active levels' Line Number: 397
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'dotGraphics.width = dotSize;' Line Number: 373
User prompt
rename the embedded asset dotGraphics to upgradelevelIndicator
User prompt
Enemies seems to render by half a cell_size to the left and up, please fix this
User prompt
Enemies now seem to render misaligned from the debug grid, please fix this
User prompt
Enemies no longer seem to correctly be removed from the game, please fix this
/**** * 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; self.isTracking = true; // Flag to indicate if bullet is actively tracking target self.flightTime = 0; // Track how long the bullet has been flying self.maxFlightTime = 180; // 3 seconds at 60fps // Visual representation of bullet var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); // Initialize any active tweens to null self.moveTween = null; // Method to predict where enemy will be self.predictTargetPosition = function (timeToImpact) { if (!self.targetEnemy || !self.targetEnemy.parent) { return null; } // Calculate current enemy velocity vectors based on their movement var enemyVelocityX = Math.cos(Math.atan2(self.targetEnemy.currentTarget ? self.targetEnemy.currentTarget.y - self.targetEnemy.currentCellY : 0, self.targetEnemy.currentTarget ? self.targetEnemy.currentTarget.x - self.targetEnemy.currentCellX : 0)) * self.targetEnemy.speed; var enemyVelocityY = Math.sin(Math.atan2(self.targetEnemy.currentTarget ? self.targetEnemy.currentTarget.y - self.targetEnemy.currentCellY : 0, self.targetEnemy.currentTarget ? self.targetEnemy.currentTarget.x - self.targetEnemy.currentCellX : 0)) * self.targetEnemy.speed; // Predict where enemy will be return { x: self.targetEnemy.x + enemyVelocityX * timeToImpact * CELL_SIZE, y: self.targetEnemy.y + enemyVelocityY * timeToImpact * CELL_SIZE }; }; // Method to update bullet trajectory with tweening self.updateTrajectory = function () { // If we already have an active tween, stop it if (self.moveTween) { tween.stop(self, { x: true, y: true }); } if (!self.targetEnemy || !self.targetEnemy.parent) { return false; } // Calculate base distance to current enemy position var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Calculate time to impact based on bullet speed var timeToImpact = distance / (self.speed * CELL_SIZE / 60); // Convert to frames // Get predicted position var targetPos = self.predictTargetPosition(timeToImpact); if (!targetPos) { return false; } // Calculate adjusted distance to predicted position dx = targetPos.x - self.x; dy = targetPos.y - self.y; distance = Math.sqrt(dx * dx + dy * dy); // Calculate adjusted time to impact timeToImpact = Math.min(distance / (self.speed * CELL_SIZE / 60), 60); // Cap at 1 second // Create tween to fly to predicted position self.moveTween = tween(self, { x: targetPos.x, y: targetPos.y }, { duration: timeToImpact * 16.67, // Convert frames to ms easing: tween.linear, onFinish: function onFinish() { // Check if we're close enough to target to hit it if (self.targetEnemy && self.targetEnemy.parent) { var finalDx = self.targetEnemy.x - self.x; var finalDy = self.targetEnemy.y - self.y; var finalDistance = Math.sqrt(finalDx * finalDx + finalDy * finalDy); if (finalDistance < CELL_SIZE / 2) { // We've hit the target self.hitTarget(); } else { // Still need to track self.updateTrajectory(); } } else { // Target is gone self.destroy(); } } }); return true; }; // Method when bullet hits target self.hitTarget = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } // 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(); }; // Update method - called every frame self.update = function () { // Increment flight time self.flightTime++; // If target is gone or bullet has been flying too long, destroy it if (!self.targetEnemy || !self.targetEnemy.parent || self.flightTime > self.maxFlightTime) { self.destroy(); return; } // If actively tracking, update trajectory periodically if (self.isTracking && self.flightTime % 20 === 0) { // Update every 1/3 second self.updateTrajectory(); } // Check if we've hit the 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 < CELL_SIZE / 3) { self.hitTarget(); } }; // Start tracking immediately when created self.updateTrajectory(); 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 = Math.random() * .2 + .03; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.lastCellX = 0; // Track previous position for velocity calculation self.lastCellY = 0; // Track previous position for velocity calculation self.velocityX = 0; // Store calculated x velocity self.velocityY = 0; // Store calculated y velocity 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.5, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0.5, anchorY: 0.5 }); healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; // Position the health bar above the enemy self.healthBar = healthBar; // Method to update velocity based on movement self.updateVelocity = function () { if (self.currentTarget) { self.velocityX = self.currentTarget.x - self.currentCellX; self.velocityY = self.currentTarget.y - self.currentCellY; // Normalize velocity vector var length = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY); if (length > 0) { self.velocityX /= length; self.velocityY /= length; } } // Store current position for next update self.lastCellX = self.currentCellX; self.lastCellY = self.currentCellY; }; // Method for bullet prediction calculations self.getPredictedPosition = function (timeSteps) { if (!self.currentTarget) { return { x: self.x, y: self.y }; } var predictedX = self.currentCellX + self.velocityX * self.speed * timeSteps; var predictedY = self.currentCellY + self.velocityY * self.speed * timeSteps; return { x: grid.x + predictedX * CELL_SIZE, y: grid.y + predictedY * CELL_SIZE }; }; }); //Class for the grid, handles 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]; } // Store velocity data for bullet prediction enemy.lastCellX = enemy.currentCellX; enemy.lastCellY = enemy.currentCellY; 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; // Update enemy velocity for bullet prediction enemy.velocityX = Math.cos(angle); enemy.velocityY = Math.sin(angle); } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // If enemy has the method, call it to update velocity if (enemy.updateVelocity) { enemy.updateVelocity(); } }; }); // 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; }); // 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; // Create the tower base (a square) var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); // 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(); 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[0]; // Light up dots up to current level if (i < self.level) { towerLevelIndicator.tint = 0xFFFF00; // Yellow for active levels } else { towerLevelIndicator.tint = 0xCCCCCC; // Gray for inactive levels } } }; // Initialize level indicators self.updateLevelIndicators(); // Method to upgrade the tower self.upgrade = function () { if (self.level < self.maxLevel) { 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 self.updateLevelIndicators(); return true; } return false; }; // Method to find the nearest enemy within range self.findTarget = function () { var closestEnemy = null; var closestDistance = self.range; 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) { var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } } } return closestEnemy; }; // Update method self.update = function () { // Find target if we don't have one or if current target is out of range if (!self.targetEnemy || !self.isInRange(self.targetEnemy)) { 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; } } }; // 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 from the gun barrel var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; // Create a new bullet with increased speed based on tower level // Higher level towers get faster bullets to catch faster enemies var adjustedSpeed = self.bulletSpeed * (1 + self.level * 0.2); // Create a new bullet var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, adjustedSpeed); game.addChild(bullet); // Add bullet to global bullets array and to enemy's tracking array bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // Apply visual effect to show bullet launching LK.effects.flashObject(bullet, 0xFFFFFF, 200); } } }; // 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; // 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 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 self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) { self.blockedByEnemy = true; break; } } } // Set final placement status self.canPlace = validGridPlacement && !self.blockedByEnemy; // If we can place, tint green, otherwise red previewGraphics.tint = self.canPlace ? 0x00FF00 : 0xFF0000; }; // 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; }); /**** * 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 var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; // Array to store active bullets var defenses = []; // 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 = 112 - 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) { var tower = new Tower('default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); // Update grid pathing grid.pathFind(); grid.renderDebug(); } game.down = function (x, y, obj) { towerPreview.visible = true; isDragging = true; towerPreview.snapToGrid(x, y); }; game.move = function (x, y, obj) { if (isDragging) { towerPreview.snapToGrid(x, y); } }; game.up = function (x, y, obj) { 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(towerPreview.gridX, towerPreview.gridY); } 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; }; 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 } // 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); } } // 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); } } 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(); } };
===================================================================
--- original.js
+++ change.js
@@ -1,5 +1,10 @@
/****
+* Plugins
+****/
+var tween = LK.import("@upit/tween.v1");
+
+/****
* Classes
****/
// Enemy class
// Bullet class
@@ -10,45 +15,134 @@
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
+ self.isTracking = true; // Flag to indicate if bullet is actively tracking target
+ self.flightTime = 0; // Track how long the bullet has been flying
+ self.maxFlightTime = 180; // 3 seconds at 60fps
// Visual representation of bullet
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
+ // Initialize any active tweens to null
+ self.moveTween = null;
+ // Method to predict where enemy will be
+ self.predictTargetPosition = function (timeToImpact) {
+ if (!self.targetEnemy || !self.targetEnemy.parent) {
+ return null;
+ }
+ // Calculate current enemy velocity vectors based on their movement
+ var enemyVelocityX = Math.cos(Math.atan2(self.targetEnemy.currentTarget ? self.targetEnemy.currentTarget.y - self.targetEnemy.currentCellY : 0, self.targetEnemy.currentTarget ? self.targetEnemy.currentTarget.x - self.targetEnemy.currentCellX : 0)) * self.targetEnemy.speed;
+ var enemyVelocityY = Math.sin(Math.atan2(self.targetEnemy.currentTarget ? self.targetEnemy.currentTarget.y - self.targetEnemy.currentCellY : 0, self.targetEnemy.currentTarget ? self.targetEnemy.currentTarget.x - self.targetEnemy.currentCellX : 0)) * self.targetEnemy.speed;
+ // Predict where enemy will be
+ return {
+ x: self.targetEnemy.x + enemyVelocityX * timeToImpact * CELL_SIZE,
+ y: self.targetEnemy.y + enemyVelocityY * timeToImpact * CELL_SIZE
+ };
+ };
+ // Method to update bullet trajectory with tweening
+ self.updateTrajectory = function () {
+ // If we already have an active tween, stop it
+ if (self.moveTween) {
+ tween.stop(self, {
+ x: true,
+ y: true
+ });
+ }
+ if (!self.targetEnemy || !self.targetEnemy.parent) {
+ return false;
+ }
+ // Calculate base distance to current enemy position
+ var dx = self.targetEnemy.x - self.x;
+ var dy = self.targetEnemy.y - self.y;
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ // Calculate time to impact based on bullet speed
+ var timeToImpact = distance / (self.speed * CELL_SIZE / 60); // Convert to frames
+ // Get predicted position
+ var targetPos = self.predictTargetPosition(timeToImpact);
+ if (!targetPos) {
+ return false;
+ }
+ // Calculate adjusted distance to predicted position
+ dx = targetPos.x - self.x;
+ dy = targetPos.y - self.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+ // Calculate adjusted time to impact
+ timeToImpact = Math.min(distance / (self.speed * CELL_SIZE / 60), 60); // Cap at 1 second
+ // Create tween to fly to predicted position
+ self.moveTween = tween(self, {
+ x: targetPos.x,
+ y: targetPos.y
+ }, {
+ duration: timeToImpact * 16.67,
+ // Convert frames to ms
+ easing: tween.linear,
+ onFinish: function onFinish() {
+ // Check if we're close enough to target to hit it
+ if (self.targetEnemy && self.targetEnemy.parent) {
+ var finalDx = self.targetEnemy.x - self.x;
+ var finalDy = self.targetEnemy.y - self.y;
+ var finalDistance = Math.sqrt(finalDx * finalDx + finalDy * finalDy);
+ if (finalDistance < CELL_SIZE / 2) {
+ // We've hit the target
+ self.hitTarget();
+ } else {
+ // Still need to track
+ self.updateTrajectory();
+ }
+ } else {
+ // Target is gone
+ self.destroy();
+ }
+ }
+ });
+ return true;
+ };
+ // Method when bullet hits target
+ self.hitTarget = function () {
+ if (!self.targetEnemy || !self.targetEnemy.parent) {
+ self.destroy();
+ return;
+ }
+ // 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();
+ };
// Update method - called every frame
self.update = function () {
- // If target is gone, destroy this bullet
- if (!self.targetEnemy || !self.targetEnemy.parent) {
+ // Increment flight time
+ self.flightTime++;
+ // If target is gone or bullet has been flying too long, destroy it
+ if (!self.targetEnemy || !self.targetEnemy.parent || self.flightTime > self.maxFlightTime) {
self.destroy();
return;
}
- // Move toward target
+ // If actively tracking, update trajectory periodically
+ if (self.isTracking && self.flightTime % 20 === 0) {
+ // Update every 1/3 second
+ self.updateTrajectory();
+ }
+ // Check if we've hit the 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;
+ if (distance < CELL_SIZE / 3) {
+ self.hitTarget();
}
};
+ // Start tracking immediately when created
+ self.updateTrajectory();
return self;
});
// DebugCell class
var DebugCell = Container.expand(function () {
@@ -153,8 +247,12 @@
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
+ self.lastCellX = 0; // Track previous position for velocity calculation
+ self.lastCellY = 0; // Track previous position for velocity calculation
+ self.velocityX = 0; // Store calculated x velocity
+ self.velocityY = 0; // Store calculated y velocity
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
@@ -167,8 +265,39 @@
anchorY: 0.5
});
healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; // Position the health bar above the enemy
self.healthBar = healthBar;
+ // Method to update velocity based on movement
+ self.updateVelocity = function () {
+ if (self.currentTarget) {
+ self.velocityX = self.currentTarget.x - self.currentCellX;
+ self.velocityY = self.currentTarget.y - self.currentCellY;
+ // Normalize velocity vector
+ var length = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY);
+ if (length > 0) {
+ self.velocityX /= length;
+ self.velocityY /= length;
+ }
+ }
+ // Store current position for next update
+ self.lastCellX = self.currentCellX;
+ self.lastCellY = self.currentCellY;
+ };
+ // Method for bullet prediction calculations
+ self.getPredictedPosition = function (timeSteps) {
+ if (!self.currentTarget) {
+ return {
+ x: self.x,
+ y: self.y
+ };
+ }
+ var predictedX = self.currentCellX + self.velocityX * self.speed * timeSteps;
+ var predictedY = self.currentCellY + self.velocityY * self.speed * timeSteps;
+ return {
+ x: grid.x + predictedX * CELL_SIZE,
+ y: grid.y + predictedY * CELL_SIZE
+ };
+ };
});
//Class for the grid, handles all logic related to grid movement and interaction
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
@@ -335,8 +464,11 @@
}
if (!enemy.currentTarget) {
enemy.currentTarget = cell.targets[0];
}
+ // Store velocity data for bullet prediction
+ enemy.lastCellX = enemy.currentCellX;
+ enemy.lastCellY = enemy.currentCellY;
if (enemy.currentTarget) {
//Allows enemies to change direction right away.
if (cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
@@ -352,11 +484,18 @@
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
+ // Update enemy velocity for bullet prediction
+ enemy.velocityX = Math.cos(angle);
+ enemy.velocityY = Math.sin(angle);
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
+ // If enemy has the method, call it to update velocity
+ if (enemy.updateVelocity) {
+ enemy.updateVelocity();
+ }
};
});
// Notification class
var Notification = Container.expand(function (message) {
@@ -524,17 +663,22 @@
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
+ // Calculate bullet starting position from the gun barrel
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
+ // Create a new bullet with increased speed based on tower level
+ // Higher level towers get faster bullets to catch faster enemies
+ var adjustedSpeed = self.bulletSpeed * (1 + self.level * 0.2);
// Create a new bullet
- var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
+ var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, adjustedSpeed);
game.addChild(bullet);
// Add bullet to global bullets array and to enemy's tracking array
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
+ // Apply visual effect to show bullet launching
+ LK.effects.flashObject(bullet, 0xFFFFFF, 200);
}
}
};
// Place tower on the grid
@@ -669,8 +813,9 @@
/****
* Game Code
****/
// Define cell size as a global constant
+// Import tween plugin
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
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