User prompt
enemy units should move belwo the fog, they are still appearing on top of it.
User prompt
fog of war should be more transparent and enemy units should move below it, to give the impression of moving under the fog
User prompt
top two rows of the grid should have a fog of war on top of them, and player units should not be able to move into it
User prompt
sometimes enmies are spanwed and do not move and stay on the upper row. that should not happen.
User prompt
add some more deatils to the battlefield, like random grass, rocks small details that do not impact the game, but will make it visually nicer. just use drawing or the same technique of pixelart characters.
User prompt
nice, now lets move the coing and the count more to the right and also make sure the coin and the number do not overlap (which they are doing now
User prompt
coin icon should instead be a pixelart coin created like te units or enemies
User prompt
Instead of the word Gold, create a pixealrt coin and use it instead
User prompt
Rename Level to King Lvl
User prompt
During the first planning phaase king can be moved in the battlefield but not to the bench
User prompt
King should not count as a unit for the units limit in the battlefield
User prompt
king should not have star level. it will still however like it is now when leveling up
User prompt
on planning pahse also show info button below king, which will be hidden in battle phase
User prompt
Update the text display for max units and structures to show the current count out of the max limit.
User prompt
for units and structures, use 1/2 notation so player knows how many the have in the field.
User prompt
move them to fit in the box
User prompt
move units strucres to the left of the top UI and gold to the right
User prompt
wave completet and can add more units to field messages should last a little longer on screen
User prompt
wave completetd message or message that player can place more units, should appear below the top UI and not with the current animation, just simple appear and then disappear after a few seconds. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make enemy Ai smarter. when they engage in a fight with a unit, they should stay in the spot they are as long as they are still fighting. if not player unit close to hit, move to the closes one, if no player units remaining go for king
User prompt
Enemies should always try to move towards the king
Code edit (1 edits merged)
Please save this source code
Remix started
Copy Tower Defense Template
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var AttackInfo = Container.expand(function () { var self = Container.call(this); var background = self.attachAsset('bench_area_bg', { anchorX: 0, anchorY: 0, width: 300, height: 150 }); background.alpha = 0.8; self.attackerText = new Text2('', { size: 36, fill: 0xFFFFFF }); self.attackerText.anchor.set(0, 0.5); self.attackerText.x = 10; self.attackerText.y = 25; self.addChild(self.attackerText); self.damageText = new Text2('', { size: 36, fill: 0xFF0000 }); self.damageText.anchor.set(0, 0.5); self.damageText.x = 10; self.damageText.y = 100; self.addChild(self.damageText); self.updateInfo = function (attacker, damage) { self.attackerText.setText('Attacker: ' + attacker); self.damageText.setText('Damage: ' + damage); }; return self; }); var BenchSlot = Container.expand(function (index) { var self = Container.call(this); var slotGraphics = self.attachAsset('bench_slot', { anchorX: 0.5, anchorY: 0.5 }); self.index = index; self.unit = null; slotGraphics.alpha = 0.5; self.down = function (x, y, obj) { if (self.unit && gameState === 'planning') { draggedUnit = self.unit; // Start dragging from current position, not mouse position draggedUnit.x = self.x; draggedUnit.y = self.y; // Store original position and slot draggedUnit.originalX = self.x; draggedUnit.originalY = self.y; draggedUnit.originalSlot = self; // Free the bench slot immediately when dragging starts self.unit = null; } }; return self; }); var Bullet = Container.expand(function (damage, target) { var self = Container.call(this); // Default bullet type self.bulletType = 'default'; self.damage = damage; self.target = target; self.speed = 8; // Create bullet graphics based on type self.createGraphics = function () { // Clear any existing graphics self.removeChildren(); if (self.bulletType === 'arrow') { // Create arrow projectile var arrowShaft = self.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5 }); // Arrow head var arrowHead = self.attachAsset('arrow_head', { anchorX: 0.5, anchorY: 0.5 }); arrowHead.x = 15; arrowHead.rotation = 0.785; // 45 degrees // Arrow fletching var fletching1 = self.attachAsset('arrow_feather', { anchorX: 0.5, anchorY: 0.5 }); fletching1.x = -10; fletching1.y = -3; var fletching2 = self.attachAsset('arrow_feather', { anchorX: 0.5, anchorY: 0.5 }); fletching2.x = -10; fletching2.y = 3; self.speed = 10; // Arrows are faster } else if (self.bulletType === 'magic') { // Create magic projectile var magicCore = self.attachAsset('magic_core', { anchorX: 0.5, anchorY: 0.5 }); // Magic aura var magicAura = self.attachAsset('magic_aura', { anchorX: 0.5, anchorY: 0.5 }); magicAura.alpha = 0.5; // Magic sparkles var sparkle1 = self.attachAsset('magic_sparkle', { anchorX: 0.5, anchorY: 0.5 }); sparkle1.x = -10; sparkle1.y = -10; var sparkle2 = self.attachAsset('magic_sparkle', { anchorX: 0.5, anchorY: 0.5 }); sparkle2.x = 10; sparkle2.y = 10; // Add tween for magic glow effect tween(magicAura, { scaleX: 1.2, scaleY: 1.2, alpha: 0.3 }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { tween(magicAura, { scaleX: 1.0, scaleY: 1.0, alpha: 0.5 }, { duration: 300, easing: tween.easeInOut, onFinish: onFinish }); } }); self.speed = 6; // Magic is slower but more visible } else { // Default bullet var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); } }; // Create initial graphics self.createGraphics(); self.update = function () { if (!self.target || self.target.health <= 0) { 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 < 10) { var killed = self.target.takeDamage(self.damage); if (killed) { // Don't give gold for killing enemies LK.getSound('enemyDeath').play(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self.target) { enemies[i].destroy(); enemies.splice(i, 1); break; } } } self.destroy(); return; } var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; // Rotate arrow to face target if (self.bulletType === 'arrow') { self.rotation = Math.atan2(dy, dx); } }; return self; }); var Enemy = Container.expand(function () { var self = Container.call(this); // Create pixelart enemy character var enemyContainer = new Container(); self.addChild(enemyContainer); // Body (red goblin) var body = enemyContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); body.width = 60; body.height = 60; body.y = 10; body.tint = 0xFF4444; // Red tint to match goblin theme // Head (darker red) var head = enemyContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 40, height: 35 }); head.y = -30; head.tint = 0xcc0000; // Eyes (white dots) var leftEye = enemyContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: 8, height: 8 }); leftEye.x = -10; leftEye.y = -30; leftEye.tint = 0xFFFFFF; var rightEye = enemyContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: 8, height: 8 }); rightEye.x = 10; rightEye.y = -30; rightEye.tint = 0xFFFFFF; // Claws var leftClaw = enemyContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 20, height: 15 }); leftClaw.x = -25; leftClaw.y = 5; leftClaw.rotation = 0.5; leftClaw.tint = 0x800000; var rightClaw = enemyContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 20, height: 15 }); rightClaw.x = 25; rightClaw.y = 5; rightClaw.rotation = -0.5; rightClaw.tint = 0x800000; // Initial attributes for the enemy self.health = 25; self.maxHealth = 25; self.damage = 15; self.armor = 3; self.criticalChance = 0.15; self.magicResist = 3; self.mana = 40; self.speed = 2; self.range = 1; // Enemy always melee (adjacent cell) self.goldValue = 2; self.name = "Enemy"; self.healthBar = null; // Initialize health bar as null self.isAttacking = false; // Flag to track if enemy is currently attacking self.baseScale = 1.3; // Base scale multiplier for all enemies (30% larger) // Apply base scale self.scaleX = self.baseScale; self.scaleY = self.baseScale; self.showHealthBar = function () { if (!self.healthBar) { self.healthBar = new HealthBar(60, 8); // Create a health bar instance (smaller than units) self.healthBar.x = -30; // Position relative to the enemy's center self.healthBar.y = -60; // Position above the enemy self.addChild(self.healthBar); } self.healthBar.visible = true; // Ensure health bar is visible self.healthBar.updateHealth(self.health, self.maxHealth); }; self.updateHealthBar = function () { if (self.healthBar) { self.healthBar.updateHealth(self.health, self.maxHealth); } }; self.takeDamage = function (damage) { self.health -= damage; self.updateHealthBar(); // Update health bar when taking damage if (self.health <= 0) { self.health = 0; return true; } return false; }; self.update = function () { if (!self.gridMovement) { self.gridMovement = new GridMovement(self, grid); } self.gridMovement.update(); // Castle is now treated as a unit on the grid, no special collision logic needed // Find and attack units within range (cell-based) var closestUnit = null; var closestCellDistance = self.range + 1; // Only units within range (cell count) are valid for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { if (grid[y] && grid[y][x]) { var gridSlot = grid[y][x]; if (gridSlot.unit) { // Calculate cell-based Manhattan distance var cellDist = Math.abs(self.gridX - x) + Math.abs(self.gridY - y); // Check if the enemy is completely inside the adjacent cell before attacking if (cellDist <= self.range && cellDist < closestCellDistance && self.gridMovement.isMoving === false) { closestCellDistance = cellDist; closestUnit = gridSlot.unit; } } } } } // Attack the closest unit if found if (closestUnit) { if (!self.lastAttack) { self.lastAttack = 0; } if (LK.ticks - self.lastAttack >= 60) { self.lastAttack = LK.ticks; // Add tween animation for enemy attack var originalX = self.x; var originalY = self.y; // Calculate lunge direction towards target var dx = closestUnit.x - self.x; var dy = closestUnit.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); var offset = Math.min(30, dist * 0.25); var lungeX = self.x + (dist > 0 ? dx / dist * offset : 0); var lungeY = self.y + (dist > 0 ? dy / dist * offset : 0); // Prevent multiple attack tweens at once if (!self._isAttackTweening) { self._isAttackTweening = true; self.isAttacking = true; // Set attacking flag // Flash enemy red briefly during attack using LK effects LK.effects.flashObject(self, 0xFF4444, 300); // Lunge towards target tween(self, { x: lungeX, y: lungeY }, { duration: 120, easing: tween.easeOut, onFinish: function onFinish() { // Apply damage after lunge var killed = closestUnit.takeDamage(self.damage); if (killed) { self.isAttacking = false; // Clear attacking flag if target dies for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { if (grid[y] && grid[y][x]) { var gridSlot = grid[y][x]; if (gridSlot.unit === closestUnit) { gridSlot.unit = null; closestUnit.destroy(); for (var i = bench.length - 1; i >= 0; i--) { if (bench[i] === closestUnit) { bench.splice(i, 1); break; } } break; } } } } } // Return to original position tween(self, { x: originalX, y: originalY }, { duration: 100, easing: tween.easeIn, onFinish: function onFinish() { self._isAttackTweening = false; self.isAttacking = false; // Clear attacking flag after animation } }); } }); } } } }; return self; }); var RangedEnemy = Enemy.expand(function () { var self = Enemy.call(this); // Remove default enemy graphics self.removeChildren(); // Create pixelart ranged enemy character var enemyContainer = new Container(); self.addChild(enemyContainer); // Body (purple archer) var body = enemyContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); body.width = 55; body.height = 70; body.y = 10; body.tint = 0x8A2BE2; // Purple tint to match archer theme // Head with hood var head = enemyContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 35, height: 30 }); head.y = -35; head.tint = 0x6A1B9A; // Eyes (glowing yellow) var leftEye = enemyContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: 6, height: 6 }); leftEye.x = -8; leftEye.y = -35; leftEye.tint = 0xFFFF00; var rightEye = enemyContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: 6, height: 6 }); rightEye.x = 8; rightEye.y = -35; rightEye.tint = 0xFFFF00; // Bow var bow = enemyContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 45, height: 12 }); bow.x = -25; bow.y = 0; bow.rotation = 1.57; // 90 degrees bow.tint = 0x4B0082; // Bowstring var bowstring = enemyContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 2, height: 40 }); bowstring.x = -25; bowstring.y = 0; bowstring.rotation = 1.57; bowstring.tint = 0xDDDDDD; // Light grey string // Quiver on back var quiver = enemyContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 30 }); quiver.x = 20; quiver.y = -10; quiver.tint = 0x2B0040; // Dark purple // Override attributes for ranged enemy self.health = 20; self.maxHealth = 20; self.damage = 12; self.armor = 2; self.criticalChance = 0.1; self.magicResist = 2; self.mana = 30; self.speed = 1.5; // Slightly slower movement self.range = 4; // Long range for shooting self.goldValue = 3; // More valuable than melee enemies self.name = "Ranged Enemy"; self.fireRate = 75; // Shoots faster - every 1.25 seconds self.lastShot = 0; self.baseScale = 1.3; // Base scale multiplier for all enemies (30% larger) // Apply base scale self.scaleX = self.baseScale; self.scaleY = self.baseScale; // Override update to include ranged attack behavior self.update = function () { if (!self.gridMovement) { self.gridMovement = new GridMovement(self, grid); } self.gridMovement.update(); // Find and attack units within range (cell-based) var closestUnit = null; var closestCellDistance = self.range + 1; // Only units within range (cell count) are valid for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { if (grid[y] && grid[y][x]) { var gridSlot = grid[y][x]; if (gridSlot.unit) { // Calculate cell-based Manhattan distance var cellDist = Math.abs(self.gridX - x) + Math.abs(self.gridY - y); // Check if the enemy can shoot (doesn't need to be stationary like melee) if (cellDist <= self.range && cellDist < closestCellDistance) { closestCellDistance = cellDist; closestUnit = gridSlot.unit; } } } } } // Attack the closest unit if found if (closestUnit) { if (!self.lastShot) { self.lastShot = 0; } if (LK.ticks - self.lastShot >= self.fireRate) { self.lastShot = LK.ticks; // Create projectile var projectile = new EnemyProjectile(self.damage, closestUnit); projectile.x = self.x; projectile.y = self.y; enemyProjectiles.push(projectile); game.addChild(projectile); // Add shooting animation if (!self._isShootingTweening) { self._isShootingTweening = true; // Flash enemy blue briefly during shooting using LK effects LK.effects.flashObject(self, 0x4444FF, 200); self._isShootingTweening = false; } } } }; return self; }); var EnemyProjectile = Container.expand(function (damage, target) { var self = Container.call(this); // Create arrow projectile for enemies var arrowContainer = new Container(); self.addChild(arrowContainer); // Arrow shaft var arrowShaft = arrowContainer.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5, width: 25, height: 6 }); arrowShaft.tint = 0x4B0082; // Dark purple for enemy arrows // Arrow head var arrowHead = arrowContainer.attachAsset('arrow_head', { anchorX: 0.5, anchorY: 0.5, width: 10, height: 10 }); arrowHead.x = 12; arrowHead.rotation = 0.785; // 45 degrees arrowHead.tint = 0xFF0000; // Red tip for enemy // Arrow fletching var fletching = arrowContainer.attachAsset('arrow_feather', { anchorX: 0.5, anchorY: 0.5, width: 8, height: 5 }); fletching.x = -8; fletching.tint = 0x8B008B; // Dark magenta feathers self.damage = damage; self.target = target; self.speed = 6; self.update = function () { if (!self.target || self.target.health <= 0) { 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 < 15) { // Hit target var killed = self.target.takeDamage(self.damage); if (killed) { // Handle target death - remove from grid if it's a unit for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { if (grid[y] && grid[y][x]) { var gridSlot = grid[y][x]; if (gridSlot.unit === self.target) { gridSlot.unit = null; self.target.destroy(); for (var i = bench.length - 1; i >= 0; i--) { if (bench[i] === self.target) { bench.splice(i, 1); break; } } break; } } } } } self.destroy(); return; } var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; // Rotate arrow to face target self.rotation = Math.atan2(dy, dx); }; return self; }); var GridMovement = Container.expand(function (entity, gridRef) { var self = Container.call(this); self.entity = entity; self.gridRef = gridRef; self.currentCell = { x: entity.gridX, y: entity.gridY }; self.targetCell = { x: entity.gridX, y: entity.gridY }; self.moveSpeed = 2; // Pixels per frame for continuous movement self.isMoving = false; // Track if entity is currently moving self.pathToTarget = []; // Store calculated path self.pathIndex = 0; // Current position in path // Helper method to check if a cell is occupied self.isCellOccupied = function (x, y) { // Check if out of bounds if (x < 0 || x >= GRID_COLS || y < 0 || y >= GRID_ROWS) { return true; } // Check if position is occupied by any entity using position tracking if (isPositionOccupied(x, y, self.entity)) { return true; } // Check if cell is reserved by another entity var cellKey = x + ',' + y; if (cellReservations[cellKey] && cellReservations[cellKey] !== self.entity) { return true; } // Check if occupied by a player unit on grid if (self.gridRef[y] && self.gridRef[y][x] && self.gridRef[y][x].unit && self.gridRef[y][x].unit !== self.entity) { return true; } return false; }; // A* pathfinding implementation self.findPath = function (startX, startY, goalX, goalY) { var openSet = []; var closedSet = []; var cameFrom = {}; // Helper to create node function createNode(x, y, g, h) { return { x: x, y: y, g: g, // Cost from start h: h, // Heuristic cost to goal f: g + h // Total cost }; } // Manhattan distance heuristic function heuristic(x1, y1, x2, y2) { return Math.abs(x2 - x1) + Math.abs(y2 - y1); } // Get neighbors of a cell function getNeighbors(x, y) { var neighbors = []; var directions = [{ x: 0, y: -1 }, // Up { x: 1, y: 0 }, // Right { x: 0, y: 1 }, // Down { x: -1, y: 0 } // Left ]; for (var i = 0; i < directions.length; i++) { var newX = x + directions[i].x; var newY = y + directions[i].y; // Skip if occupied (but allow goal position even if occupied) if (newX === goalX && newY === goalY || !self.isCellOccupied(newX, newY)) { neighbors.push({ x: newX, y: newY }); } } return neighbors; } // Initialize start node var startNode = createNode(startX, startY, 0, heuristic(startX, startY, goalX, goalY)); openSet.push(startNode); // Track best g scores var gScore = {}; gScore[startX + ',' + startY] = 0; while (openSet.length > 0) { // Find node with lowest f score var current = openSet[0]; var currentIndex = 0; for (var i = 1; i < openSet.length; i++) { if (openSet[i].f < current.f) { current = openSet[i]; currentIndex = i; } } // Check if we reached goal if (current.x === goalX && current.y === goalY) { // Reconstruct path var path = []; var key = current.x + ',' + current.y; while (key && key !== startX + ',' + startY) { var coords = key.split(','); path.unshift({ x: parseInt(coords[0]), y: parseInt(coords[1]) }); key = cameFrom[key]; } return path; } // Move current from open to closed openSet.splice(currentIndex, 1); closedSet.push(current); // Check neighbors var neighbors = getNeighbors(current.x, current.y); for (var i = 0; i < neighbors.length; i++) { var neighbor = neighbors[i]; var neighborKey = neighbor.x + ',' + neighbor.y; // Skip if in closed set var inClosed = false; for (var j = 0; j < closedSet.length; j++) { if (closedSet[j].x === neighbor.x && closedSet[j].y === neighbor.y) { inClosed = true; break; } } if (inClosed) { continue; } // Calculate tentative g score var tentativeG = current.g + 1; // Check if this path is better if (!gScore[neighborKey] || tentativeG < gScore[neighborKey]) { // Record best path cameFrom[neighborKey] = current.x + ',' + current.y; gScore[neighborKey] = tentativeG; // Add to open set if not already there var inOpen = false; for (var j = 0; j < openSet.length; j++) { if (openSet[j].x === neighbor.x && openSet[j].y === neighbor.y) { inOpen = true; openSet[j].g = tentativeG; openSet[j].f = tentativeG + openSet[j].h; break; } } if (!inOpen) { var h = heuristic(neighbor.x, neighbor.y, goalX, goalY); openSet.push(createNode(neighbor.x, neighbor.y, tentativeG, h)); } } } } // No path found - try simple alternative return self.findAlternativePath(goalX, goalY); }; // Helper method to find alternative paths (fallback for when A* fails) self.findAlternativePath = function (targetX, targetY) { var currentX = self.currentCell.x; var currentY = self.currentCell.y; // Calculate primary direction var dx = targetX - currentX; var dy = targetY - currentY; // Try moving in the primary direction first var moves = []; if (Math.abs(dx) > Math.abs(dy)) { // Prioritize horizontal movement if (dx > 0 && !self.isCellOccupied(currentX + 1, currentY)) { return [{ x: currentX + 1, y: currentY }]; } if (dx < 0 && !self.isCellOccupied(currentX - 1, currentY)) { return [{ x: currentX - 1, y: currentY }]; } // Try vertical as alternative if (dy > 0 && !self.isCellOccupied(currentX, currentY + 1)) { return [{ x: currentX, y: currentY + 1 }]; } if (dy < 0 && !self.isCellOccupied(currentX, currentY - 1)) { return [{ x: currentX, y: currentY - 1 }]; } } else { // Prioritize vertical movement if (dy > 0 && !self.isCellOccupied(currentX, currentY + 1)) { return [{ x: currentX, y: currentY + 1 }]; } if (dy < 0 && !self.isCellOccupied(currentX, currentY - 1)) { return [{ x: currentX, y: currentY - 1 }]; } // Try horizontal as alternative if (dx > 0 && !self.isCellOccupied(currentX + 1, currentY)) { return [{ x: currentX + 1, y: currentY }]; } if (dx < 0 && !self.isCellOccupied(currentX - 1, currentY)) { return [{ x: currentX - 1, y: currentY }]; } } return null; }; self.moveToNextCell = function () { // Check if this is a structure unit that shouldn't move if (self.entity.isStructure) { return false; // Structures don't move } // Check if this is a player unit or enemy var isPlayerUnit = self.entity.name && (self.entity.name === "Soldier" || self.entity.name === "Knight" || self.entity.name === "Wizard" || self.entity.name === "Paladin" || self.entity.name === "Ranger" || self.entity.name === "King"); var targetGridX = -1; var targetGridY = -1; var shouldMove = false; if (isPlayerUnit) { // Player units move towards enemies var closestEnemy = null; var closestDist = 99999; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (typeof enemy.gridX === "number" && typeof enemy.gridY === "number") { var dx = enemy.gridX - self.currentCell.x; var dy = enemy.gridY - self.currentCell.y; var dist = Math.abs(dx) + Math.abs(dy); if (dist < closestDist) { closestDist = dist; closestEnemy = enemy; targetGridX = enemy.gridX; targetGridY = enemy.gridY; } } } // Only move if enemy found and not in range if (closestEnemy && closestDist > self.entity.range) { // Check if unit is currently attacking if (self.entity.isAttacking) { return false; // Don't move while attacking } shouldMove = true; } else { // Already in range or no enemy, don't move return false; } } else { // Enemy movement logic // Find closest unit on the grid var closestUnit = null; var closestDist = 99999; for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { if (self.gridRef[y] && self.gridRef[y][x] && self.gridRef[y][x].unit) { var dx = x - self.currentCell.x; var dy = y - self.currentCell.y; var dist = Math.abs(dx) + Math.abs(dy); if (dist < closestDist) { closestDist = dist; closestUnit = self.gridRef[y][x].unit; targetGridX = x; targetGridY = y; } } } } if (closestUnit) { // Check if this is a ranged enemy and if it's already within range var entityRange = self.entity.range || 1; // If enemy is within range or is attacking, don't move if (closestDist <= entityRange || self.entity.isAttacking) { // Stay in current position - don't move return false; } shouldMove = true; } else { // No target found, move down as fallback targetGridX = self.currentCell.x; targetGridY = self.currentCell.y + 1; shouldMove = true; } } if (!shouldMove) { return false; } // Use A* pathfinding to find best path if (!self.pathToTarget || self.pathToTarget.length === 0 || self.pathIndex >= self.pathToTarget.length) { // Calculate new path var path = self.findPath(self.currentCell.x, self.currentCell.y, targetGridX, targetGridY); if (path && path.length > 0) { self.pathToTarget = path; self.pathIndex = 0; } else { // No path found return false; } } // Get next cell from path var nextCell = self.pathToTarget[self.pathIndex]; if (!nextCell) { return false; } var nextX = nextCell.x; var nextY = nextCell.y; // Check if desired cell is still unoccupied if (self.isCellOccupied(nextX, nextY)) { // Path is blocked, recalculate self.pathToTarget = []; self.pathIndex = 0; return false; } // Final check if the cell is valid and unoccupied if (!self.isCellOccupied(nextX, nextY) && self.gridRef[nextY] && self.gridRef[nextY][nextX]) { var targetGridCell = self.gridRef[nextY][nextX]; // Double-check that no other entity is trying to move to same position at same time if (isPositionOccupied(nextX, nextY, self.entity)) { return false; } // Unregister old position unregisterEntityPosition(self.entity, self.currentCell.x, self.currentCell.y); // Register new position immediately to prevent conflicts registerEntityPosition(self.entity); // Release old reservation var oldCellKey = self.currentCell.x + ',' + self.currentCell.y; if (cellReservations[oldCellKey] === self.entity) { delete cellReservations[oldCellKey]; } // Reserve new cell var newCellKey = nextX + ',' + nextY; cellReservations[newCellKey] = self.entity; // Update logical position immediately self.currentCell.x = nextX; self.currentCell.y = nextY; self.entity.gridX = nextX; self.entity.gridY = nextY; // Set target position for smooth movement self.targetCell.x = targetGridCell.x; self.targetCell.y = targetGridCell.y; self.isMoving = true; // Add smooth transition animation when starting movement if (!self.entity._isMoveTweening) { self.entity._isMoveTweening = true; // Get base scale (default to 1 if not set) var baseScale = self.entity.baseScale || 1; // Slight scale and rotation animation when moving tween(self.entity, { scaleX: baseScale * 1.1, scaleY: baseScale * 0.9, rotation: self.entity.rotation + (Math.random() > 0.5 ? 0.05 : -0.05) }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(self.entity, { scaleX: baseScale, scaleY: baseScale, rotation: 0 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { self.entity._isMoveTweening = false; } }); } }); } return true; } return false; }; self.update = function () { // Periodically check if we need to recalculate path (every 30 ticks) if (LK.ticks % 30 === 0 && self.pathToTarget && self.pathToTarget.length > 0) { // Check if current path is still valid var stillValid = true; for (var i = self.pathIndex; i < self.pathToTarget.length && i < self.pathIndex + 3; i++) { if (self.pathToTarget[i] && self.isCellOccupied(self.pathToTarget[i].x, self.pathToTarget[i].y)) { stillValid = false; break; } } if (!stillValid) { // Path blocked, clear it to force recalculation self.pathToTarget = []; self.pathIndex = 0; } } // If we have a target position, move towards it smoothly if (self.isMoving) { var dx = self.targetCell.x - self.entity.x; var dy = self.targetCell.y - self.entity.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > self.moveSpeed) { // Continue moving towards target var moveX = dx / distance * self.moveSpeed; var moveY = dy / distance * self.moveSpeed; self.entity.x += moveX; self.entity.y += moveY; } else { // Reached target cell self.entity.x = self.targetCell.x; self.entity.y = self.targetCell.y; self.isMoving = false; self.pathIndex++; // Move to next cell in path // Wait a tick before trying next move to avoid simultaneous conflicts if (LK.ticks % 2 === 0) { // Immediately try to move to next cell for continuous movement self.moveToNextCell(); } } } else { // Not moving, try to start moving to next cell // Add slight randomization to prevent all entities from moving at exact same time if (LK.ticks % (2 + Math.floor(Math.random() * 3)) === 0) { self.moveToNextCell(); } } }; return self; }); var GridSlot = Container.expand(function (lane, gridX, gridY) { var self = Container.call(this); var slotGraphics = self.attachAsset('grid_slot', { anchorX: 0.5, anchorY: 0.5 }); self.lane = lane; self.gridX = gridX; self.gridY = gridY; self.unit = null; slotGraphics.alpha = 0.3; self.down = function (x, y, obj) { if (self.unit && !draggedUnit && gameState === 'planning') { // Prevent moving king after the first placement if (self.unit.name === 'King' && wave > 1) { return; // King can only be moved at the beginning of the game } // Allow dragging any unit during planning phase draggedUnit = self.unit; draggedUnit.originalX = self.x; draggedUnit.originalY = self.y; draggedUnit.originalGridSlot = self; draggedFromGrid = true; } }; self.up = function (x, y, obj) { if (draggedUnit && !self.unit) { self.placeUnit(draggedUnit); draggedUnit = null; } }; self.placeUnit = function (unit) { if (self.unit) { return false; } // Check if position is already occupied by another entity if (isPositionOccupied(self.gridX, self.gridY, unit)) { return false; } // Check unit placement limits (don't apply to king) if (unit.name !== "King" && !canPlaceMoreUnits(unit.isStructure)) { // Show message about reaching unit limit var limitType = unit.isStructure ? 'structures' : 'units'; var limitMessage = new Text2('You have no more room for ' + limitType + ' in the battlefield, level up to increase it!', { size: 36, fill: 0xFF4444 }); limitMessage.anchor.set(0.5, 0.5); limitMessage.x = 1024; // Center of screen limitMessage.y = 800; // Middle of screen game.addChild(limitMessage); // Flash the message and fade it out LK.effects.flashObject(limitMessage, 0xFFFFFF, 300); tween(limitMessage, { alpha: 0, y: 700 }, { duration: 3000, easing: tween.easeOut, onFinish: function onFinish() { limitMessage.destroy(); } }); // Return unit to bench - find first available bench slot for (var i = 0; i < benchSlots.length; i++) { if (!benchSlots[i].unit) { benchSlots[i].unit = unit; unit.x = benchSlots[i].x; unit.y = benchSlots[i].y; unit.gridX = -1; unit.gridY = -1; unit.lane = -1; unit.hideHealthBar(); unit.hideStarIndicator(); updateBenchDisplay(); break; } } return false; // Can't place more units } self.unit = unit; // Unregister old position if unit was previously on grid if (unit.gridX >= 0 && unit.gridY >= 0) { unregisterEntityPosition(unit, unit.gridX, unit.gridY); } // Ensure unit is positioned exactly at grid cell center (snap to grid) unit.x = self.x; unit.y = self.y; unit.gridX = self.gridX; unit.gridY = self.gridY; unit.lane = self.lane; // Double-check positioning to prevent floating between cells var expectedX = GRID_START_X + self.gridX * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; var expectedY = GRID_START_Y + self.gridY * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; unit.x = expectedX; unit.y = expectedY; // Add placement animation var targetScale = unit.baseScale || 1.3; // Use base scale or default to 1.3 if (unit.tier === 2) { targetScale = (unit.baseScale || 1.3) * 1.2; } else if (unit.tier === 3) { targetScale = (unit.baseScale || 1.3) * 1.3; } unit.scaleX = 0; unit.scaleY = 0; tween(unit, { scaleX: targetScale * 1.2, scaleY: targetScale * 1.2 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(unit, { scaleX: targetScale, scaleY: targetScale }, { duration: 150, easing: tween.easeIn }); } }); // Register new position registerEntityPosition(unit); unit.showHealthBar(); // Show health bar when placed on grid unit.updateHealthBar(); // Update health bar to show current health unit.showStarIndicator(); // Show star indicator when placed on grid // Don't remove from bench - units stay in both places updateBenchDisplay(); return true; }; return self; }); var HealthBar = Container.expand(function (width, height) { var self = Container.call(this); self.maxWidth = width; // Background of the health bar self.background = self.attachAsset('healthbar_bg', { anchorX: 0, anchorY: 0.5, width: width, height: height }); // Foreground of the health bar self.foreground = self.attachAsset('healthbar_fg', { anchorX: 0, anchorY: 0.5, width: width, height: height }); self.updateHealth = function (currentHealth, maxHealth) { var healthRatio = currentHealth / maxHealth; self.foreground.width = self.maxWidth * healthRatio; if (healthRatio > 0.6) { self.foreground.tint = 0x00ff00; // Green } else if (healthRatio > 0.3) { self.foreground.tint = 0xffff00; // Yellow } else { self.foreground.tint = 0xff0000; // Red } }; return self; }); var StarIndicator = Container.expand(function (tier) { var self = Container.call(this); self.tier = tier || 1; self.dots = []; self.updateStars = function (newTier) { self.tier = newTier; // Remove existing dots for (var i = 0; i < self.dots.length; i++) { self.dots[i].destroy(); } self.dots = []; // Create new dots based on tier var dotSpacing = 12; var totalWidth = (self.tier - 1) * dotSpacing; var startX = -totalWidth / 2; for (var i = 0; i < self.tier; i++) { var dot = self.attachAsset('star_dot', { anchorX: 0.5, anchorY: 0.5 }); dot.x = startX + i * dotSpacing; dot.y = 0; self.dots.push(dot); } }; // Initialize with current tier self.updateStars(self.tier); return self; }); var Unit = Container.expand(function (tier) { var self = Container.call(this); self.tier = tier || 1; var assetName = 'unit_tier' + self.tier; var unitGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); self.health = 100; self.maxHealth = 100; self.damage = self.tier; self.speed = 1; // Attack speed multiplier self.fireRate = 60; // Base fire rate in ticks self.lastShot = 0; self.gridX = -1; self.gridY = -1; self.lane = -1; self.range = 1; // Default range in cells (adjacent/melee). Override in subclasses for ranged units. self.healthBar = null; // Initialize health bar as null self.starIndicator = null; // Initialize star indicator as null self.isAttacking = false; // Flag to track if unit is currently attacking self.baseScale = 1.3; // Base scale multiplier for all units (30% larger) // Apply base scale self.scaleX = self.baseScale; self.scaleY = self.baseScale; self.showHealthBar = function () { if (!self.healthBar) { self.healthBar = new HealthBar(80, 10); // Create a health bar instance self.healthBar.x = -40; // Position relative to the unit's center self.healthBar.y = -60; // Position above the unit self.addChild(self.healthBar); } self.healthBar.visible = true; // Ensure health bar is visible self.healthBar.updateHealth(self.health, self.maxHealth); }; self.showStarIndicator = function () { if (!self.starIndicator) { self.starIndicator = new StarIndicator(self.tier); self.starIndicator.x = 0; // Center relative to unit self.starIndicator.y = 55; // Position below the unit self.addChild(self.starIndicator); } self.starIndicator.visible = true; self.starIndicator.updateStars(self.tier); // Scale unit based on tier - two star units are 30% larger self.updateUnitScale(); }; self.updateUnitScale = function () { // Stop any ongoing scale tweens first tween.stop(self, { scaleX: true, scaleY: true }); if (self.tier === 2) { // Two star units are 20% larger on top of base scale var targetScale = self.baseScale * 1.2; self.scaleX = targetScale; self.scaleY = targetScale; tween(self, { scaleX: targetScale, scaleY: targetScale }, { duration: 300, easing: tween.easeOut }); } else if (self.tier === 3) { // Three star units are 30% larger on top of base scale var targetScale = self.baseScale * 1.3; self.scaleX = targetScale; self.scaleY = targetScale; tween(self, { scaleX: targetScale, scaleY: targetScale }, { duration: 300, easing: tween.easeOut }); } else { // Tier 1 units use base scale self.scaleX = self.baseScale; self.scaleY = self.baseScale; tween(self, { scaleX: self.baseScale, scaleY: self.baseScale }, { duration: 300, easing: tween.easeOut }); } }; self.hideHealthBar = function () { if (self.healthBar) { self.healthBar.destroy(); self.healthBar = null; } }; self.hideStarIndicator = function () { if (self.starIndicator) { self.starIndicator.destroy(); self.starIndicator = null; } }; self.updateHealthBar = function () { if (self.healthBar) { self.healthBar.updateHealth(self.health, self.maxHealth); } }; self.takeDamage = function (damage) { self.health -= damage; if (self.health < 0) { self.health = 0; } // Ensure health doesn't go below 0 self.updateHealthBar(); // Update health bar when taking damage if (self.health <= 0) { self.hideHealthBar(); // Hide health bar when destroyed return true; } return false; }; self.canShoot = function () { // Use speed to modify fire rate - higher speed means faster attacks var adjustedFireRate = Math.floor(self.fireRate / self.speed); return LK.ticks - self.lastShot >= adjustedFireRate; }; self.shoot = function (target) { if (!self.canShoot()) { return null; } self.lastShot = LK.ticks; // For melee units (range 1), don't create bullets if (self.range <= 1) { // Melee attack: do incline movement using tween, then apply damage var originalX = self.x; var originalY = self.y; // Calculate incline offset (move 30% toward target, max 40px) var dx = target.x - self.x; var dy = target.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); var offset = Math.min(40, dist * 0.3); var inclineX = self.x + (dist > 0 ? dx / dist * offset : 0); var inclineY = self.y + (dist > 0 ? dy / dist * offset : 0); // Prevent multiple tweens at once if (!self._isMeleeTweening) { self._isMeleeTweening = true; self.isAttacking = true; // Set attacking flag tween(self, { x: inclineX, y: inclineY }, { duration: 80, easing: tween.easeOut, onFinish: function onFinish() { // Apply damage after lunge var killed = target.takeDamage(self.damage); if (killed) { self.isAttacking = false; // Clear attacking flag if target dies // Don't give gold for killing enemies LK.getSound('enemyDeath').play(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === target) { enemies[i].destroy(); enemies.splice(i, 1); break; } } } // Tween back to original position tween(self, { x: originalX, y: originalY }, { duration: 100, easing: tween.easeIn, onFinish: function onFinish() { self._isMeleeTweening = false; self.isAttacking = false; // Clear attacking flag after animation } }); } }); } return null; // No bullet for melee } // Ranged attack - create bullet var bullet = new Bullet(self.damage, target); bullet.x = self.x; bullet.y = self.y; // Customize bullet appearance based on unit type if (self.name === "Ranger") { // Archer units shoot arrows bullet.bulletType = 'arrow'; bullet.createGraphics(); // Recreate graphics with correct type } else if (self.name === "Wizard") { // Wizard units shoot magic bullet.bulletType = 'magic'; bullet.createGraphics(); // Recreate graphics with correct type } return bullet; }; self.findTarget = function () { var closestEnemy = null; var closestCellDistance = self.range + 1; // Only enemies within range (cell count) are valid for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calculate cell-based Manhattan distance var cellDist = -1; if (typeof self.gridX === "number" && typeof self.gridY === "number" && typeof enemy.gridX === "number" && typeof enemy.gridY === "number") { cellDist = Math.abs(self.gridX - enemy.gridX) + Math.abs(self.gridY - enemy.gridY); } if (cellDist >= 0 && cellDist <= self.range && cellDist < closestCellDistance) { closestCellDistance = cellDist; closestEnemy = enemy; } } return closestEnemy; }; self.update = function () { // Only initialize and update GridMovement if unit is placed on grid and during battle phase if (self.gridX >= 0 && self.gridY >= 0 && gameState === 'battle') { if (!self.gridMovement) { self.gridMovement = new GridMovement(self, grid); } self.gridMovement.update(); } // Add idle animation when not attacking if (!self.isAttacking && self.gridX >= 0 && self.gridY >= 0) { // Only start idle animation if not already animating if (!self._isIdleAnimating) { self._isIdleAnimating = true; // Random delay before starting idle animation var delay = Math.random() * 2000; LK.setTimeout(function () { if (!self.isAttacking && self._isIdleAnimating) { // Calculate target scale based on tier var targetScale = self.baseScale; if (self.tier === 2) { targetScale = self.baseScale * 1.2; } else if (self.tier === 3) { targetScale = self.baseScale * 1.3; } // Gentle bobbing animation that respects tier scale tween(self, { scaleX: targetScale * 1.05, scaleY: targetScale * 0.95 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(self, { scaleX: targetScale, scaleY: targetScale }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { self._isIdleAnimating = false; } }); } }); } }, delay); } } }; return self; }); var Wizard = Unit.expand(function () { var self = Unit.call(this, 1); // Remove default unit graphics self.removeChildren(); // Create pixelart character container var characterContainer = new Container(); self.addChild(characterContainer); // Body (blue mage robe) var body = characterContainer.attachAsset('wizard', { anchorX: 0.5, anchorY: 0.5 }); body.width = 65; body.height = 85; body.y = 10; body.tint = 0x191970; // Midnight blue to match mage robe theme // Head with wizard hat var head = characterContainer.attachAsset('wizard', { anchorX: 0.5, anchorY: 0.5, width: 30, height: 30 }); head.y = -35; head.tint = 0xFFDBB5; // Skin color // Wizard hat (triangle) var hat = characterContainer.attachAsset('wizard', { anchorX: 0.5, anchorY: 0.5, width: 40, height: 35 }); hat.y = -55; hat.tint = 0x000066; // Staff (right side) var staff = characterContainer.attachAsset('wizard', { anchorX: 0.5, anchorY: 0.5, width: 10, height: 80 }); staff.x = 35; staff.y = -10; staff.tint = 0x8B4513; // Staff crystal var staffCrystal = characterContainer.attachAsset('wizard', { anchorX: 0.5, anchorY: 0.5, width: 20, height: 20 }); staffCrystal.x = 35; staffCrystal.y = -50; staffCrystal.tint = 0x00FFFF; // Cyan crystal // Initial attributes for the unit self.health = 150; self.maxHealth = 150; self.damage = 15; self.armor = 8; self.criticalChance = 0.18; self.magicResist = 8; self.mana = 70; self.speed = 0.7; self.range = 3; // Example: medium range (3 cells) self.name = "Wizard"; return self; }); var Wall = Unit.expand(function () { var self = Unit.call(this, 1); // Remove default unit graphics self.removeChildren(); // Create pixelart wall structure var wallContainer = new Container(); self.addChild(wallContainer); // Wall base (stone blocks) var wallBase = wallContainer.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); wallBase.width = 90; wallBase.height = 80; wallBase.y = 10; wallBase.tint = 0x808080; // Grey stone // Wall top section var wallTop = wallContainer.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5, width: 100, height: 30 }); wallTop.y = -35; wallTop.tint = 0x696969; // Darker grey // Stone texture details (left) var stoneLeft = wallContainer.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5, width: 25, height: 20 }); stoneLeft.x = -25; stoneLeft.y = 0; stoneLeft.tint = 0x606060; // Stone texture details (right) var stoneRight = wallContainer.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5, width: 25, height: 20 }); stoneRight.x = 25; stoneRight.y = 0; stoneRight.tint = 0x606060; // Stone texture details (center) var stoneCenter = wallContainer.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5, width: 30, height: 15 }); stoneCenter.x = 0; stoneCenter.y = 20; stoneCenter.tint = 0x505050; // Initial attributes for the wall self.health = 300; self.maxHealth = 300; self.damage = 0; // Walls don't attack self.armor = 20; // High defense self.criticalChance = 0; // No critical chance self.magicResist = 15; self.mana = 0; // No mana self.speed = 0; // Doesn't attack self.range = 0; // No range self.name = "Wall"; self.isStructure = true; // Mark as structure // Override update to prevent movement self.update = function () { // Walls don't move or attack, just exist as obstacles // Still need to show health bar during battle if (gameState === 'battle' && self.gridX >= 0 && self.gridY >= 0) { self.showHealthBar(); self.updateHealthBar(); self.showStarIndicator(); } }; // Override shoot to prevent attacking self.shoot = function (target) { return null; // Walls can't shoot }; // Override findTarget since walls don't attack self.findTarget = function () { return null; }; return self; }); var Soldier = Unit.expand(function () { var self = Unit.call(this, 1); // Remove default unit graphics self.removeChildren(); // Create pixelart character container var characterContainer = new Container(); self.addChild(characterContainer); // Body (green base) var body = characterContainer.attachAsset('soldier', { anchorX: 0.5, anchorY: 0.5, width: 60, height: 80 }); body.y = 10; body.tint = 0x32CD32; // Lime green to match theme // Head (lighter green circle) var head = characterContainer.attachAsset('soldier', { anchorX: 0.5, anchorY: 0.5, width: 40, height: 40 }); head.y = -30; head.tint = 0x66ff66; // Arms (small rectangles) var leftArm = characterContainer.attachAsset('soldier', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 40 }); leftArm.x = -30; leftArm.y = 0; leftArm.rotation = 0.3; var rightArm = characterContainer.attachAsset('soldier', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 40 }); rightArm.x = 30; rightArm.y = 0; rightArm.rotation = -0.3; // Sword (held in right hand) var sword = characterContainer.attachAsset('soldier', { anchorX: 0.5, anchorY: 0.5, width: 12, height: 50 }); sword.x = 35; sword.y = -10; sword.rotation = -0.2; sword.tint = 0xC0C0C0; // Silver color // Sword hilt var swordHilt = characterContainer.attachAsset('soldier', { anchorX: 0.5, anchorY: 0.5, width: 20, height: 8 }); swordHilt.x = 35; swordHilt.y = 15; swordHilt.rotation = -0.2; swordHilt.tint = 0x8B4513; // Brown hilt // Initial attributes for the unit self.health = 120; self.maxHealth = 120; self.damage = 12; self.armor = 6; self.criticalChance = 0.15; self.magicResist = 6; self.mana = 60; self.speed = 0.7; self.range = 1; // Melee (adjacent cell) self.name = "Soldier"; return self; }); var Ranger = Unit.expand(function () { var self = Unit.call(this, 1); // Remove default unit graphics self.removeChildren(); // Create pixelart character container var characterContainer = new Container(); self.addChild(characterContainer); // Body (light blue ranger outfit) var body = characterContainer.attachAsset('ranger', { anchorX: 0.5, anchorY: 0.5 }); body.width = 60; body.height = 80; body.y = 10; body.tint = 0x87CEEB; // Sky blue to match ranger theme // Head with hood var head = characterContainer.attachAsset('ranger', { anchorX: 0.5, anchorY: 0.5, width: 30, height: 30 }); head.y = -35; head.tint = 0xFFDBB5; // Skin color // Hood var hood = characterContainer.attachAsset('ranger', { anchorX: 0.5, anchorY: 0.5, width: 50, height: 40 }); hood.y = -40; hood.tint = 0x9d9dff; // Bow (left side) var bow = characterContainer.attachAsset('ranger', { anchorX: 0.5, anchorY: 0.5, width: 50, height: 15 }); bow.x = -30; bow.y = 0; bow.rotation = 1.57; // 90 degrees bow.tint = 0x8B4513; // Bowstring var bowstring = characterContainer.attachAsset('ranger', { anchorX: 0.5, anchorY: 0.5, width: 2, height: 45 }); bowstring.x = -30; bowstring.y = 0; bowstring.rotation = 1.57; bowstring.tint = 0xFFFFFF; // White string // Arrow on right hand var arrow = characterContainer.attachAsset('ranger', { anchorX: 0.5, anchorY: 0.5, width: 4, height: 35 }); arrow.x = 25; arrow.y = -5; arrow.rotation = -0.1; arrow.tint = 0x654321; // Dark brown arrow // Arrow tip var arrowTip = characterContainer.attachAsset('ranger', { anchorX: 0.5, anchorY: 0.5, width: 8, height: 8 }); arrowTip.x = 25; arrowTip.y = -22; arrowTip.rotation = 0.785; // 45 degrees arrowTip.tint = 0x808080; // Grey tip // Initial attributes for the unit self.health = 170; self.maxHealth = 170; self.damage = 17; self.armor = 10; self.criticalChance = 0.22; self.magicResist = 10; self.mana = 80; self.speed = 0.7; self.range = 2; // Example: short range (2 cells) self.name = "Ranger"; return self; }); var Paladin = Unit.expand(function () { var self = Unit.call(this, 1); // Remove default unit graphics self.removeChildren(); // Create pixelart character container var characterContainer = new Container(); self.addChild(characterContainer); // Body (blue armor) var body = characterContainer.attachAsset('paladin', { anchorX: 0.5, anchorY: 0.5 }); body.width = 75; body.height = 75; body.y = 10; body.tint = 0x4169E1; // Royal blue to match armor theme // Head var head = characterContainer.attachAsset('paladin', { anchorX: 0.5, anchorY: 0.5, width: 35, height: 35 }); head.y = -35; head.tint = 0xFFDBB5; // Skin color // Helmet var helmet = characterContainer.attachAsset('paladin', { anchorX: 0.5, anchorY: 0.5, width: 45, height: 25 }); helmet.y = -45; helmet.tint = 0x4444FF; // Sword (right side) var sword = characterContainer.attachAsset('paladin', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 60 }); sword.x = 35; sword.y = -5; sword.rotation = -0.2; sword.tint = 0xC0C0C0; // Sword pommel var swordPommel = characterContainer.attachAsset('paladin', { anchorX: 0.5, anchorY: 0.5, width: 12, height: 12 }); swordPommel.x = 35; swordPommel.y = 20; swordPommel.tint = 0xFFD700; // Gold pommel // Initial attributes for the unit self.health = 160; self.maxHealth = 160; self.damage = 16; self.armor = 9; self.criticalChance = 0.2; self.magicResist = 9; self.mana = 75; self.speed = 0.7; self.range = 1; // Example: short range (2 cells) self.name = "Paladin"; return self; }); var Knight = Unit.expand(function () { var self = Unit.call(this, 1); // Remove default unit graphics self.removeChildren(); // Create pixelart character container var characterContainer = new Container(); self.addChild(characterContainer); // Body (darker green base) var body = characterContainer.attachAsset('knight', { anchorX: 0.5, anchorY: 0.5, width: 70, height: 70 }); body.y = 10; body.tint = 0x228B22; // Forest green to match theme // Head (round shape) var head = characterContainer.attachAsset('knight', { anchorX: 0.5, anchorY: 0.5, width: 35, height: 35 }); head.y = -35; head.tint = 0x4da24d; // Shield (left side) var shield = characterContainer.attachAsset('knight', { anchorX: 0.5, anchorY: 0.5, width: 30, height: 45 }); shield.x = -35; shield.y = 0; shield.tint = 0x666666; // Sword (right side) var sword = characterContainer.attachAsset('knight', { anchorX: 0.5, anchorY: 0.5, width: 10, height: 45 }); sword.x = 30; sword.y = -5; sword.rotation = -0.15; sword.tint = 0xC0C0C0; // Silver color // Sword guard var swordGuard = characterContainer.attachAsset('knight', { anchorX: 0.5, anchorY: 0.5, width: 18, height: 6 }); swordGuard.x = 30; swordGuard.y = 13; swordGuard.rotation = -0.15; swordGuard.tint = 0x808080; // Dark grey // Initial attributes for the unit self.health = 110; self.maxHealth = 110; self.damage = 11; self.armor = 5; self.criticalChance = 0.12; self.magicResist = 5; self.mana = 55; self.speed = 0.7; self.range = 1; // Melee (adjacent cell) self.name = "Knight"; return self; }); var King = Unit.expand(function () { var self = Unit.call(this, 1); // Remove default unit graphics self.removeChildren(); // Create pixelart king character var kingContainer = new Container(); self.addChild(kingContainer); // King body (royal robe) var body = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5 }); body.width = 80; body.height = 100; body.y = 15; body.tint = 0x4B0082; // Royal purple // King head (flesh tone) var head = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 50, height: 50 }); head.x = 0; head.y = -40; head.tint = 0xFFDBB5; // Skin color // Crown base var crownBase = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 60, height: 20 }); crownBase.x = 0; crownBase.y = -65; crownBase.tint = 0xFFFF00; // Yellow // Crown points (left) var crownLeft = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 12, height: 25 }); crownLeft.x = -20; crownLeft.y = -75; crownLeft.tint = 0xFFFF00; // Yellow // Crown points (center - tallest) var crownCenter = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 35 }); crownCenter.x = 0; crownCenter.y = -80; crownCenter.tint = 0xFFFF00; // Yellow // Crown points (right) var crownRight = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 12, height: 25 }); crownRight.x = 20; crownRight.y = -75; crownRight.tint = 0xFFFF00; // Yellow // Crown jewel (center) var crownJewel = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 8, height: 8 }); crownJewel.x = 0; crownJewel.y = -80; crownJewel.tint = 0xFF0000; // Red jewel // Beard var beard = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 30, height: 20 }); beard.x = 0; beard.y = -25; beard.tint = 0xC0C0C0; // Grey beard // Sword (right hand) var sword = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 80 }); sword.x = 45; sword.y = 0; sword.rotation = -0.2; sword.tint = 0xC0C0C0; // Silver blade // Sword hilt var swordHilt = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 25, height: 12 }); swordHilt.x = 45; swordHilt.y = 35; swordHilt.rotation = -0.2; swordHilt.tint = 0xC0C0C0; // Silver hilt // Royal cape/cloak var cape = kingContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 70, height: 80 }); cape.x = -5; cape.y = 20; cape.tint = 0x8B0000; // Dark red cape // Initial attributes for the king (like a very strong unit) self.health = 500; self.maxHealth = 500; self.damage = 100; self.armor = 10; self.criticalChance = 0.05; self.magicResist = 8; self.mana = 100; self.speed = 0.5; // Slower attack speed self.range = 1; // Melee range self.name = "King"; self.fireRate = 120; // Slower fire rate // Override takeDamage to handle king-specific game over self.takeDamage = function (damage) { self.health -= damage; if (self.health < 0) { self.health = 0; } self.updateHealthBar(); if (self.health <= 0) { LK.effects.flashScreen(0xFF0000, 500); LK.showGameOver(); return true; } return false; }; // Override update - King can move if it's the only unit left self.update = function () { // Check if king is the only player unit left on the battlefield var playerUnitsOnGrid = 0; for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { if (grid[y] && grid[y][x] && grid[y][x].unit) { playerUnitsOnGrid++; } } } // If king is the only unit left, allow movement if (playerUnitsOnGrid === 1) { // Only initialize and update GridMovement if unit is placed on grid if (self.gridX >= 0 && self.gridY >= 0) { if (!self.gridMovement) { self.gridMovement = new GridMovement(self, grid); } self.gridMovement.update(); } } // King stays in place if other units exist }; // Override showHealthBar for king-specific styling self.showHealthBar = function () { if (!self.healthBar) { self.healthBar = new HealthBar(150, 10); // Larger health bar for king self.healthBar.x = -75; // Position relative to the king's center self.healthBar.y = -110; // Position above the king (higher due to crown) self.addChild(self.healthBar); } self.healthBar.visible = true; self.healthBar.updateHealth(self.health, self.maxHealth); }; return self; }); var CrossbowTower = Unit.expand(function () { var self = Unit.call(this, 1); // Remove default unit graphics self.removeChildren(); // Create pixelart crossbow tower structure var towerContainer = new Container(); self.addChild(towerContainer); // Tower base (stone foundation) var towerBase = towerContainer.attachAsset('crossbow_tower', { anchorX: 0.5, anchorY: 0.5 }); towerBase.width = 80; towerBase.height = 60; towerBase.y = 30; towerBase.tint = 0x696969; // Dark grey stone // Tower middle section var towerMiddle = towerContainer.attachAsset('crossbow_tower', { anchorX: 0.5, anchorY: 0.5, width: 70, height: 50 }); towerMiddle.y = -10; towerMiddle.tint = 0x808080; // Grey stone // Tower top platform var towerTop = towerContainer.attachAsset('crossbow_tower', { anchorX: 0.5, anchorY: 0.5, width: 90, height: 20 }); towerTop.y = -40; towerTop.tint = 0x5C4033; // Brown wood platform // Crossbow mechanism (horizontal) var crossbowBase = towerContainer.attachAsset('crossbow_tower', { anchorX: 0.5, anchorY: 0.5, width: 60, height: 15 }); crossbowBase.y = -55; crossbowBase.rotation = 0; crossbowBase.tint = 0x8B4513; // Saddle brown // Crossbow arms (vertical) var crossbowArms = towerContainer.attachAsset('crossbow_tower', { anchorX: 0.5, anchorY: 0.5, width: 8, height: 50 }); crossbowArms.y = -55; crossbowArms.rotation = 1.57; // 90 degrees crossbowArms.tint = 0x654321; // Dark brown // Crossbow string var crossbowString = towerContainer.attachAsset('crossbow_tower', { anchorX: 0.5, anchorY: 0.5, width: 2, height: 48 }); crossbowString.y = -55; crossbowString.rotation = 1.57; crossbowString.tint = 0xDDDDDD; // Light grey string // Loaded bolt var loadedBolt = towerContainer.attachAsset('crossbow_tower', { anchorX: 0.5, anchorY: 0.5, width: 4, height: 30 }); loadedBolt.x = 0; loadedBolt.y = -55; loadedBolt.rotation = 0; loadedBolt.tint = 0x4B0082; // Dark purple bolt // Initial attributes for the crossbow tower self.health = 200; self.maxHealth = 200; self.damage = 25; // Higher damage than regular ranged units self.armor = 12; self.criticalChance = 0.25; // High critical chance self.magicResist = 10; self.mana = 0; // No mana self.speed = 0.8; // Slower attack speed self.range = 4; // Long range self.name = "Crossbow Tower"; self.isStructure = true; // Mark as structure self.fireRate = 90; // Slower fire rate than mobile units // Override update to prevent movement but allow shooting self.update = function () { // Towers don't move but can attack if (gameState === 'battle' && self.gridX >= 0 && self.gridY >= 0) { self.showHealthBar(); self.updateHealthBar(); self.showStarIndicator(); } }; // Override shoot to create arrow projectiles self.shoot = function (target) { if (!self.canShoot()) { return null; } self.lastShot = LK.ticks; // Create arrow projectile var bullet = new Bullet(self.damage, target); bullet.x = self.x; bullet.y = self.y - 55; // Shoot from crossbow position // Crossbow towers shoot arrows bullet.bulletType = 'arrow'; bullet.createGraphics(); // Add shooting animation - rotate crossbow slightly tween(crossbowBase, { rotation: -0.1 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(crossbowBase, { rotation: 0 }, { duration: 200, easing: tween.easeIn }); } }); return bullet; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x2F4F2F }); /**** * Game Code ****/ var GRID_COLS = 9; var GRID_ROWS = 9; var GRID_CELL_SIZE = 227; // 2048 / 9 ≈ 227 var GRID_START_X = (2048 - GRID_COLS * GRID_CELL_SIZE) / 2; var GRID_START_Y = 100; var PLAYABLE_HEIGHT = 2400; // Define playable game area var enemies = []; var units = []; var bullets = []; var enemyProjectiles = []; var bench = []; var grid = []; var benchSlots = []; var gold = 999; var wave = 1; var enemiesSpawned = 0; var enemiesPerWave = 5; var waveDelay = 0; var draggedUnit = null; var draggedFromGrid = false; var gameState = 'planning'; // 'planning' or 'battle' var goldText; // Declare goldText variable var cellReservations = {}; // Track reserved cells during movement var entityPositions = {}; // Track exact entity positions to prevent overlap var movementQueue = []; // Queue movements to prevent simultaneous conflicts var playerLevel = 1; // Player's current level var maxUnitsOnField = 2; // Starting with 2 units max var maxStructuresOnField = 2; // Starting with 2 structures max var structuresOnField = 0; // Track structures placed var unitsOnField = 0; // Track regular units placed // Helper function to register entity position function registerEntityPosition(entity) { var posKey = entity.gridX + ',' + entity.gridY; entityPositions[posKey] = entity; } // Helper function to unregister entity position function unregisterEntityPosition(entity, oldGridX, oldGridY) { var posKey = oldGridX + ',' + oldGridY; if (entityPositions[posKey] === entity) { delete entityPositions[posKey]; } } // Helper function to check if position is occupied by any entity function isPositionOccupied(gridX, gridY, excludeEntity) { var posKey = gridX + ',' + gridY; var occupant = entityPositions[posKey]; return occupant && occupant !== excludeEntity; } // Function to calculate level up cost function getLevelUpCost(level) { return level * 4; // Cost increases by 4 gold per level } // Function to count units on field function countUnitsOnField() { structuresOnField = 0; unitsOnField = 0; for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { if (grid[y] && grid[y][x] && grid[y][x].unit) { var unit = grid[y][x].unit; if (unit.name === "King") { continue; // Don't count king } if (unit.isStructure) { structuresOnField++; } else { unitsOnField++; } } } } } // Function to check if can place more units function canPlaceMoreUnits(isStructure) { countUnitsOnField(); if (isStructure) { return structuresOnField < maxStructuresOnField; } else { return unitsOnField < maxUnitsOnField; } } // Function to level up player function levelUpPlayer() { var cost = getLevelUpCost(playerLevel); if (gold >= cost) { gold -= cost; playerLevel++; maxUnitsOnField++; // Increase unit limit by 1 maxStructuresOnField++; // Increase structure limit by 1 goldText.setText('Gold: ' + gold); levelText.setText('Level: ' + playerLevel); maxUnitsText.setText('Units: ' + maxUnitsOnField + ' | Structures: ' + maxStructuresOnField); // Level up the king levelUpKing(); // Play purchase sound LK.getSound('purchase').play(); // Visual effect LK.effects.flashScreen(0xFFD700, 500); } } // Function to level up king function levelUpKing() { if (kings.length > 0) { var king = kings[0]; // Increase king stats king.health = Math.floor(king.health * 1.1); king.maxHealth = Math.floor(king.maxHealth * 1.1); king.damage = Math.floor(king.damage * 1.1); king.armor = Math.floor(king.armor * 1.1); king.magicResist = Math.floor(king.magicResist * 1.1); // Increase king scale by 10% var currentScale = king.scaleX; var newScale = currentScale * 1.1; tween(king, { scaleX: newScale, scaleY: newScale }, { duration: 500, easing: tween.easeOut }); // Update health bar king.updateHealthBar(); // Flash effect LK.effects.flashObject(king, 0xFFD700, 800); } } // AttackInfo display removed // Double-tap detection variables var lastTapTime = 0; var lastTapX = 0; var lastTapY = 0; var doubleTapDelay = 300; // 300ms window for double tap var doubleTapDistance = 50; // Max distance between taps // Create grid slots for (var y = 0; y < GRID_ROWS; y++) { grid[y] = []; for (var x = 0; x < GRID_COLS; x++) { var gridSlot = game.addChild(new GridSlot(0, x, y)); // Using lane 0 for all slots gridSlot.x = GRID_START_X + x * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; gridSlot.y = GRID_START_Y + y * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; grid[y][x] = gridSlot; } } // Create king at bottom center of grid var kings = []; var king = game.addChild(new King()); // Place king at center column (column 4 for 9x9 grid) var kingGridX = 4; var kingGridY = GRID_ROWS - 1; king.x = GRID_START_X + kingGridX * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; king.y = GRID_START_Y + kingGridY * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; king.gridX = kingGridX; king.gridY = kingGridY; king.lane = 0; // King already has baseScale from Unit class, ensure it's applied king.updateUnitScale(); // Place king on the grid slot grid[kingGridY][kingGridX].unit = king; king.showHealthBar(); // Show health bar for king from start kings.push(king); // Register king position registerEntityPosition(king); // Create bench area background var benchAreaBg = game.addChild(LK.getAsset('bench_area_bg', { anchorX: 0.5, anchorY: 0.5 })); benchAreaBg.x = 1024; // Center of screen benchAreaBg.y = PLAYABLE_HEIGHT - 120; // Move bench up by 40px benchAreaBg.alpha = 0.7; // Create bench slots var benchTotalWidth = 10 * 120 + 9 * 40; // 10 slots of 120px width with 40px spacing var benchStartX = (2048 - benchTotalWidth) / 2 + 60; // Center horizontally for (var i = 0; i < 10; i++) { var benchSlot = game.addChild(new BenchSlot(i)); benchSlot.x = benchStartX + i * 160; benchSlot.y = PLAYABLE_HEIGHT - 120; // Move bench up by 40px benchSlots[i] = benchSlot; } // Shop configuration var shopSlots = []; var unitPool = [1, 1, 1, 1, 1, 2, 2, 2, 2, 2]; // Pool of available unit tiers var refreshCost = 2; // Calculate shop area dimensions var benchBottomY = PLAYABLE_HEIGHT - 120 + 100; // Bench center + half height var screenBottomY = 2732; // Full screen height var shopAreaHeight = screenBottomY - benchBottomY; var shopCenterY = benchBottomY + shopAreaHeight / 2; // Create shop area background var shopAreaBg = game.addChild(LK.getAsset('shop_area_bg', { anchorX: 0.5, anchorY: 0.5, width: 2048, height: shopAreaHeight })); shopAreaBg.x = 1024; // Center of screen shopAreaBg.y = shopCenterY; // Center vertically in available space shopAreaBg.alpha = 0.6; // Create shop slots var shopTotalWidth = 5 * 200 + 4 * 40; // 5 slots of 200px width with 40px spacing var shopStartX = (2048 - shopTotalWidth) / 2 + 60; // Center horizontally for (var i = 0; i < 5; i++) { var shopSlot = game.addChild(new Container()); shopSlot.x = shopStartX + i * 240; shopSlot.y = PLAYABLE_HEIGHT + 150; // Position shop units further down shopSlot.index = i; shopSlot.unit = null; shopSlot.tierText = null; shopSlot.nameText = null; shopSlots.push(shopSlot); } // Create refresh button var refreshButton = game.addChild(new Container()); var refreshButtonBg = refreshButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 200, height: 80 }); refreshButtonBg.tint = 0xFF8800; var refreshText = new Text2('Refresh', { size: 40, fill: 0xFFFFFF }); refreshText.anchor.set(0.5, 0.5); refreshButton.addChild(refreshButtonBg); refreshButton.addChild(refreshText); refreshButton.x = 2048 - 200; refreshButton.y = PLAYABLE_HEIGHT + 150; // Match shop area position refreshButton.up = function () { if (gold >= refreshCost) { gold -= refreshCost; goldText.setText('Gold: ' + gold); refreshShop(); LK.getSound('purchase').play(); } }; // Create level up button var levelUpButton = game.addChild(new Container()); var levelUpButtonBg = levelUpButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 200, height: 80 }); levelUpButtonBg.tint = 0xFF8800; var levelUpText = new Text2('Level Up', { size: 40, fill: 0xFFFFFF }); levelUpText.anchor.set(0.5, 0.5); levelUpButton.addChild(levelUpText); levelUpButton.x = 200; levelUpButton.y = PLAYABLE_HEIGHT + 150; // Position in shop area levelUpButton.up = function () { levelUpPlayer(); }; // Create level up cost text var levelUpCostText = new Text2('Cost: ' + getLevelUpCost(playerLevel) + 'g', { size: 32, fill: 0xFFD700 }); levelUpCostText.anchor.set(0.5, 0); levelUpCostText.x = 200; levelUpCostText.y = PLAYABLE_HEIGHT + 200; game.addChild(levelUpCostText); // Create refresh cost text var refreshCostText = new Text2('Cost: ' + refreshCost + 'g', { size: 32, fill: 0xFFD700 }); refreshCostText.anchor.set(0.5, 0); refreshCostText.x = 2048 - 200; refreshCostText.y = PLAYABLE_HEIGHT + 200; game.addChild(refreshCostText); // Function to refresh shop with new units function refreshShop() { // Clear existing shop units for (var i = 0; i < shopSlots.length; i++) { if (shopSlots[i].unit) { shopSlots[i].unit.destroy(); shopSlots[i].unit = null; } if (shopSlots[i].tierText) { shopSlots[i].tierText.destroy(); shopSlots[i].tierText = null; } if (shopSlots[i].nameText) { shopSlots[i].nameText.destroy(); shopSlots[i].nameText = null; } if (shopSlots[i].infoButton) { shopSlots[i].infoButton.destroy(); shopSlots[i].infoButton = null; } } // Fill shop with new random units for (var i = 0; i < shopSlots.length; i++) { var randomTier = unitPool[Math.floor(Math.random() * unitPool.length)]; var shopUnit; var unitConstructor; switch (randomTier) { case 1: var unitTypes = [Soldier, Knight, Wall]; unitConstructor = unitTypes[Math.floor(Math.random() * unitTypes.length)]; shopUnit = new unitConstructor(); break; case 2: var unitTypes = [Wizard, Paladin, Ranger, CrossbowTower]; unitConstructor = unitTypes[Math.floor(Math.random() * unitTypes.length)]; shopUnit = new unitConstructor(); break; } shopUnit.x = shopSlots[i].x; shopUnit.y = shopSlots[i].y; // Center the unit in the shop slot game.addChild(shopUnit); shopSlots[i].unit = shopUnit; shopSlots[i].unitConstructor = unitConstructor; // Store the constructor reference // Add gentle floating animation for shop units var baseY = shopUnit.y; tween(shopUnit, { y: baseY - 10 }, { duration: 1000 + Math.random() * 500, easing: tween.easeInOut, onFinish: function onFinish() { tween(shopUnit, { y: baseY }, { duration: 1000 + Math.random() * 500, easing: tween.easeInOut, onFinish: onFinish }); } }); // Add unit name text above the image with more spacing var nameText = new Text2(shopUnit.name, { size: 36, fill: 0xFFFFFF }); nameText.anchor.set(0.5, 1); //{3P} // Anchor to bottom center nameText.x = shopSlots[i].x; nameText.y = shopSlots[i].y - 100; // Position higher above the unit image game.addChild(nameText); shopSlots[i].nameText = nameText; // Add cost text below the image with more spacing var cost = randomTier; // Special cost for Wall (1g) and CrossbowTower (2g) if (shopUnit.name === "Wall") { cost = 1; } else if (shopUnit.name === "CrossbowTower") { cost = 2; } var costText = new Text2(cost + 'g', { size: 36, fill: 0xFFD700 }); costText.anchor.set(0.5, 0); costText.x = shopSlots[i].x; costText.y = shopSlots[i].y + 70; // Position well below the unit image game.addChild(costText); shopSlots[i].tierText = costText; // Add info button below cost var infoButton = new Container(); var infoButtonBg = infoButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 80, height: 40 }); infoButtonBg.tint = 0x4444FF; var infoButtonText = new Text2('Info', { size: 32, fill: 0xFFFFFF }); infoButtonText.anchor.set(0.5, 0.5); infoButton.addChild(infoButtonText); infoButton.x = shopSlots[i].x; infoButton.y = shopSlots[i].y + 130; // Position below cost text with more spacing game.addChild(infoButton); shopSlots[i].infoButton = infoButton; // Store unit reference on info button for click handler infoButton.shopUnit = shopUnit; infoButton.up = function () { showUnitInfoModal(this.shopUnit); }; } } // Function to show unit info modal function showUnitInfoModal(unit) { // Create modal overlay var modalOverlay = game.addChild(new Container()); modalOverlay.x = 1024; // Center of screen modalOverlay.y = 1366; // Center of screen // Create modal background var modalBg = modalOverlay.attachAsset('shop_area_bg', { anchorX: 0.5, anchorY: 0.5, width: 600, height: 800 }); modalBg.alpha = 0.95; // Add title var titleText = new Text2(unit.name, { size: 48, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0.5); titleText.y = -350; modalOverlay.addChild(titleText); // Add attributes var attributes = ['Health: ' + unit.maxHealth, 'Damage: ' + unit.damage, 'Armor: ' + unit.armor, 'Magic Resist: ' + unit.magicResist, 'Critical Chance: ' + unit.criticalChance * 100 + '%', 'Mana: ' + unit.mana, 'Attack Speed: ' + unit.speed, 'Range: ' + unit.range]; for (var i = 0; i < attributes.length; i++) { var attrText = new Text2(attributes[i], { size: 36, fill: 0xFFFFFF }); attrText.anchor.set(0.5, 0.5); attrText.y = -250 + i * 60; modalOverlay.addChild(attrText); } // Add close button var closeButton = new Container(); var closeButtonBg = closeButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 150, height: 60 }); closeButtonBg.tint = 0xFF4444; var closeButtonText = new Text2('Close', { size: 36, fill: 0xFFFFFF }); closeButtonText.anchor.set(0.5, 0.5); closeButton.addChild(closeButtonText); closeButton.y = 330; modalOverlay.addChild(closeButton); closeButton.up = function () { modalOverlay.destroy(); }; } // Initialize shop with units refreshShop(); // Create top UI container background var topUIBg = game.addChild(LK.getAsset('bench_area_bg', { anchorX: 0.5, anchorY: 0, width: 1800, height: 160 })); topUIBg.x = 1024; // Center of screen topUIBg.y = 10; // Top of screen with small margin topUIBg.alpha = 0.7; topUIBg.tint = 0x3a3a3a; // Darker tint for top section // Create top row elements (Gold, Level, Max Units) goldText = new Text2('Gold: ' + gold, { size: 50, fill: 0xFFD700 }); goldText.anchor.set(0.5, 0.5); goldText.x = 1024 - 400; // Left of center goldText.y = 50; game.addChild(goldText); var levelText = new Text2('Level: ' + playerLevel, { size: 50, fill: 0x44FF44 }); levelText.anchor.set(0.5, 0.5); levelText.x = 1024; // Center levelText.y = 50; game.addChild(levelText); var maxUnitsText = new Text2('Units: ' + maxUnitsOnField + ' | Structures: ' + maxStructuresOnField, { size: 50, fill: 0xFFFFFF }); maxUnitsText.anchor.set(0.5, 0.5); maxUnitsText.x = 1024 + 400; // Right of center maxUnitsText.y = 50; game.addChild(maxUnitsText); // Create bottom row elements (Planning Phase, Next Wave) var stateText = new Text2('Planning Phase', { size: 40, fill: 0x44FF44 }); stateText.anchor.set(0.5, 0.5); stateText.x = 1024 - 300; // Left of center in bottom row stateText.y = 120; game.addChild(stateText); var waveText = new Text2('Wave: ' + wave, { size: 40, fill: 0xFFFFFF }); waveText.anchor.set(0.5, 0.5); waveText.x = 1024 + 300; // Right of center in bottom row waveText.y = 120; game.addChild(waveText); // Function to update state indicator function updateStateIndicator() { if (gameState === 'planning') { stateText.setText('Planning Phase'); // Remove old text and create new one with correct color var parent = stateText.parent; var x = stateText.x; var y = stateText.y; stateText.destroy(); stateText = new Text2('Planning Phase', { size: 40, fill: 0x44FF44 // Green for planning }); stateText.anchor.set(0.5, 0.5); stateText.x = x; stateText.y = y; parent.addChild(stateText); // Enable ready button during planning readyButtonBg.tint = 0x44AA44; // Green tint readyButtonText.setText('Ready!'); } else { stateText.setText('Battle Phase'); // Remove old text and create new one with correct color var parent = stateText.parent; var x = stateText.x; var y = stateText.y; stateText.destroy(); stateText = new Text2('Battle Phase', { size: 40, fill: 0xFF4444 // Red for battle }); stateText.anchor.set(0.5, 0.5); stateText.x = x; stateText.y = y; parent.addChild(stateText); // Grey out ready button during battle readyButtonBg.tint = 0x666666; // Grey tint readyButtonText.setText('Battle'); } } var readyButton = new Container(); var readyButtonBg = LK.getAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 200, height: 60 }); readyButtonBg.tint = 0x44AA44; var readyButtonText = new Text2('Ready!', { size: 36, fill: 0xFFFFFF }); readyButtonText.anchor.set(0.5, 0.5); readyButton.addChild(readyButtonBg); readyButton.addChild(readyButtonText); game.addChild(readyButton); readyButton.x = 1024; // Center horizontally readyButton.y = 120; // Same row as state and wave text readyButton.up = function () { if (gameState === 'planning') { // Check for upgrades before starting battle checkAndUpgradeUnits(); gameState = 'battle'; waveDelay = 0; enemiesSpawned = 0; // Reset enemies spawned for new wave updateStateIndicator(); // Update UI to show battle phase } // Do nothing if gameState is 'battle' (button is disabled) }; // Function to check and perform unit upgrades function checkAndUpgradeUnits() { // Group units by name and type var unitGroups = {}; // Count units in bench for (var i = 0; i < bench.length; i++) { var unit = bench[i]; if (unit.gridX === -1 && unit.gridY === -1) { // Only count bench units var key = unit.name + '_' + unit.tier; if (!unitGroups[key]) { unitGroups[key] = []; } unitGroups[key].push({ unit: unit, location: 'bench', index: i }); } } // Count units on grid for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { var gridSlot = grid[y][x]; if (gridSlot.unit) { var unit = gridSlot.unit; var key = unit.name + '_' + unit.tier; if (!unitGroups[key]) { unitGroups[key] = []; } unitGroups[key].push({ unit: unit, location: 'grid', gridX: x, gridY: y, slot: gridSlot }); } } } // Check for upgrades for (var key in unitGroups) { var group = unitGroups[key]; if (group.length >= 3) { // Find upgrade priority: bench first, then grid var benchUnits = group.filter(function (item) { return item.location === 'bench'; }); var gridUnits = group.filter(function (item) { return item.location === 'grid'; }); var unitsToMerge = []; var upgradeTarget = null; var upgradeLocation = null; if (gridUnits.length >= 2 && benchUnits.length >= 1) { // Priority: Upgrade units on grid when 2 are on field and 1 in bench unitsToMerge = gridUnits.slice(0, 2).concat(benchUnits.slice(0, 1)); upgradeTarget = unitsToMerge[0]; upgradeLocation = 'grid'; } else if (gridUnits.length >= 1 && benchUnits.length >= 2) { // Upgrade unit on grid using bench units unitsToMerge = [gridUnits[0]].concat(benchUnits.slice(0, 2)); upgradeTarget = unitsToMerge[0]; upgradeLocation = 'grid'; } else if (benchUnits.length >= 3) { // Upgrade in bench unitsToMerge = benchUnits.slice(0, 3); upgradeTarget = unitsToMerge[0]; upgradeLocation = 'bench'; } if (upgradeTarget && unitsToMerge.length === 3) { // Perform upgrade var baseUnit = upgradeTarget.unit; // Upgrade attributes based on tier if (baseUnit.tier === 2) { // Three star upgrade - multiply stats by 3 baseUnit.health = Math.floor(baseUnit.health * 3); baseUnit.maxHealth = Math.floor(baseUnit.maxHealth * 3); baseUnit.damage = Math.floor(baseUnit.damage * 3); baseUnit.armor = Math.floor(baseUnit.armor * 3); baseUnit.magicResist = Math.floor(baseUnit.magicResist * 3); baseUnit.mana = Math.floor(baseUnit.mana * 3); } else { // Two star upgrade - multiply by 1.8x baseUnit.health = Math.floor(baseUnit.health * 1.8); baseUnit.maxHealth = Math.floor(baseUnit.maxHealth * 1.8); baseUnit.damage = Math.floor(baseUnit.damage * 1.8); baseUnit.armor = Math.floor(baseUnit.armor * 1.8); baseUnit.magicResist = Math.floor(baseUnit.magicResist * 1.8); baseUnit.mana = Math.floor(baseUnit.mana * 1.8); } baseUnit.tier = baseUnit.tier + 1; // Update health bar to reflect new max health if (baseUnit.healthBar) { baseUnit.updateHealthBar(); } // Update star indicator to reflect new tier if (baseUnit.starIndicator) { baseUnit.starIndicator.updateStars(baseUnit.tier); } // Force immediate scale application by stopping any ongoing scale tweens tween.stop(baseUnit, { scaleX: true, scaleY: true }); // Apply the scale immediately based on tier var baseScale = baseUnit.baseScale || 1.3; var finalScale = baseScale; if (baseUnit.tier === 2) { finalScale = baseScale * 1.2; } else if (baseUnit.tier === 3) { finalScale = baseScale * 1.3; } // Set scale immediately baseUnit.scaleX = finalScale; baseUnit.scaleY = finalScale; // Update unit scale method to ensure consistency baseUnit.updateUnitScale(); // Visual upgrade effect with bounce animation that respects final scale LK.effects.flashObject(baseUnit, 0xFFD700, 800); // Gold flash tween(baseUnit, { scaleX: finalScale * 1.2, scaleY: finalScale * 1.2 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(baseUnit, { scaleX: finalScale, scaleY: finalScale }, { duration: 200, easing: tween.easeIn }); } }); // Remove the other two units for (var i = 1; i < unitsToMerge.length; i++) { var unitToRemove = unitsToMerge[i]; if (unitToRemove.location === 'bench') { // Remove from bench for (var j = bench.length - 1; j >= 0; j--) { if (bench[j] === unitToRemove.unit) { bench[j].destroy(); bench.splice(j, 1); break; } } } else if (unitToRemove.location === 'grid') { // Remove from grid unitToRemove.slot.unit = null; unregisterEntityPosition(unitToRemove.unit, unitToRemove.gridX, unitToRemove.gridY); unitToRemove.unit.destroy(); } } // Update displays updateBenchDisplay(); } } } } // Function to show ready button for next wave function showReadyButton() { if (gameState === 'battle') { readyButtonBg.tint = 0x666666; // Grey tint during battle readyButtonText.setText('Battle'); } else { readyButtonBg.tint = 0x44AA44; // Green tint for next wave readyButtonText.setText('Next Wave'); } readyButton.visible = true; } // Hide ready button initially (will show after first wave) function hideReadyButton() { readyButton.visible = false; } function updateBenchDisplay() { // Clear all bench slot references for (var i = 0; i < benchSlots.length; i++) { benchSlots[i].unit = null; } // Only display units in bench that are not on the battlefield for (var i = 0; i < bench.length; i++) { // Skip units that are already placed on the grid if (bench[i].gridX !== -1 && bench[i].gridY !== -1) { continue; } // Find the first empty bench slot for (var j = 0; j < benchSlots.length; j++) { if (!benchSlots[j].unit) { benchSlots[j].unit = bench[i]; // Center the unit in the bench slot bench[i].x = benchSlots[j].x; bench[i].y = benchSlots[j].y; // Show star indicator for units in bench bench[i].showStarIndicator(); break; } } } } function spawnEnemy() { var spawnX = Math.floor(Math.random() * GRID_COLS); // Increase ranged enemy chance as waves progress var rangedChance = Math.min(0.3 + wave * 0.05, 0.7); var enemy; if (Math.random() < rangedChance) { enemy = new RangedEnemy(); } else { enemy = new Enemy(); } // Position enemy at the first row (top) of the grid enemy.gridX = spawnX; enemy.gridY = 0; // Set visual position to match grid position var topCell = grid[0][spawnX]; enemy.x = topCell.x; enemy.y = topCell.y; enemy.lane = 0; // Single grid, so lane is always 0 // Scale up enemy by 30% enemy.scaleX = 1.3; enemy.scaleY = 1.3; // Significantly increase enemy scaling per wave enemy.health = 20 + Math.floor(wave * 10) + Math.floor(Math.pow(wave, 1.5) * 5); enemy.maxHealth = enemy.health; enemy.damage = 15 + Math.floor(wave * 5) + Math.floor(Math.pow(wave, 1.3) * 3); enemy.goldValue = Math.max(2, Math.floor(wave * 1.5)); enemy.showHealthBar(); // Show health bar when enemy is spawned enemy.updateHealthBar(); enemies.push(enemy); game.addChild(enemy); // Add spawn animation enemy.alpha = 0; enemy.scaleX = 0.65; // 0.5 * 1.3 enemy.scaleY = 1.95; // 1.5 * 1.3 tween(enemy, { alpha: 1, scaleX: 1.3, scaleY: 1.3 }, { duration: 300, easing: tween.easeOut }); // Register enemy position registerEntityPosition(enemy); } game.move = function (x, y, obj) { if (draggedUnit) { draggedUnit.x = x; draggedUnit.y = y; } }; game.up = function (x, y, obj) { // Check for double-tap on grid units when not dragging if (!draggedUnit) { var currentTime = Date.now(); var timeDiff = currentTime - lastTapTime; var dx = x - lastTapX; var dy = y - lastTapY; var distance = Math.sqrt(dx * dx + dy * dy); // Check if this is a double-tap (within time window and close to last tap) if (timeDiff < doubleTapDelay && distance < doubleTapDistance) { // Check if double-tap is on a unit in the grid for (var gridY = 0; gridY < GRID_ROWS; gridY++) { for (var gridX = 0; gridX < GRID_COLS; gridX++) { var slot = grid[gridY][gridX]; if (slot.unit) { var unitDx = x - slot.x; var unitDy = y - slot.y; var unitDistance = Math.sqrt(unitDx * unitDx + unitDy * unitDy); if (unitDistance < GRID_CELL_SIZE / 2) { // Double-tapped on this unit - show info modal showUnitInfoModal(slot.unit); lastTapTime = 0; // Reset to prevent triple-tap return; } } } } // Check if double-tap is on a unit in the bench during planning phase if (gameState === 'planning') { for (var i = 0; i < benchSlots.length; i++) { var benchSlot = benchSlots[i]; if (benchSlot.unit) { var unitDx = x - benchSlot.x; var unitDy = y - benchSlot.y; var unitDistance = Math.sqrt(unitDx * unitDx + unitDy * unitDy); if (unitDistance < 60) { // Double-tapped on this bench unit - show info modal showUnitInfoModal(benchSlot.unit); lastTapTime = 0; // Reset to prevent triple-tap return; } } } } // Check if double-tap is on an enemy for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var enemyDx = x - enemy.x; var enemyDy = y - enemy.y; var enemyDistance = Math.sqrt(enemyDx * enemyDx + enemyDy * enemyDy); if (enemyDistance < 60) { // Double-tapped on this enemy - show info modal showUnitInfoModal(enemy); lastTapTime = 0; // Reset to prevent triple-tap return; } } } // Update last tap info for next potential double-tap lastTapTime = currentTime; lastTapX = x; lastTapY = y; } if (draggedUnit) { var dropped = false; // Check if dropped on a valid grid slot for (var gridY = 0; gridY < GRID_ROWS; gridY++) { for (var gridX = 0; gridX < GRID_COLS; gridX++) { var slot = grid[gridY][gridX]; var dx = x - slot.x; var dy = y - slot.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < GRID_CELL_SIZE / 2 && !slot.unit) { // Valid empty grid slot - ensure exact positioning slot.placeUnit(draggedUnit); if (draggedFromGrid && draggedUnit.originalGridSlot) { // Unregister from old position before clearing slot unregisterEntityPosition(draggedUnit, draggedUnit.originalGridSlot.gridX, draggedUnit.originalGridSlot.gridY); draggedUnit.originalGridSlot.unit = null; } dropped = true; break; } } if (dropped) { break; } } // Check if dropped on bench slot if (!dropped) { for (var i = 0; i < benchSlots.length; i++) { var benchSlot = benchSlots[i]; var dx = x - benchSlot.x; var dy = y - benchSlot.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 60 && !benchSlot.unit) { // Valid empty bench slot benchSlot.unit = draggedUnit; draggedUnit.x = benchSlot.x; draggedUnit.y = benchSlot.y; // Remove from grid if it was on grid if (draggedFromGrid && draggedUnit.originalGridSlot) { // Unregister from position tracking unregisterEntityPosition(draggedUnit, draggedUnit.originalGridSlot.gridX, draggedUnit.originalGridSlot.gridY); draggedUnit.originalGridSlot.unit = null; } draggedUnit.gridX = -1; draggedUnit.gridY = -1; draggedUnit.lane = -1; draggedUnit.hideHealthBar(); // Hide health bar when moved to bench draggedUnit.hideStarIndicator(); // Hide star indicator when moved to bench dropped = true; updateBenchDisplay(); break; } } } // If not dropped on valid slot, return to original position if (!dropped) { // Always return to bench if not placed in a valid cell if (draggedUnit.originalSlot) { // Return to original bench slot draggedUnit.x = draggedUnit.originalX; draggedUnit.y = draggedUnit.originalY; draggedUnit.originalSlot.unit = draggedUnit; } else if (draggedFromGrid && draggedUnit.originalGridSlot) { // Unit was dragged from grid but not placed in valid cell - return to bench // Find first available bench slot var returnedToBench = false; for (var i = 0; i < benchSlots.length; i++) { if (!benchSlots[i].unit) { benchSlots[i].unit = draggedUnit; draggedUnit.x = benchSlots[i].x; draggedUnit.y = benchSlots[i].y; // Unregister from grid position unregisterEntityPosition(draggedUnit, draggedUnit.originalGridSlot.gridX, draggedUnit.originalGridSlot.gridY); draggedUnit.originalGridSlot.unit = null; draggedUnit.gridX = -1; draggedUnit.gridY = -1; draggedUnit.lane = -1; draggedUnit.hideHealthBar(); draggedUnit.hideStarIndicator(); // Hide star indicator when moved to bench returnedToBench = true; updateBenchDisplay(); break; } } // If no bench slot available, return to original grid position if (!returnedToBench) { draggedUnit.x = draggedUnit.originalX; draggedUnit.y = draggedUnit.originalY; draggedUnit.originalGridSlot.unit = draggedUnit; // Restore unit to original grid slot } } else { // Unit doesn't have original position - find first available bench slot for (var i = 0; i < benchSlots.length; i++) { if (!benchSlots[i].unit) { benchSlots[i].unit = draggedUnit; draggedUnit.x = benchSlots[i].x; draggedUnit.y = benchSlots[i].y; draggedUnit.gridX = -1; draggedUnit.gridY = -1; draggedUnit.lane = -1; draggedUnit.hideHealthBar(); draggedUnit.hideStarIndicator(); // Hide star indicator when moved to bench updateBenchDisplay(); break; } } } } // Clean up draggedUnit.originalX = undefined; draggedUnit.originalY = undefined; draggedUnit.originalSlot = undefined; draggedUnit.originalGridSlot = undefined; draggedUnit = null; draggedFromGrid = false; } else { // Check if clicking on a shop unit for (var i = 0; i < shopSlots.length; i++) { var slot = shopSlots[i]; if (slot.unit) { var dx = x - slot.x; var dy = y - slot.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 80) { // Increase click area for better usability // Within click range of shop unit var unitCost = slot.unit.tier; // Special cost for Wall (1g) and CrossbowTower (2g) if (slot.unit.name === "Wall") { unitCost = 1; } else if (slot.unit.name === "CrossbowTower") { unitCost = 2; } if (gold >= unitCost) { // Find available bench slot var availableBenchSlot = false; for (var slotIndex = 0; slotIndex < benchSlots.length; slotIndex++) { if (!benchSlots[slotIndex].unit) { availableBenchSlot = true; break; } } if (availableBenchSlot) { gold -= unitCost; goldText.setText('Gold: ' + gold); // Create new unit for bench using the same constructor as the shop unit var newUnit = new slot.unitConstructor(); // All units purchased from shop start at tier 1 (1 star) regardless of unit type newUnit.tier = 1; // Update star indicator to show tier 1 if (newUnit.starIndicator) { newUnit.starIndicator.updateStars(1); } // Initialize grid position properties newUnit.gridX = -1; newUnit.gridY = -1; newUnit.lane = -1; bench.push(newUnit); game.addChild(newUnit); updateBenchDisplay(); // Remove purchased unit from shop slot.unit.destroy(); slot.unit = null; if (slot.tierText) { slot.tierText.destroy(); slot.tierText = null; } if (slot.nameText) { slot.nameText.destroy(); slot.nameText = null; } if (slot.infoButton) { slot.infoButton.destroy(); slot.infoButton = null; } LK.getSound('purchase').play(); // Check for upgrades after purchasing a unit checkAndUpgradeUnits(); } else { // No bench space available - could add visual feedback here } } break; } } } } }; game.update = function () { // Spawn enemies if (gameState === 'battle') { // Ensure health bars are visible for all enemies for (var i = 0; i < enemies.length; i++) { enemies[i].showHealthBar(); } // Ensure health bars are visible for all units on the grid for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { var gridSlot = grid[y][x]; if (gridSlot.unit) { gridSlot.unit.showHealthBar(); } } } if (waveDelay <= 0) { if (enemiesSpawned < enemiesPerWave) { if (LK.ticks % 60 === 0) { spawnEnemy(); enemiesSpawned++; } } else if (enemies.length === 0) { // Wave complete - transition to planning phase gameState = 'planning'; wave++; enemiesPerWave = Math.min(5 + wave * 2, 30); waveText.setText('Wave: ' + wave); // Calculate interest: 1 gold per 10 saved, max 5 var interest = Math.min(Math.floor(gold / 10), 5); var waveReward = 5 + interest; gold += waveReward; goldText.setText('Gold: ' + gold); // Check for unit upgrades when entering planning phase checkAndUpgradeUnits(); // Show reward message var rewardText = new Text2('Wave Complete! +' + waveReward + ' Gold (5 + ' + interest + ' interest)', { size: 48, fill: 0xFFD700 }); rewardText.anchor.set(0.5, 0.5); rewardText.x = 1024; rewardText.y = 800; game.addChild(rewardText); // Fade out reward text tween(rewardText, { alpha: 0, y: 700 }, { duration: 2000, easing: tween.easeOut, onFinish: function onFinish() { rewardText.destroy(); } }); // Refresh shop with new units after wave completion refreshShop(); showReadyButton(); // Show button for next wave updateStateIndicator(); // Update UI to show planning phase } } else { waveDelay--; } // Castle health bar is now handled as a regular unit } // Update enemies for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; enemy.update(); // Castle is now a regular unit on the grid, no special handling needed enemy.updateHealthBar(); enemy.showHealthBar(); // Ensure health bar is shown during battle } // Clean up reservations and position tracking for destroyed entities var keysToRemove = []; for (var key in cellReservations) { var entity = cellReservations[key]; if (!entity.parent) { keysToRemove.push(key); } } for (var i = 0; i < keysToRemove.length; i++) { delete cellReservations[keysToRemove[i]]; } // Clean up position tracking for destroyed entities var posKeysToRemove = []; for (var posKey in entityPositions) { var entity = entityPositions[posKey]; if (!entity.parent) { posKeysToRemove.push(posKey); } } for (var i = 0; i < posKeysToRemove.length; i++) { delete entityPositions[posKeysToRemove[i]]; } // Update units for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { var gridSlot = grid[y][x]; if (gridSlot.unit) { gridSlot.unit.update(); if (gameState === 'battle') { gridSlot.unit.showHealthBar(); // Ensure health bar is shown during battle gridSlot.unit.updateHealthBar(); gridSlot.unit.showStarIndicator(); // Ensure star indicator is shown during battle } else { // Hide health bars during planning phase if (gridSlot.unit.healthBar) { gridSlot.unit.healthBar.visible = false; } // Show star indicators during planning phase for easy tier identification gridSlot.unit.showStarIndicator(); } } } } // Unit shooting (only during battle) if (gameState === 'battle') { for (var y = 0; y < GRID_ROWS; y++) { for (var x = 0; x < GRID_COLS; x++) { var gridSlot = grid[y][x]; if (gridSlot.unit) { var target = gridSlot.unit.findTarget(); if (target) { var bullet = gridSlot.unit.shoot(target); if (bullet) { bullets.push(bullet); game.addChild(bullet); LK.getSound('shoot').play(); } else if (gridSlot.unit.range <= 100) { // Melee attack sound effect (no bullet created) if (gridSlot.unit.canShoot()) { LK.getSound('shoot').play(); } } } } } } } // Update bullets for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; if (bullet && bullet.update) { bullet.update(); } if (!bullet.parent) { bullets.splice(i, 1); } } // Update enemy projectiles for (var i = enemyProjectiles.length - 1; i >= 0; i--) { var projectile = enemyProjectiles[i]; if (projectile && projectile.update) { projectile.update(); } if (!projectile.parent) { enemyProjectiles.splice(i, 1); } } // Update level up cost text levelUpCostText.setText('Cost: ' + getLevelUpCost(playerLevel) + 'g'); // Check win condition if (wave >= 10 && enemies.length === 0 && enemiesSpawned >= enemiesPerWave) { LK.showYouWin(); } };
===================================================================
--- original.js
+++ change.js
@@ -5,2675 +5,3543 @@
/****
* Classes
****/
-var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
+var AttackInfo = Container.expand(function () {
var self = Container.call(this);
- self.targetEnemy = targetEnemy;
- self.damage = damage || 10;
- self.speed = speed || 5;
- self.x = startX;
- self.y = startY;
- var bulletGraphics = self.attachAsset('bullet', {
+ var background = self.attachAsset('bench_area_bg', {
+ anchorX: 0,
+ anchorY: 0,
+ width: 300,
+ height: 150
+ });
+ background.alpha = 0.8;
+ self.attackerText = new Text2('', {
+ size: 36,
+ fill: 0xFFFFFF
+ });
+ self.attackerText.anchor.set(0, 0.5);
+ self.attackerText.x = 10;
+ self.attackerText.y = 25;
+ self.addChild(self.attackerText);
+ self.damageText = new Text2('', {
+ size: 36,
+ fill: 0xFF0000
+ });
+ self.damageText.anchor.set(0, 0.5);
+ self.damageText.x = 10;
+ self.damageText.y = 100;
+ self.addChild(self.damageText);
+ self.updateInfo = function (attacker, damage) {
+ self.attackerText.setText('Attacker: ' + attacker);
+ self.damageText.setText('Damage: ' + damage);
+ };
+ return self;
+});
+var BenchSlot = Container.expand(function (index) {
+ var self = Container.call(this);
+ var slotGraphics = self.attachAsset('bench_slot', {
anchorX: 0.5,
anchorY: 0.5
});
+ self.index = index;
+ self.unit = null;
+ slotGraphics.alpha = 0.5;
+ self.down = function (x, y, obj) {
+ if (self.unit && gameState === 'planning') {
+ draggedUnit = self.unit;
+ // Start dragging from current position, not mouse position
+ draggedUnit.x = self.x;
+ draggedUnit.y = self.y;
+ // Store original position and slot
+ draggedUnit.originalX = self.x;
+ draggedUnit.originalY = self.y;
+ draggedUnit.originalSlot = self;
+ // Free the bench slot immediately when dragging starts
+ self.unit = null;
+ }
+ };
+ return self;
+});
+var Bullet = Container.expand(function (damage, target) {
+ var self = Container.call(this);
+ // Default bullet type
+ self.bulletType = 'default';
+ self.damage = damage;
+ self.target = target;
+ self.speed = 8;
+ // Create bullet graphics based on type
+ self.createGraphics = function () {
+ // Clear any existing graphics
+ self.removeChildren();
+ if (self.bulletType === 'arrow') {
+ // Create arrow projectile
+ var arrowShaft = self.attachAsset('arrow', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ // Arrow head
+ var arrowHead = self.attachAsset('arrow_head', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ arrowHead.x = 15;
+ arrowHead.rotation = 0.785; // 45 degrees
+ // Arrow fletching
+ var fletching1 = self.attachAsset('arrow_feather', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ fletching1.x = -10;
+ fletching1.y = -3;
+ var fletching2 = self.attachAsset('arrow_feather', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ fletching2.x = -10;
+ fletching2.y = 3;
+ self.speed = 10; // Arrows are faster
+ } else if (self.bulletType === 'magic') {
+ // Create magic projectile
+ var magicCore = self.attachAsset('magic_core', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ // Magic aura
+ var magicAura = self.attachAsset('magic_aura', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ magicAura.alpha = 0.5;
+ // Magic sparkles
+ var sparkle1 = self.attachAsset('magic_sparkle', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ sparkle1.x = -10;
+ sparkle1.y = -10;
+ var sparkle2 = self.attachAsset('magic_sparkle', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ sparkle2.x = 10;
+ sparkle2.y = 10;
+ // Add tween for magic glow effect
+ tween(magicAura, {
+ scaleX: 1.2,
+ scaleY: 1.2,
+ alpha: 0.3
+ }, {
+ duration: 300,
+ easing: tween.easeInOut,
+ onFinish: function onFinish() {
+ tween(magicAura, {
+ scaleX: 1.0,
+ scaleY: 1.0,
+ alpha: 0.5
+ }, {
+ duration: 300,
+ easing: tween.easeInOut,
+ onFinish: onFinish
+ });
+ }
+ });
+ self.speed = 6; // Magic is slower but more visible
+ } else {
+ // Default bullet
+ var bulletGraphics = self.attachAsset('bullet', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ }
+ };
+ // Create initial graphics
+ self.createGraphics();
self.update = function () {
- if (!self.targetEnemy || !self.targetEnemy.parent) {
+ if (!self.target || self.target.health <= 0) {
self.destroy();
return;
}
- var dx = self.targetEnemy.x - self.x;
- var dy = self.targetEnemy.y - self.y;
+ 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) {
- // Apply damage to target enemy
- self.targetEnemy.health -= self.damage;
- if (self.targetEnemy.health <= 0) {
- self.targetEnemy.health = 0;
- } else {
- self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
- }
- // Apply special effects based on bullet type
- if (self.type === 'splash') {
- // Create visual splash effect
- var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
- game.addChild(splashEffect);
- // Splash damage to nearby enemies
- var splashRadius = CELL_SIZE * 1.5;
- for (var i = 0; i < enemies.length; i++) {
- var otherEnemy = enemies[i];
- if (otherEnemy !== self.targetEnemy) {
- var splashDx = otherEnemy.x - self.targetEnemy.x;
- var splashDy = otherEnemy.y - self.targetEnemy.y;
- var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
- if (splashDistance <= splashRadius) {
- // Apply splash damage (50% of original damage)
- otherEnemy.health -= self.damage * 0.5;
- if (otherEnemy.health <= 0) {
- otherEnemy.health = 0;
- } else {
- otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
- }
- }
+ if (distance < 10) {
+ var killed = self.target.takeDamage(self.damage);
+ if (killed) {
+ // Don't give gold for killing enemies
+ LK.getSound('enemyDeath').play();
+ for (var i = enemies.length - 1; i >= 0; i--) {
+ if (enemies[i] === self.target) {
+ enemies[i].destroy();
+ enemies.splice(i, 1);
+ break;
}
}
- } else if (self.type === 'slow') {
- // Prevent slow effect on immune enemies
- if (!self.targetEnemy.isImmune) {
- // Create visual slow effect
- var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
- game.addChild(slowEffect);
- // Apply slow effect
- // Make slow percentage scale with tower level (default 50%, up to 80% at max level)
- var slowPct = 0.5;
- if (self.sourceTowerLevel !== undefined) {
- // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
- var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
- var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
- slowPct = slowLevels[idx];
- }
- if (!self.targetEnemy.slowed) {
- self.targetEnemy.originalSpeed = self.targetEnemy.speed;
- self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
- self.targetEnemy.slowed = true;
- self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
- } else {
- self.targetEnemy.slowDuration = 180; // Reset duration
- }
- }
- } else if (self.type === 'poison') {
- // Prevent poison effect on immune enemies
- if (!self.targetEnemy.isImmune) {
- // Create visual poison effect
- var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
- game.addChild(poisonEffect);
- // Apply poison effect
- self.targetEnemy.poisoned = true;
- self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
- self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
- }
- } else if (self.type === 'sniper') {
- // Create visual critical hit effect for sniper
- var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
- game.addChild(sniperEffect);
}
self.destroy();
- } else {
- var angle = Math.atan2(dy, dx);
- self.x += Math.cos(angle) * self.speed;
- self.y += Math.sin(angle) * self.speed;
+ return;
}
+ var moveX = dx / distance * self.speed;
+ var moveY = dy / distance * self.speed;
+ self.x += moveX;
+ self.y += moveY;
+ // Rotate arrow to face target
+ if (self.bulletType === 'arrow') {
+ self.rotation = Math.atan2(dy, dx);
+ }
};
return self;
});
-var DebugCell = Container.expand(function () {
+var Enemy = Container.expand(function () {
var self = Container.call(this);
- var cellGraphics = self.attachAsset('cell', {
+ // Create pixelart enemy character
+ var enemyContainer = new Container();
+ self.addChild(enemyContainer);
+ // Body (red goblin)
+ var body = enemyContainer.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
- cellGraphics.tint = Math.random() * 0xffffff;
- var debugArrows = [];
- var numberLabel = new Text2('0', {
- size: 30,
- fill: 0xFFFFFF,
- weight: 800
+ body.width = 60;
+ body.height = 60;
+ body.y = 10;
+ body.tint = 0xFF4444; // Red tint to match goblin theme
+ // Head (darker red)
+ var head = enemyContainer.attachAsset('enemy', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 40,
+ height: 35
});
- 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();
+ head.y = -30;
+ head.tint = 0xcc0000;
+ // Eyes (white dots)
+ var leftEye = enemyContainer.attachAsset('bullet', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 8,
+ height: 8
+ });
+ leftEye.x = -10;
+ leftEye.y = -30;
+ leftEye.tint = 0xFFFFFF;
+ var rightEye = enemyContainer.attachAsset('bullet', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 8,
+ height: 8
+ });
+ rightEye.x = 10;
+ rightEye.y = -30;
+ rightEye.tint = 0xFFFFFF;
+ // Claws
+ var leftClaw = enemyContainer.attachAsset('enemy', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 20,
+ height: 15
+ });
+ leftClaw.x = -25;
+ leftClaw.y = 5;
+ leftClaw.rotation = 0.5;
+ leftClaw.tint = 0x800000;
+ var rightClaw = enemyContainer.attachAsset('enemy', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 20,
+ height: 15
+ });
+ rightClaw.x = 25;
+ rightClaw.y = 5;
+ rightClaw.rotation = -0.5;
+ rightClaw.tint = 0x800000;
+ // Initial attributes for the enemy
+ self.health = 25;
+ self.maxHealth = 25;
+ self.damage = 15;
+ self.armor = 3;
+ self.criticalChance = 0.15;
+ self.magicResist = 3;
+ self.mana = 40;
+ self.speed = 2;
+ self.range = 1; // Enemy always melee (adjacent cell)
+ self.goldValue = 2;
+ self.name = "Enemy";
+ self.healthBar = null; // Initialize health bar as null
+ self.isAttacking = false; // Flag to track if enemy is currently attacking
+ self.baseScale = 1.3; // Base scale multiplier for all enemies (30% larger)
+ // Apply base scale
+ self.scaleX = self.baseScale;
+ self.scaleY = self.baseScale;
+ self.showHealthBar = function () {
+ if (!self.healthBar) {
+ self.healthBar = new HealthBar(60, 8); // Create a health bar instance (smaller than units)
+ self.healthBar.x = -30; // Position relative to the enemy's center
+ self.healthBar.y = -60; // Position above the enemy
+ self.addChild(self.healthBar);
}
+ self.healthBar.visible = true; // Ensure health bar is visible
+ self.healthBar.updateHealth(self.health, self.maxHealth);
};
- self.removeArrows = function () {
- while (debugArrows.length) {
- self.removeChild(debugArrows.pop());
+ self.updateHealthBar = function () {
+ if (self.healthBar) {
+ self.healthBar.updateHealth(self.health, self.maxHealth);
}
};
- self.render = function (data) {
- switch (data.type) {
- case 0:
- case 2:
- {
- if (data.pathId != pathId) {
- self.removeArrows();
- numberLabel.setText("-");
- cellGraphics.tint = 0x880000;
- return;
+ self.takeDamage = function (damage) {
+ self.health -= damage;
+ self.updateHealthBar(); // Update health bar when taking damage
+ if (self.health <= 0) {
+ self.health = 0;
+ return true;
+ }
+ return false;
+ };
+ self.update = function () {
+ if (!self.gridMovement) {
+ self.gridMovement = new GridMovement(self, grid);
+ }
+ self.gridMovement.update();
+ // Castle is now treated as a unit on the grid, no special collision logic needed
+ // Find and attack units within range (cell-based)
+ var closestUnit = null;
+ var closestCellDistance = self.range + 1; // Only units within range (cell count) are valid
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ if (grid[y] && grid[y][x]) {
+ var gridSlot = grid[y][x];
+ if (gridSlot.unit) {
+ // Calculate cell-based Manhattan distance
+ var cellDist = Math.abs(self.gridX - x) + Math.abs(self.gridY - y);
+ // Check if the enemy is completely inside the adjacent cell before attacking
+ if (cellDist <= self.range && cellDist < closestCellDistance && self.gridMovement.isMoving === false) {
+ closestCellDistance = cellDist;
+ closestUnit = gridSlot.unit;
+ }
}
- numberLabel.visible = true;
- var tint = Math.floor(data.score / maxScore * 0x88);
- var towerInRangeHighlight = false;
- if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
- towerInRangeHighlight = true;
- cellGraphics.tint = 0x0088ff;
- } else {
- 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
+ }
+ }
+ }
+ // Attack the closest unit if found
+ if (closestUnit) {
+ if (!self.lastAttack) {
+ self.lastAttack = 0;
+ }
+ if (LK.ticks - self.lastAttack >= 60) {
+ self.lastAttack = LK.ticks;
+ // Add tween animation for enemy attack
+ var originalX = self.x;
+ var originalY = self.y;
+ // Calculate lunge direction towards target
+ var dx = closestUnit.x - self.x;
+ var dy = closestUnit.y - self.y;
+ var dist = Math.sqrt(dx * dx + dy * dy);
+ var offset = Math.min(30, dist * 0.25);
+ var lungeX = self.x + (dist > 0 ? dx / dist * offset : 0);
+ var lungeY = self.y + (dist > 0 ? dy / dist * offset : 0);
+ // Prevent multiple attack tweens at once
+ if (!self._isAttackTweening) {
+ self._isAttackTweening = true;
+ self.isAttacking = true; // Set attacking flag
+ // Flash enemy red briefly during attack using LK effects
+ LK.effects.flashObject(self, 0xFF4444, 300);
+ // Lunge towards target
+ tween(self, {
+ x: lungeX,
+ y: lungeY
+ }, {
+ duration: 120,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ // Apply damage after lunge
+ var killed = closestUnit.takeDamage(self.damage);
+ if (killed) {
+ self.isAttacking = false; // Clear attacking flag if target dies
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ if (grid[y] && grid[y][x]) {
+ var gridSlot = grid[y][x];
+ if (gridSlot.unit === closestUnit) {
+ gridSlot.unit = null;
+ closestUnit.destroy();
+ for (var i = bench.length - 1; i >= 0; i--) {
+ if (bench[i] === closestUnit) {
+ bench.splice(i, 1);
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ // Return to original position
+ tween(self, {
+ x: originalX,
+ y: originalY
+ }, {
+ duration: 100,
+ easing: tween.easeIn,
+ onFinish: function onFinish() {
+ self._isAttackTweening = false;
+ self.isAttacking = false; // Clear attacking flag after animation
+ }
});
- debugArrows[a].alpha = .5;
- self.addChildAt(debugArrows[a], 1);
}
- debugArrows[a].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);
};
+ return self;
});
-// This update method was incorrectly placed here and should be removed
-var EffectIndicator = Container.expand(function (x, y, type) {
- var self = Container.call(this);
- self.x = x;
- self.y = y;
- var effectGraphics = self.attachAsset('rangeCircle', {
+var RangedEnemy = Enemy.expand(function () {
+ var self = Enemy.call(this);
+ // Remove default enemy graphics
+ self.removeChildren();
+ // Create pixelart ranged enemy character
+ var enemyContainer = new Container();
+ self.addChild(enemyContainer);
+ // Body (purple archer)
+ var body = enemyContainer.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
- effectGraphics.blendMode = 1;
- switch (type) {
- case 'splash':
- effectGraphics.tint = 0x33CC00;
- effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
- break;
- case 'slow':
- effectGraphics.tint = 0x9900FF;
- effectGraphics.width = effectGraphics.height = CELL_SIZE;
- break;
- case 'poison':
- effectGraphics.tint = 0x00FFAA;
- effectGraphics.width = effectGraphics.height = CELL_SIZE;
- break;
- case 'sniper':
- effectGraphics.tint = 0xFF5500;
- effectGraphics.width = effectGraphics.height = CELL_SIZE;
- break;
- }
- effectGraphics.alpha = 0.7;
- self.alpha = 0;
- // Animate the effect
- tween(self, {
- alpha: 0.8,
- scaleX: 1.5,
- scaleY: 1.5
- }, {
- duration: 200,
- easing: tween.easeOut,
- onFinish: function onFinish() {
- tween(self, {
- alpha: 0,
- scaleX: 2,
- scaleY: 2
- }, {
- duration: 300,
- easing: tween.easeIn,
- onFinish: function onFinish() {
- self.destroy();
- }
- });
- }
+ body.width = 55;
+ body.height = 70;
+ body.y = 10;
+ body.tint = 0x8A2BE2; // Purple tint to match archer theme
+ // Head with hood
+ var head = enemyContainer.attachAsset('enemy', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 35,
+ height: 30
});
- return self;
-});
-// Base enemy class for common functionality
-var Enemy = Container.expand(function (type) {
- var self = Container.call(this);
- self.type = type || 'normal';
- self.speed = .01;
- self.cellX = 0;
- self.cellY = 0;
- self.currentCellX = 0;
- self.currentCellY = 0;
- self.currentTarget = undefined;
- self.maxHealth = 100;
- self.health = self.maxHealth;
- self.bulletsTargetingThis = [];
- self.waveNumber = currentWave;
- self.isFlying = false;
- self.isImmune = false;
- self.isBoss = false;
- // Check if this is a boss wave
- // Check if this is a boss wave
- // Apply different stats based on enemy type
- switch (self.type) {
- case 'fast':
- self.speed *= 2; // Twice as fast
- self.maxHealth = 100;
- break;
- case 'immune':
- self.isImmune = true;
- self.maxHealth = 80;
- break;
- case 'flying':
- self.isFlying = true;
- self.maxHealth = 80;
- break;
- case 'swarm':
- self.maxHealth = 50; // Weaker enemies
- break;
- case 'normal':
- default:
- // Normal enemy uses default values
- break;
- }
- if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
- self.isBoss = true;
- // Boss enemies have 20x health and are larger
- self.maxHealth *= 20;
- // Slower speed for bosses
- self.speed = self.speed * 0.7;
- }
- self.health = self.maxHealth;
- // Get appropriate asset for this enemy type
- var assetId = 'enemy';
- if (self.type !== 'normal') {
- assetId = 'enemy_' + self.type;
- }
- var enemyGraphics = self.attachAsset(assetId, {
+ head.y = -35;
+ head.tint = 0x6A1B9A;
+ // Eyes (glowing yellow)
+ var leftEye = enemyContainer.attachAsset('bullet', {
anchorX: 0.5,
- anchorY: 0.5
+ anchorY: 0.5,
+ width: 6,
+ height: 6
});
- // Scale up boss enemies
- if (self.isBoss) {
- enemyGraphics.scaleX = 1.8;
- enemyGraphics.scaleY = 1.8;
- }
- // Fall back to regular enemy asset if specific type asset not found
- // Apply tint to differentiate enemy types
- /*switch (self.type) {
- case 'fast':
- enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
- break;
- case 'immune':
- enemyGraphics.tint = 0xAA0000; // Red for immune enemies
- break;
- case 'flying':
- enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
- break;
- case 'swarm':
- enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
- break;
- }*/
- // Create shadow for flying enemies
- if (self.isFlying) {
- // Create a shadow container that will be added to the shadow layer
- self.shadow = new Container();
- // Clone the enemy graphics for the shadow
- var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- // Apply shadow effect
- shadowGraphics.tint = 0x000000; // Black shadow
- shadowGraphics.alpha = 0.4; // Semi-transparent
- // If this is a boss, scale up the shadow to match
- if (self.isBoss) {
- shadowGraphics.scaleX = 1.8;
- shadowGraphics.scaleY = 1.8;
- }
- // Position shadow slightly offset
- self.shadow.x = 20; // Offset right
- self.shadow.y = 20; // Offset down
- // Ensure shadow has the same rotation as the enemy
- shadowGraphics.rotation = enemyGraphics.rotation;
- }
- var healthBarOutline = self.attachAsset('healthBarOutline', {
- anchorX: 0,
- anchorY: 0.5
+ leftEye.x = -8;
+ leftEye.y = -35;
+ leftEye.tint = 0xFFFF00;
+ var rightEye = enemyContainer.attachAsset('bullet', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 6,
+ height: 6
});
- var healthBarBG = self.attachAsset('healthBar', {
- anchorX: 0,
- anchorY: 0.5
+ rightEye.x = 8;
+ rightEye.y = -35;
+ rightEye.tint = 0xFFFF00;
+ // Bow
+ var bow = enemyContainer.attachAsset('enemy', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 45,
+ height: 12
});
- var healthBar = self.attachAsset('healthBar', {
- anchorX: 0,
- anchorY: 0.5
+ bow.x = -25;
+ bow.y = 0;
+ bow.rotation = 1.57; // 90 degrees
+ bow.tint = 0x4B0082;
+ // Bowstring
+ var bowstring = enemyContainer.attachAsset('enemy', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 2,
+ height: 40
});
- healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
- healthBarOutline.x = -healthBarOutline.width / 2;
- healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
- healthBar.tint = 0x00ff00;
- healthBarBG.tint = 0xff0000;
- self.healthBar = healthBar;
+ bowstring.x = -25;
+ bowstring.y = 0;
+ bowstring.rotation = 1.57;
+ bowstring.tint = 0xDDDDDD; // Light grey string
+ // Quiver on back
+ var quiver = enemyContainer.attachAsset('enemy', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 15,
+ height: 30
+ });
+ quiver.x = 20;
+ quiver.y = -10;
+ quiver.tint = 0x2B0040; // Dark purple
+ // Override attributes for ranged enemy
+ self.health = 20;
+ self.maxHealth = 20;
+ self.damage = 12;
+ self.armor = 2;
+ self.criticalChance = 0.1;
+ self.magicResist = 2;
+ self.mana = 30;
+ self.speed = 1.5; // Slightly slower movement
+ self.range = 4; // Long range for shooting
+ self.goldValue = 3; // More valuable than melee enemies
+ self.name = "Ranged Enemy";
+ self.fireRate = 75; // Shoots faster - every 1.25 seconds
+ self.lastShot = 0;
+ self.baseScale = 1.3; // Base scale multiplier for all enemies (30% larger)
+ // Apply base scale
+ self.scaleX = self.baseScale;
+ self.scaleY = self.baseScale;
+ // Override update to include ranged attack behavior
self.update = function () {
- if (self.health <= 0) {
- self.health = 0;
- self.healthBar.width = 0;
+ if (!self.gridMovement) {
+ self.gridMovement = new GridMovement(self, grid);
}
- // Handle slow effect
- if (self.isImmune) {
- // Immune enemies cannot be slowed or poisoned, clear any such effects
- self.slowed = false;
- self.slowEffect = false;
- self.poisoned = false;
- self.poisonEffect = false;
- // Reset speed to original if needed
- if (self.originalSpeed !== undefined) {
- self.speed = self.originalSpeed;
- }
- } else {
- // Handle slow effect
- if (self.slowed) {
- // Visual indication of slowed status
- if (!self.slowEffect) {
- self.slowEffect = true;
- }
- self.slowDuration--;
- if (self.slowDuration <= 0) {
- self.speed = self.originalSpeed;
- self.slowed = false;
- self.slowEffect = false;
- // Only reset tint if not poisoned
- if (!self.poisoned) {
- enemyGraphics.tint = 0xFFFFFF; // Reset tint
+ self.gridMovement.update();
+ // Find and attack units within range (cell-based)
+ var closestUnit = null;
+ var closestCellDistance = self.range + 1; // Only units within range (cell count) are valid
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ if (grid[y] && grid[y][x]) {
+ var gridSlot = grid[y][x];
+ if (gridSlot.unit) {
+ // Calculate cell-based Manhattan distance
+ var cellDist = Math.abs(self.gridX - x) + Math.abs(self.gridY - y);
+ // Check if the enemy can shoot (doesn't need to be stationary like melee)
+ if (cellDist <= self.range && cellDist < closestCellDistance) {
+ closestCellDistance = cellDist;
+ closestUnit = gridSlot.unit;
+ }
}
}
}
- // Handle poison effect
- if (self.poisoned) {
- // Visual indication of poisoned status
- if (!self.poisonEffect) {
- self.poisonEffect = true;
- }
- // Apply poison damage every 30 frames (twice per second)
- if (LK.ticks % 30 === 0) {
- self.health -= self.poisonDamage;
- if (self.health <= 0) {
- self.health = 0;
- }
- self.healthBar.width = self.health / self.maxHealth * 70;
- }
- self.poisonDuration--;
- if (self.poisonDuration <= 0) {
- self.poisoned = false;
- self.poisonEffect = false;
- // Only reset tint if not slowed
- if (!self.slowed) {
- enemyGraphics.tint = 0xFFFFFF; // Reset tint
- }
- }
- }
}
- // Set tint based on effect status
- if (self.isImmune) {
- enemyGraphics.tint = 0xFFFFFF;
- } else if (self.poisoned && self.slowed) {
- // Combine poison (0x00FFAA) and slow (0x9900FF) colors
- // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
- enemyGraphics.tint = 0x4C7FD4;
- } else if (self.poisoned) {
- enemyGraphics.tint = 0x00FFAA;
- } else if (self.slowed) {
- enemyGraphics.tint = 0x9900FF;
- } else {
- enemyGraphics.tint = 0xFFFFFF;
- }
- if (self.currentTarget) {
- var ox = self.currentTarget.x - self.currentCellX;
- var oy = self.currentTarget.y - self.currentCellY;
- if (ox !== 0 || oy !== 0) {
- var angle = Math.atan2(oy, ox);
- if (enemyGraphics.targetRotation === undefined) {
- enemyGraphics.targetRotation = angle;
- enemyGraphics.rotation = angle;
- } else {
- if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
- tween.stop(enemyGraphics, {
- rotation: true
- });
- // Calculate the shortest angle to rotate
- var currentRotation = enemyGraphics.rotation;
- var angleDiff = angle - currentRotation;
- // Normalize angle difference to -PI to PI range for shortest path
- while (angleDiff > Math.PI) {
- angleDiff -= Math.PI * 2;
- }
- while (angleDiff < -Math.PI) {
- angleDiff += Math.PI * 2;
- }
- enemyGraphics.targetRotation = angle;
- tween(enemyGraphics, {
- rotation: currentRotation + angleDiff
- }, {
- duration: 250,
- easing: tween.easeOut
- });
- }
+ // Attack the closest unit if found
+ if (closestUnit) {
+ if (!self.lastShot) {
+ self.lastShot = 0;
+ }
+ if (LK.ticks - self.lastShot >= self.fireRate) {
+ self.lastShot = LK.ticks;
+ // Create projectile
+ var projectile = new EnemyProjectile(self.damage, closestUnit);
+ projectile.x = self.x;
+ projectile.y = self.y;
+ enemyProjectiles.push(projectile);
+ game.addChild(projectile);
+ // Add shooting animation
+ if (!self._isShootingTweening) {
+ self._isShootingTweening = true;
+ // Flash enemy blue briefly during shooting using LK effects
+ LK.effects.flashObject(self, 0x4444FF, 200);
+ self._isShootingTweening = false;
}
}
}
- healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
-var GoldIndicator = Container.expand(function (value, x, y) {
+var EnemyProjectile = Container.expand(function (damage, target) {
var self = Container.call(this);
- var shadowText = new Text2("+" + value, {
- size: 45,
- fill: 0x000000,
- weight: 800
+ // Create arrow projectile for enemies
+ var arrowContainer = new Container();
+ self.addChild(arrowContainer);
+ // Arrow shaft
+ var arrowShaft = arrowContainer.attachAsset('arrow', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 25,
+ height: 6
});
- shadowText.anchor.set(0.5, 0.5);
- shadowText.x = 2;
- shadowText.y = 2;
- self.addChild(shadowText);
- var goldText = new Text2("+" + value, {
- size: 45,
- fill: 0xFFD700,
- weight: 800
+ arrowShaft.tint = 0x4B0082; // Dark purple for enemy arrows
+ // Arrow head
+ var arrowHead = arrowContainer.attachAsset('arrow_head', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 10,
+ height: 10
});
- goldText.anchor.set(0.5, 0.5);
- self.addChild(goldText);
- self.x = x;
- self.y = y;
- self.alpha = 0;
- self.scaleX = 0.5;
- self.scaleY = 0.5;
- tween(self, {
- alpha: 1,
- scaleX: 1.2,
- scaleY: 1.2,
- y: y - 40
- }, {
- duration: 50,
- easing: tween.easeOut,
- onFinish: function onFinish() {
- tween(self, {
- alpha: 0,
- scaleX: 1.5,
- scaleY: 1.5,
- y: y - 80
- }, {
- duration: 600,
- easing: tween.easeIn,
- delay: 800,
- onFinish: function onFinish() {
- self.destroy();
+ arrowHead.x = 12;
+ arrowHead.rotation = 0.785; // 45 degrees
+ arrowHead.tint = 0xFF0000; // Red tip for enemy
+ // Arrow fletching
+ var fletching = arrowContainer.attachAsset('arrow_feather', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 8,
+ height: 5
+ });
+ fletching.x = -8;
+ fletching.tint = 0x8B008B; // Dark magenta feathers
+ self.damage = damage;
+ self.target = target;
+ self.speed = 6;
+ self.update = function () {
+ if (!self.target || self.target.health <= 0) {
+ 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 < 15) {
+ // Hit target
+ var killed = self.target.takeDamage(self.damage);
+ if (killed) {
+ // Handle target death - remove from grid if it's a unit
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ if (grid[y] && grid[y][x]) {
+ var gridSlot = grid[y][x];
+ if (gridSlot.unit === self.target) {
+ gridSlot.unit = null;
+ self.target.destroy();
+ for (var i = bench.length - 1; i >= 0; i--) {
+ if (bench[i] === self.target) {
+ bench.splice(i, 1);
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
}
- });
+ }
+ self.destroy();
+ return;
}
- });
+ var moveX = dx / distance * self.speed;
+ var moveY = dy / distance * self.speed;
+ self.x += moveX;
+ self.y += moveY;
+ // Rotate arrow to face target
+ self.rotation = Math.atan2(dy, dx);
+ };
return self;
});
-var Grid = Container.expand(function (gridWidth, gridHeight) {
+var GridMovement = Container.expand(function (entity, gridRef) {
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: []
- };
+ self.entity = entity;
+ self.gridRef = gridRef;
+ self.currentCell = {
+ x: entity.gridX,
+ y: entity.gridY
+ };
+ self.targetCell = {
+ x: entity.gridX,
+ y: entity.gridY
+ };
+ self.moveSpeed = 2; // Pixels per frame for continuous movement
+ self.isMoving = false; // Track if entity is currently moving
+ self.pathToTarget = []; // Store calculated path
+ self.pathIndex = 0; // Current position in path
+ // Helper method to check if a cell is occupied
+ self.isCellOccupied = function (x, y) {
+ // Check if out of bounds
+ if (x < 0 || x >= GRID_COLS || y < 0 || y >= GRID_ROWS) {
+ return true;
}
- }
- /*
- Cell Types
- 0: Transparent floor
- 1: Wall
- 2: Spawn
- 3: Goal
- */
- for (var i = 0; i < gridWidth; i++) {
- for (var j = 0; j < gridHeight; j++) {
- var cell = self.cells[i][j];
- var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0;
- if (i > 11 - 3 && i <= 11 + 3) {
- if (j === 0) {
- cellType = 2;
- self.spawns.push(cell);
- } else if (j <= 4) {
- cellType = 0;
- } else if (j === gridHeight - 1) {
- cellType = 3;
- self.goals.push(cell);
- } else if (j >= gridHeight - 4) {
- cellType = 0;
- }
- }
- cell.type = cellType;
- cell.x = i;
- cell.y = j;
- cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
- cell.up = self.cells[i - 1] && self.cells[i - 1][j];
- cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
- cell.left = self.cells[i][j - 1];
- cell.right = self.cells[i][j + 1];
- cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
- cell.down = self.cells[i + 1] && self.cells[i + 1][j];
- cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
- cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
- cell.targets = [];
- if (j > 3 && j <= gridHeight - 4) {
- var debugCell = new DebugCell();
- self.addChild(debugCell);
- debugCell.cell = cell;
- debugCell.x = i * CELL_SIZE;
- debugCell.y = j * CELL_SIZE;
- cell.debugCell = debugCell;
- }
+ // Check if position is occupied by any entity using position tracking
+ if (isPositionOccupied(x, y, self.entity)) {
+ return true;
}
- }
- self.getCell = function (x, y) {
- return self.cells[x] && self.cells[x][y];
+ // Check if cell is reserved by another entity
+ var cellKey = x + ',' + y;
+ if (cellReservations[cellKey] && cellReservations[cellKey] !== self.entity) {
+ return true;
+ }
+ // Check if occupied by a player unit on grid
+ if (self.gridRef[y] && self.gridRef[y][x] && self.gridRef[y][x].unit && self.gridRef[y][x].unit !== self.entity) {
+ return true;
+ }
+ return false;
};
- 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;
+ // A* pathfinding implementation
+ self.findPath = function (startX, startY, goalX, goalY) {
+ var openSet = [];
+ var closedSet = [];
+ var cameFrom = {};
+ // Helper to create node
+ function createNode(x, y, g, h) {
+ return {
+ x: x,
+ y: y,
+ g: g,
+ // Cost from start
+ h: h,
+ // Heuristic cost to goal
+ f: g + h // Total cost
+ };
}
- 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);
+ // Manhattan distance heuristic
+ function heuristic(x1, y1, x2, y2) {
+ return Math.abs(x2 - x1) + Math.abs(y2 - y1);
+ }
+ // Get neighbors of a cell
+ function getNeighbors(x, y) {
+ var neighbors = [];
+ var directions = [{
+ x: 0,
+ y: -1
+ },
+ // Up
+ {
+ x: 1,
+ y: 0
+ },
+ // Right
+ {
+ x: 0,
+ y: 1
+ },
+ // Down
+ {
+ x: -1,
+ y: 0
+ } // Left
+ ];
+ for (var i = 0; i < directions.length; i++) {
+ var newX = x + directions[i].x;
+ var newY = y + directions[i].y;
+ // Skip if occupied (but allow goal position even if occupied)
+ if (newX === goalX && newY === goalY || !self.isCellOccupied(newX, newY)) {
+ neighbors.push({
+ x: newX,
+ y: newY
+ });
}
- 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;
- }
- }
}
+ return neighbors;
}
- while (toProcess.length) {
- var nodes = toProcess;
- toProcess = [];
- for (var a = 0; a < nodes.length; a++) {
- var node = nodes[a];
- var targetScore = node.score + 14142;
- if (node.up && node.left && node.up.type != 1 && node.left.type != 1) {
- processNode(node.upLeft, targetScore, node);
+ // Initialize start node
+ var startNode = createNode(startX, startY, 0, heuristic(startX, startY, goalX, goalY));
+ openSet.push(startNode);
+ // Track best g scores
+ var gScore = {};
+ gScore[startX + ',' + startY] = 0;
+ while (openSet.length > 0) {
+ // Find node with lowest f score
+ var current = openSet[0];
+ var currentIndex = 0;
+ for (var i = 1; i < openSet.length; i++) {
+ if (openSet[i].f < current.f) {
+ current = openSet[i];
+ currentIndex = i;
}
- if (node.up && node.right && node.up.type != 1 && node.right.type != 1) {
- processNode(node.upRight, targetScore, node);
+ }
+ // Check if we reached goal
+ if (current.x === goalX && current.y === goalY) {
+ // Reconstruct path
+ var path = [];
+ var key = current.x + ',' + current.y;
+ while (key && key !== startX + ',' + startY) {
+ var coords = key.split(',');
+ path.unshift({
+ x: parseInt(coords[0]),
+ y: parseInt(coords[1])
+ });
+ key = cameFrom[key];
}
- if (node.down && node.right && node.down.type != 1 && node.right.type != 1) {
- processNode(node.downRight, targetScore, node);
+ return path;
+ }
+ // Move current from open to closed
+ openSet.splice(currentIndex, 1);
+ closedSet.push(current);
+ // Check neighbors
+ var neighbors = getNeighbors(current.x, current.y);
+ for (var i = 0; i < neighbors.length; i++) {
+ var neighbor = neighbors[i];
+ var neighborKey = neighbor.x + ',' + neighbor.y;
+ // Skip if in closed set
+ var inClosed = false;
+ for (var j = 0; j < closedSet.length; j++) {
+ if (closedSet[j].x === neighbor.x && closedSet[j].y === neighbor.y) {
+ inClosed = true;
+ break;
+ }
}
- if (node.down && node.left && node.down.type != 1 && node.left.type != 1) {
- processNode(node.downLeft, targetScore, node);
+ if (inClosed) {
+ continue;
}
- targetScore = node.score + 10000;
- processNode(node.up, targetScore, node);
- processNode(node.right, targetScore, node);
- processNode(node.down, targetScore, node);
- processNode(node.left, targetScore, node);
+ // Calculate tentative g score
+ var tentativeG = current.g + 1;
+ // Check if this path is better
+ if (!gScore[neighborKey] || tentativeG < gScore[neighborKey]) {
+ // Record best path
+ cameFrom[neighborKey] = current.x + ',' + current.y;
+ gScore[neighborKey] = tentativeG;
+ // Add to open set if not already there
+ var inOpen = false;
+ for (var j = 0; j < openSet.length; j++) {
+ if (openSet[j].x === neighbor.x && openSet[j].y === neighbor.y) {
+ inOpen = true;
+ openSet[j].g = tentativeG;
+ openSet[j].f = tentativeG + openSet[j].h;
+ break;
+ }
+ }
+ if (!inOpen) {
+ var h = heuristic(neighbor.x, neighbor.y, goalX, goalY);
+ openSet.push(createNode(neighbor.x, neighbor.y, tentativeG, h));
+ }
+ }
}
}
- for (var a = 0; a < self.spawns.length; a++) {
- if (self.spawns[a].pathId != pathId) {
- console.warn("Spawn blocked");
- return true;
+ // No path found - try simple alternative
+ return self.findAlternativePath(goalX, goalY);
+ };
+ // Helper method to find alternative paths (fallback for when A* fails)
+ self.findAlternativePath = function (targetX, targetY) {
+ var currentX = self.currentCell.x;
+ var currentY = self.currentCell.y;
+ // Calculate primary direction
+ var dx = targetX - currentX;
+ var dy = targetY - currentY;
+ // Try moving in the primary direction first
+ var moves = [];
+ if (Math.abs(dx) > Math.abs(dy)) {
+ // Prioritize horizontal movement
+ if (dx > 0 && !self.isCellOccupied(currentX + 1, currentY)) {
+ return [{
+ x: currentX + 1,
+ y: currentY
+ }];
}
- }
- for (var a = 0; a < enemies.length; a++) {
- var enemy = enemies[a];
- // Skip enemies that haven't entered the viewable area yet
- if (enemy.currentCellY < 4) {
- continue;
+ if (dx < 0 && !self.isCellOccupied(currentX - 1, currentY)) {
+ return [{
+ x: currentX - 1,
+ y: currentY
+ }];
}
- // Skip flying enemies from path check as they can fly over obstacles
- if (enemy.isFlying) {
- continue;
+ // Try vertical as alternative
+ if (dy > 0 && !self.isCellOccupied(currentX, currentY + 1)) {
+ return [{
+ x: currentX,
+ y: currentY + 1
+ }];
}
- var target = self.getCell(enemy.cellX, enemy.cellY);
- if (enemy.currentTarget) {
- if (enemy.currentTarget.pathId != pathId) {
- if (!target || target.pathId != pathId) {
- console.warn("Enemy blocked 1 ");
- return true;
- }
- }
- } else if (!target || target.pathId != pathId) {
- console.warn("Enemy blocked 2");
- return true;
+ if (dy < 0 && !self.isCellOccupied(currentX, currentY - 1)) {
+ return [{
+ x: currentX,
+ y: currentY - 1
+ }];
}
- }
- 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]);
- }
+ } else {
+ // Prioritize vertical movement
+ if (dy > 0 && !self.isCellOccupied(currentX, currentY + 1)) {
+ return [{
+ x: currentX,
+ y: currentY + 1
+ }];
}
+ if (dy < 0 && !self.isCellOccupied(currentX, currentY - 1)) {
+ return [{
+ x: currentX,
+ y: currentY - 1
+ }];
+ }
+ // Try horizontal as alternative
+ if (dx > 0 && !self.isCellOccupied(currentX + 1, currentY)) {
+ return [{
+ x: currentX + 1,
+ y: currentY
+ }];
+ }
+ if (dx < 0 && !self.isCellOccupied(currentX - 1, currentY)) {
+ return [{
+ x: currentX - 1,
+ y: currentY
+ }];
+ }
}
+ return null;
};
- self.updateEnemy = function (enemy) {
- var cell = grid.getCell(enemy.cellX, enemy.cellY);
- if (cell.type == 3) {
- return true;
+ self.moveToNextCell = function () {
+ // Check if this is a structure unit that shouldn't move
+ if (self.entity.isStructure) {
+ return false; // Structures don't move
}
- if (enemy.isFlying && enemy.shadow) {
- enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
- enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
- // Match shadow rotation with enemy rotation
- if (enemy.children[0] && enemy.shadow.children[0]) {
- enemy.shadow.children[0].rotation = enemy.children[0].rotation;
- }
- }
- // Check if the enemy has reached the entry area (y position is at least 5)
- var hasReachedEntryArea = enemy.currentCellY >= 4;
- // If enemy hasn't reached the entry area yet, just move down vertically
- if (!hasReachedEntryArea) {
- // Move directly downward
- enemy.currentCellY += enemy.speed;
- // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
- var angle = Math.PI / 2;
- if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
- enemy.children[0].targetRotation = angle;
- enemy.children[0].rotation = angle;
- } else if (enemy.children[0]) {
- if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
- tween.stop(enemy.children[0], {
- rotation: true
- });
- // Calculate the shortest angle to rotate
- var currentRotation = enemy.children[0].rotation;
- var angleDiff = angle - currentRotation;
- // Normalize angle difference to -PI to PI range for shortest path
- while (angleDiff > Math.PI) {
- angleDiff -= Math.PI * 2;
+ // Check if this is a player unit or enemy
+ var isPlayerUnit = self.entity.name && (self.entity.name === "Soldier" || self.entity.name === "Knight" || self.entity.name === "Wizard" || self.entity.name === "Paladin" || self.entity.name === "Ranger" || self.entity.name === "King");
+ var targetGridX = -1;
+ var targetGridY = -1;
+ var shouldMove = false;
+ if (isPlayerUnit) {
+ // Player units move towards enemies
+ var closestEnemy = null;
+ var closestDist = 99999;
+ for (var i = 0; i < enemies.length; i++) {
+ var enemy = enemies[i];
+ if (typeof enemy.gridX === "number" && typeof enemy.gridY === "number") {
+ var dx = enemy.gridX - self.currentCell.x;
+ var dy = enemy.gridY - self.currentCell.y;
+ var dist = Math.abs(dx) + Math.abs(dy);
+ if (dist < closestDist) {
+ closestDist = dist;
+ closestEnemy = enemy;
+ targetGridX = enemy.gridX;
+ targetGridY = enemy.gridY;
}
- while (angleDiff < -Math.PI) {
- angleDiff += Math.PI * 2;
- }
- // Set target rotation and animate to it
- enemy.children[0].targetRotation = angle;
- tween(enemy.children[0], {
- rotation: currentRotation + angleDiff
- }, {
- duration: 250,
- easing: tween.easeOut
- });
}
}
- // Update enemy's position
- enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
- enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
- // If enemy has now reached the entry area, update cell coordinates
- if (enemy.currentCellY >= 4) {
- enemy.cellX = Math.round(enemy.currentCellX);
- enemy.cellY = Math.round(enemy.currentCellY);
+ // Only move if enemy found and not in range
+ if (closestEnemy && closestDist > self.entity.range) {
+ // Check if unit is currently attacking
+ if (self.entity.isAttacking) {
+ return false; // Don't move while attacking
+ }
+ shouldMove = true;
+ } else {
+ // Already in range or no enemy, don't move
+ return false;
}
- return false;
- }
- // After reaching entry area, handle flying enemies differently
- if (enemy.isFlying) {
- // Flying enemies head straight to the closest goal
- if (!enemy.flyingTarget) {
- // Set flying target to the closest goal
- enemy.flyingTarget = self.goals[0];
- // Find closest goal if there are multiple
- if (self.goals.length > 1) {
- var closestDist = Infinity;
- for (var i = 0; i < self.goals.length; i++) {
- var goal = self.goals[i];
- var dx = goal.x - enemy.cellX;
- var dy = goal.y - enemy.cellY;
- var dist = dx * dx + dy * dy;
+ } else {
+ // Enemy movement logic
+ // Find closest unit on the grid
+ var closestUnit = null;
+ var closestDist = 99999;
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ if (self.gridRef[y] && self.gridRef[y][x] && self.gridRef[y][x].unit) {
+ var dx = x - self.currentCell.x;
+ var dy = y - self.currentCell.y;
+ var dist = Math.abs(dx) + Math.abs(dy);
if (dist < closestDist) {
closestDist = dist;
- enemy.flyingTarget = goal;
+ closestUnit = self.gridRef[y][x].unit;
+ targetGridX = x;
+ targetGridY = y;
}
}
}
}
- // Move directly toward the goal
- var ox = enemy.flyingTarget.x - enemy.currentCellX;
- var oy = enemy.flyingTarget.y - enemy.currentCellY;
- var dist = Math.sqrt(ox * ox + oy * oy);
- if (dist < enemy.speed) {
- // Reached the goal
- return true;
- }
- var angle = Math.atan2(oy, ox);
- // Rotate enemy graphic to match movement direction
- if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
- enemy.children[0].targetRotation = angle;
- enemy.children[0].rotation = angle;
- } else if (enemy.children[0]) {
- if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
- tween.stop(enemy.children[0], {
- rotation: true
- });
- // Calculate the shortest angle to rotate
- var currentRotation = enemy.children[0].rotation;
- var angleDiff = angle - currentRotation;
- // Normalize angle difference to -PI to PI range for shortest path
- while (angleDiff > Math.PI) {
- angleDiff -= Math.PI * 2;
- }
- while (angleDiff < -Math.PI) {
- angleDiff += Math.PI * 2;
- }
- // Set target rotation and animate to it
- enemy.children[0].targetRotation = angle;
- tween(enemy.children[0], {
- rotation: currentRotation + angleDiff
- }, {
- duration: 250,
- easing: tween.easeOut
- });
+ if (closestUnit) {
+ // Check if this is a ranged enemy and if it's already within range
+ var entityRange = self.entity.range || 1;
+ // If enemy is within range or is attacking, don't move
+ if (closestDist <= entityRange || self.entity.isAttacking) {
+ // Stay in current position - don't move
+ return false;
}
+ shouldMove = true;
+ } else {
+ // No target found, move down as fallback
+ targetGridX = self.currentCell.x;
+ targetGridY = self.currentCell.y + 1;
+ shouldMove = true;
}
- // Update the cell position to track where the flying enemy is
- enemy.cellX = Math.round(enemy.currentCellX);
- enemy.cellY = Math.round(enemy.currentCellY);
- enemy.currentCellX += Math.cos(angle) * enemy.speed;
- enemy.currentCellY += Math.sin(angle) * enemy.speed;
- enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
- enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
- // Update shadow position if this is a flying enemy
+ }
+ if (!shouldMove) {
return false;
}
- // Handle normal pathfinding enemies
- if (!enemy.currentTarget) {
- enemy.currentTarget = cell.targets[0];
+ // Use A* pathfinding to find best path
+ if (!self.pathToTarget || self.pathToTarget.length === 0 || self.pathIndex >= self.pathToTarget.length) {
+ // Calculate new path
+ var path = self.findPath(self.currentCell.x, self.currentCell.y, targetGridX, targetGridY);
+ if (path && path.length > 0) {
+ self.pathToTarget = path;
+ self.pathIndex = 0;
+ } else {
+ // No path found
+ return false;
+ }
}
- if (enemy.currentTarget) {
- if (cell.score < enemy.currentTarget.score) {
- enemy.currentTarget = cell;
+ // Get next cell from path
+ var nextCell = self.pathToTarget[self.pathIndex];
+ if (!nextCell) {
+ return false;
+ }
+ var nextX = nextCell.x;
+ var nextY = nextCell.y;
+ // Check if desired cell is still unoccupied
+ if (self.isCellOccupied(nextX, nextY)) {
+ // Path is blocked, recalculate
+ self.pathToTarget = [];
+ self.pathIndex = 0;
+ return false;
+ }
+ // Final check if the cell is valid and unoccupied
+ if (!self.isCellOccupied(nextX, nextY) && self.gridRef[nextY] && self.gridRef[nextY][nextX]) {
+ var targetGridCell = self.gridRef[nextY][nextX];
+ // Double-check that no other entity is trying to move to same position at same time
+ if (isPositionOccupied(nextX, nextY, self.entity)) {
+ return false;
}
- 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;
+ // Unregister old position
+ unregisterEntityPosition(self.entity, self.currentCell.x, self.currentCell.y);
+ // Register new position immediately to prevent conflicts
+ registerEntityPosition(self.entity);
+ // Release old reservation
+ var oldCellKey = self.currentCell.x + ',' + self.currentCell.y;
+ if (cellReservations[oldCellKey] === self.entity) {
+ delete cellReservations[oldCellKey];
}
- var angle = Math.atan2(oy, ox);
- enemy.currentCellX += Math.cos(angle) * enemy.speed;
- enemy.currentCellY += Math.sin(angle) * enemy.speed;
+ // Reserve new cell
+ var newCellKey = nextX + ',' + nextY;
+ cellReservations[newCellKey] = self.entity;
+ // Update logical position immediately
+ self.currentCell.x = nextX;
+ self.currentCell.y = nextY;
+ self.entity.gridX = nextX;
+ self.entity.gridY = nextY;
+ // Set target position for smooth movement
+ self.targetCell.x = targetGridCell.x;
+ self.targetCell.y = targetGridCell.y;
+ self.isMoving = true;
+ // Add smooth transition animation when starting movement
+ if (!self.entity._isMoveTweening) {
+ self.entity._isMoveTweening = true;
+ // Get base scale (default to 1 if not set)
+ var baseScale = self.entity.baseScale || 1;
+ // Slight scale and rotation animation when moving
+ tween(self.entity, {
+ scaleX: baseScale * 1.1,
+ scaleY: baseScale * 0.9,
+ rotation: self.entity.rotation + (Math.random() > 0.5 ? 0.05 : -0.05)
+ }, {
+ duration: 150,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ tween(self.entity, {
+ scaleX: baseScale,
+ scaleY: baseScale,
+ rotation: 0
+ }, {
+ duration: 150,
+ easing: tween.easeIn,
+ onFinish: function onFinish() {
+ self.entity._isMoveTweening = false;
+ }
+ });
+ }
+ });
+ }
+ return true;
}
- enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
- enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
+ return false;
};
+ self.update = function () {
+ // Periodically check if we need to recalculate path (every 30 ticks)
+ if (LK.ticks % 30 === 0 && self.pathToTarget && self.pathToTarget.length > 0) {
+ // Check if current path is still valid
+ var stillValid = true;
+ for (var i = self.pathIndex; i < self.pathToTarget.length && i < self.pathIndex + 3; i++) {
+ if (self.pathToTarget[i] && self.isCellOccupied(self.pathToTarget[i].x, self.pathToTarget[i].y)) {
+ stillValid = false;
+ break;
+ }
+ }
+ if (!stillValid) {
+ // Path blocked, clear it to force recalculation
+ self.pathToTarget = [];
+ self.pathIndex = 0;
+ }
+ }
+ // If we have a target position, move towards it smoothly
+ if (self.isMoving) {
+ var dx = self.targetCell.x - self.entity.x;
+ var dy = self.targetCell.y - self.entity.y;
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance > self.moveSpeed) {
+ // Continue moving towards target
+ var moveX = dx / distance * self.moveSpeed;
+ var moveY = dy / distance * self.moveSpeed;
+ self.entity.x += moveX;
+ self.entity.y += moveY;
+ } else {
+ // Reached target cell
+ self.entity.x = self.targetCell.x;
+ self.entity.y = self.targetCell.y;
+ self.isMoving = false;
+ self.pathIndex++; // Move to next cell in path
+ // Wait a tick before trying next move to avoid simultaneous conflicts
+ if (LK.ticks % 2 === 0) {
+ // Immediately try to move to next cell for continuous movement
+ self.moveToNextCell();
+ }
+ }
+ } else {
+ // Not moving, try to start moving to next cell
+ // Add slight randomization to prevent all entities from moving at exact same time
+ if (LK.ticks % (2 + Math.floor(Math.random() * 3)) === 0) {
+ self.moveToNextCell();
+ }
+ }
+ };
+ return self;
});
-var NextWaveButton = Container.expand(function () {
+var GridSlot = Container.expand(function (lane, gridX, gridY) {
var self = Container.call(this);
- var buttonBackground = self.attachAsset('notification', {
+ var slotGraphics = self.attachAsset('grid_slot', {
anchorX: 0.5,
anchorY: 0.5
});
- buttonBackground.width = 300;
- buttonBackground.height = 100;
- buttonBackground.tint = 0x0088FF;
- var buttonText = new Text2("Next Wave", {
- size: 50,
- fill: 0xFFFFFF,
- weight: 800
- });
- buttonText.anchor.set(0.5, 0.5);
- self.addChild(buttonText);
- self.enabled = false;
- self.visible = false;
- self.update = function () {
- if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
- self.enabled = true;
- self.visible = true;
- buttonBackground.tint = 0x0088FF;
- self.alpha = 1;
- } else {
- self.enabled = false;
- self.visible = false;
- buttonBackground.tint = 0x888888;
- self.alpha = 0.7;
+ self.lane = lane;
+ self.gridX = gridX;
+ self.gridY = gridY;
+ self.unit = null;
+ slotGraphics.alpha = 0.3;
+ self.down = function (x, y, obj) {
+ if (self.unit && !draggedUnit && gameState === 'planning') {
+ // Prevent moving king after the first placement
+ if (self.unit.name === 'King' && wave > 1) {
+ return; // King can only be moved at the beginning of the game
+ }
+ // Allow dragging any unit during planning phase
+ draggedUnit = self.unit;
+ draggedUnit.originalX = self.x;
+ draggedUnit.originalY = self.y;
+ draggedUnit.originalGridSlot = self;
+ draggedFromGrid = true;
}
};
- self.down = function () {
- if (!self.enabled) {
- return;
+ self.up = function (x, y, obj) {
+ if (draggedUnit && !self.unit) {
+ self.placeUnit(draggedUnit);
+ draggedUnit = null;
}
- if (waveIndicator.gameStarted && currentWave < totalWaves) {
- currentWave++; // Increment to the next wave directly
- waveTimer = 0; // Reset wave timer
- waveInProgress = true;
- waveSpawned = false;
- // Get the type of the current wave (which is now the next wave)
- var waveType = waveIndicator.getWaveTypeName(currentWave);
- var enemyCount = waveIndicator.getEnemyCount(currentWave);
- var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 150;
+ };
+ self.placeUnit = function (unit) {
+ if (self.unit) {
+ return false;
}
+ // Check if position is already occupied by another entity
+ if (isPositionOccupied(self.gridX, self.gridY, unit)) {
+ return false;
+ }
+ // Check unit placement limits (don't apply to king)
+ if (unit.name !== "King" && !canPlaceMoreUnits(unit.isStructure)) {
+ // Show message about reaching unit limit
+ var limitType = unit.isStructure ? 'structures' : 'units';
+ var limitMessage = new Text2('You have no more room for ' + limitType + ' in the battlefield, level up to increase it!', {
+ size: 36,
+ fill: 0xFF4444
+ });
+ limitMessage.anchor.set(0.5, 0.5);
+ limitMessage.x = 1024; // Center of screen
+ limitMessage.y = 800; // Middle of screen
+ game.addChild(limitMessage);
+ // Flash the message and fade it out
+ LK.effects.flashObject(limitMessage, 0xFFFFFF, 300);
+ tween(limitMessage, {
+ alpha: 0,
+ y: 700
+ }, {
+ duration: 3000,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ limitMessage.destroy();
+ }
+ });
+ // Return unit to bench - find first available bench slot
+ for (var i = 0; i < benchSlots.length; i++) {
+ if (!benchSlots[i].unit) {
+ benchSlots[i].unit = unit;
+ unit.x = benchSlots[i].x;
+ unit.y = benchSlots[i].y;
+ unit.gridX = -1;
+ unit.gridY = -1;
+ unit.lane = -1;
+ unit.hideHealthBar();
+ unit.hideStarIndicator();
+ updateBenchDisplay();
+ break;
+ }
+ }
+ return false; // Can't place more units
+ }
+ self.unit = unit;
+ // Unregister old position if unit was previously on grid
+ if (unit.gridX >= 0 && unit.gridY >= 0) {
+ unregisterEntityPosition(unit, unit.gridX, unit.gridY);
+ }
+ // Ensure unit is positioned exactly at grid cell center (snap to grid)
+ unit.x = self.x;
+ unit.y = self.y;
+ unit.gridX = self.gridX;
+ unit.gridY = self.gridY;
+ unit.lane = self.lane;
+ // Double-check positioning to prevent floating between cells
+ var expectedX = GRID_START_X + self.gridX * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
+ var expectedY = GRID_START_Y + self.gridY * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
+ unit.x = expectedX;
+ unit.y = expectedY;
+ // Add placement animation
+ var targetScale = unit.baseScale || 1.3; // Use base scale or default to 1.3
+ if (unit.tier === 2) {
+ targetScale = (unit.baseScale || 1.3) * 1.2;
+ } else if (unit.tier === 3) {
+ targetScale = (unit.baseScale || 1.3) * 1.3;
+ }
+ unit.scaleX = 0;
+ unit.scaleY = 0;
+ tween(unit, {
+ scaleX: targetScale * 1.2,
+ scaleY: targetScale * 1.2
+ }, {
+ duration: 200,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ tween(unit, {
+ scaleX: targetScale,
+ scaleY: targetScale
+ }, {
+ duration: 150,
+ easing: tween.easeIn
+ });
+ }
+ });
+ // Register new position
+ registerEntityPosition(unit);
+ unit.showHealthBar(); // Show health bar when placed on grid
+ unit.updateHealthBar(); // Update health bar to show current health
+ unit.showStarIndicator(); // Show star indicator when placed on grid
+ // Don't remove from bench - units stay in both places
+ updateBenchDisplay();
+ return true;
};
return self;
});
-var Notification = Container.expand(function (message) {
+var HealthBar = Container.expand(function (width, height) {
var self = Container.call(this);
- var notificationGraphics = self.attachAsset('notification', {
- anchorX: 0.5,
- anchorY: 0.5
+ self.maxWidth = width;
+ // Background of the health bar
+ self.background = self.attachAsset('healthbar_bg', {
+ anchorX: 0,
+ anchorY: 0.5,
+ width: width,
+ height: height
});
- var notificationText = new Text2(message, {
- size: 50,
- fill: 0x000000,
- weight: 800
+ // Foreground of the health bar
+ self.foreground = self.attachAsset('healthbar_fg', {
+ anchorX: 0,
+ anchorY: 0.5,
+ width: width,
+ height: height
});
- notificationText.anchor.set(0.5, 0.5);
- notificationGraphics.width = notificationText.width + 30;
- self.addChild(notificationText);
- self.alpha = 1;
- var fadeOutTime = 120;
- self.update = function () {
- if (fadeOutTime > 0) {
- fadeOutTime--;
- self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
+ self.updateHealth = function (currentHealth, maxHealth) {
+ var healthRatio = currentHealth / maxHealth;
+ self.foreground.width = self.maxWidth * healthRatio;
+ if (healthRatio > 0.6) {
+ self.foreground.tint = 0x00ff00; // Green
+ } else if (healthRatio > 0.3) {
+ self.foreground.tint = 0xffff00; // Yellow
} else {
- self.destroy();
+ self.foreground.tint = 0xff0000; // Red
}
};
return self;
});
-var SourceTower = Container.expand(function (towerType) {
+var StarIndicator = Container.expand(function (tier) {
var self = Container.call(this);
- self.towerType = towerType || 'default';
- // Increase size of base for easier touch
- var baseGraphics = self.attachAsset('tower', {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 1.3,
- scaleY: 1.3
- });
- switch (self.towerType) {
- case 'rapid':
- baseGraphics.tint = 0x00AAFF;
- break;
- case 'sniper':
- baseGraphics.tint = 0xFF5500;
- break;
- case 'splash':
- baseGraphics.tint = 0x33CC00;
- break;
- case 'slow':
- baseGraphics.tint = 0x9900FF;
- break;
- case 'poison':
- baseGraphics.tint = 0x00FFAA;
- break;
- default:
- baseGraphics.tint = 0xAAAAAA;
- }
- var towerCost = getTowerCost(self.towerType);
- // Add shadow for tower type label
- var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
- size: 50,
- fill: 0x000000,
- weight: 800
- });
- typeLabelShadow.anchor.set(0.5, 0.5);
- typeLabelShadow.x = 4;
- typeLabelShadow.y = -20 + 4;
- self.addChild(typeLabelShadow);
- // Add tower type label
- var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
- size: 50,
- fill: 0xFFFFFF,
- weight: 800
- });
- typeLabel.anchor.set(0.5, 0.5);
- typeLabel.y = -20; // Position above center of tower
- self.addChild(typeLabel);
- // Add cost shadow
- var costLabelShadow = new Text2(towerCost, {
- size: 50,
- fill: 0x000000,
- weight: 800
- });
- costLabelShadow.anchor.set(0.5, 0.5);
- costLabelShadow.x = 4;
- costLabelShadow.y = 24 + 12;
- self.addChild(costLabelShadow);
- // Add cost label
- var costLabel = new Text2(towerCost, {
- size: 50,
- fill: 0xFFD700,
- weight: 800
- });
- costLabel.anchor.set(0.5, 0.5);
- costLabel.y = 20 + 12;
- self.addChild(costLabel);
- self.update = function () {
- // Check if player can afford this tower
- var canAfford = gold >= getTowerCost(self.towerType);
- // Set opacity based on affordability
- self.alpha = canAfford ? 1 : 0.5;
+ self.tier = tier || 1;
+ self.dots = [];
+ self.updateStars = function (newTier) {
+ self.tier = newTier;
+ // Remove existing dots
+ for (var i = 0; i < self.dots.length; i++) {
+ self.dots[i].destroy();
+ }
+ self.dots = [];
+ // Create new dots based on tier
+ var dotSpacing = 12;
+ var totalWidth = (self.tier - 1) * dotSpacing;
+ var startX = -totalWidth / 2;
+ for (var i = 0; i < self.tier; i++) {
+ var dot = self.attachAsset('star_dot', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ dot.x = startX + i * dotSpacing;
+ dot.y = 0;
+ self.dots.push(dot);
+ }
};
+ // Initialize with current tier
+ self.updateStars(self.tier);
return self;
});
-var Tower = Container.expand(function (id) {
+var Unit = Container.expand(function (tier) {
var self = Container.call(this);
- self.id = id || 'default';
- self.level = 1;
- self.maxLevel = 6;
- self.gridX = 0;
- self.gridY = 0;
- self.range = 3 * CELL_SIZE;
- // Standardized method to get the current range of the tower
- self.getRange = function () {
- // Always calculate range based on tower type and level
- switch (self.id) {
- case 'sniper':
- // Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost
- if (self.level === self.maxLevel) {
- return 12 * CELL_SIZE; // Significantly increased range for max level
- }
- return (5 + (self.level - 1) * 0.8) * CELL_SIZE;
- case 'splash':
- // Splash: base 2, +0.2 per level (max ~4 blocks at max level)
- return (2 + (self.level - 1) * 0.2) * CELL_SIZE;
- case 'rapid':
- // Rapid: base 2.5, +0.5 per level
- return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
- case 'slow':
- // Slow: base 3.5, +0.5 per level
- return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
- case 'poison':
- // Poison: base 3.2, +0.5 per level
- return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
- default:
- // Default: base 3, +0.5 per level
- return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
- }
- };
- self.cellsInRange = [];
- self.fireRate = 60;
- self.bulletSpeed = 5;
- self.damage = 10;
- self.lastFired = 0;
- self.targetEnemy = null;
- switch (self.id) {
- case 'rapid':
- self.fireRate = 30;
- self.damage = 5;
- self.range = 2.5 * CELL_SIZE;
- self.bulletSpeed = 7;
- break;
- case 'sniper':
- self.fireRate = 90;
- self.damage = 25;
- self.range = 5 * CELL_SIZE;
- self.bulletSpeed = 25;
- break;
- case 'splash':
- self.fireRate = 75;
- self.damage = 15;
- self.range = 2 * CELL_SIZE;
- self.bulletSpeed = 4;
- break;
- case 'slow':
- self.fireRate = 50;
- self.damage = 8;
- self.range = 3.5 * CELL_SIZE;
- self.bulletSpeed = 5;
- break;
- case 'poison':
- self.fireRate = 70;
- self.damage = 12;
- self.range = 3.2 * CELL_SIZE;
- self.bulletSpeed = 5;
- break;
- }
- var baseGraphics = self.attachAsset('tower', {
+ self.tier = tier || 1;
+ var assetName = 'unit_tier' + self.tier;
+ var unitGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
- switch (self.id) {
- case 'rapid':
- baseGraphics.tint = 0x00AAFF;
- break;
- case 'sniper':
- baseGraphics.tint = 0xFF5500;
- break;
- case 'splash':
- baseGraphics.tint = 0x33CC00;
- break;
- case 'slow':
- baseGraphics.tint = 0x9900FF;
- break;
- case 'poison':
- baseGraphics.tint = 0x00FFAA;
- break;
- default:
- baseGraphics.tint = 0xAAAAAA;
- }
- var levelIndicators = [];
- var maxDots = self.maxLevel;
- var dotSpacing = baseGraphics.width / (maxDots + 1);
- var dotSize = CELL_SIZE / 6;
- for (var i = 0; i < maxDots; i++) {
- var dot = new Container();
- var outlineCircle = dot.attachAsset('towerLevelIndicator', {
- anchorX: 0.5,
- anchorY: 0.5
+ self.health = 100;
+ self.maxHealth = 100;
+ self.damage = self.tier;
+ self.speed = 1; // Attack speed multiplier
+ self.fireRate = 60; // Base fire rate in ticks
+ self.lastShot = 0;
+ self.gridX = -1;
+ self.gridY = -1;
+ self.lane = -1;
+ self.range = 1; // Default range in cells (adjacent/melee). Override in subclasses for ranged units.
+ self.healthBar = null; // Initialize health bar as null
+ self.starIndicator = null; // Initialize star indicator as null
+ self.isAttacking = false; // Flag to track if unit is currently attacking
+ self.baseScale = 1.3; // Base scale multiplier for all units (30% larger)
+ // Apply base scale
+ self.scaleX = self.baseScale;
+ self.scaleY = self.baseScale;
+ self.showHealthBar = function () {
+ if (!self.healthBar) {
+ self.healthBar = new HealthBar(80, 10); // Create a health bar instance
+ self.healthBar.x = -40; // Position relative to the unit's center
+ self.healthBar.y = -60; // Position above the unit
+ self.addChild(self.healthBar);
+ }
+ self.healthBar.visible = true; // Ensure health bar is visible
+ self.healthBar.updateHealth(self.health, self.maxHealth);
+ };
+ self.showStarIndicator = function () {
+ if (!self.starIndicator) {
+ self.starIndicator = new StarIndicator(self.tier);
+ self.starIndicator.x = 0; // Center relative to unit
+ self.starIndicator.y = 55; // Position below the unit
+ self.addChild(self.starIndicator);
+ }
+ self.starIndicator.visible = true;
+ self.starIndicator.updateStars(self.tier);
+ // Scale unit based on tier - two star units are 30% larger
+ self.updateUnitScale();
+ };
+ self.updateUnitScale = function () {
+ // Stop any ongoing scale tweens first
+ tween.stop(self, {
+ scaleX: true,
+ scaleY: true
});
- outlineCircle.width = dotSize + 4;
- outlineCircle.height = dotSize + 4;
- outlineCircle.tint = 0x000000;
- var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- towerLevelIndicator.width = dotSize;
- towerLevelIndicator.height = dotSize;
- towerLevelIndicator.tint = 0xCCCCCC;
- dot.x = -CELL_SIZE + dotSpacing * (i + 1);
- dot.y = CELL_SIZE * 0.7;
- self.addChild(dot);
- levelIndicators.push(dot);
- }
- var gunContainer = new Container();
- self.addChild(gunContainer);
- var gunGraphics = gunContainer.attachAsset('defense', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- self.updateLevelIndicators = function () {
- for (var i = 0; i < maxDots; i++) {
- var dot = levelIndicators[i];
- var towerLevelIndicator = dot.children[1];
- if (i < self.level) {
- towerLevelIndicator.tint = 0xFFFFFF;
- } else {
- switch (self.id) {
- case 'rapid':
- towerLevelIndicator.tint = 0x00AAFF;
- break;
- case 'sniper':
- towerLevelIndicator.tint = 0xFF5500;
- break;
- case 'splash':
- towerLevelIndicator.tint = 0x33CC00;
- break;
- case 'slow':
- towerLevelIndicator.tint = 0x9900FF;
- break;
- case 'poison':
- towerLevelIndicator.tint = 0x00FFAA;
- break;
- default:
- towerLevelIndicator.tint = 0xAAAAAA;
- }
- }
+ if (self.tier === 2) {
+ // Two star units are 20% larger on top of base scale
+ var targetScale = self.baseScale * 1.2;
+ self.scaleX = targetScale;
+ self.scaleY = targetScale;
+ tween(self, {
+ scaleX: targetScale,
+ scaleY: targetScale
+ }, {
+ duration: 300,
+ easing: tween.easeOut
+ });
+ } else if (self.tier === 3) {
+ // Three star units are 30% larger on top of base scale
+ var targetScale = self.baseScale * 1.3;
+ self.scaleX = targetScale;
+ self.scaleY = targetScale;
+ tween(self, {
+ scaleX: targetScale,
+ scaleY: targetScale
+ }, {
+ duration: 300,
+ easing: tween.easeOut
+ });
+ } else {
+ // Tier 1 units use base scale
+ self.scaleX = self.baseScale;
+ self.scaleY = self.baseScale;
+ tween(self, {
+ scaleX: self.baseScale,
+ scaleY: self.baseScale
+ }, {
+ duration: 300,
+ easing: tween.easeOut
+ });
}
};
- self.updateLevelIndicators();
- self.refreshCellsInRange = function () {
- for (var i = 0; i < self.cellsInRange.length; i++) {
- var cell = self.cellsInRange[i];
- var towerIndex = cell.towersInRange.indexOf(self);
- if (towerIndex !== -1) {
- cell.towersInRange.splice(towerIndex, 1);
- }
+ self.hideHealthBar = function () {
+ if (self.healthBar) {
+ self.healthBar.destroy();
+ self.healthBar = null;
}
- self.cellsInRange = [];
- var rangeRadius = self.getRange() / CELL_SIZE;
- var centerX = self.gridX + 1;
- var centerY = self.gridY + 1;
- var minI = Math.floor(centerX - rangeRadius - 0.5);
- var maxI = Math.ceil(centerX + rangeRadius + 0.5);
- var minJ = Math.floor(centerY - rangeRadius - 0.5);
- var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
- for (var i = minI; i <= maxI; i++) {
- for (var j = minJ; j <= maxJ; j++) {
- var closestX = Math.max(i, Math.min(centerX, i + 1));
- var closestY = Math.max(j, Math.min(centerY, j + 1));
- var deltaX = closestX - centerX;
- var deltaY = closestY - centerY;
- var distanceSquared = deltaX * deltaX + deltaY * deltaY;
- if (distanceSquared <= rangeRadius * rangeRadius) {
- var cell = grid.getCell(i, j);
- if (cell) {
- self.cellsInRange.push(cell);
- cell.towersInRange.push(self);
- }
- }
- }
+ };
+ self.hideStarIndicator = function () {
+ if (self.starIndicator) {
+ self.starIndicator.destroy();
+ self.starIndicator = null;
}
- grid.renderDebug();
};
- self.getTotalValue = function () {
- var baseTowerCost = getTowerCost(self.id);
- var totalInvestment = baseTowerCost;
- var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
- for (var i = 1; i < self.level; i++) {
- totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
+ self.updateHealthBar = function () {
+ if (self.healthBar) {
+ self.healthBar.updateHealth(self.health, self.maxHealth);
}
- return totalInvestment;
};
- self.upgrade = function () {
- if (self.level < self.maxLevel) {
- // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
- var baseUpgradeCost = getTowerCost(self.id);
- var upgradeCost;
- // Make last upgrade level extra expensive
- if (self.level === self.maxLevel - 1) {
- upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade
- } else {
- upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
- }
- if (gold >= upgradeCost) {
- setGold(gold - upgradeCost);
- self.level++;
- // No need to update self.range here; getRange() is now the source of truth
- // Apply tower-specific upgrades based on type
- if (self.id === 'rapid') {
- if (self.level === self.maxLevel) {
- // Extra powerful last upgrade (double the effect)
- self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect
- self.damage = 5 + self.level * 10; // double the effect
- self.bulletSpeed = 7 + self.level * 2.4; // double the effect
- } else {
- self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades
- self.damage = 5 + self.level * 3;
- self.bulletSpeed = 7 + self.level * 0.7;
- }
- } else {
- if (self.level === self.maxLevel) {
- // Extra powerful last upgrade for all other towers (double the effect)
- self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect
- self.damage = 10 + self.level * 20; // double the effect
- self.bulletSpeed = 5 + self.level * 2.4; // double the effect
- } else {
- self.fireRate = Math.max(20, 60 - self.level * 8);
- self.damage = 10 + self.level * 5;
- self.bulletSpeed = 5 + self.level * 0.5;
- }
- }
- self.refreshCellsInRange();
- self.updateLevelIndicators();
- if (self.level > 1) {
- var levelDot = levelIndicators[self.level - 1].children[1];
- tween(levelDot, {
- scaleX: 1.5,
- scaleY: 1.5
- }, {
- duration: 300,
- easing: tween.elasticOut,
- onFinish: function onFinish() {
- tween(levelDot, {
- scaleX: 1,
- scaleY: 1
- }, {
- duration: 200,
- easing: tween.easeOut
- });
+ self.takeDamage = function (damage) {
+ self.health -= damage;
+ if (self.health < 0) {
+ self.health = 0;
+ } // Ensure health doesn't go below 0
+ self.updateHealthBar(); // Update health bar when taking damage
+ if (self.health <= 0) {
+ self.hideHealthBar(); // Hide health bar when destroyed
+ return true;
+ }
+ return false;
+ };
+ self.canShoot = function () {
+ // Use speed to modify fire rate - higher speed means faster attacks
+ var adjustedFireRate = Math.floor(self.fireRate / self.speed);
+ return LK.ticks - self.lastShot >= adjustedFireRate;
+ };
+ self.shoot = function (target) {
+ if (!self.canShoot()) {
+ return null;
+ }
+ self.lastShot = LK.ticks;
+ // For melee units (range 1), don't create bullets
+ if (self.range <= 1) {
+ // Melee attack: do incline movement using tween, then apply damage
+ var originalX = self.x;
+ var originalY = self.y;
+ // Calculate incline offset (move 30% toward target, max 40px)
+ var dx = target.x - self.x;
+ var dy = target.y - self.y;
+ var dist = Math.sqrt(dx * dx + dy * dy);
+ var offset = Math.min(40, dist * 0.3);
+ var inclineX = self.x + (dist > 0 ? dx / dist * offset : 0);
+ var inclineY = self.y + (dist > 0 ? dy / dist * offset : 0);
+ // Prevent multiple tweens at once
+ if (!self._isMeleeTweening) {
+ self._isMeleeTweening = true;
+ self.isAttacking = true; // Set attacking flag
+ tween(self, {
+ x: inclineX,
+ y: inclineY
+ }, {
+ duration: 80,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ // Apply damage after lunge
+ var killed = target.takeDamage(self.damage);
+ if (killed) {
+ self.isAttacking = false; // Clear attacking flag if target dies
+ // Don't give gold for killing enemies
+ LK.getSound('enemyDeath').play();
+ for (var i = enemies.length - 1; i >= 0; i--) {
+ if (enemies[i] === target) {
+ enemies[i].destroy();
+ enemies.splice(i, 1);
+ break;
+ }
+ }
}
- });
- }
- return true;
- } else {
- var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 50;
- return false;
+ // Tween back to original position
+ tween(self, {
+ x: originalX,
+ y: originalY
+ }, {
+ duration: 100,
+ easing: tween.easeIn,
+ onFinish: function onFinish() {
+ self._isMeleeTweening = false;
+ self.isAttacking = false; // Clear attacking flag after animation
+ }
+ });
+ }
+ });
}
+ return null; // No bullet for melee
}
- return false;
+ // Ranged attack - create bullet
+ var bullet = new Bullet(self.damage, target);
+ bullet.x = self.x;
+ bullet.y = self.y;
+ // Customize bullet appearance based on unit type
+ if (self.name === "Ranger") {
+ // Archer units shoot arrows
+ bullet.bulletType = 'arrow';
+ bullet.createGraphics(); // Recreate graphics with correct type
+ } else if (self.name === "Wizard") {
+ // Wizard units shoot magic
+ bullet.bulletType = 'magic';
+ bullet.createGraphics(); // Recreate graphics with correct type
+ }
+ return bullet;
};
self.findTarget = function () {
var closestEnemy = null;
- var closestScore = Infinity;
+ var closestCellDistance = self.range + 1; // Only enemies within range (cell count) are valid
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
- var dx = enemy.x - self.x;
- var dy = enemy.y - self.y;
- var distance = Math.sqrt(dx * dx + dy * dy);
- // Check if enemy is in range
- if (distance <= self.getRange()) {
- // Handle flying enemies differently - they can be targeted regardless of path
- if (enemy.isFlying) {
- // For flying enemies, prioritize by distance to the goal
- if (enemy.flyingTarget) {
- var goalX = enemy.flyingTarget.x;
- var goalY = enemy.flyingTarget.y;
- var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
- // Use distance to goal as score
- if (distToGoal < closestScore) {
- closestScore = distToGoal;
- closestEnemy = enemy;
- }
- } else {
- // If no flying target yet (shouldn't happen), prioritize by distance to tower
- if (distance < closestScore) {
- closestScore = distance;
- closestEnemy = enemy;
- }
- }
- } else {
- // For ground enemies, use the original path-based targeting
- // Get the cell for this enemy
- var cell = grid.getCell(enemy.cellX, enemy.cellY);
- if (cell && cell.pathId === pathId) {
- // Use the cell's score (distance to exit) for prioritization
- // Lower score means closer to exit
- if (cell.score < closestScore) {
- closestScore = cell.score;
- closestEnemy = enemy;
- }
- }
- }
+ // Calculate cell-based Manhattan distance
+ var cellDist = -1;
+ if (typeof self.gridX === "number" && typeof self.gridY === "number" && typeof enemy.gridX === "number" && typeof enemy.gridY === "number") {
+ cellDist = Math.abs(self.gridX - enemy.gridX) + Math.abs(self.gridY - enemy.gridY);
}
+ if (cellDist >= 0 && cellDist <= self.range && cellDist < closestCellDistance) {
+ closestCellDistance = cellDist;
+ closestEnemy = enemy;
+ }
}
- if (!closestEnemy) {
- self.targetEnemy = null;
- }
return closestEnemy;
};
self.update = function () {
- self.targetEnemy = self.findTarget();
- if (self.targetEnemy) {
- var dx = self.targetEnemy.x - self.x;
- var dy = self.targetEnemy.y - self.y;
- var angle = Math.atan2(dy, dx);
- gunContainer.rotation = angle;
- if (LK.ticks - self.lastFired >= self.fireRate) {
- self.fire();
- self.lastFired = LK.ticks;
+ // Only initialize and update GridMovement if unit is placed on grid and during battle phase
+ if (self.gridX >= 0 && self.gridY >= 0 && gameState === 'battle') {
+ if (!self.gridMovement) {
+ self.gridMovement = new GridMovement(self, grid);
}
+ self.gridMovement.update();
}
- };
- self.down = function (x, y, obj) {
- var existingMenus = game.children.filter(function (child) {
- return child instanceof UpgradeMenu;
- });
- var hasOwnMenu = false;
- var rangeCircle = null;
- for (var i = 0; i < game.children.length; i++) {
- if (game.children[i].isTowerRange && game.children[i].tower === self) {
- rangeCircle = game.children[i];
- break;
- }
- }
- for (var i = 0; i < existingMenus.length; i++) {
- if (existingMenus[i].tower === self) {
- hasOwnMenu = true;
- break;
- }
- }
- if (hasOwnMenu) {
- for (var i = 0; i < existingMenus.length; i++) {
- if (existingMenus[i].tower === self) {
- hideUpgradeMenu(existingMenus[i]);
- }
- }
- if (rangeCircle) {
- game.removeChild(rangeCircle);
- }
- selectedTower = null;
- grid.renderDebug();
- return;
- }
- for (var i = 0; i < existingMenus.length; i++) {
- existingMenus[i].destroy();
- }
- for (var i = game.children.length - 1; i >= 0; i--) {
- if (game.children[i].isTowerRange) {
- game.removeChild(game.children[i]);
- }
- }
- selectedTower = self;
- var rangeIndicator = new Container();
- rangeIndicator.isTowerRange = true;
- rangeIndicator.tower = self;
- game.addChild(rangeIndicator);
- rangeIndicator.x = self.x;
- rangeIndicator.y = self.y;
- var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
- rangeGraphics.alpha = 0.3;
- var upgradeMenu = new UpgradeMenu(self);
- game.addChild(upgradeMenu);
- upgradeMenu.x = 2048 / 2;
- tween(upgradeMenu, {
- y: 2732 - 225
- }, {
- duration: 200,
- easing: tween.backOut
- });
- grid.renderDebug();
- };
- self.isInRange = function (enemy) {
- if (!enemy) {
- return false;
- }
- var dx = enemy.x - self.x;
- var dy = enemy.y - self.y;
- var distance = Math.sqrt(dx * dx + dy * dy);
- return distance <= self.getRange();
- };
- self.fire = function () {
- if (self.targetEnemy) {
- var potentialDamage = 0;
- for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
- potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
- }
- if (self.targetEnemy.health > potentialDamage) {
- var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
- var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
- var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
- // Set bullet type based on tower type
- bullet.type = self.id;
- // For slow tower, pass level for scaling slow effect
- if (self.id === 'slow') {
- bullet.sourceTowerLevel = self.level;
- }
- // Customize bullet appearance based on tower type
- switch (self.id) {
- case 'rapid':
- bullet.children[0].tint = 0x00AAFF;
- bullet.children[0].width = 20;
- bullet.children[0].height = 20;
- break;
- case 'sniper':
- bullet.children[0].tint = 0xFF5500;
- bullet.children[0].width = 15;
- bullet.children[0].height = 15;
- break;
- case 'splash':
- bullet.children[0].tint = 0x33CC00;
- bullet.children[0].width = 40;
- bullet.children[0].height = 40;
- break;
- case 'slow':
- bullet.children[0].tint = 0x9900FF;
- bullet.children[0].width = 35;
- bullet.children[0].height = 35;
- break;
- case 'poison':
- bullet.children[0].tint = 0x00FFAA;
- bullet.children[0].width = 35;
- bullet.children[0].height = 35;
- break;
- }
- game.addChild(bullet);
- bullets.push(bullet);
- self.targetEnemy.bulletsTargetingThis.push(bullet);
- // --- Fire recoil effect for gunContainer ---
- // Stop any ongoing recoil tweens before starting a new one
- tween.stop(gunContainer, {
- x: true,
- y: true,
- scaleX: true,
- scaleY: true
- });
- // Always use the original resting position for recoil, never accumulate offset
- if (gunContainer._restX === undefined) {
- gunContainer._restX = 0;
- }
- if (gunContainer._restY === undefined) {
- gunContainer._restY = 0;
- }
- if (gunContainer._restScaleX === undefined) {
- gunContainer._restScaleX = 1;
- }
- if (gunContainer._restScaleY === undefined) {
- gunContainer._restScaleY = 1;
- }
- // Reset to resting position before animating (in case of interrupted tweens)
- gunContainer.x = gunContainer._restX;
- gunContainer.y = gunContainer._restY;
- gunContainer.scaleX = gunContainer._restScaleX;
- gunContainer.scaleY = gunContainer._restScaleY;
- // Calculate recoil offset (recoil back along the gun's rotation)
- var recoilDistance = 8;
- var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
- var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
- // Animate recoil back from the resting position
- tween(gunContainer, {
- x: gunContainer._restX + recoilX,
- y: gunContainer._restY + recoilY
- }, {
- duration: 60,
- easing: tween.cubicOut,
- onFinish: function onFinish() {
- // Animate return to original position/scale
- tween(gunContainer, {
- x: gunContainer._restX,
- y: gunContainer._restY
+ // Add idle animation when not attacking
+ if (!self.isAttacking && self.gridX >= 0 && self.gridY >= 0) {
+ // Only start idle animation if not already animating
+ if (!self._isIdleAnimating) {
+ self._isIdleAnimating = true;
+ // Random delay before starting idle animation
+ var delay = Math.random() * 2000;
+ LK.setTimeout(function () {
+ if (!self.isAttacking && self._isIdleAnimating) {
+ // Calculate target scale based on tier
+ var targetScale = self.baseScale;
+ if (self.tier === 2) {
+ targetScale = self.baseScale * 1.2;
+ } else if (self.tier === 3) {
+ targetScale = self.baseScale * 1.3;
+ }
+ // Gentle bobbing animation that respects tier scale
+ tween(self, {
+ scaleX: targetScale * 1.05,
+ scaleY: targetScale * 0.95
}, {
- duration: 90,
- easing: tween.cubicIn
+ duration: 800,
+ easing: tween.easeInOut,
+ onFinish: function onFinish() {
+ tween(self, {
+ scaleX: targetScale,
+ scaleY: targetScale
+ }, {
+ duration: 800,
+ easing: tween.easeInOut,
+ onFinish: function onFinish() {
+ self._isIdleAnimating = false;
+ }
+ });
+ }
});
}
- });
+ }, delay);
}
}
};
- self.placeOnGrid = function (gridX, gridY) {
- self.gridX = gridX;
- self.gridY = gridY;
- self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
- self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
- for (var i = 0; i < 2; i++) {
- for (var j = 0; j < 2; j++) {
- var cell = grid.getCell(gridX + i, gridY + j);
- if (cell) {
- cell.type = 1;
- }
- }
- }
- self.refreshCellsInRange();
- };
return self;
});
-var TowerPreview = Container.expand(function () {
- var self = Container.call(this);
- var towerRange = 3;
- var rangeInPixels = towerRange * CELL_SIZE;
- self.towerType = 'default';
- self.hasEnoughGold = true;
- var rangeIndicator = new Container();
- self.addChild(rangeIndicator);
- var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
+var Wizard = Unit.expand(function () {
+ var self = Unit.call(this, 1);
+ // Remove default unit graphics
+ self.removeChildren();
+ // Create pixelart character container
+ var characterContainer = new Container();
+ self.addChild(characterContainer);
+ // Body (blue mage robe)
+ var body = characterContainer.attachAsset('wizard', {
anchorX: 0.5,
anchorY: 0.5
});
- rangeGraphics.alpha = 0.3;
- var previewGraphics = self.attachAsset('towerpreview', {
+ body.width = 65;
+ body.height = 85;
+ body.y = 10;
+ body.tint = 0x191970; // Midnight blue to match mage robe theme
+ // Head with wizard hat
+ var head = characterContainer.attachAsset('wizard', {
anchorX: 0.5,
+ anchorY: 0.5,
+ width: 30,
+ height: 30
+ });
+ head.y = -35;
+ head.tint = 0xFFDBB5; // Skin color
+ // Wizard hat (triangle)
+ var hat = characterContainer.attachAsset('wizard', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 40,
+ height: 35
+ });
+ hat.y = -55;
+ hat.tint = 0x000066;
+ // Staff (right side)
+ var staff = characterContainer.attachAsset('wizard', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 10,
+ height: 80
+ });
+ staff.x = 35;
+ staff.y = -10;
+ staff.tint = 0x8B4513;
+ // Staff crystal
+ var staffCrystal = characterContainer.attachAsset('wizard', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 20,
+ height: 20
+ });
+ staffCrystal.x = 35;
+ staffCrystal.y = -50;
+ staffCrystal.tint = 0x00FFFF; // Cyan crystal
+ // Initial attributes for the unit
+ self.health = 150;
+ self.maxHealth = 150;
+ self.damage = 15;
+ self.armor = 8;
+ self.criticalChance = 0.18;
+ self.magicResist = 8;
+ self.mana = 70;
+ self.speed = 0.7;
+ self.range = 3; // Example: medium range (3 cells)
+ self.name = "Wizard";
+ return self;
+});
+var Wall = Unit.expand(function () {
+ var self = Unit.call(this, 1);
+ // Remove default unit graphics
+ self.removeChildren();
+ // Create pixelart wall structure
+ var wallContainer = new Container();
+ self.addChild(wallContainer);
+ // Wall base (stone blocks)
+ var wallBase = wallContainer.attachAsset('wall', {
+ anchorX: 0.5,
anchorY: 0.5
});
- previewGraphics.width = CELL_SIZE * 2;
- previewGraphics.height = CELL_SIZE * 2;
- self.canPlace = false;
- self.gridX = 0;
- self.gridY = 0;
- self.blockedByEnemy = false;
+ wallBase.width = 90;
+ wallBase.height = 80;
+ wallBase.y = 10;
+ wallBase.tint = 0x808080; // Grey stone
+ // Wall top section
+ var wallTop = wallContainer.attachAsset('wall', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 100,
+ height: 30
+ });
+ wallTop.y = -35;
+ wallTop.tint = 0x696969; // Darker grey
+ // Stone texture details (left)
+ var stoneLeft = wallContainer.attachAsset('wall', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 25,
+ height: 20
+ });
+ stoneLeft.x = -25;
+ stoneLeft.y = 0;
+ stoneLeft.tint = 0x606060;
+ // Stone texture details (right)
+ var stoneRight = wallContainer.attachAsset('wall', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 25,
+ height: 20
+ });
+ stoneRight.x = 25;
+ stoneRight.y = 0;
+ stoneRight.tint = 0x606060;
+ // Stone texture details (center)
+ var stoneCenter = wallContainer.attachAsset('wall', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 30,
+ height: 15
+ });
+ stoneCenter.x = 0;
+ stoneCenter.y = 20;
+ stoneCenter.tint = 0x505050;
+ // Initial attributes for the wall
+ self.health = 300;
+ self.maxHealth = 300;
+ self.damage = 0; // Walls don't attack
+ self.armor = 20; // High defense
+ self.criticalChance = 0; // No critical chance
+ self.magicResist = 15;
+ self.mana = 0; // No mana
+ self.speed = 0; // Doesn't attack
+ self.range = 0; // No range
+ self.name = "Wall";
+ self.isStructure = true; // Mark as structure
+ // Override update to prevent movement
self.update = function () {
- var previousHasEnoughGold = self.hasEnoughGold;
- self.hasEnoughGold = gold >= getTowerCost(self.towerType);
- // Only update appearance if the affordability status has changed
- if (previousHasEnoughGold !== self.hasEnoughGold) {
- self.updateAppearance();
+ // Walls don't move or attack, just exist as obstacles
+ // Still need to show health bar during battle
+ if (gameState === 'battle' && self.gridX >= 0 && self.gridY >= 0) {
+ self.showHealthBar();
+ self.updateHealthBar();
+ self.showStarIndicator();
}
};
- self.updateAppearance = function () {
- // Use Tower class to get the source of truth for range
- var tempTower = new Tower(self.towerType);
- var previewRange = tempTower.getRange();
- // Clean up tempTower to avoid memory leaks
- if (tempTower && tempTower.destroy) {
- tempTower.destroy();
- }
- // Set range indicator using unified range logic
- rangeGraphics.width = rangeGraphics.height = previewRange * 2;
- switch (self.towerType) {
- case 'rapid':
- previewGraphics.tint = 0x00AAFF;
- break;
- case 'sniper':
- previewGraphics.tint = 0xFF5500;
- break;
- case 'splash':
- previewGraphics.tint = 0x33CC00;
- break;
- case 'slow':
- previewGraphics.tint = 0x9900FF;
- break;
- case 'poison':
- previewGraphics.tint = 0x00FFAA;
- break;
- default:
- previewGraphics.tint = 0xAAAAAA;
- }
- if (!self.canPlace || !self.hasEnoughGold) {
- previewGraphics.tint = 0xFF0000;
- }
+ // Override shoot to prevent attacking
+ self.shoot = function (target) {
+ return null; // Walls can't shoot
};
- self.updatePlacementStatus = function () {
- var validGridPlacement = true;
- if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
- validGridPlacement = false;
- } else {
- for (var i = 0; i < 2; i++) {
- for (var j = 0; j < 2; j++) {
- var cell = grid.getCell(self.gridX + i, self.gridY + j);
- if (!cell || cell.type !== 0) {
- validGridPlacement = false;
- break;
- }
- }
- if (!validGridPlacement) {
- break;
- }
- }
- }
- self.blockedByEnemy = false;
- if (validGridPlacement) {
- for (var i = 0; i < enemies.length; i++) {
- var enemy = enemies[i];
- if (enemy.currentCellY < 4) {
- continue;
- }
- // Only check non-flying enemies, flying enemies can pass over towers
- if (!enemy.isFlying) {
- if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
- self.blockedByEnemy = true;
- break;
- }
- if (enemy.currentTarget) {
- var targetX = enemy.currentTarget.x;
- var targetY = enemy.currentTarget.y;
- if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) {
- self.blockedByEnemy = true;
- break;
- }
- }
- }
- }
- }
- self.canPlace = validGridPlacement && !self.blockedByEnemy;
- self.hasEnoughGold = gold >= getTowerCost(self.towerType);
- self.updateAppearance();
+ // Override findTarget since walls don't attack
+ self.findTarget = function () {
+ return null;
};
- self.checkPlacement = function () {
- self.updatePlacementStatus();
- };
- self.snapToGrid = function (x, y) {
- var gridPosX = x - grid.x;
- var gridPosY = y - grid.y;
- self.gridX = Math.floor(gridPosX / CELL_SIZE);
- self.gridY = Math.floor(gridPosY / CELL_SIZE);
- self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
- self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
- self.checkPlacement();
- };
return self;
});
-var UpgradeMenu = Container.expand(function (tower) {
- var self = Container.call(this);
- self.tower = tower;
- self.y = 2732 + 225;
- var menuBackground = self.attachAsset('notification', {
+var Soldier = Unit.expand(function () {
+ var self = Unit.call(this, 1);
+ // Remove default unit graphics
+ self.removeChildren();
+ // Create pixelart character container
+ var characterContainer = new Container();
+ self.addChild(characterContainer);
+ // Body (green base)
+ var body = characterContainer.attachAsset('soldier', {
anchorX: 0.5,
- anchorY: 0.5
+ anchorY: 0.5,
+ width: 60,
+ height: 80
});
- menuBackground.width = 2048;
- menuBackground.height = 500;
- menuBackground.tint = 0x444444;
- menuBackground.alpha = 0.9;
- var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', {
- size: 80,
- fill: 0xFFFFFF,
- weight: 800
+ body.y = 10;
+ body.tint = 0x32CD32; // Lime green to match theme
+ // Head (lighter green circle)
+ var head = characterContainer.attachAsset('soldier', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 40,
+ height: 40
});
- towerTypeText.anchor.set(0, 0);
- towerTypeText.x = -840;
- towerTypeText.y = -160;
- self.addChild(towerTypeText);
- var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
- size: 70,
- fill: 0xFFFFFF,
- weight: 400
+ head.y = -30;
+ head.tint = 0x66ff66;
+ // Arms (small rectangles)
+ var leftArm = characterContainer.attachAsset('soldier', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 15,
+ height: 40
});
- statsText.anchor.set(0, 0.5);
- statsText.x = -840;
- statsText.y = 50;
- self.addChild(statsText);
- var buttonsContainer = new Container();
- buttonsContainer.x = 500;
- self.addChild(buttonsContainer);
- var upgradeButton = new Container();
- buttonsContainer.addChild(upgradeButton);
- var buttonBackground = upgradeButton.attachAsset('notification', {
+ leftArm.x = -30;
+ leftArm.y = 0;
+ leftArm.rotation = 0.3;
+ var rightArm = characterContainer.attachAsset('soldier', {
anchorX: 0.5,
+ anchorY: 0.5,
+ width: 15,
+ height: 40
+ });
+ rightArm.x = 30;
+ rightArm.y = 0;
+ rightArm.rotation = -0.3;
+ // Sword (held in right hand)
+ var sword = characterContainer.attachAsset('soldier', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 12,
+ height: 50
+ });
+ sword.x = 35;
+ sword.y = -10;
+ sword.rotation = -0.2;
+ sword.tint = 0xC0C0C0; // Silver color
+ // Sword hilt
+ var swordHilt = characterContainer.attachAsset('soldier', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 20,
+ height: 8
+ });
+ swordHilt.x = 35;
+ swordHilt.y = 15;
+ swordHilt.rotation = -0.2;
+ swordHilt.tint = 0x8B4513; // Brown hilt
+ // Initial attributes for the unit
+ self.health = 120;
+ self.maxHealth = 120;
+ self.damage = 12;
+ self.armor = 6;
+ self.criticalChance = 0.15;
+ self.magicResist = 6;
+ self.mana = 60;
+ self.speed = 0.7;
+ self.range = 1; // Melee (adjacent cell)
+ self.name = "Soldier";
+ return self;
+});
+var Ranger = Unit.expand(function () {
+ var self = Unit.call(this, 1);
+ // Remove default unit graphics
+ self.removeChildren();
+ // Create pixelart character container
+ var characterContainer = new Container();
+ self.addChild(characterContainer);
+ // Body (light blue ranger outfit)
+ var body = characterContainer.attachAsset('ranger', {
+ anchorX: 0.5,
anchorY: 0.5
});
- buttonBackground.width = 500;
- buttonBackground.height = 150;
- var isMaxLevel = self.tower.level >= self.tower.maxLevel;
- // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
- var baseUpgradeCost = getTowerCost(self.tower.id);
- var upgradeCost;
- if (isMaxLevel) {
- upgradeCost = 0;
- } else if (self.tower.level === self.tower.maxLevel - 1) {
- upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
- } else {
- upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
- }
- buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
- var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
- size: 60,
- fill: 0xFFFFFF,
- weight: 800
+ body.width = 60;
+ body.height = 80;
+ body.y = 10;
+ body.tint = 0x87CEEB; // Sky blue to match ranger theme
+ // Head with hood
+ var head = characterContainer.attachAsset('ranger', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 30,
+ height: 30
});
- buttonText.anchor.set(0.5, 0.5);
- upgradeButton.addChild(buttonText);
- var sellButton = new Container();
- buttonsContainer.addChild(sellButton);
- var sellButtonBackground = sellButton.attachAsset('notification', {
+ head.y = -35;
+ head.tint = 0xFFDBB5; // Skin color
+ // Hood
+ var hood = characterContainer.attachAsset('ranger', {
anchorX: 0.5,
+ anchorY: 0.5,
+ width: 50,
+ height: 40
+ });
+ hood.y = -40;
+ hood.tint = 0x9d9dff;
+ // Bow (left side)
+ var bow = characterContainer.attachAsset('ranger', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 50,
+ height: 15
+ });
+ bow.x = -30;
+ bow.y = 0;
+ bow.rotation = 1.57; // 90 degrees
+ bow.tint = 0x8B4513;
+ // Bowstring
+ var bowstring = characterContainer.attachAsset('ranger', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 2,
+ height: 45
+ });
+ bowstring.x = -30;
+ bowstring.y = 0;
+ bowstring.rotation = 1.57;
+ bowstring.tint = 0xFFFFFF; // White string
+ // Arrow on right hand
+ var arrow = characterContainer.attachAsset('ranger', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 4,
+ height: 35
+ });
+ arrow.x = 25;
+ arrow.y = -5;
+ arrow.rotation = -0.1;
+ arrow.tint = 0x654321; // Dark brown arrow
+ // Arrow tip
+ var arrowTip = characterContainer.attachAsset('ranger', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 8,
+ height: 8
+ });
+ arrowTip.x = 25;
+ arrowTip.y = -22;
+ arrowTip.rotation = 0.785; // 45 degrees
+ arrowTip.tint = 0x808080; // Grey tip
+ // Initial attributes for the unit
+ self.health = 170;
+ self.maxHealth = 170;
+ self.damage = 17;
+ self.armor = 10;
+ self.criticalChance = 0.22;
+ self.magicResist = 10;
+ self.mana = 80;
+ self.speed = 0.7;
+ self.range = 2; // Example: short range (2 cells)
+ self.name = "Ranger";
+ return self;
+});
+var Paladin = Unit.expand(function () {
+ var self = Unit.call(this, 1);
+ // Remove default unit graphics
+ self.removeChildren();
+ // Create pixelart character container
+ var characterContainer = new Container();
+ self.addChild(characterContainer);
+ // Body (blue armor)
+ var body = characterContainer.attachAsset('paladin', {
+ anchorX: 0.5,
anchorY: 0.5
});
- sellButtonBackground.width = 500;
- sellButtonBackground.height = 150;
- sellButtonBackground.tint = 0xCC0000;
- var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
- var sellValue = getTowerSellValue(totalInvestment);
- var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
- size: 60,
- fill: 0xFFFFFF,
- weight: 800
+ body.width = 75;
+ body.height = 75;
+ body.y = 10;
+ body.tint = 0x4169E1; // Royal blue to match armor theme
+ // Head
+ var head = characterContainer.attachAsset('paladin', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 35,
+ height: 35
});
- sellButtonText.anchor.set(0.5, 0.5);
- sellButton.addChild(sellButtonText);
- upgradeButton.y = -85;
- sellButton.y = 85;
- var closeButton = new Container();
- self.addChild(closeButton);
- var closeBackground = closeButton.attachAsset('notification', {
+ head.y = -35;
+ head.tint = 0xFFDBB5; // Skin color
+ // Helmet
+ var helmet = characterContainer.attachAsset('paladin', {
anchorX: 0.5,
+ anchorY: 0.5,
+ width: 45,
+ height: 25
+ });
+ helmet.y = -45;
+ helmet.tint = 0x4444FF;
+ // Sword (right side)
+ var sword = characterContainer.attachAsset('paladin', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 15,
+ height: 60
+ });
+ sword.x = 35;
+ sword.y = -5;
+ sword.rotation = -0.2;
+ sword.tint = 0xC0C0C0;
+ // Sword pommel
+ var swordPommel = characterContainer.attachAsset('paladin', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 12,
+ height: 12
+ });
+ swordPommel.x = 35;
+ swordPommel.y = 20;
+ swordPommel.tint = 0xFFD700; // Gold pommel
+ // Initial attributes for the unit
+ self.health = 160;
+ self.maxHealth = 160;
+ self.damage = 16;
+ self.armor = 9;
+ self.criticalChance = 0.2;
+ self.magicResist = 9;
+ self.mana = 75;
+ self.speed = 0.7;
+ self.range = 1; // Example: short range (2 cells)
+ self.name = "Paladin";
+ return self;
+});
+var Knight = Unit.expand(function () {
+ var self = Unit.call(this, 1);
+ // Remove default unit graphics
+ self.removeChildren();
+ // Create pixelart character container
+ var characterContainer = new Container();
+ self.addChild(characterContainer);
+ // Body (darker green base)
+ var body = characterContainer.attachAsset('knight', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 70,
+ height: 70
+ });
+ body.y = 10;
+ body.tint = 0x228B22; // Forest green to match theme
+ // Head (round shape)
+ var head = characterContainer.attachAsset('knight', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 35,
+ height: 35
+ });
+ head.y = -35;
+ head.tint = 0x4da24d;
+ // Shield (left side)
+ var shield = characterContainer.attachAsset('knight', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 30,
+ height: 45
+ });
+ shield.x = -35;
+ shield.y = 0;
+ shield.tint = 0x666666;
+ // Sword (right side)
+ var sword = characterContainer.attachAsset('knight', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 10,
+ height: 45
+ });
+ sword.x = 30;
+ sword.y = -5;
+ sword.rotation = -0.15;
+ sword.tint = 0xC0C0C0; // Silver color
+ // Sword guard
+ var swordGuard = characterContainer.attachAsset('knight', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 18,
+ height: 6
+ });
+ swordGuard.x = 30;
+ swordGuard.y = 13;
+ swordGuard.rotation = -0.15;
+ swordGuard.tint = 0x808080; // Dark grey
+ // Initial attributes for the unit
+ self.health = 110;
+ self.maxHealth = 110;
+ self.damage = 11;
+ self.armor = 5;
+ self.criticalChance = 0.12;
+ self.magicResist = 5;
+ self.mana = 55;
+ self.speed = 0.7;
+ self.range = 1; // Melee (adjacent cell)
+ self.name = "Knight";
+ return self;
+});
+var King = Unit.expand(function () {
+ var self = Unit.call(this, 1);
+ // Remove default unit graphics
+ self.removeChildren();
+ // Create pixelart king character
+ var kingContainer = new Container();
+ self.addChild(kingContainer);
+ // King body (royal robe)
+ var body = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
anchorY: 0.5
});
- closeBackground.width = 90;
- closeBackground.height = 90;
- closeBackground.tint = 0xAA0000;
- var closeText = new Text2('X', {
- size: 68,
- fill: 0xFFFFFF,
- weight: 800
+ body.width = 80;
+ body.height = 100;
+ body.y = 15;
+ body.tint = 0x4B0082; // Royal purple
+ // King head (flesh tone)
+ var head = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 50,
+ height: 50
});
- closeText.anchor.set(0.5, 0.5);
- closeButton.addChild(closeText);
- closeButton.x = menuBackground.width / 2 - 57;
- closeButton.y = -menuBackground.height / 2 + 57;
- upgradeButton.down = function (x, y, obj) {
- if (self.tower.level >= self.tower.maxLevel) {
- var notification = game.addChild(new Notification("Tower is already at max level!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 50;
- return;
+ head.x = 0;
+ head.y = -40;
+ head.tint = 0xFFDBB5; // Skin color
+ // Crown base
+ var crownBase = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 60,
+ height: 20
+ });
+ crownBase.x = 0;
+ crownBase.y = -65;
+ crownBase.tint = 0xFFFF00; // Yellow
+ // Crown points (left)
+ var crownLeft = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 12,
+ height: 25
+ });
+ crownLeft.x = -20;
+ crownLeft.y = -75;
+ crownLeft.tint = 0xFFFF00; // Yellow
+ // Crown points (center - tallest)
+ var crownCenter = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 15,
+ height: 35
+ });
+ crownCenter.x = 0;
+ crownCenter.y = -80;
+ crownCenter.tint = 0xFFFF00; // Yellow
+ // Crown points (right)
+ var crownRight = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 12,
+ height: 25
+ });
+ crownRight.x = 20;
+ crownRight.y = -75;
+ crownRight.tint = 0xFFFF00; // Yellow
+ // Crown jewel (center)
+ var crownJewel = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 8,
+ height: 8
+ });
+ crownJewel.x = 0;
+ crownJewel.y = -80;
+ crownJewel.tint = 0xFF0000; // Red jewel
+ // Beard
+ var beard = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 30,
+ height: 20
+ });
+ beard.x = 0;
+ beard.y = -25;
+ beard.tint = 0xC0C0C0; // Grey beard
+ // Sword (right hand)
+ var sword = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 15,
+ height: 80
+ });
+ sword.x = 45;
+ sword.y = 0;
+ sword.rotation = -0.2;
+ sword.tint = 0xC0C0C0; // Silver blade
+ // Sword hilt
+ var swordHilt = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 25,
+ height: 12
+ });
+ swordHilt.x = 45;
+ swordHilt.y = 35;
+ swordHilt.rotation = -0.2;
+ swordHilt.tint = 0xC0C0C0; // Silver hilt
+ // Royal cape/cloak
+ var cape = kingContainer.attachAsset('king', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 70,
+ height: 80
+ });
+ cape.x = -5;
+ cape.y = 20;
+ cape.tint = 0x8B0000; // Dark red cape
+ // Initial attributes for the king (like a very strong unit)
+ self.health = 500;
+ self.maxHealth = 500;
+ self.damage = 100;
+ self.armor = 10;
+ self.criticalChance = 0.05;
+ self.magicResist = 8;
+ self.mana = 100;
+ self.speed = 0.5; // Slower attack speed
+ self.range = 1; // Melee range
+ self.name = "King";
+ self.fireRate = 120; // Slower fire rate
+ // Override takeDamage to handle king-specific game over
+ self.takeDamage = function (damage) {
+ self.health -= damage;
+ if (self.health < 0) {
+ self.health = 0;
}
- if (self.tower.upgrade()) {
- // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
- var baseUpgradeCost = getTowerCost(self.tower.id);
- if (self.tower.level >= self.tower.maxLevel) {
- upgradeCost = 0;
- } else if (self.tower.level === self.tower.maxLevel - 1) {
- upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
- } else {
- upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
- }
- statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
- buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
- var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
- var sellValue = Math.floor(totalInvestment * 0.6);
- sellButtonText.setText('Sell: +' + sellValue + ' gold');
- if (self.tower.level >= self.tower.maxLevel) {
- buttonBackground.tint = 0x888888;
- buttonText.setText('Max Level');
- }
- var rangeCircle = null;
- for (var i = 0; i < game.children.length; i++) {
- if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
- rangeCircle = game.children[i];
- break;
- }
- }
- if (rangeCircle) {
- var rangeGraphics = rangeCircle.children[0];
- rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
- } else {
- var newRangeIndicator = new Container();
- newRangeIndicator.isTowerRange = true;
- newRangeIndicator.tower = self.tower;
- game.addChildAt(newRangeIndicator, 0);
- newRangeIndicator.x = self.tower.x;
- newRangeIndicator.y = self.tower.y;
- var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
- rangeGraphics.alpha = 0.3;
- }
- tween(self, {
- scaleX: 1.05,
- scaleY: 1.05
- }, {
- duration: 100,
- easing: tween.easeOut,
- onFinish: function onFinish() {
- tween(self, {
- scaleX: 1,
- scaleY: 1
- }, {
- duration: 100,
- easing: tween.easeIn
- });
- }
- });
+ self.updateHealthBar();
+ if (self.health <= 0) {
+ LK.effects.flashScreen(0xFF0000, 500);
+ LK.showGameOver();
+ return true;
}
+ return false;
};
- sellButton.down = function (x, y, obj) {
- var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
- var sellValue = getTowerSellValue(totalInvestment);
- setGold(gold + sellValue);
- var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 50;
- var gridX = self.tower.gridX;
- var gridY = self.tower.gridY;
- for (var i = 0; i < 2; i++) {
- for (var j = 0; j < 2; j++) {
- var cell = grid.getCell(gridX + i, gridY + j);
- if (cell) {
- cell.type = 0;
- var towerIndex = cell.towersInRange.indexOf(self.tower);
- if (towerIndex !== -1) {
- cell.towersInRange.splice(towerIndex, 1);
- }
+ // Override update - King can move if it's the only unit left
+ self.update = function () {
+ // Check if king is the only player unit left on the battlefield
+ var playerUnitsOnGrid = 0;
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ if (grid[y] && grid[y][x] && grid[y][x].unit) {
+ playerUnitsOnGrid++;
}
}
}
- if (selectedTower === self.tower) {
- selectedTower = null;
- }
- var towerIndex = towers.indexOf(self.tower);
- if (towerIndex !== -1) {
- towers.splice(towerIndex, 1);
- }
- towerLayer.removeChild(self.tower);
- grid.pathFind();
- grid.renderDebug();
- self.destroy();
- for (var i = 0; i < game.children.length; i++) {
- if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
- game.removeChild(game.children[i]);
- break;
+ // If king is the only unit left, allow movement
+ if (playerUnitsOnGrid === 1) {
+ // Only initialize and update GridMovement if unit is placed on grid
+ if (self.gridX >= 0 && self.gridY >= 0) {
+ if (!self.gridMovement) {
+ self.gridMovement = new GridMovement(self, grid);
+ }
+ self.gridMovement.update();
}
}
+ // King stays in place if other units exist
};
- closeButton.down = function (x, y, obj) {
- hideUpgradeMenu(self);
- selectedTower = null;
- grid.renderDebug();
- };
- self.update = function () {
- if (self.tower.level >= self.tower.maxLevel) {
- if (buttonText.text !== 'Max Level') {
- buttonText.setText('Max Level');
- buttonBackground.tint = 0x888888;
- }
- return;
+ // Override showHealthBar for king-specific styling
+ self.showHealthBar = function () {
+ if (!self.healthBar) {
+ self.healthBar = new HealthBar(150, 10); // Larger health bar for king
+ self.healthBar.x = -75; // Position relative to the king's center
+ self.healthBar.y = -110; // Position above the king (higher due to crown)
+ self.addChild(self.healthBar);
}
- // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
- var baseUpgradeCost = getTowerCost(self.tower.id);
- var currentUpgradeCost;
- if (self.tower.level >= self.tower.maxLevel) {
- currentUpgradeCost = 0;
- } else if (self.tower.level === self.tower.maxLevel - 1) {
- currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
- } else {
- currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
- }
- var canAfford = gold >= currentUpgradeCost;
- buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
- var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
- if (buttonText.text !== newText) {
- buttonText.setText(newText);
- }
+ self.healthBar.visible = true;
+ self.healthBar.updateHealth(self.health, self.maxHealth);
};
return self;
});
-var WaveIndicator = Container.expand(function () {
- var self = Container.call(this);
- self.gameStarted = false;
- self.waveMarkers = [];
- self.waveTypes = [];
- self.enemyCounts = [];
- self.indicatorWidth = 0;
- self.lastBossType = null; // Track the last boss type to avoid repeating
- var blockWidth = 400;
- var totalBlocksWidth = blockWidth * totalWaves;
- var startMarker = new Container();
- var startBlock = startMarker.attachAsset('notification', {
+var CrossbowTower = Unit.expand(function () {
+ var self = Unit.call(this, 1);
+ // Remove default unit graphics
+ self.removeChildren();
+ // Create pixelart crossbow tower structure
+ var towerContainer = new Container();
+ self.addChild(towerContainer);
+ // Tower base (stone foundation)
+ var towerBase = towerContainer.attachAsset('crossbow_tower', {
anchorX: 0.5,
anchorY: 0.5
});
- startBlock.width = blockWidth - 10;
- startBlock.height = 70 * 2;
- startBlock.tint = 0x00AA00;
- // Add shadow for start text
- var startTextShadow = new Text2("Start Game", {
- size: 50,
- fill: 0x000000,
- weight: 800
+ towerBase.width = 80;
+ towerBase.height = 60;
+ towerBase.y = 30;
+ towerBase.tint = 0x696969; // Dark grey stone
+ // Tower middle section
+ var towerMiddle = towerContainer.attachAsset('crossbow_tower', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 70,
+ height: 50
});
- startTextShadow.anchor.set(0.5, 0.5);
- startTextShadow.x = 4;
- startTextShadow.y = 4;
- startMarker.addChild(startTextShadow);
- var startText = new Text2("Start Game", {
- size: 50,
- fill: 0xFFFFFF,
- weight: 800
+ towerMiddle.y = -10;
+ towerMiddle.tint = 0x808080; // Grey stone
+ // Tower top platform
+ var towerTop = towerContainer.attachAsset('crossbow_tower', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 90,
+ height: 20
});
- startText.anchor.set(0.5, 0.5);
- startMarker.addChild(startText);
- startMarker.x = -self.indicatorWidth;
- self.addChild(startMarker);
- self.waveMarkers.push(startMarker);
- startMarker.down = function () {
- if (!self.gameStarted) {
- self.gameStarted = true;
- currentWave = 0;
- waveTimer = nextWaveTime;
- startBlock.tint = 0x00FF00;
- startText.setText("Started!");
- startTextShadow.setText("Started!");
- // Make sure shadow position remains correct after text change
- startTextShadow.x = 4;
- startTextShadow.y = 4;
- var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 150;
- }
- };
- for (var i = 0; i < totalWaves; i++) {
- var marker = new Container();
- var block = marker.attachAsset('notification', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- block.width = blockWidth - 10;
- block.height = 70 * 2;
- // --- Begin new unified wave logic ---
- var waveType = "normal";
- var enemyType = "normal";
- var enemyCount = 10;
- var isBossWave = (i + 1) % 10 === 0;
- // Ensure all types appear in early waves
- if (i === 0) {
- block.tint = 0xAAAAAA;
- waveType = "Normal";
- enemyType = "normal";
- enemyCount = 10;
- } else if (i === 1) {
- block.tint = 0x00AAFF;
- waveType = "Fast";
- enemyType = "fast";
- enemyCount = 10;
- } else if (i === 2) {
- block.tint = 0xAA0000;
- waveType = "Immune";
- enemyType = "immune";
- enemyCount = 10;
- } else if (i === 3) {
- block.tint = 0xFFFF00;
- waveType = "Flying";
- enemyType = "flying";
- enemyCount = 10;
- } else if (i === 4) {
- block.tint = 0xFF00FF;
- waveType = "Swarm";
- enemyType = "swarm";
- enemyCount = 30;
- } else if (isBossWave) {
- // Boss waves: cycle through all boss types, last boss is always flying
- var bossTypes = ['normal', 'fast', 'immune', 'flying'];
- var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
- if (i === totalWaves - 1) {
- // Last boss is always flying
- enemyType = 'flying';
- waveType = "Boss Flying";
- block.tint = 0xFFFF00;
- } else {
- enemyType = bossTypes[bossTypeIndex % bossTypes.length];
- switch (enemyType) {
- case 'normal':
- block.tint = 0xAAAAAA;
- waveType = "Boss Normal";
- break;
- case 'fast':
- block.tint = 0x00AAFF;
- waveType = "Boss Fast";
- break;
- case 'immune':
- block.tint = 0xAA0000;
- waveType = "Boss Immune";
- break;
- case 'flying':
- block.tint = 0xFFFF00;
- waveType = "Boss Flying";
- break;
- }
- }
- enemyCount = 1;
- // Make the wave indicator for boss waves stand out
- // Set boss wave color to the color of the wave type
- switch (enemyType) {
- case 'normal':
- block.tint = 0xAAAAAA;
- break;
- case 'fast':
- block.tint = 0x00AAFF;
- break;
- case 'immune':
- block.tint = 0xAA0000;
- break;
- case 'flying':
- block.tint = 0xFFFF00;
- break;
- default:
- block.tint = 0xFF0000;
- break;
- }
- } else if ((i + 1) % 5 === 0) {
- // Every 5th non-boss wave is fast
- block.tint = 0x00AAFF;
- waveType = "Fast";
- enemyType = "fast";
- enemyCount = 10;
- } else if ((i + 1) % 4 === 0) {
- // Every 4th non-boss wave is immune
- block.tint = 0xAA0000;
- waveType = "Immune";
- enemyType = "immune";
- enemyCount = 10;
- } else if ((i + 1) % 7 === 0) {
- // Every 7th non-boss wave is flying
- block.tint = 0xFFFF00;
- waveType = "Flying";
- enemyType = "flying";
- enemyCount = 10;
- } else if ((i + 1) % 3 === 0) {
- // Every 3rd non-boss wave is swarm
- block.tint = 0xFF00FF;
- waveType = "Swarm";
- enemyType = "swarm";
- enemyCount = 30;
- } else {
- block.tint = 0xAAAAAA;
- waveType = "Normal";
- enemyType = "normal";
- enemyCount = 10;
- }
- // --- End new unified wave logic ---
- // Mark boss waves with a special visual indicator
- if (isBossWave && enemyType !== 'swarm') {
- // Add a crown or some indicator to the wave marker for boss waves
- var bossIndicator = marker.attachAsset('towerLevelIndicator', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- bossIndicator.width = 30;
- bossIndicator.height = 30;
- bossIndicator.tint = 0xFFD700; // Gold color
- bossIndicator.y = -block.height / 2 - 15;
- // Change the wave type text to indicate boss
- waveType = "BOSS";
- }
- // Store the wave type and enemy count
- self.waveTypes[i] = enemyType;
- self.enemyCounts[i] = enemyCount;
- // Add shadow for wave type - 30% smaller than before
- var waveTypeShadow = new Text2(waveType, {
- size: 56,
- fill: 0x000000,
- weight: 800
- });
- waveTypeShadow.anchor.set(0.5, 0.5);
- waveTypeShadow.x = 4;
- waveTypeShadow.y = 4;
- marker.addChild(waveTypeShadow);
- // Add wave type text - 30% smaller than before
- var waveTypeText = new Text2(waveType, {
- size: 56,
- fill: 0xFFFFFF,
- weight: 800
- });
- waveTypeText.anchor.set(0.5, 0.5);
- waveTypeText.y = 0;
- marker.addChild(waveTypeText);
- // Add shadow for wave number - 20% larger than before
- var waveNumShadow = new Text2((i + 1).toString(), {
- size: 48,
- fill: 0x000000,
- weight: 800
- });
- waveNumShadow.anchor.set(1.0, 1.0);
- waveNumShadow.x = blockWidth / 2 - 16 + 5;
- waveNumShadow.y = block.height / 2 - 12 + 5;
- marker.addChild(waveNumShadow);
- // Main wave number text - 20% larger than before
- var waveNum = new Text2((i + 1).toString(), {
- size: 48,
- fill: 0xFFFFFF,
- weight: 800
- });
- waveNum.anchor.set(1.0, 1.0);
- waveNum.x = blockWidth / 2 - 16;
- waveNum.y = block.height / 2 - 12;
- marker.addChild(waveNum);
- marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
- self.addChild(marker);
- self.waveMarkers.push(marker);
- }
- // Get wave type for a specific wave number
- self.getWaveType = function (waveNumber) {
- if (waveNumber < 1 || waveNumber > totalWaves) {
- return "normal";
- }
- // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType
- // then we should return a different boss type
- var waveType = self.waveTypes[waveNumber - 1];
- return waveType;
- };
- // Get enemy count for a specific wave number
- self.getEnemyCount = function (waveNumber) {
- if (waveNumber < 1 || waveNumber > totalWaves) {
- return 10;
- }
- return self.enemyCounts[waveNumber - 1];
- };
- // Get display name for a wave type
- self.getWaveTypeName = function (waveNumber) {
- var type = self.getWaveType(waveNumber);
- var typeName = type.charAt(0).toUpperCase() + type.slice(1);
- // Add boss prefix for boss waves (every 10th wave)
- if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
- typeName = "BOSS";
- }
- return typeName;
- };
- self.positionIndicator = new Container();
- var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
+ towerTop.y = -40;
+ towerTop.tint = 0x5C4033; // Brown wood platform
+ // Crossbow mechanism (horizontal)
+ var crossbowBase = towerContainer.attachAsset('crossbow_tower', {
anchorX: 0.5,
- anchorY: 0.5
+ anchorY: 0.5,
+ width: 60,
+ height: 15
});
- indicator.width = blockWidth - 10;
- indicator.height = 16;
- indicator.tint = 0xffad0e;
- indicator.y = -65;
- var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
+ crossbowBase.y = -55;
+ crossbowBase.rotation = 0;
+ crossbowBase.tint = 0x8B4513; // Saddle brown
+ // Crossbow arms (vertical)
+ var crossbowArms = towerContainer.attachAsset('crossbow_tower', {
anchorX: 0.5,
- anchorY: 0.5
+ anchorY: 0.5,
+ width: 8,
+ height: 50
});
- indicator2.width = blockWidth - 10;
- indicator2.height = 16;
- indicator2.tint = 0xffad0e;
- indicator2.y = 65;
- var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
+ crossbowArms.y = -55;
+ crossbowArms.rotation = 1.57; // 90 degrees
+ crossbowArms.tint = 0x654321; // Dark brown
+ // Crossbow string
+ var crossbowString = towerContainer.attachAsset('crossbow_tower', {
anchorX: 0.5,
- anchorY: 0.5
+ anchorY: 0.5,
+ width: 2,
+ height: 48
});
- leftWall.width = 16;
- leftWall.height = 146;
- leftWall.tint = 0xffad0e;
- leftWall.x = -(blockWidth - 16) / 2;
- var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
+ crossbowString.y = -55;
+ crossbowString.rotation = 1.57;
+ crossbowString.tint = 0xDDDDDD; // Light grey string
+ // Loaded bolt
+ var loadedBolt = towerContainer.attachAsset('crossbow_tower', {
anchorX: 0.5,
- anchorY: 0.5
+ anchorY: 0.5,
+ width: 4,
+ height: 30
});
- rightWall.width = 16;
- rightWall.height = 146;
- rightWall.tint = 0xffad0e;
- rightWall.x = (blockWidth - 16) / 2;
- self.addChild(self.positionIndicator);
+ loadedBolt.x = 0;
+ loadedBolt.y = -55;
+ loadedBolt.rotation = 0;
+ loadedBolt.tint = 0x4B0082; // Dark purple bolt
+ // Initial attributes for the crossbow tower
+ self.health = 200;
+ self.maxHealth = 200;
+ self.damage = 25; // Higher damage than regular ranged units
+ self.armor = 12;
+ self.criticalChance = 0.25; // High critical chance
+ self.magicResist = 10;
+ self.mana = 0; // No mana
+ self.speed = 0.8; // Slower attack speed
+ self.range = 4; // Long range
+ self.name = "Crossbow Tower";
+ self.isStructure = true; // Mark as structure
+ self.fireRate = 90; // Slower fire rate than mobile units
+ // Override update to prevent movement but allow shooting
self.update = function () {
- var progress = waveTimer / nextWaveTime;
- var moveAmount = (progress + currentWave) * blockWidth;
- for (var i = 0; i < self.waveMarkers.length; i++) {
- var marker = self.waveMarkers[i];
- marker.x = -moveAmount + i * blockWidth;
+ // Towers don't move but can attack
+ if (gameState === 'battle' && self.gridX >= 0 && self.gridY >= 0) {
+ self.showHealthBar();
+ self.updateHealthBar();
+ self.showStarIndicator();
}
- self.positionIndicator.x = 0;
- for (var i = 0; i < totalWaves + 1; i++) {
- var marker = self.waveMarkers[i];
- if (i === 0) {
- continue;
- }
- var block = marker.children[0];
- if (i - 1 < currentWave) {
- block.alpha = .5;
- }
+ };
+ // Override shoot to create arrow projectiles
+ self.shoot = function (target) {
+ if (!self.canShoot()) {
+ return null;
}
- self.handleWaveProgression = function () {
- if (!self.gameStarted) {
- return;
+ self.lastShot = LK.ticks;
+ // Create arrow projectile
+ var bullet = new Bullet(self.damage, target);
+ bullet.x = self.x;
+ bullet.y = self.y - 55; // Shoot from crossbow position
+ // Crossbow towers shoot arrows
+ bullet.bulletType = 'arrow';
+ bullet.createGraphics();
+ // Add shooting animation - rotate crossbow slightly
+ tween(crossbowBase, {
+ rotation: -0.1
+ }, {
+ duration: 100,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ tween(crossbowBase, {
+ rotation: 0
+ }, {
+ duration: 200,
+ easing: tween.easeIn
+ });
}
- if (currentWave < totalWaves) {
- waveTimer++;
- if (waveTimer >= nextWaveTime) {
- waveTimer = 0;
- currentWave++;
- waveInProgress = true;
- waveSpawned = false;
- if (currentWave != 1) {
- var waveType = self.getWaveTypeName(currentWave);
- var enemyCount = self.getEnemyCount(currentWave);
- var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 150;
- }
- }
- }
- };
- self.handleWaveProgression();
+ });
+ return bullet;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
- backgroundColor: 0x333333
+ backgroundColor: 0x2F4F2F
});
/****
* Game Code
****/
-var isHidingUpgradeMenu = false;
-function hideUpgradeMenu(menu) {
- if (isHidingUpgradeMenu) {
- return;
- }
- isHidingUpgradeMenu = true;
- tween(menu, {
- y: 2732 + 225
- }, {
- duration: 150,
- easing: tween.easeIn,
- onFinish: function onFinish() {
- menu.destroy();
- isHidingUpgradeMenu = false;
- }
- });
-}
-var CELL_SIZE = 76;
-var pathId = 1;
-var maxScore = 0;
+var GRID_COLS = 9;
+var GRID_ROWS = 9;
+var GRID_CELL_SIZE = 227; // 2048 / 9 ≈ 227
+var GRID_START_X = (2048 - GRID_COLS * GRID_CELL_SIZE) / 2;
+var GRID_START_Y = 100;
+var PLAYABLE_HEIGHT = 2400; // Define playable game area
var enemies = [];
-var towers = [];
+var units = [];
var bullets = [];
-var defenses = [];
-var selectedTower = null;
-var gold = 80;
-var lives = 20;
-var score = 0;
-var currentWave = 0;
-var totalWaves = 50;
-var waveTimer = 0;
-var waveInProgress = false;
-var waveSpawned = false;
-var nextWaveTime = 12000 / 2;
-var sourceTower = null;
-var enemiesToSpawn = 10; // Default number of enemies per wave
-var goldText = new Text2('Gold: ' + gold, {
- size: 60,
- fill: 0xFFD700,
- weight: 800
-});
-goldText.anchor.set(0.5, 0.5);
-var livesText = new Text2('Lives: ' + lives, {
- size: 60,
- fill: 0x00FF00,
- weight: 800
-});
-livesText.anchor.set(0.5, 0.5);
-var scoreText = new Text2('Score: ' + score, {
- size: 60,
- fill: 0xFF0000,
- weight: 800
-});
-scoreText.anchor.set(0.5, 0.5);
-var topMargin = 50;
-var centerX = 2048 / 2;
-var spacing = 400;
-LK.gui.top.addChild(goldText);
-LK.gui.top.addChild(livesText);
-LK.gui.top.addChild(scoreText);
-livesText.x = 0;
-livesText.y = topMargin;
-goldText.x = -spacing;
-goldText.y = topMargin;
-scoreText.x = spacing;
-scoreText.y = topMargin;
-function updateUI() {
- goldText.setText('Gold: ' + gold);
- livesText.setText('Lives: ' + lives);
- scoreText.setText('Score: ' + score);
+var enemyProjectiles = [];
+var bench = [];
+var grid = [];
+var benchSlots = [];
+var gold = 999;
+var wave = 1;
+var enemiesSpawned = 0;
+var enemiesPerWave = 5;
+var waveDelay = 0;
+var draggedUnit = null;
+var draggedFromGrid = false;
+var gameState = 'planning'; // 'planning' or 'battle'
+var goldText; // Declare goldText variable
+var cellReservations = {}; // Track reserved cells during movement
+var entityPositions = {}; // Track exact entity positions to prevent overlap
+var movementQueue = []; // Queue movements to prevent simultaneous conflicts
+var playerLevel = 1; // Player's current level
+var maxUnitsOnField = 2; // Starting with 2 units max
+var maxStructuresOnField = 2; // Starting with 2 structures max
+var structuresOnField = 0; // Track structures placed
+var unitsOnField = 0; // Track regular units placed
+// Helper function to register entity position
+function registerEntityPosition(entity) {
+ var posKey = entity.gridX + ',' + entity.gridY;
+ entityPositions[posKey] = entity;
}
-function setGold(value) {
- gold = value;
- updateUI();
+// Helper function to unregister entity position
+function unregisterEntityPosition(entity, oldGridX, oldGridY) {
+ var posKey = oldGridX + ',' + oldGridY;
+ if (entityPositions[posKey] === entity) {
+ delete entityPositions[posKey];
+ }
}
-var debugLayer = new Container();
-var towerLayer = new Container();
-// Create three separate layers for enemy hierarchy
-var enemyLayerBottom = new Container(); // For normal enemies
-var enemyLayerMiddle = new Container(); // For shadows
-var enemyLayerTop = new Container(); // For flying enemies
-var enemyLayer = new Container(); // Main container to hold all enemy layers
-// Add layers in correct order (bottom first, then middle for shadows, then top)
-enemyLayer.addChild(enemyLayerBottom);
-enemyLayer.addChild(enemyLayerMiddle);
-enemyLayer.addChild(enemyLayerTop);
-var grid = new Grid(24, 29 + 6);
-grid.x = 150;
-grid.y = 200 - CELL_SIZE * 4;
-grid.pathFind();
-grid.renderDebug();
-debugLayer.addChild(grid);
-game.addChild(debugLayer);
-game.addChild(towerLayer);
-game.addChild(enemyLayer);
-var offset = 0;
-var towerPreview = new TowerPreview();
-game.addChild(towerPreview);
-towerPreview.visible = false;
-var isDragging = false;
-function wouldBlockPath(gridX, gridY) {
- var cells = [];
- for (var i = 0; i < 2; i++) {
- for (var j = 0; j < 2; j++) {
- var cell = grid.getCell(gridX + i, gridY + j);
- if (cell) {
- cells.push({
- cell: cell,
- originalType: cell.type
- });
- cell.type = 1;
+// Helper function to check if position is occupied by any entity
+function isPositionOccupied(gridX, gridY, excludeEntity) {
+ var posKey = gridX + ',' + gridY;
+ var occupant = entityPositions[posKey];
+ return occupant && occupant !== excludeEntity;
+}
+// Function to calculate level up cost
+function getLevelUpCost(level) {
+ return level * 4; // Cost increases by 4 gold per level
+}
+// Function to count units on field
+function countUnitsOnField() {
+ structuresOnField = 0;
+ unitsOnField = 0;
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ if (grid[y] && grid[y][x] && grid[y][x].unit) {
+ var unit = grid[y][x].unit;
+ if (unit.name === "King") {
+ continue; // Don't count king
+ }
+ if (unit.isStructure) {
+ structuresOnField++;
+ } else {
+ unitsOnField++;
+ }
}
}
}
- var blocked = grid.pathFind();
- for (var i = 0; i < cells.length; i++) {
- cells[i].cell.type = cells[i].originalType;
+}
+// Function to check if can place more units
+function canPlaceMoreUnits(isStructure) {
+ countUnitsOnField();
+ if (isStructure) {
+ return structuresOnField < maxStructuresOnField;
+ } else {
+ return unitsOnField < maxUnitsOnField;
}
- grid.pathFind();
- grid.renderDebug();
- return blocked;
}
-function getTowerCost(towerType) {
- var cost = 5;
- switch (towerType) {
- case 'rapid':
- cost = 15;
- break;
- case 'sniper':
- cost = 25;
- break;
- case 'splash':
- cost = 35;
- break;
- case 'slow':
- cost = 45;
- break;
- case 'poison':
- cost = 55;
- break;
+// Function to level up player
+function levelUpPlayer() {
+ var cost = getLevelUpCost(playerLevel);
+ if (gold >= cost) {
+ gold -= cost;
+ playerLevel++;
+ maxUnitsOnField++; // Increase unit limit by 1
+ maxStructuresOnField++; // Increase structure limit by 1
+ goldText.setText('Gold: ' + gold);
+ levelText.setText('Level: ' + playerLevel);
+ maxUnitsText.setText('Units: ' + maxUnitsOnField + ' | Structures: ' + maxStructuresOnField);
+ // Level up the king
+ levelUpKing();
+ // Play purchase sound
+ LK.getSound('purchase').play();
+ // Visual effect
+ LK.effects.flashScreen(0xFFD700, 500);
}
- return cost;
}
-function getTowerSellValue(totalValue) {
- return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
+// Function to level up king
+function levelUpKing() {
+ if (kings.length > 0) {
+ var king = kings[0];
+ // Increase king stats
+ king.health = Math.floor(king.health * 1.1);
+ king.maxHealth = Math.floor(king.maxHealth * 1.1);
+ king.damage = Math.floor(king.damage * 1.1);
+ king.armor = Math.floor(king.armor * 1.1);
+ king.magicResist = Math.floor(king.magicResist * 1.1);
+ // Increase king scale by 10%
+ var currentScale = king.scaleX;
+ var newScale = currentScale * 1.1;
+ tween(king, {
+ scaleX: newScale,
+ scaleY: newScale
+ }, {
+ duration: 500,
+ easing: tween.easeOut
+ });
+ // Update health bar
+ king.updateHealthBar();
+ // Flash effect
+ LK.effects.flashObject(king, 0xFFD700, 800);
+ }
}
-function placeTower(gridX, gridY, towerType) {
- var towerCost = getTowerCost(towerType);
- if (gold >= towerCost) {
- var tower = new Tower(towerType || 'default');
- tower.placeOnGrid(gridX, gridY);
- towerLayer.addChild(tower);
- towers.push(tower);
- setGold(gold - towerCost);
- grid.pathFind();
- grid.renderDebug();
- return true;
- } else {
- var notification = game.addChild(new Notification("Not enough gold!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 50;
- return false;
+// AttackInfo display removed
+// Double-tap detection variables
+var lastTapTime = 0;
+var lastTapX = 0;
+var lastTapY = 0;
+var doubleTapDelay = 300; // 300ms window for double tap
+var doubleTapDistance = 50; // Max distance between taps
+// Create grid slots
+for (var y = 0; y < GRID_ROWS; y++) {
+ grid[y] = [];
+ for (var x = 0; x < GRID_COLS; x++) {
+ var gridSlot = game.addChild(new GridSlot(0, x, y)); // Using lane 0 for all slots
+ gridSlot.x = GRID_START_X + x * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
+ gridSlot.y = GRID_START_Y + y * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
+ grid[y][x] = gridSlot;
}
}
-game.down = function (x, y, obj) {
- var upgradeMenuVisible = game.children.some(function (child) {
- return child instanceof UpgradeMenu;
- });
- if (upgradeMenuVisible) {
- return;
+// Create king at bottom center of grid
+var kings = [];
+var king = game.addChild(new King());
+// Place king at center column (column 4 for 9x9 grid)
+var kingGridX = 4;
+var kingGridY = GRID_ROWS - 1;
+king.x = GRID_START_X + kingGridX * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
+king.y = GRID_START_Y + kingGridY * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
+king.gridX = kingGridX;
+king.gridY = kingGridY;
+king.lane = 0;
+// King already has baseScale from Unit class, ensure it's applied
+king.updateUnitScale();
+// Place king on the grid slot
+grid[kingGridY][kingGridX].unit = king;
+king.showHealthBar(); // Show health bar for king from start
+kings.push(king);
+// Register king position
+registerEntityPosition(king);
+// Create bench area background
+var benchAreaBg = game.addChild(LK.getAsset('bench_area_bg', {
+ anchorX: 0.5,
+ anchorY: 0.5
+}));
+benchAreaBg.x = 1024; // Center of screen
+benchAreaBg.y = PLAYABLE_HEIGHT - 120; // Move bench up by 40px
+benchAreaBg.alpha = 0.7;
+// Create bench slots
+var benchTotalWidth = 10 * 120 + 9 * 40; // 10 slots of 120px width with 40px spacing
+var benchStartX = (2048 - benchTotalWidth) / 2 + 60; // Center horizontally
+for (var i = 0; i < 10; i++) {
+ var benchSlot = game.addChild(new BenchSlot(i));
+ benchSlot.x = benchStartX + i * 160;
+ benchSlot.y = PLAYABLE_HEIGHT - 120; // Move bench up by 40px
+ benchSlots[i] = benchSlot;
+}
+// Shop configuration
+var shopSlots = [];
+var unitPool = [1, 1, 1, 1, 1, 2, 2, 2, 2, 2]; // Pool of available unit tiers
+var refreshCost = 2;
+// Calculate shop area dimensions
+var benchBottomY = PLAYABLE_HEIGHT - 120 + 100; // Bench center + half height
+var screenBottomY = 2732; // Full screen height
+var shopAreaHeight = screenBottomY - benchBottomY;
+var shopCenterY = benchBottomY + shopAreaHeight / 2;
+// Create shop area background
+var shopAreaBg = game.addChild(LK.getAsset('shop_area_bg', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 2048,
+ height: shopAreaHeight
+}));
+shopAreaBg.x = 1024; // Center of screen
+shopAreaBg.y = shopCenterY; // Center vertically in available space
+shopAreaBg.alpha = 0.6;
+// Create shop slots
+var shopTotalWidth = 5 * 200 + 4 * 40; // 5 slots of 200px width with 40px spacing
+var shopStartX = (2048 - shopTotalWidth) / 2 + 60; // Center horizontally
+for (var i = 0; i < 5; i++) {
+ var shopSlot = game.addChild(new Container());
+ shopSlot.x = shopStartX + i * 240;
+ shopSlot.y = PLAYABLE_HEIGHT + 150; // Position shop units further down
+ shopSlot.index = i;
+ shopSlot.unit = null;
+ shopSlot.tierText = null;
+ shopSlot.nameText = null;
+ shopSlots.push(shopSlot);
+}
+// Create refresh button
+var refreshButton = game.addChild(new Container());
+var refreshButtonBg = refreshButton.attachAsset('shop_button', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 200,
+ height: 80
+});
+refreshButtonBg.tint = 0xFF8800;
+var refreshText = new Text2('Refresh', {
+ size: 40,
+ fill: 0xFFFFFF
+});
+refreshText.anchor.set(0.5, 0.5);
+refreshButton.addChild(refreshButtonBg);
+refreshButton.addChild(refreshText);
+refreshButton.x = 2048 - 200;
+refreshButton.y = PLAYABLE_HEIGHT + 150; // Match shop area position
+refreshButton.up = function () {
+ if (gold >= refreshCost) {
+ gold -= refreshCost;
+ goldText.setText('Gold: ' + gold);
+ refreshShop();
+ LK.getSound('purchase').play();
}
- 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;
- towerPreview.towerType = tower.towerType;
- towerPreview.updateAppearance();
- // Apply the same offset as in move handler to ensure consistency when starting drag
- towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
- break;
- }
- }
};
-game.move = function (x, y, obj) {
- if (isDragging) {
- // Shift the y position upward by 1.5 tiles to show preview above finger
- towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
- }
+// Create level up button
+var levelUpButton = game.addChild(new Container());
+var levelUpButtonBg = levelUpButton.attachAsset('shop_button', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 200,
+ height: 80
+});
+levelUpButtonBg.tint = 0xFF8800;
+var levelUpText = new Text2('Level Up', {
+ size: 40,
+ fill: 0xFFFFFF
+});
+levelUpText.anchor.set(0.5, 0.5);
+levelUpButton.addChild(levelUpText);
+levelUpButton.x = 200;
+levelUpButton.y = PLAYABLE_HEIGHT + 150; // Position in shop area
+levelUpButton.up = function () {
+ levelUpPlayer();
};
-game.up = function (x, y, obj) {
- var clickedOnTower = false;
- for (var i = 0; i < towers.length; i++) {
- var tower = towers[i];
- var towerLeft = tower.x - tower.width / 2;
- var towerRight = tower.x + tower.width / 2;
- var towerTop = tower.y - tower.height / 2;
- var towerBottom = tower.y + tower.height / 2;
- if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) {
- clickedOnTower = true;
- break;
+// Create level up cost text
+var levelUpCostText = new Text2('Cost: ' + getLevelUpCost(playerLevel) + 'g', {
+ size: 32,
+ fill: 0xFFD700
+});
+levelUpCostText.anchor.set(0.5, 0);
+levelUpCostText.x = 200;
+levelUpCostText.y = PLAYABLE_HEIGHT + 200;
+game.addChild(levelUpCostText);
+// Create refresh cost text
+var refreshCostText = new Text2('Cost: ' + refreshCost + 'g', {
+ size: 32,
+ fill: 0xFFD700
+});
+refreshCostText.anchor.set(0.5, 0);
+refreshCostText.x = 2048 - 200;
+refreshCostText.y = PLAYABLE_HEIGHT + 200;
+game.addChild(refreshCostText);
+// Function to refresh shop with new units
+function refreshShop() {
+ // Clear existing shop units
+ for (var i = 0; i < shopSlots.length; i++) {
+ if (shopSlots[i].unit) {
+ shopSlots[i].unit.destroy();
+ shopSlots[i].unit = null;
}
+ if (shopSlots[i].tierText) {
+ shopSlots[i].tierText.destroy();
+ shopSlots[i].tierText = null;
+ }
+ if (shopSlots[i].nameText) {
+ shopSlots[i].nameText.destroy();
+ shopSlots[i].nameText = null;
+ }
+ if (shopSlots[i].infoButton) {
+ shopSlots[i].infoButton.destroy();
+ shopSlots[i].infoButton = null;
+ }
}
- var upgradeMenus = game.children.filter(function (child) {
- return child instanceof UpgradeMenu;
- });
- if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
- var clickedOnMenu = false;
- for (var i = 0; i < upgradeMenus.length; i++) {
- var menu = upgradeMenus[i];
- var menuWidth = 2048;
- var menuHeight = 450;
- var menuLeft = menu.x - menuWidth / 2;
- var menuRight = menu.x + menuWidth / 2;
- var menuTop = menu.y - menuHeight / 2;
- var menuBottom = menu.y + menuHeight / 2;
- if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
- clickedOnMenu = true;
+ // Fill shop with new random units
+ for (var i = 0; i < shopSlots.length; i++) {
+ var randomTier = unitPool[Math.floor(Math.random() * unitPool.length)];
+ var shopUnit;
+ var unitConstructor;
+ switch (randomTier) {
+ case 1:
+ var unitTypes = [Soldier, Knight, Wall];
+ unitConstructor = unitTypes[Math.floor(Math.random() * unitTypes.length)];
+ shopUnit = new unitConstructor();
break;
+ case 2:
+ var unitTypes = [Wizard, Paladin, Ranger, CrossbowTower];
+ unitConstructor = unitTypes[Math.floor(Math.random() * unitTypes.length)];
+ shopUnit = new unitConstructor();
+ break;
+ }
+ shopUnit.x = shopSlots[i].x;
+ shopUnit.y = shopSlots[i].y; // Center the unit in the shop slot
+ game.addChild(shopUnit);
+ shopSlots[i].unit = shopUnit;
+ shopSlots[i].unitConstructor = unitConstructor; // Store the constructor reference
+ // Add gentle floating animation for shop units
+ var baseY = shopUnit.y;
+ tween(shopUnit, {
+ y: baseY - 10
+ }, {
+ duration: 1000 + Math.random() * 500,
+ easing: tween.easeInOut,
+ onFinish: function onFinish() {
+ tween(shopUnit, {
+ y: baseY
+ }, {
+ duration: 1000 + Math.random() * 500,
+ easing: tween.easeInOut,
+ onFinish: onFinish
+ });
}
+ });
+ // Add unit name text above the image with more spacing
+ var nameText = new Text2(shopUnit.name, {
+ size: 36,
+ fill: 0xFFFFFF
+ });
+ nameText.anchor.set(0.5, 1); //{3P} // Anchor to bottom center
+ nameText.x = shopSlots[i].x;
+ nameText.y = shopSlots[i].y - 100; // Position higher above the unit image
+ game.addChild(nameText);
+ shopSlots[i].nameText = nameText;
+ // Add cost text below the image with more spacing
+ var cost = randomTier;
+ // Special cost for Wall (1g) and CrossbowTower (2g)
+ if (shopUnit.name === "Wall") {
+ cost = 1;
+ } else if (shopUnit.name === "CrossbowTower") {
+ cost = 2;
}
- if (!clickedOnMenu) {
- for (var i = 0; i < upgradeMenus.length; i++) {
- var menu = upgradeMenus[i];
- hideUpgradeMenu(menu);
+ var costText = new Text2(cost + 'g', {
+ size: 36,
+ fill: 0xFFD700
+ });
+ costText.anchor.set(0.5, 0);
+ costText.x = shopSlots[i].x;
+ costText.y = shopSlots[i].y + 70; // Position well below the unit image
+ game.addChild(costText);
+ shopSlots[i].tierText = costText;
+ // Add info button below cost
+ var infoButton = new Container();
+ var infoButtonBg = infoButton.attachAsset('shop_button', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 80,
+ height: 40
+ });
+ infoButtonBg.tint = 0x4444FF;
+ var infoButtonText = new Text2('Info', {
+ size: 32,
+ fill: 0xFFFFFF
+ });
+ infoButtonText.anchor.set(0.5, 0.5);
+ infoButton.addChild(infoButtonText);
+ infoButton.x = shopSlots[i].x;
+ infoButton.y = shopSlots[i].y + 130; // Position below cost text with more spacing
+ game.addChild(infoButton);
+ shopSlots[i].infoButton = infoButton;
+ // Store unit reference on info button for click handler
+ infoButton.shopUnit = shopUnit;
+ infoButton.up = function () {
+ showUnitInfoModal(this.shopUnit);
+ };
+ }
+}
+// Function to show unit info modal
+function showUnitInfoModal(unit) {
+ // Create modal overlay
+ var modalOverlay = game.addChild(new Container());
+ modalOverlay.x = 1024; // Center of screen
+ modalOverlay.y = 1366; // Center of screen
+ // Create modal background
+ var modalBg = modalOverlay.attachAsset('shop_area_bg', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 600,
+ height: 800
+ });
+ modalBg.alpha = 0.95;
+ // Add title
+ var titleText = new Text2(unit.name, {
+ size: 48,
+ fill: 0xFFFFFF
+ });
+ titleText.anchor.set(0.5, 0.5);
+ titleText.y = -350;
+ modalOverlay.addChild(titleText);
+ // Add attributes
+ var attributes = ['Health: ' + unit.maxHealth, 'Damage: ' + unit.damage, 'Armor: ' + unit.armor, 'Magic Resist: ' + unit.magicResist, 'Critical Chance: ' + unit.criticalChance * 100 + '%', 'Mana: ' + unit.mana, 'Attack Speed: ' + unit.speed, 'Range: ' + unit.range];
+ for (var i = 0; i < attributes.length; i++) {
+ var attrText = new Text2(attributes[i], {
+ size: 36,
+ fill: 0xFFFFFF
+ });
+ attrText.anchor.set(0.5, 0.5);
+ attrText.y = -250 + i * 60;
+ modalOverlay.addChild(attrText);
+ }
+ // Add close button
+ var closeButton = new Container();
+ var closeButtonBg = closeButton.attachAsset('shop_button', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 150,
+ height: 60
+ });
+ closeButtonBg.tint = 0xFF4444;
+ var closeButtonText = new Text2('Close', {
+ size: 36,
+ fill: 0xFFFFFF
+ });
+ closeButtonText.anchor.set(0.5, 0.5);
+ closeButton.addChild(closeButtonText);
+ closeButton.y = 330;
+ modalOverlay.addChild(closeButton);
+ closeButton.up = function () {
+ modalOverlay.destroy();
+ };
+}
+// Initialize shop with units
+refreshShop();
+// Create top UI container background
+var topUIBg = game.addChild(LK.getAsset('bench_area_bg', {
+ anchorX: 0.5,
+ anchorY: 0,
+ width: 1800,
+ height: 160
+}));
+topUIBg.x = 1024; // Center of screen
+topUIBg.y = 10; // Top of screen with small margin
+topUIBg.alpha = 0.7;
+topUIBg.tint = 0x3a3a3a; // Darker tint for top section
+// Create top row elements (Gold, Level, Max Units)
+goldText = new Text2('Gold: ' + gold, {
+ size: 50,
+ fill: 0xFFD700
+});
+goldText.anchor.set(0.5, 0.5);
+goldText.x = 1024 - 400; // Left of center
+goldText.y = 50;
+game.addChild(goldText);
+var levelText = new Text2('Level: ' + playerLevel, {
+ size: 50,
+ fill: 0x44FF44
+});
+levelText.anchor.set(0.5, 0.5);
+levelText.x = 1024; // Center
+levelText.y = 50;
+game.addChild(levelText);
+var maxUnitsText = new Text2('Units: ' + maxUnitsOnField + ' | Structures: ' + maxStructuresOnField, {
+ size: 50,
+ fill: 0xFFFFFF
+});
+maxUnitsText.anchor.set(0.5, 0.5);
+maxUnitsText.x = 1024 + 400; // Right of center
+maxUnitsText.y = 50;
+game.addChild(maxUnitsText);
+// Create bottom row elements (Planning Phase, Next Wave)
+var stateText = new Text2('Planning Phase', {
+ size: 40,
+ fill: 0x44FF44
+});
+stateText.anchor.set(0.5, 0.5);
+stateText.x = 1024 - 300; // Left of center in bottom row
+stateText.y = 120;
+game.addChild(stateText);
+var waveText = new Text2('Wave: ' + wave, {
+ size: 40,
+ fill: 0xFFFFFF
+});
+waveText.anchor.set(0.5, 0.5);
+waveText.x = 1024 + 300; // Right of center in bottom row
+waveText.y = 120;
+game.addChild(waveText);
+// Function to update state indicator
+function updateStateIndicator() {
+ if (gameState === 'planning') {
+ stateText.setText('Planning Phase');
+ // Remove old text and create new one with correct color
+ var parent = stateText.parent;
+ var x = stateText.x;
+ var y = stateText.y;
+ stateText.destroy();
+ stateText = new Text2('Planning Phase', {
+ size: 40,
+ fill: 0x44FF44 // Green for planning
+ });
+ stateText.anchor.set(0.5, 0.5);
+ stateText.x = x;
+ stateText.y = y;
+ parent.addChild(stateText);
+ // Enable ready button during planning
+ readyButtonBg.tint = 0x44AA44; // Green tint
+ readyButtonText.setText('Ready!');
+ } else {
+ stateText.setText('Battle Phase');
+ // Remove old text and create new one with correct color
+ var parent = stateText.parent;
+ var x = stateText.x;
+ var y = stateText.y;
+ stateText.destroy();
+ stateText = new Text2('Battle Phase', {
+ size: 40,
+ fill: 0xFF4444 // Red for battle
+ });
+ stateText.anchor.set(0.5, 0.5);
+ stateText.x = x;
+ stateText.y = y;
+ parent.addChild(stateText);
+ // Grey out ready button during battle
+ readyButtonBg.tint = 0x666666; // Grey tint
+ readyButtonText.setText('Battle');
+ }
+}
+var readyButton = new Container();
+var readyButtonBg = LK.getAsset('shop_button', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ width: 200,
+ height: 60
+});
+readyButtonBg.tint = 0x44AA44;
+var readyButtonText = new Text2('Ready!', {
+ size: 36,
+ fill: 0xFFFFFF
+});
+readyButtonText.anchor.set(0.5, 0.5);
+readyButton.addChild(readyButtonBg);
+readyButton.addChild(readyButtonText);
+game.addChild(readyButton);
+readyButton.x = 1024; // Center horizontally
+readyButton.y = 120; // Same row as state and wave text
+readyButton.up = function () {
+ if (gameState === 'planning') {
+ // Check for upgrades before starting battle
+ checkAndUpgradeUnits();
+ gameState = 'battle';
+ waveDelay = 0;
+ enemiesSpawned = 0; // Reset enemies spawned for new wave
+ updateStateIndicator(); // Update UI to show battle phase
+ }
+ // Do nothing if gameState is 'battle' (button is disabled)
+};
+// Function to check and perform unit upgrades
+function checkAndUpgradeUnits() {
+ // Group units by name and type
+ var unitGroups = {};
+ // Count units in bench
+ for (var i = 0; i < bench.length; i++) {
+ var unit = bench[i];
+ if (unit.gridX === -1 && unit.gridY === -1) {
+ // Only count bench units
+ var key = unit.name + '_' + unit.tier;
+ if (!unitGroups[key]) {
+ unitGroups[key] = [];
}
- for (var i = game.children.length - 1; i >= 0; i--) {
- if (game.children[i].isTowerRange) {
- game.removeChild(game.children[i]);
- }
- }
- selectedTower = null;
- grid.renderDebug();
+ unitGroups[key].push({
+ unit: unit,
+ location: 'bench',
+ index: i
+ });
}
}
- if (isDragging) {
- isDragging = false;
- if (towerPreview.canPlace) {
- if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
- placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
- } else {
- var notification = game.addChild(new Notification("Tower would block the path!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 50;
+ // Count units on grid
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ var gridSlot = grid[y][x];
+ if (gridSlot.unit) {
+ var unit = gridSlot.unit;
+ var key = unit.name + '_' + unit.tier;
+ if (!unitGroups[key]) {
+ unitGroups[key] = [];
+ }
+ unitGroups[key].push({
+ unit: unit,
+ location: 'grid',
+ gridX: x,
+ gridY: y,
+ slot: gridSlot
+ });
}
- } else if (towerPreview.blockedByEnemy) {
- var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 50;
- } else if (towerPreview.visible) {
- var notification = game.addChild(new Notification("Cannot build here!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 50;
}
- towerPreview.visible = false;
- if (isDragging) {
- var upgradeMenus = game.children.filter(function (child) {
- return child instanceof UpgradeMenu;
+ }
+ // Check for upgrades
+ for (var key in unitGroups) {
+ var group = unitGroups[key];
+ if (group.length >= 3) {
+ // Find upgrade priority: bench first, then grid
+ var benchUnits = group.filter(function (item) {
+ return item.location === 'bench';
});
- for (var i = 0; i < upgradeMenus.length; i++) {
- upgradeMenus[i].destroy();
+ var gridUnits = group.filter(function (item) {
+ return item.location === 'grid';
+ });
+ var unitsToMerge = [];
+ var upgradeTarget = null;
+ var upgradeLocation = null;
+ if (gridUnits.length >= 2 && benchUnits.length >= 1) {
+ // Priority: Upgrade units on grid when 2 are on field and 1 in bench
+ unitsToMerge = gridUnits.slice(0, 2).concat(benchUnits.slice(0, 1));
+ upgradeTarget = unitsToMerge[0];
+ upgradeLocation = 'grid';
+ } else if (gridUnits.length >= 1 && benchUnits.length >= 2) {
+ // Upgrade unit on grid using bench units
+ unitsToMerge = [gridUnits[0]].concat(benchUnits.slice(0, 2));
+ upgradeTarget = unitsToMerge[0];
+ upgradeLocation = 'grid';
+ } else if (benchUnits.length >= 3) {
+ // Upgrade in bench
+ unitsToMerge = benchUnits.slice(0, 3);
+ upgradeTarget = unitsToMerge[0];
+ upgradeLocation = 'bench';
}
+ if (upgradeTarget && unitsToMerge.length === 3) {
+ // Perform upgrade
+ var baseUnit = upgradeTarget.unit;
+ // Upgrade attributes based on tier
+ if (baseUnit.tier === 2) {
+ // Three star upgrade - multiply stats by 3
+ baseUnit.health = Math.floor(baseUnit.health * 3);
+ baseUnit.maxHealth = Math.floor(baseUnit.maxHealth * 3);
+ baseUnit.damage = Math.floor(baseUnit.damage * 3);
+ baseUnit.armor = Math.floor(baseUnit.armor * 3);
+ baseUnit.magicResist = Math.floor(baseUnit.magicResist * 3);
+ baseUnit.mana = Math.floor(baseUnit.mana * 3);
+ } else {
+ // Two star upgrade - multiply by 1.8x
+ baseUnit.health = Math.floor(baseUnit.health * 1.8);
+ baseUnit.maxHealth = Math.floor(baseUnit.maxHealth * 1.8);
+ baseUnit.damage = Math.floor(baseUnit.damage * 1.8);
+ baseUnit.armor = Math.floor(baseUnit.armor * 1.8);
+ baseUnit.magicResist = Math.floor(baseUnit.magicResist * 1.8);
+ baseUnit.mana = Math.floor(baseUnit.mana * 1.8);
+ }
+ baseUnit.tier = baseUnit.tier + 1;
+ // Update health bar to reflect new max health
+ if (baseUnit.healthBar) {
+ baseUnit.updateHealthBar();
+ }
+ // Update star indicator to reflect new tier
+ if (baseUnit.starIndicator) {
+ baseUnit.starIndicator.updateStars(baseUnit.tier);
+ }
+ // Force immediate scale application by stopping any ongoing scale tweens
+ tween.stop(baseUnit, {
+ scaleX: true,
+ scaleY: true
+ });
+ // Apply the scale immediately based on tier
+ var baseScale = baseUnit.baseScale || 1.3;
+ var finalScale = baseScale;
+ if (baseUnit.tier === 2) {
+ finalScale = baseScale * 1.2;
+ } else if (baseUnit.tier === 3) {
+ finalScale = baseScale * 1.3;
+ }
+ // Set scale immediately
+ baseUnit.scaleX = finalScale;
+ baseUnit.scaleY = finalScale;
+ // Update unit scale method to ensure consistency
+ baseUnit.updateUnitScale();
+ // Visual upgrade effect with bounce animation that respects final scale
+ LK.effects.flashObject(baseUnit, 0xFFD700, 800); // Gold flash
+ tween(baseUnit, {
+ scaleX: finalScale * 1.2,
+ scaleY: finalScale * 1.2
+ }, {
+ duration: 300,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ tween(baseUnit, {
+ scaleX: finalScale,
+ scaleY: finalScale
+ }, {
+ duration: 200,
+ easing: tween.easeIn
+ });
+ }
+ });
+ // Remove the other two units
+ for (var i = 1; i < unitsToMerge.length; i++) {
+ var unitToRemove = unitsToMerge[i];
+ if (unitToRemove.location === 'bench') {
+ // Remove from bench
+ for (var j = bench.length - 1; j >= 0; j--) {
+ if (bench[j] === unitToRemove.unit) {
+ bench[j].destroy();
+ bench.splice(j, 1);
+ break;
+ }
+ }
+ } else if (unitToRemove.location === 'grid') {
+ // Remove from grid
+ unitToRemove.slot.unit = null;
+ unregisterEntityPosition(unitToRemove.unit, unitToRemove.gridX, unitToRemove.gridY);
+ unitToRemove.unit.destroy();
+ }
+ }
+ // Update displays
+ updateBenchDisplay();
+ }
}
}
-};
-var waveIndicator = new WaveIndicator();
-waveIndicator.x = 2048 / 2;
-waveIndicator.y = 2732 - 80;
-game.addChild(waveIndicator);
-var nextWaveButtonContainer = new Container();
-var nextWaveButton = new NextWaveButton();
-nextWaveButton.x = 2048 - 200;
-nextWaveButton.y = 2732 - 100 + 20;
-nextWaveButtonContainer.addChild(nextWaveButton);
-game.addChild(nextWaveButtonContainer);
-var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
-var sourceTowers = [];
-var towerSpacing = 300; // Increase spacing for larger towers
-var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
-var towerY = 2732 - CELL_SIZE * 3 - 90;
-for (var i = 0; i < towerTypes.length; i++) {
- var tower = new SourceTower(towerTypes[i]);
- tower.x = startX + i * towerSpacing;
- tower.y = towerY;
- towerLayer.addChild(tower);
- sourceTowers.push(tower);
}
-sourceTower = null;
-enemiesToSpawn = 10;
-game.update = function () {
- if (waveInProgress) {
- if (!waveSpawned) {
- waveSpawned = true;
- // Get wave type and enemy count from the wave indicator
- var waveType = waveIndicator.getWaveType(currentWave);
- var enemyCount = waveIndicator.getEnemyCount(currentWave);
- // Check if this is a boss wave
- var isBossWave = currentWave % 10 === 0 && currentWave > 0;
- if (isBossWave && waveType !== 'swarm') {
- // Boss waves have just 1 enemy regardless of what the wave indicator says
- enemyCount = 1;
- // Show boss announcement
- var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 200;
+// Function to show ready button for next wave
+function showReadyButton() {
+ if (gameState === 'battle') {
+ readyButtonBg.tint = 0x666666; // Grey tint during battle
+ readyButtonText.setText('Battle');
+ } else {
+ readyButtonBg.tint = 0x44AA44; // Green tint for next wave
+ readyButtonText.setText('Next Wave');
+ }
+ readyButton.visible = true;
+}
+// Hide ready button initially (will show after first wave)
+function hideReadyButton() {
+ readyButton.visible = false;
+}
+function updateBenchDisplay() {
+ // Clear all bench slot references
+ for (var i = 0; i < benchSlots.length; i++) {
+ benchSlots[i].unit = null;
+ }
+ // Only display units in bench that are not on the battlefield
+ for (var i = 0; i < bench.length; i++) {
+ // Skip units that are already placed on the grid
+ if (bench[i].gridX !== -1 && bench[i].gridY !== -1) {
+ continue;
+ }
+ // Find the first empty bench slot
+ for (var j = 0; j < benchSlots.length; j++) {
+ if (!benchSlots[j].unit) {
+ benchSlots[j].unit = bench[i];
+ // Center the unit in the bench slot
+ bench[i].x = benchSlots[j].x;
+ bench[i].y = benchSlots[j].y;
+ // Show star indicator for units in bench
+ bench[i].showStarIndicator();
+ break;
}
- // Spawn the appropriate number of enemies
- for (var i = 0; i < enemyCount; i++) {
- var enemy = new Enemy(waveType);
- // Add enemy to the appropriate layer based on type
- if (enemy.isFlying) {
- // Add flying enemy to the top layer
- enemyLayerTop.addChild(enemy);
- // If it's a flying enemy, add its shadow to the middle layer
- if (enemy.shadow) {
- enemyLayerMiddle.addChild(enemy.shadow);
+ }
+ }
+}
+function spawnEnemy() {
+ var spawnX = Math.floor(Math.random() * GRID_COLS);
+ // Increase ranged enemy chance as waves progress
+ var rangedChance = Math.min(0.3 + wave * 0.05, 0.7);
+ var enemy;
+ if (Math.random() < rangedChance) {
+ enemy = new RangedEnemy();
+ } else {
+ enemy = new Enemy();
+ }
+ // Position enemy at the first row (top) of the grid
+ enemy.gridX = spawnX;
+ enemy.gridY = 0;
+ // Set visual position to match grid position
+ var topCell = grid[0][spawnX];
+ enemy.x = topCell.x;
+ enemy.y = topCell.y;
+ enemy.lane = 0; // Single grid, so lane is always 0
+ // Scale up enemy by 30%
+ enemy.scaleX = 1.3;
+ enemy.scaleY = 1.3;
+ // Significantly increase enemy scaling per wave
+ enemy.health = 20 + Math.floor(wave * 10) + Math.floor(Math.pow(wave, 1.5) * 5);
+ enemy.maxHealth = enemy.health;
+ enemy.damage = 15 + Math.floor(wave * 5) + Math.floor(Math.pow(wave, 1.3) * 3);
+ enemy.goldValue = Math.max(2, Math.floor(wave * 1.5));
+ enemy.showHealthBar(); // Show health bar when enemy is spawned
+ enemy.updateHealthBar();
+ enemies.push(enemy);
+ game.addChild(enemy);
+ // Add spawn animation
+ enemy.alpha = 0;
+ enemy.scaleX = 0.65; // 0.5 * 1.3
+ enemy.scaleY = 1.95; // 1.5 * 1.3
+ tween(enemy, {
+ alpha: 1,
+ scaleX: 1.3,
+ scaleY: 1.3
+ }, {
+ duration: 300,
+ easing: tween.easeOut
+ });
+ // Register enemy position
+ registerEntityPosition(enemy);
+}
+game.move = function (x, y, obj) {
+ if (draggedUnit) {
+ draggedUnit.x = x;
+ draggedUnit.y = y;
+ }
+};
+game.up = function (x, y, obj) {
+ // Check for double-tap on grid units when not dragging
+ if (!draggedUnit) {
+ var currentTime = Date.now();
+ var timeDiff = currentTime - lastTapTime;
+ var dx = x - lastTapX;
+ var dy = y - lastTapY;
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ // Check if this is a double-tap (within time window and close to last tap)
+ if (timeDiff < doubleTapDelay && distance < doubleTapDistance) {
+ // Check if double-tap is on a unit in the grid
+ for (var gridY = 0; gridY < GRID_ROWS; gridY++) {
+ for (var gridX = 0; gridX < GRID_COLS; gridX++) {
+ var slot = grid[gridY][gridX];
+ if (slot.unit) {
+ var unitDx = x - slot.x;
+ var unitDy = y - slot.y;
+ var unitDistance = Math.sqrt(unitDx * unitDx + unitDy * unitDy);
+ if (unitDistance < GRID_CELL_SIZE / 2) {
+ // Double-tapped on this unit - show info modal
+ showUnitInfoModal(slot.unit);
+ lastTapTime = 0; // Reset to prevent triple-tap
+ return;
+ }
}
- } else {
- // Add normal/ground enemies to the bottom layer
- enemyLayerBottom.addChild(enemy);
}
- // Scale difficulty with wave number but don't apply to boss
- // as bosses already have their health multiplier
- // Use exponential scaling for health
- var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave
- enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
- enemy.health = enemy.maxHealth;
- // Increment speed slightly with wave number
- //enemy.speed = enemy.speed + currentWave * 0.002;
- // All enemy types now spawn in the middle 6 tiles at the top spacing
- var gridWidth = 24;
- var midPoint = Math.floor(gridWidth / 2); // 12
- // Find a column that isn't occupied by another enemy that's not yet in view
- var availableColumns = [];
- for (var col = midPoint - 3; col < midPoint + 3; col++) {
- var columnOccupied = false;
- // Check if any enemy is already in this column but not yet in view
- for (var e = 0; e < enemies.length; e++) {
- if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
- columnOccupied = true;
- break;
+ }
+ // Check if double-tap is on a unit in the bench during planning phase
+ if (gameState === 'planning') {
+ for (var i = 0; i < benchSlots.length; i++) {
+ var benchSlot = benchSlots[i];
+ if (benchSlot.unit) {
+ var unitDx = x - benchSlot.x;
+ var unitDy = y - benchSlot.y;
+ var unitDistance = Math.sqrt(unitDx * unitDx + unitDy * unitDy);
+ if (unitDistance < 60) {
+ // Double-tapped on this bench unit - show info modal
+ showUnitInfoModal(benchSlot.unit);
+ lastTapTime = 0; // Reset to prevent triple-tap
+ return;
}
}
- if (!columnOccupied) {
- availableColumns.push(col);
- }
}
- // If all columns are occupied, use original random method
- var spawnX;
- if (availableColumns.length > 0) {
- // Choose a random unoccupied column
- spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
- } else {
- // Fallback to random if all columns are occupied
- spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
+ }
+ // Check if double-tap is on an enemy
+ for (var i = 0; i < enemies.length; i++) {
+ var enemy = enemies[i];
+ var enemyDx = x - enemy.x;
+ var enemyDy = y - enemy.y;
+ var enemyDistance = Math.sqrt(enemyDx * enemyDx + enemyDy * enemyDy);
+ if (enemyDistance < 60) {
+ // Double-tapped on this enemy - show info modal
+ showUnitInfoModal(enemy);
+ lastTapTime = 0; // Reset to prevent triple-tap
+ return;
}
- var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading
- enemy.cellX = spawnX;
- enemy.cellY = 5; // Position after entry
- enemy.currentCellX = spawnX;
- enemy.currentCellY = spawnY;
- enemy.waveNumber = currentWave;
- enemies.push(enemy);
}
}
- var currentWaveEnemiesRemaining = false;
- for (var i = 0; i < enemies.length; i++) {
- if (enemies[i].waveNumber === currentWave) {
- currentWaveEnemiesRemaining = true;
+ // Update last tap info for next potential double-tap
+ lastTapTime = currentTime;
+ lastTapX = x;
+ lastTapY = y;
+ }
+ if (draggedUnit) {
+ var dropped = false;
+ // Check if dropped on a valid grid slot
+ for (var gridY = 0; gridY < GRID_ROWS; gridY++) {
+ for (var gridX = 0; gridX < GRID_COLS; gridX++) {
+ var slot = grid[gridY][gridX];
+ var dx = x - slot.x;
+ var dy = y - slot.y;
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance < GRID_CELL_SIZE / 2 && !slot.unit) {
+ // Valid empty grid slot - ensure exact positioning
+ slot.placeUnit(draggedUnit);
+ if (draggedFromGrid && draggedUnit.originalGridSlot) {
+ // Unregister from old position before clearing slot
+ unregisterEntityPosition(draggedUnit, draggedUnit.originalGridSlot.gridX, draggedUnit.originalGridSlot.gridY);
+ draggedUnit.originalGridSlot.unit = null;
+ }
+ dropped = true;
+ break;
+ }
+ }
+ if (dropped) {
break;
}
}
- if (waveSpawned && !currentWaveEnemiesRemaining) {
- waveInProgress = false;
- waveSpawned = false;
- }
- }
- for (var a = enemies.length - 1; a >= 0; a--) {
- var enemy = enemies[a];
- if (enemy.health <= 0) {
- for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
- var bullet = enemy.bulletsTargetingThis[i];
- bullet.targetEnemy = null;
+ // Check if dropped on bench slot
+ if (!dropped) {
+ for (var i = 0; i < benchSlots.length; i++) {
+ var benchSlot = benchSlots[i];
+ var dx = x - benchSlot.x;
+ var dy = y - benchSlot.y;
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance < 60 && !benchSlot.unit) {
+ // Valid empty bench slot
+ benchSlot.unit = draggedUnit;
+ draggedUnit.x = benchSlot.x;
+ draggedUnit.y = benchSlot.y;
+ // Remove from grid if it was on grid
+ if (draggedFromGrid && draggedUnit.originalGridSlot) {
+ // Unregister from position tracking
+ unregisterEntityPosition(draggedUnit, draggedUnit.originalGridSlot.gridX, draggedUnit.originalGridSlot.gridY);
+ draggedUnit.originalGridSlot.unit = null;
+ }
+ draggedUnit.gridX = -1;
+ draggedUnit.gridY = -1;
+ draggedUnit.lane = -1;
+ draggedUnit.hideHealthBar(); // Hide health bar when moved to bench
+ draggedUnit.hideStarIndicator(); // Hide star indicator when moved to bench
+ dropped = true;
+ updateBenchDisplay();
+ break;
+ }
}
- // Boss enemies give more gold and score
- var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5);
- var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
- game.addChild(goldIndicator);
- setGold(gold + goldEarned);
- // Give more score for defeating a boss
- var scoreValue = enemy.isBoss ? 100 : 5;
- score += scoreValue;
- // Add a notification for boss defeat
- if (enemy.isBoss) {
- var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 150;
- }
- updateUI();
- // Clean up shadow if it's a flying enemy
- if (enemy.isFlying && enemy.shadow) {
- enemyLayerMiddle.removeChild(enemy.shadow);
- enemy.shadow = null;
- }
- // Remove enemy from the appropriate layer
- if (enemy.isFlying) {
- enemyLayerTop.removeChild(enemy);
+ }
+ // If not dropped on valid slot, return to original position
+ if (!dropped) {
+ // Always return to bench if not placed in a valid cell
+ if (draggedUnit.originalSlot) {
+ // Return to original bench slot
+ draggedUnit.x = draggedUnit.originalX;
+ draggedUnit.y = draggedUnit.originalY;
+ draggedUnit.originalSlot.unit = draggedUnit;
+ } else if (draggedFromGrid && draggedUnit.originalGridSlot) {
+ // Unit was dragged from grid but not placed in valid cell - return to bench
+ // Find first available bench slot
+ var returnedToBench = false;
+ for (var i = 0; i < benchSlots.length; i++) {
+ if (!benchSlots[i].unit) {
+ benchSlots[i].unit = draggedUnit;
+ draggedUnit.x = benchSlots[i].x;
+ draggedUnit.y = benchSlots[i].y;
+ // Unregister from grid position
+ unregisterEntityPosition(draggedUnit, draggedUnit.originalGridSlot.gridX, draggedUnit.originalGridSlot.gridY);
+ draggedUnit.originalGridSlot.unit = null;
+ draggedUnit.gridX = -1;
+ draggedUnit.gridY = -1;
+ draggedUnit.lane = -1;
+ draggedUnit.hideHealthBar();
+ draggedUnit.hideStarIndicator(); // Hide star indicator when moved to bench
+ returnedToBench = true;
+ updateBenchDisplay();
+ break;
+ }
+ }
+ // If no bench slot available, return to original grid position
+ if (!returnedToBench) {
+ draggedUnit.x = draggedUnit.originalX;
+ draggedUnit.y = draggedUnit.originalY;
+ draggedUnit.originalGridSlot.unit = draggedUnit; // Restore unit to original grid slot
+ }
} else {
- enemyLayerBottom.removeChild(enemy);
+ // Unit doesn't have original position - find first available bench slot
+ for (var i = 0; i < benchSlots.length; i++) {
+ if (!benchSlots[i].unit) {
+ benchSlots[i].unit = draggedUnit;
+ draggedUnit.x = benchSlots[i].x;
+ draggedUnit.y = benchSlots[i].y;
+ draggedUnit.gridX = -1;
+ draggedUnit.gridY = -1;
+ draggedUnit.lane = -1;
+ draggedUnit.hideHealthBar();
+ draggedUnit.hideStarIndicator(); // Hide star indicator when moved to bench
+ updateBenchDisplay();
+ break;
+ }
+ }
}
- enemies.splice(a, 1);
- continue;
}
- if (grid.updateEnemy(enemy)) {
- // Clean up shadow if it's a flying enemy
- if (enemy.isFlying && enemy.shadow) {
- enemyLayerMiddle.removeChild(enemy.shadow);
- enemy.shadow = null;
+ // Clean up
+ draggedUnit.originalX = undefined;
+ draggedUnit.originalY = undefined;
+ draggedUnit.originalSlot = undefined;
+ draggedUnit.originalGridSlot = undefined;
+ draggedUnit = null;
+ draggedFromGrid = false;
+ } else {
+ // Check if clicking on a shop unit
+ for (var i = 0; i < shopSlots.length; i++) {
+ var slot = shopSlots[i];
+ if (slot.unit) {
+ var dx = x - slot.x;
+ var dy = y - slot.y;
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance < 80) {
+ // Increase click area for better usability
+ // Within click range of shop unit
+ var unitCost = slot.unit.tier;
+ // Special cost for Wall (1g) and CrossbowTower (2g)
+ if (slot.unit.name === "Wall") {
+ unitCost = 1;
+ } else if (slot.unit.name === "CrossbowTower") {
+ unitCost = 2;
+ }
+ if (gold >= unitCost) {
+ // Find available bench slot
+ var availableBenchSlot = false;
+ for (var slotIndex = 0; slotIndex < benchSlots.length; slotIndex++) {
+ if (!benchSlots[slotIndex].unit) {
+ availableBenchSlot = true;
+ break;
+ }
+ }
+ if (availableBenchSlot) {
+ gold -= unitCost;
+ goldText.setText('Gold: ' + gold);
+ // Create new unit for bench using the same constructor as the shop unit
+ var newUnit = new slot.unitConstructor();
+ // All units purchased from shop start at tier 1 (1 star) regardless of unit type
+ newUnit.tier = 1;
+ // Update star indicator to show tier 1
+ if (newUnit.starIndicator) {
+ newUnit.starIndicator.updateStars(1);
+ }
+ // Initialize grid position properties
+ newUnit.gridX = -1;
+ newUnit.gridY = -1;
+ newUnit.lane = -1;
+ bench.push(newUnit);
+ game.addChild(newUnit);
+ updateBenchDisplay();
+ // Remove purchased unit from shop
+ slot.unit.destroy();
+ slot.unit = null;
+ if (slot.tierText) {
+ slot.tierText.destroy();
+ slot.tierText = null;
+ }
+ if (slot.nameText) {
+ slot.nameText.destroy();
+ slot.nameText = null;
+ }
+ if (slot.infoButton) {
+ slot.infoButton.destroy();
+ slot.infoButton = null;
+ }
+ LK.getSound('purchase').play();
+ // Check for upgrades after purchasing a unit
+ checkAndUpgradeUnits();
+ } else {
+ // No bench space available - could add visual feedback here
+ }
+ }
+ break;
+ }
}
- // Remove enemy from the appropriate layer
- if (enemy.isFlying) {
- enemyLayerTop.removeChild(enemy);
- } else {
- enemyLayerBottom.removeChild(enemy);
+ }
+ }
+};
+game.update = function () {
+ // Spawn enemies
+ if (gameState === 'battle') {
+ // Ensure health bars are visible for all enemies
+ for (var i = 0; i < enemies.length; i++) {
+ enemies[i].showHealthBar();
+ }
+ // Ensure health bars are visible for all units on the grid
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ var gridSlot = grid[y][x];
+ if (gridSlot.unit) {
+ gridSlot.unit.showHealthBar();
+ }
}
- enemies.splice(a, 1);
- lives = Math.max(0, lives - 1);
- updateUI();
- if (lives <= 0) {
- LK.showGameOver();
+ }
+ if (waveDelay <= 0) {
+ if (enemiesSpawned < enemiesPerWave) {
+ if (LK.ticks % 60 === 0) {
+ spawnEnemy();
+ enemiesSpawned++;
+ }
+ } else if (enemies.length === 0) {
+ // Wave complete - transition to planning phase
+ gameState = 'planning';
+ wave++;
+ enemiesPerWave = Math.min(5 + wave * 2, 30);
+ waveText.setText('Wave: ' + wave);
+ // Calculate interest: 1 gold per 10 saved, max 5
+ var interest = Math.min(Math.floor(gold / 10), 5);
+ var waveReward = 5 + interest;
+ gold += waveReward;
+ goldText.setText('Gold: ' + gold);
+ // Check for unit upgrades when entering planning phase
+ checkAndUpgradeUnits();
+ // Show reward message
+ var rewardText = new Text2('Wave Complete! +' + waveReward + ' Gold (5 + ' + interest + ' interest)', {
+ size: 48,
+ fill: 0xFFD700
+ });
+ rewardText.anchor.set(0.5, 0.5);
+ rewardText.x = 1024;
+ rewardText.y = 800;
+ game.addChild(rewardText);
+ // Fade out reward text
+ tween(rewardText, {
+ alpha: 0,
+ y: 700
+ }, {
+ duration: 2000,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ rewardText.destroy();
+ }
+ });
+ // Refresh shop with new units after wave completion
+ refreshShop();
+ showReadyButton(); // Show button for next wave
+ updateStateIndicator(); // Update UI to show planning phase
}
+ } else {
+ waveDelay--;
}
+ // Castle health bar is now handled as a regular unit
}
- for (var i = bullets.length - 1; i >= 0; i--) {
- if (!bullets[i].parent) {
- if (bullets[i].targetEnemy) {
- var targetEnemy = bullets[i].targetEnemy;
- var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
- if (bulletIndex !== -1) {
- targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
+ // Update enemies
+ for (var i = enemies.length - 1; i >= 0; i--) {
+ var enemy = enemies[i];
+ enemy.update();
+ // Castle is now a regular unit on the grid, no special handling needed
+ enemy.updateHealthBar();
+ enemy.showHealthBar(); // Ensure health bar is shown during battle
+ }
+ // Clean up reservations and position tracking for destroyed entities
+ var keysToRemove = [];
+ for (var key in cellReservations) {
+ var entity = cellReservations[key];
+ if (!entity.parent) {
+ keysToRemove.push(key);
+ }
+ }
+ for (var i = 0; i < keysToRemove.length; i++) {
+ delete cellReservations[keysToRemove[i]];
+ }
+ // Clean up position tracking for destroyed entities
+ var posKeysToRemove = [];
+ for (var posKey in entityPositions) {
+ var entity = entityPositions[posKey];
+ if (!entity.parent) {
+ posKeysToRemove.push(posKey);
+ }
+ }
+ for (var i = 0; i < posKeysToRemove.length; i++) {
+ delete entityPositions[posKeysToRemove[i]];
+ }
+ // Update units
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ var gridSlot = grid[y][x];
+ if (gridSlot.unit) {
+ gridSlot.unit.update();
+ if (gameState === 'battle') {
+ gridSlot.unit.showHealthBar(); // Ensure health bar is shown during battle
+ gridSlot.unit.updateHealthBar();
+ gridSlot.unit.showStarIndicator(); // Ensure star indicator is shown during battle
+ } else {
+ // Hide health bars during planning phase
+ if (gridSlot.unit.healthBar) {
+ gridSlot.unit.healthBar.visible = false;
+ }
+ // Show star indicators during planning phase for easy tier identification
+ gridSlot.unit.showStarIndicator();
}
}
+ }
+ }
+ // Unit shooting (only during battle)
+ if (gameState === 'battle') {
+ for (var y = 0; y < GRID_ROWS; y++) {
+ for (var x = 0; x < GRID_COLS; x++) {
+ var gridSlot = grid[y][x];
+ if (gridSlot.unit) {
+ var target = gridSlot.unit.findTarget();
+ if (target) {
+ var bullet = gridSlot.unit.shoot(target);
+ if (bullet) {
+ bullets.push(bullet);
+ game.addChild(bullet);
+ LK.getSound('shoot').play();
+ } else if (gridSlot.unit.range <= 100) {
+ // Melee attack sound effect (no bullet created)
+ if (gridSlot.unit.canShoot()) {
+ LK.getSound('shoot').play();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ // Update bullets
+ for (var i = bullets.length - 1; i >= 0; i--) {
+ var bullet = bullets[i];
+ if (bullet && bullet.update) {
+ bullet.update();
+ }
+ if (!bullet.parent) {
bullets.splice(i, 1);
}
}
- if (towerPreview.visible) {
- towerPreview.checkPlacement();
+ // Update enemy projectiles
+ for (var i = enemyProjectiles.length - 1; i >= 0; i--) {
+ var projectile = enemyProjectiles[i];
+ if (projectile && projectile.update) {
+ projectile.update();
+ }
+ if (!projectile.parent) {
+ enemyProjectiles.splice(i, 1);
+ }
}
- if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
+ // Update level up cost text
+ levelUpCostText.setText('Cost: ' + getLevelUpCost(playerLevel) + 'g');
+ // Check win condition
+ if (wave >= 10 && enemies.length === 0 && enemiesSpawned >= enemiesPerWave) {
LK.showYouWin();
}
};
\ No newline at end of file