/**** * 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 - always target the king var kingFound = false; // Find the king on the grid 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 && self.gridRef[y][x].unit.name === "King") { var dx = x - self.currentCell.x; var dy = y - self.currentCell.y; var dist = Math.abs(dx) + Math.abs(dy); targetGridX = x; targetGridY = y; kingFound = true; var entityRange = self.entity.range || 1; // If enemy is within range of king or is attacking, don't move if (dist <= entityRange || self.entity.isAttacking) { // Stay in current position - don't move return false; } shouldMove = true; break; } } if (kingFound) { break; } } if (!kingFound) { // No king 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
@@ -894,38 +894,36 @@
// 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;
+ // Enemy movement logic - always target the king
+ var kingFound = false;
+ // Find the king on the grid
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) {
+ if (self.gridRef[y] && self.gridRef[y][x] && self.gridRef[y][x].unit && self.gridRef[y][x].unit.name === "King") {
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;
+ targetGridX = x;
+ targetGridY = y;
+ kingFound = true;
+ var entityRange = self.entity.range || 1;
+ // If enemy is within range of king or is attacking, don't move
+ if (dist <= entityRange || self.entity.isAttacking) {
+ // Stay in current position - don't move
+ return false;
}
+ shouldMove = true;
+ break;
}
}
- }
- 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;
+ if (kingFound) {
+ break;
}
- shouldMove = true;
- } else {
- // No target found, move down as fallback
+ }
+ if (!kingFound) {
+ // No king found, move down as fallback
targetGridX = self.currentCell.x;
targetGridY = self.currentCell.y + 1;
shouldMove = true;
}