Code edit (1 edits merged)
Please save this source code
User prompt
reduce the distance between cursor and the dragged tower!
User prompt
when i buy tower drag it with same position of the cursor
User prompt
Create healthbar for the enemy red color
User prompt
Fix double buildable tiles by ensuring each tile is only marked once
User prompt
Do one line on each side of the roads with the buildabletile
User prompt
make the dragging towers distance closer to the cursor its far from it.
User prompt
the tower clicking detection is not working!
User prompt
add drag to the 3 towers icons to be dragged after buying
User prompt
Buy tower directly when i click on it duplicate the same image of it with the cursor to be placed.
User prompt
make the buying directly by clicking on the towers assets.
User prompt
center the tower_1 in its Uibutton.
User prompt
the towers assets are not in the middle of their UIbuttons!
User prompt
the towers assets are not in the middle of their buttons!
User prompt
If i click on any tower to buy it duplicate it to the cursor to be placed to the grid cell
User prompt
if i click on a tower stop the dragging of the gamebackground to place a tower
User prompt
Make the towers in the middle of their grids cells buttons
User prompt
fix the dragging of towers to be dragged to the cursor not in the top left corner!
User prompt
Please fix the bug: 'Uncaught TypeError: towerButton1.containsPoint is not a function' in or related to this line: 'if (towerButton1.containsPoint(uiContainer.toLocal({' Line Number: 378
Code edit (1 edits merged)
Please save this source code
User prompt
Serpent Siege
Initial prompt
Make a game about tower defense, do a background for the game can be dragged to see max distance of it, size of the gamebackground 3000x4000, do the road with many squares of 200x200 from top of background to bottom as serpent shape where enemies walk, do tower button can be clicked and dragged to the grid 1x1 around the road like one grid cell for one tower in each corner, towers can bought by score each tower by 50 score,do 2 other buttons of towers to buy by score but they are hard a bit from first to 3rd and by more score secondtower by 100 score, 3rd by 150 score.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Enemy = Container.expand(function (path, enemyType) { var self = Container.call(this); var config = enemyConfigs[enemyType] || enemyConfigs['serpent1']; var enemyGraphics = self.attachAsset(config.asset, { anchorX: 0.5, anchorY: 0.5 }); if (config.scale) { enemyGraphics.scaleX = config.scale; enemyGraphics.scaleY = config.scale; } if (config.tint) { enemyGraphics.tint = config.tint; } self.health = config.health; self.maxHealth = config.health; self.speed = config.speed; // Increase speed for small serpents by 0.1 each wave if (enemyType === 'serpent1' || enemyType === 'serpent2' || enemyType === 'serpent3' || enemyType === 'serpent4') { self.speed += (currentWave - 1) * 0.1; } self.path = path; self.currentWaypointIndex = 1; self.value = config.value; // Score awarded on defeat self.enemyType = enemyType; // Store enemy type for boss tracking // Create health bar only if config says to show it if (config.showHealthBar) { self.healthBar = self.addChild(LK.getAsset('healthBar', { anchorX: 0.5, anchorY: 0.5 })); self.healthBar.y = -80; // Position above enemy } self.update = function () { if (self.currentWaypointIndex >= self.path.length) { // Reached the end LK.showGameOver(); self.destroy(); // Self-destruct to avoid multiple game overs return; } var targetWaypoint = self.path[self.currentWaypointIndex]; var dx = targetWaypoint.x - self.x; var dy = targetWaypoint.y - self.y; if (config.asset === 'Serpent1' || config.asset === 'Serpent2' || config.asset === 'Serpent3' || config.asset === 'Serpent4') { var scale = config.scale || 1; if (dx > 0.1) { // moving right enemyGraphics.scaleX = -scale; } else if (dx < -0.1) { // moving left enemyGraphics.scaleX = scale; } } var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { self.currentWaypointIndex++; } }; self.takeDamage = function (amount) { self.health -= amount; // Update health bar width based on remaining health if it exists if (self.healthBar) { var healthPercentage = self.health / self.maxHealth; self.healthBar.scaleX = healthPercentage; } if (self.health <= 0) { return true; // Is dead } LK.effects.flashObject(self, 0xffffff, 100); return false; // Is alive }; return self; }); var Projectile = Container.expand(function (start, target, damage, projectileType) { var self = Container.call(this); projectileType = projectileType || 'projectile'; self.attachAsset(projectileType, { anchorX: 0.5, anchorY: 0.5 }); self.x = start.x; self.y = start.y; self.target = target; self.damage = damage; self.speed = 12; self.update = function () { if (!self.target || self.target.health <= 0) { // Target is gone projectiles.splice(projectiles.indexOf(self), 1); self.destroy(); return; } var dx = self.target.x - self.x; var dy = self.target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Hit target if (self.target.takeDamage(self.damage)) { // Enemy died LK.getSound('enemy_die').play(); LK.setScore(LK.getScore() + self.target.value); scoreTxt.setText('SCORE: ' + LK.getScore()); // Check if this was a boss - just mark it as killed, don't progress wave immediately if (self.target.enemyType && (self.target.enemyType === 'boss1' || self.target.enemyType === 'boss2' || self.target.enemyType === 'boss3' || self.target.enemyType === 'boss4')) { // For wave 3 and 6, we need both bosses killed if (currentWave === 3 || currentWave === 6) { // Check if all bosses are killed (excluding the one we just killed) var bossesRemaining = 0; for (var j = 0; j < enemies.length; j++) { if (enemies[j] !== self.target && (enemies[j].enemyType === 'boss1' || enemies[j].enemyType === 'boss2' || enemies[j].enemyType === 'boss3' || enemies[j].enemyType === 'boss4')) { bossesRemaining++; } } if (bossesRemaining === 0) { waveBossKilled = true; } } else { // For waves 1, 2, 4, 5 - single boss per wave waveBossKilled = true; } } enemies.splice(enemies.indexOf(self.target), 1); self.target.destroy(); } projectiles.splice(projectiles.indexOf(self), 1); self.destroy(); } else { // Move towards target var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var Tower = Container.expand(function (config) { var self = Container.call(this); self.config = config; self.attachAsset(config.asset, { anchorX: 0.5, anchorY: 0.5 }); self.cost = config.cost; self.damage = config.damage; self.range = 400; // All towers have same range for now self.fireRate = config.fireRate; // shots per second self.cooldown = 0; self.isTower = true; self.down = function (x, y, obj) { // This makes the tower a "button" console.log("Tower clicked! Damage: " + self.damage); LK.effects.flashObject(self, 0xffffff, 200); }; self.update = function () { if (self.cooldown > 0) { self.cooldown--; return; } var target = findClosestEnemy(self.x, self.y, self.range); if (target) { fireProjectile(self, target); self.cooldown = 60 / self.fireRate; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x1e272e }); /**** * Game Code ****/ // The engine will automatically create these assets based on their usage below. // --- Constants & Game State --- var MAP_WIDTH = 4000; var MAP_HEIGHT = 4800; var CELL_SIZE = 200; var GRID_WIDTH = MAP_WIDTH / CELL_SIZE; var GRID_HEIGHT = MAP_HEIGHT / CELL_SIZE; // Grid states: 0=empty, 1=path, 2=buildable, 3=occupied var grid = []; // Wave system var currentWave = 1; var enemiesSpawnedInWave = 0; var waveSpawnTimer = null; var waveBossKilled = false; var isShowingWaveText = false; var enemyConfigs = { 'serpent1': { asset: 'Serpent1', health: 250, speed: 0.7, value: 10, showHealthBar: true }, 'serpent2': { asset: 'Serpent2', health: 300, speed: 0.6, value: 15, showHealthBar: true }, 'serpent1_big': { asset: 'Serpent1', health: 600, speed: 0.5, value: 30, scale: 1.5, showHealthBar: true }, 'serpent2_big': { asset: 'Serpent2', health: 750, speed: 0.4, value: 40, scale: 1.5, showHealthBar: true }, 'boss1': { asset: 'Serpent1', health: 2000, speed: 0.3, value: 100, scale: 2, tint: 0xff0000, showHealthBar: true }, 'boss2': { asset: 'Serpent2', health: 2500, speed: 0.3, value: 120, scale: 2, tint: 0xff0000, showHealthBar: true }, 'serpent3': { asset: 'Serpent3', health: 400, speed: 0.6, value: 20, showHealthBar: true }, 'serpent4': { asset: 'Serpent4', health: 500, speed: 0.5, value: 25, showHealthBar: true }, 'serpent3_big': { asset: 'Serpent3', health: 900, speed: 0.4, value: 50, scale: 1.5, showHealthBar: true }, 'serpent4_big': { asset: 'Serpent4', health: 1100, speed: 0.4, value: 60, scale: 1.5, showHealthBar: true }, 'boss3': { asset: 'Serpent3', health: 3000, speed: 0.3, value: 150, scale: 2, tint: 0xff0000, showHealthBar: true }, 'boss4': { asset: 'Serpent4', health: 3500, speed: 0.3, value: 180, scale: 2, tint: 0xff0000, showHealthBar: true } }; var pathWaypoints = [{ x: 2000 + CELL_SIZE / 2, y: -100 }, { x: 2000 + CELL_SIZE / 2, y: 800 + CELL_SIZE / 2 }, { x: 800 + CELL_SIZE / 2, y: 800 + CELL_SIZE / 2 }, { x: 800 + CELL_SIZE / 2, y: 1600 + CELL_SIZE / 2 }, { x: 3200 + CELL_SIZE / 2, y: 1600 + CELL_SIZE / 2 }, { x: 3200 + CELL_SIZE / 2, y: 2400 + CELL_SIZE / 2 }, { x: 1200 + CELL_SIZE / 2, y: 2400 + CELL_SIZE / 2 }, { x: 1200 + CELL_SIZE / 2, y: 3200 + CELL_SIZE / 2 }, { x: 2800 + CELL_SIZE / 2, y: 3200 + CELL_SIZE / 2 }, { x: 2800 + CELL_SIZE / 2, y: 4000 + CELL_SIZE / 2 }, { x: 2800 + CELL_SIZE / 2, y: 4900 }]; var enemies = []; var towers = []; var projectiles = []; var mapContainer = game.addChild(new Container()); mapContainer.width = MAP_WIDTH; mapContainer.height = MAP_HEIGHT; // Add background var gameBackground = mapContainer.addChild(LK.getAsset('gameBackground', { anchorX: 0, anchorY: 0 })); gameBackground.x = 0; gameBackground.y = 0; gameBackground.width = MAP_WIDTH; gameBackground.height = MAP_HEIGHT; var placingTowerConfig = null; var ghostTower = null; var isDraggingMap = false; var dragStart = { x: 0, y: 0 }; var mapStart = { x: 0, y: 0 }; // --- Tower Configurations --- var towerTypes = { '1': { asset: 'tower_1', cost: 50, damage: 15, fireRate: 1.5, projectileTypes: ['star'] }, '2': { asset: 'tower_2', cost: 100, damage: 35, fireRate: 1, projectileTypes: ['arrow'] }, '3': { asset: 'tower_3', cost: 150, damage: 60, fireRate: 0.75, projectileTypes: ['star', 'arrow'] } }; // --- UI --- LK.setScore(200); // Starting cash var scoreTxt = new Text2('SCORE: ' + LK.getScore(), { size: 70, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); scoreTxt.y = 20; LK.gui.top.addChild(scoreTxt); var uiContainer = LK.gui.bottom.addChild(new Container()); uiContainer.y = -20; // Move it up slightly from the absolute bottom // Create tower 1 group var towerButton1 = uiContainer.addChild(LK.getAsset('uiButton', { anchorX: 0.5, anchorY: 1 })); towerButton1.x = -300; var towerIcon1 = uiContainer.addChild(LK.getAsset('tower_1', { scaleX: 0.8, scaleY: 0.8, anchorX: 0.5, anchorY: 0.5 })); towerIcon1.x = -305; towerIcon1.y = -115; towerIcon1.down = function (x, y, obj) { startPlacingTower('1'); if (placingTowerConfig) { towerIcon1.isDragging = true; game.move(x, y, obj); } }; var towerText1 = uiContainer.addChild(new Text2('50', { size: 50, fill: '#fff', anchorX: 0.5, anchorY: 0.5 })); towerText1.x = -305 - 25; towerText1.y = -55; // Create tower 2 group var towerButton2 = uiContainer.addChild(LK.getAsset('uiButton', { anchorX: 0.5, anchorY: 1 })); var towerIcon2 = uiContainer.addChild(LK.getAsset('tower_2', { scaleX: 0.8, scaleY: 0.8, anchorX: 0.5, anchorY: 0.5 })); towerIcon2.x = -5; towerIcon2.y = -115; towerIcon2.down = function (x, y, obj) { startPlacingTower('2'); if (placingTowerConfig) { towerIcon2.isDragging = true; game.move(x, y, obj); } }; var towerText2 = uiContainer.addChild(new Text2('100', { size: 50, fill: '#fff', anchorX: 0.5, anchorY: 0.5 })); towerText2.x = -15 - 25; towerText2.y = -55; // Create tower 3 group var towerButton3 = uiContainer.addChild(LK.getAsset('uiButton', { anchorX: 0.5, anchorY: 1 })); towerButton3.x = 300; var towerIcon3 = uiContainer.addChild(LK.getAsset('tower_3', { scaleX: 0.8, scaleY: 0.8, anchorX: 0.5, anchorY: 0.5 })); towerIcon3.x = 300; towerIcon3.y = -115; towerIcon3.down = function (x, y, obj) { startPlacingTower('3'); if (placingTowerConfig) { towerIcon3.isDragging = true; game.move(x, y, obj); } }; var towerText3 = uiContainer.addChild(new Text2('150', { size: 50, fill: '#fff', anchorX: 0.5, anchorY: 0.5 })); towerText3.x = 285 - 25; towerText3.y = -55; // --- Initialization Functions --- function initializeGrid() { for (var i = 0; i < GRID_WIDTH; i++) { grid[i] = []; for (var j = 0; j < GRID_HEIGHT; j++) { grid[i][j] = 0; // Empty } } // Mark path tiles for (var k = 0; k < pathWaypoints.length - 1; k++) { var start = pathWaypoints[k]; var end = pathWaypoints[k + 1]; var p1 = { x: Math.floor(start.x / CELL_SIZE), y: Math.floor(start.y / CELL_SIZE) }; var p2 = { x: Math.floor(end.x / CELL_SIZE), y: Math.floor(end.y / CELL_SIZE) }; var x = p1.x; var y = p1.y; var dx = Math.abs(p2.x - x); var dy = Math.abs(p2.y - y); var sx = x < p2.x ? 1 : -1; var sy = y < p2.y ? 1 : -1; var err = dx - dy; while (true) { if (x >= 0 && x < GRID_WIDTH && y >= 0 && y < GRID_HEIGHT) { grid[x][y] = 1; } if (x === p2.x && y === p2.y) { break; } var e2 = 2 * err; if (e2 > -dy) { err -= dy; x += sx; } if (e2 < dx) { err += dx; y += sy; } } } // Mark buildable tiles - only those on the sides of the road segments for (var k = 0; k < pathWaypoints.length - 1; k++) { var start = pathWaypoints[k]; var end = pathWaypoints[k + 1]; var p1 = { x: Math.floor(start.x / CELL_SIZE), y: Math.floor(start.y / CELL_SIZE) }; var p2 = { x: Math.floor(end.x / CELL_SIZE), y: Math.floor(end.y / CELL_SIZE) }; // Determine if this segment is horizontal or vertical var isHorizontal = p1.y === p2.y; var isVertical = p1.x === p2.x; // Determine direction of movement to know which side to place tiles var movingRight = p2.x > p1.x; var movingDown = p2.y > p1.y; var movingLeft = p2.x < p1.x; var movingUp = p2.y < p1.y; if (isVertical) { // For vertical segments, place buildable tiles on left and right var x = p1.x; var minY = Math.min(p1.y, p2.y); var maxY = Math.max(p1.y, p2.y); for (var y = minY; y <= maxY; y++) { // Only place on sides, avoiding corners where segments meet var isCorner = false; // Check if this position is a corner (where path changes direction) for (var w = 0; w < pathWaypoints.length; w++) { var wp = pathWaypoints[w]; if (Math.floor(wp.x / CELL_SIZE) === x && Math.floor(wp.y / CELL_SIZE) === y) { isCorner = true; break; } } if (!isCorner) { // Left side if (x - 1 >= 0 && grid[x - 1][y] === 0) { grid[x - 1][y] = 2; } // Right side if (x + 1 < GRID_WIDTH && grid[x + 1][y] === 0) { grid[x + 1][y] = 2; } } } } else if (isHorizontal) { // For horizontal segments, only place on one side to avoid doubles var y = p1.y; var minX = Math.min(p1.x, p2.x); var maxX = Math.max(p1.x, p2.x); for (var x = minX; x <= maxX; x++) { // Only place on one side (top) to avoid double placement if (y - 1 >= 0 && grid[x][y - 1] === 0) { grid[x][y - 1] = 2; } } } } // Draw visual grid - buildable tiles first (background layer) for (var i = 0; i < GRID_WIDTH; i++) { for (var j = 0; j < GRID_HEIGHT; j++) { if (grid[i][j] === 2) { var buildableTile = mapContainer.addChild(LK.getAsset('buildableTile', {})); buildableTile.alpha = 0.5; buildableTile.x = i * CELL_SIZE; buildableTile.y = j * CELL_SIZE; } } } // Draw path tiles on top for (var i = 0; i < GRID_WIDTH; i++) { for (var j = 0; j < GRID_HEIGHT; j++) { if (grid[i][j] === 1) { var pathTile = mapContainer.addChild(LK.getAsset('pathTile', {})); pathTile.x = i * CELL_SIZE; pathTile.y = j * CELL_SIZE; } } } } initializeGrid(); function startPlacingTower(type) { var config = towerTypes[type]; if (LK.getScore() >= config.cost) { placingTowerConfig = config; ghostTower = mapContainer.addChild(LK.getAsset(config.asset, { anchorX: 0, anchorY: 0 })); ghostTower.alpha = 0.7; } } // --- Event Handlers --- game.down = function (x, y, obj) { var localPoint = uiContainer.toLocal({ x: x, y: y }); // If the clicked object is a tower on the map, duplicate it for placement if (obj && obj.target && (obj.target.isTower || obj.target.parent && obj.target.parent.isTower)) { var clickedTower = obj.target.isTower ? obj.target : obj.target.parent; // Find tower type from its config var myType = null; if (clickedTower.config) { for (var key in towerTypes) { if (towerTypes[key].asset === clickedTower.config.asset) { myType = key; break; } } } if (myType) { startPlacingTower(myType); if (placingTowerConfig) { game.move(x, y, obj); } } return; // Stop further processing (like map dragging) } if (!placingTowerConfig) { isDraggingMap = true; dragStart.x = x; dragStart.y = y; mapStart.x = mapContainer.x; mapStart.y = mapContainer.y; } }; game.move = function (x, y, obj) { if (placingTowerConfig) { var mapPos = mapContainer.toLocal({ x: x, y: y }); // Position ghost tower directly at cursor position without anchor offset ghostTower.x = mapPos.x; ghostTower.y = mapPos.y; var gridX = Math.floor(mapPos.x / CELL_SIZE); var gridY = Math.floor(mapPos.y / CELL_SIZE); if (gridX >= 0 && gridX < GRID_WIDTH && gridY >= 0 && gridY < GRID_HEIGHT && grid[gridX][gridY] === 2) { ghostTower.tint = 0x00ff00; // Green for valid } else { ghostTower.tint = 0xff0000; // Red for invalid } } else if (isDraggingMap) { var dx = x - dragStart.x; var dy = y - dragStart.y; // Clamp map position var newX = mapStart.x + dx; var newY = mapStart.y + dy; var gameWidth = 2048; var gameHeight = 2732; if (newX > 0) { newX = 0; } if (newY > 0) { newY = 0; } if (newX < gameWidth - MAP_WIDTH) { newX = gameWidth - MAP_WIDTH; } if (newY < gameHeight - MAP_HEIGHT) { newY = gameHeight - MAP_HEIGHT; } mapContainer.x = newX; mapContainer.y = newY; } }; game.up = function (x, y, obj) { if (placingTowerConfig) { var mapPos = mapContainer.toLocal({ x: x, y: y }); var gridX = Math.floor(mapPos.x / CELL_SIZE); var gridY = Math.floor(mapPos.y / CELL_SIZE); if (gridX >= 0 && gridX < GRID_WIDTH && gridY >= 0 && gridY < GRID_HEIGHT && grid[gridX][gridY] === 2) { if (LK.getScore() >= placingTowerConfig.cost) { var newTower = new Tower(placingTowerConfig); newTower.x = gridX * CELL_SIZE + CELL_SIZE / 2; newTower.y = gridY * CELL_SIZE + CELL_SIZE / 2; mapContainer.addChild(newTower); towers.push(newTower); grid[gridX][gridY] = 3; // Mark as occupied LK.setScore(LK.getScore() - placingTowerConfig.cost); scoreTxt.setText('SCORE: ' + LK.getScore()); LK.getSound('place_tower').play(); } } ghostTower.destroy(); ghostTower = null; placingTowerConfig = null; // Reset drag states towerIcon1.isDragging = false; towerIcon2.isDragging = false; towerIcon3.isDragging = false; } isDraggingMap = false; }; // --- Game Logic Functions --- function findClosestEnemy(x, y, range) { var closestEnemy = null; var minDistanceSq = range * range; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - x; var dy = enemy.y - y; var distanceSq = dx * dx + dy * dy; if (distanceSq < minDistanceSq) { minDistanceSq = distanceSq; closestEnemy = enemy; } } return closestEnemy; } function fireProjectile(tower, target) { var projectileTypes = tower.config.projectileTypes; for (var i = 0; i < projectileTypes.length; i++) { var projectile = new Projectile({ x: tower.x, y: tower.y }, target, tower.damage, projectileTypes[i]); // Flip arrow if enemy is to the left of tower_2 and projectile is arrow if (tower.config.asset === 'tower_2' && projectileTypes[i] === 'arrow' && target.x < tower.x) { projectile.scaleX = -1; } // Add rotation animation to star projectiles if (projectileTypes[i] === 'star') { tween(projectile, { rotation: Math.PI * 4 }, { duration: 2000 }); } projectiles.push(projectile); mapContainer.addChild(projectile); } LK.getSound('shoot').play(); } function startWave() { enemiesSpawnedInWave = 0; waveBossKilled = false; waveSpawnTimer = LK.setInterval(function () { if (enemiesSpawnedInWave >= 20) { LK.clearInterval(waveSpawnTimer); waveSpawnTimer = null; // Check for next wave after all enemies are defeated return; } enemiesSpawnedInWave++; var enemyType; // Determine enemy type based on position in wave if (currentWave === 3) { // Wave 3: Mix of serpent1 and serpent2 if (enemiesSpawnedInWave === 19) { // First boss - serpent1 boss enemyType = 'boss1'; } else if (enemiesSpawnedInWave === 20) { // Second boss - serpent2 boss enemyType = 'boss2'; } else if (enemiesSpawnedInWave === 15 || enemiesSpawnedInWave === 17) { // Big serpent1 enemies enemyType = 'serpent1_big'; } else if (enemiesSpawnedInWave === 16 || enemiesSpawnedInWave === 18) { // Big serpent2 enemies enemyType = 'serpent2_big'; } else { // Regular enemies - alternate between serpent1 and serpent2 enemyType = enemiesSpawnedInWave % 2 === 1 ? 'serpent1' : 'serpent2'; } } else if (currentWave === 4) { // Wave 4: serpent3 based wave if (enemiesSpawnedInWave === 20) { // Boss - serpent3 boss enemyType = 'boss3'; } else if (enemiesSpawnedInWave === 18 || enemiesSpawnedInWave === 19) { // Big serpent3 enemies enemyType = 'serpent3_big'; } else { // Regular serpent3 enemies enemyType = 'serpent3'; } } else if (currentWave === 5) { // Wave 5: serpent4 based wave if (enemiesSpawnedInWave === 20) { // Boss - serpent4 boss enemyType = 'boss4'; } else if (enemiesSpawnedInWave === 18 || enemiesSpawnedInWave === 19) { // Big serpent4 enemies enemyType = 'serpent4_big'; } else { // Regular serpent4 enemies enemyType = 'serpent4'; } } else if (currentWave === 6) { // Wave 6: Mix of all serpent types with two bosses if (enemiesSpawnedInWave === 19) { // First boss - serpent3 boss enemyType = 'boss3'; } else if (enemiesSpawnedInWave === 20) { // Second boss - serpent4 boss enemyType = 'boss4'; } else if (enemiesSpawnedInWave === 15) { // Big serpent1 enemy enemyType = 'serpent1_big'; } else if (enemiesSpawnedInWave === 16) { // Big serpent2 enemy enemyType = 'serpent2_big'; } else if (enemiesSpawnedInWave === 17) { // Big serpent3 enemy enemyType = 'serpent3_big'; } else if (enemiesSpawnedInWave === 18) { // Big serpent4 enemy enemyType = 'serpent4_big'; } else { // Regular enemies - cycle through all serpent types var serpentTypes = ['serpent1', 'serpent2', 'serpent3', 'serpent4']; enemyType = serpentTypes[(enemiesSpawnedInWave - 1) % 4]; } } else { // Fallback for other waves - single boss at enemy 20 if (enemiesSpawnedInWave === 20) { // Boss - use wave-specific boss type if (currentWave === 1) { enemyType = 'boss1'; } else if (currentWave === 2) { enemyType = 'boss2'; } } else if (enemiesSpawnedInWave === 18 || enemiesSpawnedInWave === 19) { // Big enemies - use wave-specific serpent type if (currentWave === 1) { enemyType = 'serpent1_big'; } else if (currentWave === 2) { enemyType = 'serpent2_big'; } } else { // Regular enemy based on wave if (currentWave === 1) { enemyType = 'serpent1'; } else if (currentWave === 2) { enemyType = 'serpent2'; } } } var enemy = new Enemy(pathWaypoints, enemyType); enemy.x = pathWaypoints[0].x; enemy.y = pathWaypoints[0].y; enemies.push(enemy); mapContainer.addChild(enemy); }, 800); } // Wave text will be created dynamically when needed function showWaveText(waveNumber) { // Set flag to indicate wave text is showing isShowingWaveText = true; // Create wave text dynamically var waveText = new Text2('WAVE ' + waveNumber, { size: 120, fill: 0xFFA500 // Orange color }); waveText.anchor.set(0.5, 0.5); waveText.x = 2048 / 2; waveText.y = 2732 / 2; game.addChild(waveText); // Create blinking animation with proper recursion control var blinkCount = 0; var maxBlinks = 6; // 3 complete blink cycles (fade out + fade in = 1 cycle, so 6 total for 3 cycles) function blink() { if (blinkCount >= maxBlinks) { // Stop blinking and fade out tween(waveText, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { waveText.destroy(); // Clear flag when wave text is destroyed isShowingWaveText = false; // Start spawning enemies after wave text disappears startWave(); } }); return; } tween(waveText, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { tween(waveText, { alpha: 1 }, { duration: 500, onFinish: function onFinish() { blinkCount++; blink(); } }); } }); } blink(); } // Show initial wave text (startWave will be called after text animation) showWaveText(1); game.update = function () { for (var i = enemies.length - 1; i >= 0; i--) { enemies[i].update(); } for (var i = towers.length - 1; i >= 0; i--) { towers[i].update(); } for (var i = projectiles.length - 1; i >= 0; i--) { projectiles[i].update(); } // Check if wave is complete and start next wave if (enemiesSpawnedInWave === 20 && waveBossKilled && !waveSpawnTimer && !isShowingWaveText && enemies.length === 0) { currentWave++; if (currentWave <= 6) { // Reset wave state for next wave enemiesSpawnedInWave = 0; waveBossKilled = false; // Show wave text for new wave (startWave will be called after text animation) showWaveText(currentWave); } else { // All waves completed, show you win screen LK.showYouWin(); } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Enemy = Container.expand(function (path, enemyType) {
var self = Container.call(this);
var config = enemyConfigs[enemyType] || enemyConfigs['serpent1'];
var enemyGraphics = self.attachAsset(config.asset, {
anchorX: 0.5,
anchorY: 0.5
});
if (config.scale) {
enemyGraphics.scaleX = config.scale;
enemyGraphics.scaleY = config.scale;
}
if (config.tint) {
enemyGraphics.tint = config.tint;
}
self.health = config.health;
self.maxHealth = config.health;
self.speed = config.speed;
// Increase speed for small serpents by 0.1 each wave
if (enemyType === 'serpent1' || enemyType === 'serpent2' || enemyType === 'serpent3' || enemyType === 'serpent4') {
self.speed += (currentWave - 1) * 0.1;
}
self.path = path;
self.currentWaypointIndex = 1;
self.value = config.value; // Score awarded on defeat
self.enemyType = enemyType; // Store enemy type for boss tracking
// Create health bar only if config says to show it
if (config.showHealthBar) {
self.healthBar = self.addChild(LK.getAsset('healthBar', {
anchorX: 0.5,
anchorY: 0.5
}));
self.healthBar.y = -80; // Position above enemy
}
self.update = function () {
if (self.currentWaypointIndex >= self.path.length) {
// Reached the end
LK.showGameOver();
self.destroy(); // Self-destruct to avoid multiple game overs
return;
}
var targetWaypoint = self.path[self.currentWaypointIndex];
var dx = targetWaypoint.x - self.x;
var dy = targetWaypoint.y - self.y;
if (config.asset === 'Serpent1' || config.asset === 'Serpent2' || config.asset === 'Serpent3' || config.asset === 'Serpent4') {
var scale = config.scale || 1;
if (dx > 0.1) {
// moving right
enemyGraphics.scaleX = -scale;
} else if (dx < -0.1) {
// moving left
enemyGraphics.scaleX = scale;
}
}
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
self.currentWaypointIndex++;
}
};
self.takeDamage = function (amount) {
self.health -= amount;
// Update health bar width based on remaining health if it exists
if (self.healthBar) {
var healthPercentage = self.health / self.maxHealth;
self.healthBar.scaleX = healthPercentage;
}
if (self.health <= 0) {
return true; // Is dead
}
LK.effects.flashObject(self, 0xffffff, 100);
return false; // Is alive
};
return self;
});
var Projectile = Container.expand(function (start, target, damage, projectileType) {
var self = Container.call(this);
projectileType = projectileType || 'projectile';
self.attachAsset(projectileType, {
anchorX: 0.5,
anchorY: 0.5
});
self.x = start.x;
self.y = start.y;
self.target = target;
self.damage = damage;
self.speed = 12;
self.update = function () {
if (!self.target || self.target.health <= 0) {
// Target is gone
projectiles.splice(projectiles.indexOf(self), 1);
self.destroy();
return;
}
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Hit target
if (self.target.takeDamage(self.damage)) {
// Enemy died
LK.getSound('enemy_die').play();
LK.setScore(LK.getScore() + self.target.value);
scoreTxt.setText('SCORE: ' + LK.getScore());
// Check if this was a boss - just mark it as killed, don't progress wave immediately
if (self.target.enemyType && (self.target.enemyType === 'boss1' || self.target.enemyType === 'boss2' || self.target.enemyType === 'boss3' || self.target.enemyType === 'boss4')) {
// For wave 3 and 6, we need both bosses killed
if (currentWave === 3 || currentWave === 6) {
// Check if all bosses are killed (excluding the one we just killed)
var bossesRemaining = 0;
for (var j = 0; j < enemies.length; j++) {
if (enemies[j] !== self.target && (enemies[j].enemyType === 'boss1' || enemies[j].enemyType === 'boss2' || enemies[j].enemyType === 'boss3' || enemies[j].enemyType === 'boss4')) {
bossesRemaining++;
}
}
if (bossesRemaining === 0) {
waveBossKilled = true;
}
} else {
// For waves 1, 2, 4, 5 - single boss per wave
waveBossKilled = true;
}
}
enemies.splice(enemies.indexOf(self.target), 1);
self.target.destroy();
}
projectiles.splice(projectiles.indexOf(self), 1);
self.destroy();
} else {
// Move towards target
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
var Tower = Container.expand(function (config) {
var self = Container.call(this);
self.config = config;
self.attachAsset(config.asset, {
anchorX: 0.5,
anchorY: 0.5
});
self.cost = config.cost;
self.damage = config.damage;
self.range = 400; // All towers have same range for now
self.fireRate = config.fireRate; // shots per second
self.cooldown = 0;
self.isTower = true;
self.down = function (x, y, obj) {
// This makes the tower a "button"
console.log("Tower clicked! Damage: " + self.damage);
LK.effects.flashObject(self, 0xffffff, 200);
};
self.update = function () {
if (self.cooldown > 0) {
self.cooldown--;
return;
}
var target = findClosestEnemy(self.x, self.y, self.range);
if (target) {
fireProjectile(self, target);
self.cooldown = 60 / self.fireRate;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1e272e
});
/****
* Game Code
****/
// The engine will automatically create these assets based on their usage below.
// --- Constants & Game State ---
var MAP_WIDTH = 4000;
var MAP_HEIGHT = 4800;
var CELL_SIZE = 200;
var GRID_WIDTH = MAP_WIDTH / CELL_SIZE;
var GRID_HEIGHT = MAP_HEIGHT / CELL_SIZE;
// Grid states: 0=empty, 1=path, 2=buildable, 3=occupied
var grid = [];
// Wave system
var currentWave = 1;
var enemiesSpawnedInWave = 0;
var waveSpawnTimer = null;
var waveBossKilled = false;
var isShowingWaveText = false;
var enemyConfigs = {
'serpent1': {
asset: 'Serpent1',
health: 250,
speed: 0.7,
value: 10,
showHealthBar: true
},
'serpent2': {
asset: 'Serpent2',
health: 300,
speed: 0.6,
value: 15,
showHealthBar: true
},
'serpent1_big': {
asset: 'Serpent1',
health: 600,
speed: 0.5,
value: 30,
scale: 1.5,
showHealthBar: true
},
'serpent2_big': {
asset: 'Serpent2',
health: 750,
speed: 0.4,
value: 40,
scale: 1.5,
showHealthBar: true
},
'boss1': {
asset: 'Serpent1',
health: 2000,
speed: 0.3,
value: 100,
scale: 2,
tint: 0xff0000,
showHealthBar: true
},
'boss2': {
asset: 'Serpent2',
health: 2500,
speed: 0.3,
value: 120,
scale: 2,
tint: 0xff0000,
showHealthBar: true
},
'serpent3': {
asset: 'Serpent3',
health: 400,
speed: 0.6,
value: 20,
showHealthBar: true
},
'serpent4': {
asset: 'Serpent4',
health: 500,
speed: 0.5,
value: 25,
showHealthBar: true
},
'serpent3_big': {
asset: 'Serpent3',
health: 900,
speed: 0.4,
value: 50,
scale: 1.5,
showHealthBar: true
},
'serpent4_big': {
asset: 'Serpent4',
health: 1100,
speed: 0.4,
value: 60,
scale: 1.5,
showHealthBar: true
},
'boss3': {
asset: 'Serpent3',
health: 3000,
speed: 0.3,
value: 150,
scale: 2,
tint: 0xff0000,
showHealthBar: true
},
'boss4': {
asset: 'Serpent4',
health: 3500,
speed: 0.3,
value: 180,
scale: 2,
tint: 0xff0000,
showHealthBar: true
}
};
var pathWaypoints = [{
x: 2000 + CELL_SIZE / 2,
y: -100
}, {
x: 2000 + CELL_SIZE / 2,
y: 800 + CELL_SIZE / 2
}, {
x: 800 + CELL_SIZE / 2,
y: 800 + CELL_SIZE / 2
}, {
x: 800 + CELL_SIZE / 2,
y: 1600 + CELL_SIZE / 2
}, {
x: 3200 + CELL_SIZE / 2,
y: 1600 + CELL_SIZE / 2
}, {
x: 3200 + CELL_SIZE / 2,
y: 2400 + CELL_SIZE / 2
}, {
x: 1200 + CELL_SIZE / 2,
y: 2400 + CELL_SIZE / 2
}, {
x: 1200 + CELL_SIZE / 2,
y: 3200 + CELL_SIZE / 2
}, {
x: 2800 + CELL_SIZE / 2,
y: 3200 + CELL_SIZE / 2
}, {
x: 2800 + CELL_SIZE / 2,
y: 4000 + CELL_SIZE / 2
}, {
x: 2800 + CELL_SIZE / 2,
y: 4900
}];
var enemies = [];
var towers = [];
var projectiles = [];
var mapContainer = game.addChild(new Container());
mapContainer.width = MAP_WIDTH;
mapContainer.height = MAP_HEIGHT;
// Add background
var gameBackground = mapContainer.addChild(LK.getAsset('gameBackground', {
anchorX: 0,
anchorY: 0
}));
gameBackground.x = 0;
gameBackground.y = 0;
gameBackground.width = MAP_WIDTH;
gameBackground.height = MAP_HEIGHT;
var placingTowerConfig = null;
var ghostTower = null;
var isDraggingMap = false;
var dragStart = {
x: 0,
y: 0
};
var mapStart = {
x: 0,
y: 0
};
// --- Tower Configurations ---
var towerTypes = {
'1': {
asset: 'tower_1',
cost: 50,
damage: 15,
fireRate: 1.5,
projectileTypes: ['star']
},
'2': {
asset: 'tower_2',
cost: 100,
damage: 35,
fireRate: 1,
projectileTypes: ['arrow']
},
'3': {
asset: 'tower_3',
cost: 150,
damage: 60,
fireRate: 0.75,
projectileTypes: ['star', 'arrow']
}
};
// --- UI ---
LK.setScore(200); // Starting cash
var scoreTxt = new Text2('SCORE: ' + LK.getScore(), {
size: 70,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
scoreTxt.y = 20;
LK.gui.top.addChild(scoreTxt);
var uiContainer = LK.gui.bottom.addChild(new Container());
uiContainer.y = -20; // Move it up slightly from the absolute bottom
// Create tower 1 group
var towerButton1 = uiContainer.addChild(LK.getAsset('uiButton', {
anchorX: 0.5,
anchorY: 1
}));
towerButton1.x = -300;
var towerIcon1 = uiContainer.addChild(LK.getAsset('tower_1', {
scaleX: 0.8,
scaleY: 0.8,
anchorX: 0.5,
anchorY: 0.5
}));
towerIcon1.x = -305;
towerIcon1.y = -115;
towerIcon1.down = function (x, y, obj) {
startPlacingTower('1');
if (placingTowerConfig) {
towerIcon1.isDragging = true;
game.move(x, y, obj);
}
};
var towerText1 = uiContainer.addChild(new Text2('50', {
size: 50,
fill: '#fff',
anchorX: 0.5,
anchorY: 0.5
}));
towerText1.x = -305 - 25;
towerText1.y = -55;
// Create tower 2 group
var towerButton2 = uiContainer.addChild(LK.getAsset('uiButton', {
anchorX: 0.5,
anchorY: 1
}));
var towerIcon2 = uiContainer.addChild(LK.getAsset('tower_2', {
scaleX: 0.8,
scaleY: 0.8,
anchorX: 0.5,
anchorY: 0.5
}));
towerIcon2.x = -5;
towerIcon2.y = -115;
towerIcon2.down = function (x, y, obj) {
startPlacingTower('2');
if (placingTowerConfig) {
towerIcon2.isDragging = true;
game.move(x, y, obj);
}
};
var towerText2 = uiContainer.addChild(new Text2('100', {
size: 50,
fill: '#fff',
anchorX: 0.5,
anchorY: 0.5
}));
towerText2.x = -15 - 25;
towerText2.y = -55;
// Create tower 3 group
var towerButton3 = uiContainer.addChild(LK.getAsset('uiButton', {
anchorX: 0.5,
anchorY: 1
}));
towerButton3.x = 300;
var towerIcon3 = uiContainer.addChild(LK.getAsset('tower_3', {
scaleX: 0.8,
scaleY: 0.8,
anchorX: 0.5,
anchorY: 0.5
}));
towerIcon3.x = 300;
towerIcon3.y = -115;
towerIcon3.down = function (x, y, obj) {
startPlacingTower('3');
if (placingTowerConfig) {
towerIcon3.isDragging = true;
game.move(x, y, obj);
}
};
var towerText3 = uiContainer.addChild(new Text2('150', {
size: 50,
fill: '#fff',
anchorX: 0.5,
anchorY: 0.5
}));
towerText3.x = 285 - 25;
towerText3.y = -55;
// --- Initialization Functions ---
function initializeGrid() {
for (var i = 0; i < GRID_WIDTH; i++) {
grid[i] = [];
for (var j = 0; j < GRID_HEIGHT; j++) {
grid[i][j] = 0; // Empty
}
}
// Mark path tiles
for (var k = 0; k < pathWaypoints.length - 1; k++) {
var start = pathWaypoints[k];
var end = pathWaypoints[k + 1];
var p1 = {
x: Math.floor(start.x / CELL_SIZE),
y: Math.floor(start.y / CELL_SIZE)
};
var p2 = {
x: Math.floor(end.x / CELL_SIZE),
y: Math.floor(end.y / CELL_SIZE)
};
var x = p1.x;
var y = p1.y;
var dx = Math.abs(p2.x - x);
var dy = Math.abs(p2.y - y);
var sx = x < p2.x ? 1 : -1;
var sy = y < p2.y ? 1 : -1;
var err = dx - dy;
while (true) {
if (x >= 0 && x < GRID_WIDTH && y >= 0 && y < GRID_HEIGHT) {
grid[x][y] = 1;
}
if (x === p2.x && y === p2.y) {
break;
}
var e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x += sx;
}
if (e2 < dx) {
err += dx;
y += sy;
}
}
}
// Mark buildable tiles - only those on the sides of the road segments
for (var k = 0; k < pathWaypoints.length - 1; k++) {
var start = pathWaypoints[k];
var end = pathWaypoints[k + 1];
var p1 = {
x: Math.floor(start.x / CELL_SIZE),
y: Math.floor(start.y / CELL_SIZE)
};
var p2 = {
x: Math.floor(end.x / CELL_SIZE),
y: Math.floor(end.y / CELL_SIZE)
};
// Determine if this segment is horizontal or vertical
var isHorizontal = p1.y === p2.y;
var isVertical = p1.x === p2.x;
// Determine direction of movement to know which side to place tiles
var movingRight = p2.x > p1.x;
var movingDown = p2.y > p1.y;
var movingLeft = p2.x < p1.x;
var movingUp = p2.y < p1.y;
if (isVertical) {
// For vertical segments, place buildable tiles on left and right
var x = p1.x;
var minY = Math.min(p1.y, p2.y);
var maxY = Math.max(p1.y, p2.y);
for (var y = minY; y <= maxY; y++) {
// Only place on sides, avoiding corners where segments meet
var isCorner = false;
// Check if this position is a corner (where path changes direction)
for (var w = 0; w < pathWaypoints.length; w++) {
var wp = pathWaypoints[w];
if (Math.floor(wp.x / CELL_SIZE) === x && Math.floor(wp.y / CELL_SIZE) === y) {
isCorner = true;
break;
}
}
if (!isCorner) {
// Left side
if (x - 1 >= 0 && grid[x - 1][y] === 0) {
grid[x - 1][y] = 2;
}
// Right side
if (x + 1 < GRID_WIDTH && grid[x + 1][y] === 0) {
grid[x + 1][y] = 2;
}
}
}
} else if (isHorizontal) {
// For horizontal segments, only place on one side to avoid doubles
var y = p1.y;
var minX = Math.min(p1.x, p2.x);
var maxX = Math.max(p1.x, p2.x);
for (var x = minX; x <= maxX; x++) {
// Only place on one side (top) to avoid double placement
if (y - 1 >= 0 && grid[x][y - 1] === 0) {
grid[x][y - 1] = 2;
}
}
}
}
// Draw visual grid - buildable tiles first (background layer)
for (var i = 0; i < GRID_WIDTH; i++) {
for (var j = 0; j < GRID_HEIGHT; j++) {
if (grid[i][j] === 2) {
var buildableTile = mapContainer.addChild(LK.getAsset('buildableTile', {}));
buildableTile.alpha = 0.5;
buildableTile.x = i * CELL_SIZE;
buildableTile.y = j * CELL_SIZE;
}
}
}
// Draw path tiles on top
for (var i = 0; i < GRID_WIDTH; i++) {
for (var j = 0; j < GRID_HEIGHT; j++) {
if (grid[i][j] === 1) {
var pathTile = mapContainer.addChild(LK.getAsset('pathTile', {}));
pathTile.x = i * CELL_SIZE;
pathTile.y = j * CELL_SIZE;
}
}
}
}
initializeGrid();
function startPlacingTower(type) {
var config = towerTypes[type];
if (LK.getScore() >= config.cost) {
placingTowerConfig = config;
ghostTower = mapContainer.addChild(LK.getAsset(config.asset, {
anchorX: 0,
anchorY: 0
}));
ghostTower.alpha = 0.7;
}
}
// --- Event Handlers ---
game.down = function (x, y, obj) {
var localPoint = uiContainer.toLocal({
x: x,
y: y
});
// If the clicked object is a tower on the map, duplicate it for placement
if (obj && obj.target && (obj.target.isTower || obj.target.parent && obj.target.parent.isTower)) {
var clickedTower = obj.target.isTower ? obj.target : obj.target.parent;
// Find tower type from its config
var myType = null;
if (clickedTower.config) {
for (var key in towerTypes) {
if (towerTypes[key].asset === clickedTower.config.asset) {
myType = key;
break;
}
}
}
if (myType) {
startPlacingTower(myType);
if (placingTowerConfig) {
game.move(x, y, obj);
}
}
return; // Stop further processing (like map dragging)
}
if (!placingTowerConfig) {
isDraggingMap = true;
dragStart.x = x;
dragStart.y = y;
mapStart.x = mapContainer.x;
mapStart.y = mapContainer.y;
}
};
game.move = function (x, y, obj) {
if (placingTowerConfig) {
var mapPos = mapContainer.toLocal({
x: x,
y: y
});
// Position ghost tower directly at cursor position without anchor offset
ghostTower.x = mapPos.x;
ghostTower.y = mapPos.y;
var gridX = Math.floor(mapPos.x / CELL_SIZE);
var gridY = Math.floor(mapPos.y / CELL_SIZE);
if (gridX >= 0 && gridX < GRID_WIDTH && gridY >= 0 && gridY < GRID_HEIGHT && grid[gridX][gridY] === 2) {
ghostTower.tint = 0x00ff00; // Green for valid
} else {
ghostTower.tint = 0xff0000; // Red for invalid
}
} else if (isDraggingMap) {
var dx = x - dragStart.x;
var dy = y - dragStart.y;
// Clamp map position
var newX = mapStart.x + dx;
var newY = mapStart.y + dy;
var gameWidth = 2048;
var gameHeight = 2732;
if (newX > 0) {
newX = 0;
}
if (newY > 0) {
newY = 0;
}
if (newX < gameWidth - MAP_WIDTH) {
newX = gameWidth - MAP_WIDTH;
}
if (newY < gameHeight - MAP_HEIGHT) {
newY = gameHeight - MAP_HEIGHT;
}
mapContainer.x = newX;
mapContainer.y = newY;
}
};
game.up = function (x, y, obj) {
if (placingTowerConfig) {
var mapPos = mapContainer.toLocal({
x: x,
y: y
});
var gridX = Math.floor(mapPos.x / CELL_SIZE);
var gridY = Math.floor(mapPos.y / CELL_SIZE);
if (gridX >= 0 && gridX < GRID_WIDTH && gridY >= 0 && gridY < GRID_HEIGHT && grid[gridX][gridY] === 2) {
if (LK.getScore() >= placingTowerConfig.cost) {
var newTower = new Tower(placingTowerConfig);
newTower.x = gridX * CELL_SIZE + CELL_SIZE / 2;
newTower.y = gridY * CELL_SIZE + CELL_SIZE / 2;
mapContainer.addChild(newTower);
towers.push(newTower);
grid[gridX][gridY] = 3; // Mark as occupied
LK.setScore(LK.getScore() - placingTowerConfig.cost);
scoreTxt.setText('SCORE: ' + LK.getScore());
LK.getSound('place_tower').play();
}
}
ghostTower.destroy();
ghostTower = null;
placingTowerConfig = null;
// Reset drag states
towerIcon1.isDragging = false;
towerIcon2.isDragging = false;
towerIcon3.isDragging = false;
}
isDraggingMap = false;
};
// --- Game Logic Functions ---
function findClosestEnemy(x, y, range) {
var closestEnemy = null;
var minDistanceSq = range * range;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - x;
var dy = enemy.y - y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < minDistanceSq) {
minDistanceSq = distanceSq;
closestEnemy = enemy;
}
}
return closestEnemy;
}
function fireProjectile(tower, target) {
var projectileTypes = tower.config.projectileTypes;
for (var i = 0; i < projectileTypes.length; i++) {
var projectile = new Projectile({
x: tower.x,
y: tower.y
}, target, tower.damage, projectileTypes[i]);
// Flip arrow if enemy is to the left of tower_2 and projectile is arrow
if (tower.config.asset === 'tower_2' && projectileTypes[i] === 'arrow' && target.x < tower.x) {
projectile.scaleX = -1;
}
// Add rotation animation to star projectiles
if (projectileTypes[i] === 'star') {
tween(projectile, {
rotation: Math.PI * 4
}, {
duration: 2000
});
}
projectiles.push(projectile);
mapContainer.addChild(projectile);
}
LK.getSound('shoot').play();
}
function startWave() {
enemiesSpawnedInWave = 0;
waveBossKilled = false;
waveSpawnTimer = LK.setInterval(function () {
if (enemiesSpawnedInWave >= 20) {
LK.clearInterval(waveSpawnTimer);
waveSpawnTimer = null;
// Check for next wave after all enemies are defeated
return;
}
enemiesSpawnedInWave++;
var enemyType;
// Determine enemy type based on position in wave
if (currentWave === 3) {
// Wave 3: Mix of serpent1 and serpent2
if (enemiesSpawnedInWave === 19) {
// First boss - serpent1 boss
enemyType = 'boss1';
} else if (enemiesSpawnedInWave === 20) {
// Second boss - serpent2 boss
enemyType = 'boss2';
} else if (enemiesSpawnedInWave === 15 || enemiesSpawnedInWave === 17) {
// Big serpent1 enemies
enemyType = 'serpent1_big';
} else if (enemiesSpawnedInWave === 16 || enemiesSpawnedInWave === 18) {
// Big serpent2 enemies
enemyType = 'serpent2_big';
} else {
// Regular enemies - alternate between serpent1 and serpent2
enemyType = enemiesSpawnedInWave % 2 === 1 ? 'serpent1' : 'serpent2';
}
} else if (currentWave === 4) {
// Wave 4: serpent3 based wave
if (enemiesSpawnedInWave === 20) {
// Boss - serpent3 boss
enemyType = 'boss3';
} else if (enemiesSpawnedInWave === 18 || enemiesSpawnedInWave === 19) {
// Big serpent3 enemies
enemyType = 'serpent3_big';
} else {
// Regular serpent3 enemies
enemyType = 'serpent3';
}
} else if (currentWave === 5) {
// Wave 5: serpent4 based wave
if (enemiesSpawnedInWave === 20) {
// Boss - serpent4 boss
enemyType = 'boss4';
} else if (enemiesSpawnedInWave === 18 || enemiesSpawnedInWave === 19) {
// Big serpent4 enemies
enemyType = 'serpent4_big';
} else {
// Regular serpent4 enemies
enemyType = 'serpent4';
}
} else if (currentWave === 6) {
// Wave 6: Mix of all serpent types with two bosses
if (enemiesSpawnedInWave === 19) {
// First boss - serpent3 boss
enemyType = 'boss3';
} else if (enemiesSpawnedInWave === 20) {
// Second boss - serpent4 boss
enemyType = 'boss4';
} else if (enemiesSpawnedInWave === 15) {
// Big serpent1 enemy
enemyType = 'serpent1_big';
} else if (enemiesSpawnedInWave === 16) {
// Big serpent2 enemy
enemyType = 'serpent2_big';
} else if (enemiesSpawnedInWave === 17) {
// Big serpent3 enemy
enemyType = 'serpent3_big';
} else if (enemiesSpawnedInWave === 18) {
// Big serpent4 enemy
enemyType = 'serpent4_big';
} else {
// Regular enemies - cycle through all serpent types
var serpentTypes = ['serpent1', 'serpent2', 'serpent3', 'serpent4'];
enemyType = serpentTypes[(enemiesSpawnedInWave - 1) % 4];
}
} else {
// Fallback for other waves - single boss at enemy 20
if (enemiesSpawnedInWave === 20) {
// Boss - use wave-specific boss type
if (currentWave === 1) {
enemyType = 'boss1';
} else if (currentWave === 2) {
enemyType = 'boss2';
}
} else if (enemiesSpawnedInWave === 18 || enemiesSpawnedInWave === 19) {
// Big enemies - use wave-specific serpent type
if (currentWave === 1) {
enemyType = 'serpent1_big';
} else if (currentWave === 2) {
enemyType = 'serpent2_big';
}
} else {
// Regular enemy based on wave
if (currentWave === 1) {
enemyType = 'serpent1';
} else if (currentWave === 2) {
enemyType = 'serpent2';
}
}
}
var enemy = new Enemy(pathWaypoints, enemyType);
enemy.x = pathWaypoints[0].x;
enemy.y = pathWaypoints[0].y;
enemies.push(enemy);
mapContainer.addChild(enemy);
}, 800);
}
// Wave text will be created dynamically when needed
function showWaveText(waveNumber) {
// Set flag to indicate wave text is showing
isShowingWaveText = true;
// Create wave text dynamically
var waveText = new Text2('WAVE ' + waveNumber, {
size: 120,
fill: 0xFFA500 // Orange color
});
waveText.anchor.set(0.5, 0.5);
waveText.x = 2048 / 2;
waveText.y = 2732 / 2;
game.addChild(waveText);
// Create blinking animation with proper recursion control
var blinkCount = 0;
var maxBlinks = 6; // 3 complete blink cycles (fade out + fade in = 1 cycle, so 6 total for 3 cycles)
function blink() {
if (blinkCount >= maxBlinks) {
// Stop blinking and fade out
tween(waveText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
waveText.destroy();
// Clear flag when wave text is destroyed
isShowingWaveText = false;
// Start spawning enemies after wave text disappears
startWave();
}
});
return;
}
tween(waveText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tween(waveText, {
alpha: 1
}, {
duration: 500,
onFinish: function onFinish() {
blinkCount++;
blink();
}
});
}
});
}
blink();
}
// Show initial wave text (startWave will be called after text animation)
showWaveText(1);
game.update = function () {
for (var i = enemies.length - 1; i >= 0; i--) {
enemies[i].update();
}
for (var i = towers.length - 1; i >= 0; i--) {
towers[i].update();
}
for (var i = projectiles.length - 1; i >= 0; i--) {
projectiles[i].update();
}
// Check if wave is complete and start next wave
if (enemiesSpawnedInWave === 20 && waveBossKilled && !waveSpawnTimer && !isShowingWaveText && enemies.length === 0) {
currentWave++;
if (currentWave <= 6) {
// Reset wave state for next wave
enemiesSpawnedInWave = 0;
waveBossKilled = false;
// Show wave text for new wave (startWave will be called after text animation)
showWaveText(currentWave);
} else {
// All waves completed, show you win screen
LK.showYouWin();
}
}
};