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