/**** * 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) { // Trigger death animation instead of immediate destroy enemies[i].deathAnimation(); 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.deathAnimation = function () { // Hide health bar immediately if (self.healthBar) { self.healthBar.destroy(); self.healthBar = null; } // Create particle explosion effect var particleContainer = new Container(); particleContainer.x = self.x; particleContainer.y = self.y; self.parent.addChild(particleContainer); // Number of particles based on enemy type var particleCount = 12; // Default for regular enemies var particleColors = [0xFF4444, 0xFF6666, 0xFF8888, 0xFFAAAA]; // Red shades for regular enemies var particleSize = 15; var explosionRadius = 80; // Customize based on enemy type if (self.name === "Ranged Enemy") { particleColors = [0x8A2BE2, 0x9370DB, 0xBA55D3, 0xDDA0DD]; // Purple shades particleCount = 10; } else if (self.name === "Boss Monster") { particleColors = [0x800000, 0xA52A2A, 0xDC143C, 0xFF0000]; // Dark red to bright red particleCount = 20; // More particles for boss particleSize = 20; explosionRadius = 120; } // Create particles var particles = []; for (var i = 0; i < particleCount; i++) { var particle = particleContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: particleSize + Math.random() * 10, height: particleSize + Math.random() * 10 }); // Set particle color particle.tint = particleColors[Math.floor(Math.random() * particleColors.length)]; particle.alpha = 0.8 + Math.random() * 0.2; // Calculate random direction var angle = Math.PI * 2 * i / particleCount + (Math.random() - 0.5) * 0.5; var speed = explosionRadius * (0.7 + Math.random() * 0.3); // Store particle data particles.push({ sprite: particle, targetX: Math.cos(angle) * speed, targetY: Math.sin(angle) * speed, rotationSpeed: (Math.random() - 0.5) * 0.2 }); } // Animate enemy shrinking and fading tween(self, { alpha: 0, scaleX: self.scaleX * 0.5, scaleY: self.scaleY * 0.5 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { // Destroy the enemy self.destroy(); } }); // Animate particles bursting outward for (var i = 0; i < particles.length; i++) { var particleData = particles[i]; var particle = particleData.sprite; // Burst outward animation tween(particle, { x: particleData.targetX, y: particleData.targetY, scaleX: 0.1, scaleY: 0.1, alpha: 0, rotation: particle.rotation + particleData.rotationSpeed * 10 }, { duration: 600 + Math.random() * 200, easing: tween.easeOut, onFinish: function onFinish() { // Clean up after last particle if (i === particles.length - 1) { particleContainer.destroy(); } } }); } // Add a brief flash effect at the center var flash = particleContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: particleSize * 3, height: particleSize * 3 }); flash.tint = 0xFFFFFF; flash.alpha = 0.8; tween(flash, { scaleX: 3, scaleY: 3, alpha: 0 }, { duration: 300, easing: tween.easeOut }); }; 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 // First check if king exists and is in range if (kings.length > 0 && kings[0]) { var king = kings[0]; if (typeof king.gridX === "number" && typeof king.gridY === "number") { var kingCellDist = Math.abs(self.gridX - king.gridX) + Math.abs(self.gridY - king.gridY); if (kingCellDist <= self.range && kingCellDist < closestCellDistance && self.gridMovement.isMoving === false) { closestCellDistance = kingCellDist; closestUnit = king; } } } // Then check other units on the grid 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; // Trigger death animation instead of immediate destroy closestUnit.deathAnimation(); 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 // First check if king exists and is in range if (kings.length > 0 && kings[0]) { var king = kings[0]; if (typeof king.gridX === "number" && typeof king.gridY === "number") { var kingCellDist = Math.abs(self.gridX - king.gridX) + Math.abs(self.gridY - king.gridY); if (kingCellDist <= self.range && kingCellDist < closestCellDistance) { closestCellDistance = kingCellDist; closestUnit = king; } } } // Then check other units on the grid 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); enemyContainer.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 Boss = Enemy.expand(function () { var self = Enemy.call(this); // Remove default enemy graphics self.removeChildren(); // Create massive boss container var bossContainer = new Container(); self.addChild(bossContainer); // Main body (much larger and darker) var body = bossContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); body.width = 120; body.height = 120; body.y = 15; body.tint = 0x800000; // Dark red // Boss head (menacing) var head = bossContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 80, height: 70 }); head.y = -60; head.tint = 0x4a0000; // Very dark red // Glowing eyes (intimidating) var leftEye = bossContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 15 }); leftEye.x = -20; leftEye.y = -60; leftEye.tint = 0xFF0000; // Bright red glowing eyes var rightEye = bossContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 15 }); rightEye.x = 20; rightEye.y = -60; rightEye.tint = 0xFF0000; // Massive claws (larger and more threatening) var leftClaw = bossContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 40, height: 30 }); leftClaw.x = -50; leftClaw.y = 10; leftClaw.rotation = 0.7; leftClaw.tint = 0x2a0000; // Very dark var rightClaw = bossContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 40, height: 30 }); rightClaw.x = 50; rightClaw.y = 10; rightClaw.rotation = -0.7; rightClaw.tint = 0x2a0000; // Boss spikes on shoulders var leftSpike = bossContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 40 }); leftSpike.x = -40; leftSpike.y = -20; leftSpike.rotation = 0.3; leftSpike.tint = 0x1a1a1a; // Dark spikes var rightSpike = bossContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 40 }); rightSpike.x = 40; rightSpike.y = -20; rightSpike.rotation = -0.3; rightSpike.tint = 0x1a1a1a; // Boss crown/horns var horn1 = bossContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 12, height: 35 }); horn1.x = -15; horn1.y = -85; horn1.rotation = 0.2; horn1.tint = 0x000000; // Black horns var horn2 = bossContainer.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, width: 12, height: 35 }); horn2.x = 15; horn2.y = -85; horn2.rotation = -0.2; horn2.tint = 0x000000; // Override boss attributes - much more powerful self.health = 200; self.maxHealth = 200; self.damage = 60; self.armor = 15; self.criticalChance = 0.35; self.magicResist = 15; self.mana = 100; self.speed = 1.2; // Slightly slower but hits harder self.range = 3; // Even longer reach than normal enemies self.goldValue = 25; // Very high reward for defeating boss self.name = "Boss Monster"; self.fireRate = 75; // Faster attack rate with devastating damage self.baseScale = 2.0; // Much larger than regular enemies (100% larger) // Boss special abilities self.lastSpecialAttack = 0; self.specialAttackCooldown = 300; // 5 seconds self.isEnraged = false; self.rageThreshold = 0.3; // Enrage when below 30% health // Apply base scale self.scaleX = self.baseScale; self.scaleY = self.baseScale; // Add menacing glow effect to eyes tween(leftEye, { scaleX: 1.3, scaleY: 1.3, alpha: 0.7 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(leftEye, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 800, easing: tween.easeInOut, onFinish: onFinish }); } }); tween(rightEye, { scaleX: 1.3, scaleY: 1.3, alpha: 0.7 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(rightEye, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 800, easing: tween.easeInOut, onFinish: onFinish }); } }); // Boss special attack - area damage self.performAreaAttack = function () { // Get all units within 2 cells var affectedUnits = []; 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; var cellDist = Math.abs(self.gridX - x) + Math.abs(self.gridY - y); if (cellDist <= 2) { affectedUnits.push(unit); } } } } // Check king separately if (kings.length > 0 && kings[0]) { var king = kings[0]; if (typeof king.gridX === "number" && typeof king.gridY === "number") { var kingCellDist = Math.abs(self.gridX - king.gridX) + Math.abs(self.gridY - king.gridY); if (kingCellDist <= 2) { affectedUnits.push(king); } } } // Visual warning effect LK.effects.flashScreen(0x660000, 200); // Perform ground slam animation var originalScale = self.baseScale * (self.isEnraged ? 1.2 : 1.0); tween(self, { scaleX: originalScale * 0.8, scaleY: originalScale * 1.3 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { // Damage all nearby units for (var i = 0; i < affectedUnits.length; i++) { var unit = affectedUnits[i]; var areaDamage = Math.floor(self.damage * 0.5); // Half damage for area attack var killed = unit.takeDamage(areaDamage); if (killed) { // Handle unit death 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 === unit) { grid[y][x].unit = null; // Trigger death animation instead of immediate destroy unit.deathAnimation(); for (var j = bench.length - 1; j >= 0; j--) { if (bench[j] === unit) { bench.splice(j, 1); break; } } break; } } } } // Flash effect on damaged unit LK.effects.flashObject(unit, 0xFF0000, 300); } // Return to normal size tween(self, { scaleX: originalScale, scaleY: originalScale }, { duration: 150, easing: tween.easeIn }); } }); }; // Override takeDamage to handle enrage mechanic self.takeDamage = function (damage) { self.health -= damage; self.updateHealthBar(); // Check for enrage if (!self.isEnraged && self.health / self.maxHealth <= self.rageThreshold) { self.isEnraged = true; // Visual enrage effect body.tint = 0xFF0000; // Bright red when enraged head.tint = 0x990000; // Dark red head // Increase stats when enraged self.damage = Math.floor(self.damage * 1.5); self.fireRate = Math.floor(self.fireRate * 0.7); // Attack faster self.speed = self.speed * 1.3; // Scale up slightly var enragedScale = self.baseScale * 1.2; tween(self, { scaleX: enragedScale, scaleY: enragedScale }, { duration: 500, easing: tween.easeOut }); // Flash effect LK.effects.flashObject(self, 0xFF0000, 1000); LK.effects.flashScreen(0x660000, 500); } if (self.health <= 0) { self.health = 0; return true; } return false; }; // Override attack with more devastating effects self.update = function () { if (!self.gridMovement) { self.gridMovement = new GridMovement(self, grid); } self.gridMovement.update(); // Check for special area attack if (LK.ticks - self.lastSpecialAttack >= self.specialAttackCooldown) { // Check if there are multiple units nearby var nearbyUnits = 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 cellDist = Math.abs(self.gridX - x) + Math.abs(self.gridY - y); if (cellDist <= 2) { nearbyUnits++; } } } } // Perform area attack if 2+ units are nearby if (nearbyUnits >= 2) { self.lastSpecialAttack = LK.ticks; self.performAreaAttack(); return; // Skip normal attack this frame } } // Find and attack units within range (cell-based) var closestUnit = null; var closestCellDistance = self.range + 1; // Only units within range (cell count) are valid // Smart target prioritization var targets = []; // First check if king exists and is in range if (kings.length > 0 && kings[0]) { var king = kings[0]; if (typeof king.gridX === "number" && typeof king.gridY === "number") { var kingCellDist = Math.abs(self.gridX - king.gridX) + Math.abs(self.gridY - king.gridY); if (kingCellDist <= self.range && self.gridMovement.isMoving === false) { targets.push({ unit: king, distance: kingCellDist, priority: 2 // Medium priority for king }); } } } // Then check other units on the grid 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 && self.gridMovement.isMoving === false) { var unit = gridSlot.unit; var priority = 3; // Default priority // Prioritize high-damage units if (unit.name === "Wizard" || unit.name === "Ranger") { priority = 1; // Highest priority } else if (unit.name === "Wall") { priority = 4; // Lowest priority } targets.push({ unit: unit, distance: cellDist, priority: priority }); } } } } } // Sort targets by priority, then by distance targets.sort(function (a, b) { if (a.priority !== b.priority) { return a.priority - b.priority; } return a.distance - b.distance; }); // Select best target if (targets.length > 0) { closestUnit = targets[0].unit; } // Attack the closest unit if found if (closestUnit) { if (!self.lastAttack) { self.lastAttack = 0; } if (LK.ticks - self.lastAttack >= self.fireRate) { self.lastAttack = LK.ticks; // Add devastating boss attack animation 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(60, dist * 0.5); // Even larger lunge for boss 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 boss with intense red and shake screen LK.effects.flashObject(self, 0xFF0000, 500); if (self.isEnraged) { LK.effects.flashScreen(0x880000, 400); // Darker red screen flash when enraged } else { LK.effects.flashScreen(0x440000, 300); // Dark red screen flash } // Lunge towards target with more force tween(self, { x: lungeX, y: lungeY, rotation: self.rotation + (Math.random() > 0.5 ? 0.1 : -0.1) }, { duration: 120, easing: tween.easeOut, onFinish: function onFinish() { // Apply massive damage after lunge var actualDamage = self.damage; // Critical hit chance if (Math.random() < self.criticalChance) { actualDamage = Math.floor(actualDamage * 2); LK.effects.flashObject(closestUnit, 0xFFFF00, 200); // Yellow flash for crit } var killed = closestUnit.takeDamage(actualDamage); 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; // Trigger death animation instead of immediate destroy closestUnit.deathAnimation(); 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, rotation: 0 }, { duration: 100, easing: tween.easeIn, onFinish: function onFinish() { self._isAttackTweening = false; self.isAttacking = false; // Clear attacking flag after animation } }); } }); } } } }; 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; // Trigger death animation instead of immediate destroy self.target.deathAnimation(); 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 ]; // Check if this entity is a player unit 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" || self.entity.name === "Wall" || self.entity.name === "Crossbow Tower"); for (var i = 0; i < directions.length; i++) { var newX = x + directions[i].x; var newY = y + directions[i].y; // Player units cannot path into fog of war (top 2 rows) if (isPlayerUnit && newY < 2) { continue; } // 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 { // Smarter enemy movement logic // First, check if enemy is currently in combat (attacking or being attacked) if (self.entity.isAttacking) { // Stay in place while attacking return false; } // Find closest player unit (excluding king initially) var closestUnit = null; var closestDist = 99999; var kingUnit = null; var kingDist = 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 unit = 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 (unit.name === "King") { // Track king separately if (dist < kingDist) { kingDist = dist; kingUnit = unit; } } else { // Track other player units if (dist < closestDist) { closestDist = dist; closestUnit = unit; targetGridX = x; targetGridY = y; } } } } } // Determine target based on smart AI logic if (closestUnit) { // Found a non-king player unit var entityRange = self.entity.range || 1; // If enemy is within range, stay and fight if (closestDist <= entityRange) { // Stay in current position - engage in combat return false; } // Move towards the closest player unit shouldMove = true; } else if (kingUnit) { // No other player units remaining, go for the king targetGridX = kingUnit.gridX; targetGridY = kingUnit.gridY; var entityRange = self.entity.range || 1; // If enemy is within range of king, stay and fight if (kingDist <= entityRange) { // Stay in current position - engage king in combat return false; } // Move towards the king 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]) { // Prevent player units from moving into fog of war (top 2 rows) 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" || self.entity.name === "Wall" || self.entity.name === "Crossbow Tower"); if (isPlayerUnit && nextY < 2) { // Player units cannot move into fog of war self.pathToTarget = []; self.pathIndex = 0; return false; } 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 - ensure exact grid alignment var finalGridX = self.currentCell.x; var finalGridY = self.currentCell.y; var finalX = GRID_START_X + finalGridX * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; var finalY = GRID_START_Y + finalGridY * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; self.entity.x = finalX; self.entity.y = finalY; 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; } // Prevent placement in fog of war (top 2 rows) if (self.gridY < 2) { // Show message about fog of war var fogMessage = new Text2('Cannot place units in the fog of war!', { size: 72, fill: 0xFF4444 }); fogMessage.anchor.set(0.5, 0.5); fogMessage.x = 1024; // Center of screen fogMessage.y = 800; // Middle of screen game.addChild(fogMessage); // Flash the message and fade it out LK.effects.flashObject(fogMessage, 0xFFFFFF, 300); tween(fogMessage, { alpha: 0, y: 700 }, { duration: 3000, easing: tween.easeOut, onFinish: function onFinish() { fogMessage.destroy(); } }); // Return unit to bench if it was being dragged if (unit.originalSlot) { unit.originalSlot.unit = unit; unit.x = unit.originalSlot.x; unit.y = unit.originalSlot.y; } else { // 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; } // 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: 72, 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: 5000, 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); } // Calculate exact grid cell center position var gridCenterX = GRID_START_X + self.gridX * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; var gridCenterY = GRID_START_Y + self.gridY * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; // Update unit's grid coordinates unit.gridX = self.gridX; unit.gridY = self.gridY; unit.lane = self.lane; // Force unit to exact grid position (snap to grid) unit.x = gridCenterX; unit.y = gridCenterY; // Ensure the grid slot also has the correct position self.x = gridCenterX; self.y = gridCenterY; // Add placement animation with bounce effect 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; } // Stop any ongoing scale tweens before starting new bounce tween.stop(unit, { scaleX: true, scaleY: true }); // Always reset scale to 0 before bounce (fixes bug where scale is not reset) unit.scaleX = 0; unit.scaleY = 0; // Capture unit reference before any async operations var unitToAnimate = unit; // Start bounce animation immediately // Set bounce animation flag to prevent updateUnitScale interference unitToAnimate._isBounceAnimating = true; tween(unitToAnimate, { scaleX: targetScale * 1.2, scaleY: targetScale * 1.2 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(unitToAnimate, { 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 // Delay showStarIndicator until after bounce animation completes LK.setTimeout(function () { if (unitToAnimate && unitToAnimate.parent) { // Check if unit still exists unitToAnimate.showStarIndicator(); // Show star indicator after bounce animation } }, 400); // Wait for bounce animation to complete (200ms + 150ms + small buffer) // Don't remove from bench - units stay in both places updateBenchDisplay(); countUnitsOnField(); // Update unit counts 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 () { // Check if bounce animation is in progress - if so, don't interfere if (self._isBounceAnimating) { return; // Skip scale update during bounce animation } // 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.deathAnimation = function () { // Hide health bar and star indicator immediately if (self.healthBar) { self.healthBar.destroy(); self.healthBar = null; } if (self.starIndicator) { self.starIndicator.destroy(); self.starIndicator = null; } // Create particle explosion effect var particleContainer = new Container(); particleContainer.x = self.x; particleContainer.y = self.y; self.parent.addChild(particleContainer); // Number of particles based on unit type var particleCount = 10; // Default for regular units var particleColors = []; var particleSize = 12; var explosionRadius = 60; // Customize particle colors based on unit type if (self.name === "Soldier" || self.name === "Knight") { particleColors = [0x32CD32, 0x228B22, 0x66ff66, 0x4da24d]; // Green shades } else if (self.name === "Wizard") { particleColors = [0x191970, 0x000066, 0x00FFFF, 0x4444FF]; // Blue/cyan shades } else if (self.name === "Ranger") { particleColors = [0x87CEEB, 0x9d9dff, 0x4169E1, 0x6495ED]; // Light blue shades } else if (self.name === "Paladin") { particleColors = [0x4169E1, 0x4444FF, 0xFFD700, 0x1E90FF]; // Royal blue with gold } else if (self.name === "Wall") { particleColors = [0x808080, 0x696969, 0x606060, 0x505050]; // Grey stone shades particleSize = 15; } else if (self.name === "Crossbow Tower") { particleColors = [0x8B4513, 0x654321, 0x696969, 0x5C4033]; // Brown/grey shades particleSize = 15; } else if (self.name === "King") { // King gets special treatment particleColors = [0xFFD700, 0xFFFF00, 0xFF0000, 0x4B0082]; // Gold, yellow, red, purple particleCount = 20; particleSize = 18; explosionRadius = 100; } else { // Default colors particleColors = [0xFFFFFF, 0xCCCCCC, 0x999999, 0x666666]; } // Create particles var particles = []; for (var i = 0; i < particleCount; i++) { var particle = particleContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: particleSize + Math.random() * 8, height: particleSize + Math.random() * 8 }); // Set particle color particle.tint = particleColors[Math.floor(Math.random() * particleColors.length)]; particle.alpha = 0.8 + Math.random() * 0.2; // Calculate random direction var angle = Math.PI * 2 * i / particleCount + (Math.random() - 0.5) * 0.5; var speed = explosionRadius * (0.7 + Math.random() * 0.3); // Store particle data particles.push({ sprite: particle, targetX: Math.cos(angle) * speed, targetY: Math.sin(angle) * speed, rotationSpeed: (Math.random() - 0.5) * 0.2 }); } // Animate unit shrinking and fading tween(self, { alpha: 0, scaleX: self.scaleX * 0.5, scaleY: self.scaleY * 0.5 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { // Destroy the unit self.destroy(); } }); // Animate particles bursting outward for (var i = 0; i < particles.length; i++) { var particleData = particles[i]; var particle = particleData.sprite; // Burst outward animation tween(particle, { x: particleData.targetX, y: particleData.targetY, scaleX: 0.1, scaleY: 0.1, alpha: 0, rotation: particle.rotation + particleData.rotationSpeed * 10 }, { duration: 600 + Math.random() * 200, easing: tween.easeOut, onFinish: function onFinish() { // Clean up after last particle if (i === particles.length - 1) { particleContainer.destroy(); } } }); } // Add a brief flash effect at the center var flash = particleContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: particleSize * 3, height: particleSize * 3 }); flash.tint = 0xFFFFFF; flash.alpha = 0.8; tween(flash, { scaleX: 3, scaleY: 3, alpha: 0 }, { duration: 300, easing: tween.easeOut }); }; 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) { // Trigger death animation instead of immediate destroy enemies[i].deathAnimation(); 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 showStarIndicator to prevent King from showing stars self.showStarIndicator = function () { // King doesn't show star indicators }; // 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); }; // Override hideStarIndicator to handle the case where it might be called self.hideStarIndicator = function () { // King doesn't have star indicators, so nothing to hide }; // Override updateUnitScale to prevent tier-based scaling for King self.updateUnitScale = function () { // King doesn't scale based on tier, only through levelUpKing function }; 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 ****/ // Create game title overlay that appears on top of everything var titleOverlay = new Container(); // Create gradient-like background effect with multiple layers var titleOverlayBg = titleOverlay.attachAsset('shop_area_bg', { anchorX: 0.5, anchorY: 0.5, width: 2048, height: 2732 }); titleOverlayBg.alpha = 0.95; titleOverlayBg.tint = 0x0a0a1a; // Deep dark blue instead of pure black titleOverlayBg.x = 0; // Center relative to parent titleOverlayBg.y = 0; // Center relative to parent // Add subtle vignette effect var vignette = titleOverlay.attachAsset('shop_area_bg', { anchorX: 0.5, anchorY: 0.5, width: 2200, height: 2900 }); vignette.alpha = 0.3; vignette.tint = 0x000000; vignette.x = 0; vignette.y = 0; // Create magical particle system for background var particleContainer = new Container(); titleOverlay.addChild(particleContainer); // Create floating magical particles function createMagicalParticle() { var particle = particleContainer.attachAsset('star_dot', { anchorX: 0.5, anchorY: 0.5, width: 6 + Math.random() * 8, height: 6 + Math.random() * 8 }); // Random colors for magical effect var colors = [0xFFD700, 0x4169E1, 0xFF69B4, 0x00CED1, 0xFFA500]; particle.tint = colors[Math.floor(Math.random() * colors.length)]; particle.alpha = 0; // Random starting position particle.x = (Math.random() - 0.5) * 2048; particle.y = 1366 + Math.random() * 800; // Animate particle floating up with glow tween(particle, { y: particle.y - 2000, alpha: 0.8 }, { duration: 2000, easing: tween.easeIn, onFinish: function onFinish() { tween(particle, { alpha: 0 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { particle.destroy(); } }); } }); // Add gentle horizontal sway tween(particle, { x: particle.x + (Math.random() - 0.5) * 200 }, { duration: 4000, easing: tween.easeInOut }); } // Create particles periodically var particleTimer = LK.setInterval(function () { if (titleOverlay.parent) { createMagicalParticle(); } else { LK.clearInterval(particleTimer); } }, 200); // Create title container for effects var titleContainer = new Container(); titleOverlay.addChild(titleContainer); titleContainer.y = -566; // Add golden glow behind title var titleGlow = titleContainer.attachAsset('shop_area_bg', { anchorX: 0.5, anchorY: 0.5, width: 800, height: 200 }); titleGlow.alpha = 0.3; titleGlow.tint = 0xFFD700; // Animate glow pulse tween(titleGlow, { scaleX: 1.2, scaleY: 1.2, alpha: 0.5 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { tween(titleGlow, { scaleX: 1.0, scaleY: 1.0, alpha: 0.3 }, { duration: 2000, easing: tween.easeInOut, onFinish: onFinish }); } }); // Game title with shadow effect var gameTitleShadow = new Text2('Protect Your King!', { size: 120, fill: 0x000000 }); gameTitleShadow.anchor.set(0.5, 0.5); gameTitleShadow.x = 4; gameTitleShadow.y = 4; gameTitleShadow.alpha = 0.5; titleContainer.addChild(gameTitleShadow); // Add subtle animation to shadow tween(gameTitleShadow, { x: 6, y: 6, alpha: 0.3 }, { duration: 3000, easing: tween.easeInOut, onFinish: function onFinish() { tween(gameTitleShadow, { x: 2, y: 2, alpha: 0.6 }, { duration: 3000, easing: tween.easeInOut, onFinish: onFinish }); } }); // Main game title var gameTitle = new Text2('Protect Your King!', { size: 120, fill: 0xFFD700 }); gameTitle.anchor.set(0.5, 0.5); gameTitle.x = 0; gameTitle.y = 0; titleContainer.addChild(gameTitle); // Add sparkle effects around title function createTitleSparkle() { var sparkle = titleContainer.attachAsset('star_dot', { anchorX: 0.5, anchorY: 0.5, width: 10, height: 10 }); sparkle.tint = 0xFFFFFF; sparkle.alpha = 0; sparkle.x = (Math.random() - 0.5) * 600; sparkle.y = (Math.random() - 0.5) * 150; tween(sparkle, { alpha: 1, scaleX: 1.5, scaleY: 1.5 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { tween(sparkle, { alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { sparkle.destroy(); } }); } }); } // Create sparkles periodically var sparkleTimer = LK.setInterval(function () { if (titleOverlay.parent) { createTitleSparkle(); } else { LK.clearInterval(sparkleTimer); } }, 300); // Subtitle with shadow var subtitleShadow = new Text2('Tower Defense', { size: 60, fill: 0x000000 }); subtitleShadow.anchor.set(0.5, 0.5); subtitleShadow.x = 2; subtitleShadow.y = -464; subtitleShadow.alpha = 0.5; titleOverlay.addChild(subtitleShadow); // Subtitle var gameSubtitle = new Text2('Tower Defense', { size: 60, fill: 0x87CEEB }); gameSubtitle.anchor.set(0.5, 0.5); gameSubtitle.x = 0; // Center relative to overlay gameSubtitle.y = -466; // Adjusted position relative to center titleOverlay.addChild(gameSubtitle); // Add continuous floating animation to subtitle function startSubtitleAnimation() { tween(gameSubtitle, { scaleX: 1.05, scaleY: 1.05, y: -456, alpha: 0.9 }, { duration: 2500, easing: tween.easeInOut, onFinish: function onFinish() { tween(gameSubtitle, { scaleX: 0.95, scaleY: 0.95, y: -476, alpha: 1.0 }, { duration: 2500, easing: tween.easeInOut, onFinish: startSubtitleAnimation }); } }); } startSubtitleAnimation(); // Start Game button container var startGameButton = new Container(); // Button shadow var startButtonShadow = startGameButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 400, height: 100 }); startButtonShadow.tint = 0x000000; startButtonShadow.alpha = 0.3; startButtonShadow.x = 4; startButtonShadow.y = 4; // Button gradient effect background var startButtonGlow = startGameButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 420, height: 120 }); startButtonGlow.tint = 0x66FF66; startButtonGlow.alpha = 0; // Main button background var startGameButtonBg = startGameButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 400, height: 100 }); startGameButtonBg.tint = 0x4169E1; // Royal blue to match theme better // Button text with shadow var startButtonTextShadow = new Text2('Start Game', { size: 60, fill: 0x000000 }); startButtonTextShadow.anchor.set(0.5, 0.5); startButtonTextShadow.x = 2; startButtonTextShadow.y = 2; startButtonTextShadow.alpha = 0.5; startGameButton.addChild(startButtonTextShadow); var startGameButtonText = new Text2('Start Game', { size: 60, fill: 0xFFFFFF }); startGameButtonText.anchor.set(0.5, 0.5); startGameButton.addChild(startGameButtonText); startGameButton.x = 0; // Center relative to overlay startGameButton.y = -66; // Move lower, more centered titleOverlay.addChild(startGameButton); // Add floating animation to start button tween(startGameButton, { y: -56, scaleX: 1.05, scaleY: 1.05 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { tween(startGameButton, { y: -66, scaleX: 1.0, scaleY: 1.0 }, { duration: 2000, easing: tween.easeInOut, onFinish: onFinish }); } }); // Add hover animation startGameButton.down = function () { tween(startGameButtonBg, { scaleX: 0.95, scaleY: 0.95 }, { duration: 100, easing: tween.easeOut }); tween(startButtonGlow, { alpha: 0.7, scaleX: 1.2, scaleY: 1.2 }, { duration: 200, easing: tween.easeOut }); }; // How to Play button container var howToPlayButton = new Container(); // Button shadow var howToPlayShadow = howToPlayButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 400, height: 100 }); howToPlayShadow.tint = 0x000000; howToPlayShadow.alpha = 0.3; howToPlayShadow.x = 4; howToPlayShadow.y = 4; // Button glow effect var howToPlayGlow = howToPlayButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 420, height: 120 }); howToPlayGlow.tint = 0x6666FF; howToPlayGlow.alpha = 0; // Main button background var howToPlayButtonBg = howToPlayButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 400, height: 100 }); howToPlayButtonBg.tint = 0x8A2BE2; // Blue violet for distinction // Button text with shadow var howToPlayTextShadow = new Text2('How to Play', { size: 60, fill: 0x000000 }); howToPlayTextShadow.anchor.set(0.5, 0.5); howToPlayTextShadow.x = 2; howToPlayTextShadow.y = 2; howToPlayTextShadow.alpha = 0.5; howToPlayButton.addChild(howToPlayTextShadow); var howToPlayButtonText = new Text2('How to Play', { size: 60, fill: 0xFFFFFF }); howToPlayButtonText.anchor.set(0.5, 0.5); howToPlayButton.addChild(howToPlayButtonText); howToPlayButton.x = 0; // Center relative to overlay howToPlayButton.y = 84; // Move lower, more centered titleOverlay.addChild(howToPlayButton); // Add floating animation to how to play button with slight offset tween(howToPlayButton, { y: 94, scaleX: 1.05, scaleY: 1.05 }, { duration: 2300, easing: tween.easeInOut, onFinish: function onFinish() { tween(howToPlayButton, { y: 84, scaleX: 1.0, scaleY: 1.0 }, { duration: 2300, easing: tween.easeInOut, onFinish: onFinish }); } }); // Add hover animation howToPlayButton.down = function () { tween(howToPlayButtonBg, { scaleX: 0.95, scaleY: 0.95 }, { duration: 100, easing: tween.easeOut }); tween(howToPlayGlow, { alpha: 0.7, scaleX: 1.2, scaleY: 1.2 }, { duration: 200, easing: tween.easeOut }); }; // How to Play button click handler howToPlayButton.up = function () { // Reset button scale tween(howToPlayButtonBg, { scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeIn }); tween(howToPlayGlow, { alpha: 0 }, { duration: 200, easing: tween.easeIn }); // Play sound effect LK.getSound('purchase').play(); // Create how to play modal var howToPlayModal = new Container(); var howToPlayModalBg = howToPlayModal.attachAsset('shop_area_bg', { anchorX: 0.5, anchorY: 0.5, width: 1600, height: 2000 }); howToPlayModalBg.alpha = 0.95; howToPlayModalBg.tint = 0x1a1a2e; // Dark blue theme howToPlayModalBg.x = 0; howToPlayModalBg.y = 0; // Modal title with fun subtitle var modalTitle = new Text2('How to Play', { size: 80, fill: 0xFFD700 }); modalTitle.anchor.set(0.5, 0.5); modalTitle.x = 0; // Center relative to modal modalTitle.y = -900; // Adjusted position relative to center howToPlayModal.addChild(modalTitle); // Fun subtitle var subtitle = new Text2('Master the Art of Tower Defense!', { size: 40, fill: 0x44FF44 }); subtitle.anchor.set(0.5, 0.5); subtitle.x = 0; subtitle.y = -820; howToPlayModal.addChild(subtitle); // Game mechanics section var mechanicsTitle = new Text2('— Game Basics —', { size: 48, fill: 0xFFD700 }); mechanicsTitle.anchor.set(0.5, 0.5); mechanicsTitle.x = 0; mechanicsTitle.y = -700; howToPlayModal.addChild(mechanicsTitle); // Instructions with better spacing var basicInstructions = ['💰 Purchase units from the shop using gold', '🎯 Drag units to the battlefield grid', '⚔️ Units auto-battle enemies in range', '⭐ Merge 3 identical units for upgrades', '👑 Level up your King for more capacity', '🛡️ Protect your King at all costs!', '🌊 Survive 10 waves to claim victory!']; for (var i = 0; i < basicInstructions.length; i++) { var instructionText = new Text2(basicInstructions[i], { size: 36, fill: 0xFFFFFF }); instructionText.anchor.set(0.5, 0.5); instructionText.x = 0; // Horizontally centered instructionText.y = -550 + i * 60; // Better spacing howToPlayModal.addChild(instructionText); } // Unit showcase title var unitsTitle = new Text2('— Meet Your Units —', { size: 48, fill: 0xFFD700 }); unitsTitle.anchor.set(0.5, 0.5); unitsTitle.x = 0; unitsTitle.y = -50; howToPlayModal.addChild(unitsTitle); // Create unit showcase with actual unit graphics var unitShowcase = [{ unit: new Soldier(), desc: 'Soldier: Swift melee warrior', x: -600, y: 100 }, { unit: new Knight(), desc: 'Knight: Tanky defender', x: -200, y: 100 }, { unit: new Wizard(), desc: 'Wizard: Magic damage dealer', x: 200, y: 100 }, { unit: new Ranger(), desc: 'Ranger: Long-range archer', x: 600, y: 100 }, { unit: new Paladin(), desc: 'Paladin: Elite warrior', x: -400, y: 350 }, { unit: new Wall(), desc: 'Wall: Defensive structure', x: 0, y: 350 }, { unit: new CrossbowTower(), desc: 'Tower: Area controller', x: 400, y: 350 }]; // Add units with animations for (var i = 0; i < unitShowcase.length; i++) { var showcase = unitShowcase[i]; var unitDisplay = showcase.unit; unitDisplay.x = showcase.x; unitDisplay.y = showcase.y; unitDisplay.scaleX = 0.8; unitDisplay.scaleY = 0.8; howToPlayModal.addChild(unitDisplay); // Add floating animation var baseY = unitDisplay.y; tween(unitDisplay, { y: baseY - 15, scaleX: 0.85, scaleY: 0.85 }, { duration: 1500 + Math.random() * 500, easing: tween.easeInOut, onFinish: function onFinish() { tween(unitDisplay, { y: baseY, scaleX: 0.8, scaleY: 0.8 }, { duration: 1500 + Math.random() * 500, easing: tween.easeInOut, onFinish: onFinish }); } }); // Add unit description var unitDesc = new Text2(showcase.desc, { size: 28, fill: 0xFFFFFF }); unitDesc.anchor.set(0.5, 0); unitDesc.x = showcase.x; unitDesc.y = showcase.y + 60; howToPlayModal.addChild(unitDesc); } // Enemy warning section var enemyWarning = new Text2('⚠️ Enemies spawn from the fog of war above! ⚠️', { size: 40, fill: 0xFF4444 }); enemyWarning.anchor.set(0.5, 0.5); enemyWarning.x = 0; enemyWarning.y = 600; howToPlayModal.addChild(enemyWarning); // Tips section var tipsText = new Text2('💡 Pro Tip: Save gold for interest bonus!', { size: 36, fill: 0x44FF44 }); tipsText.anchor.set(0.5, 0.5); tipsText.x = 0; tipsText.y = 700; howToPlayModal.addChild(tipsText); // Close button var closeButton = new Container(); var closeButtonBg = closeButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 300, height: 100 }); closeButtonBg.tint = 0xFF4444; var closeButtonText = new Text2('Got it!', { size: 60, fill: 0xFFFFFF }); closeButtonText.anchor.set(0.5, 0.5); closeButton.addChild(closeButtonText); closeButton.x = 0; // Center relative to modal closeButton.y = 850; // Bottom of modal howToPlayModal.addChild(closeButton); closeButton.up = function () { howToPlayModal.destroy(); }; titleOverlay.addChild(howToPlayModal); }; // Start Game button click handler startGameButton.up = function () { // Reset button scale tween(startGameButtonBg, { scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeIn }); tween(startButtonGlow, { alpha: 0 }, { duration: 200, easing: tween.easeIn }); // Play purchase sound for feedback LK.getSound('purchase').play(); // Create expanding circle transition effect var transitionCircle = titleOverlay.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: 50, height: 50 }); transitionCircle.tint = 0xFFD700; transitionCircle.x = 0; transitionCircle.y = -266; // Start from button position tween(transitionCircle, { scaleX: 100, scaleY: 100, alpha: 0.8 }, { duration: 600, easing: tween.easeOut }); // Hide title overlay with fade animation tween(titleOverlay, { alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { // Clear timers LK.clearInterval(particleTimer); LK.clearInterval(sparkleTimer); titleOverlay.destroy(); } }); }; // Add title overlay to LK.gui.center to ensure it renders above all game elements LK.gui.center.addChild(titleOverlay); // No need to set x,y as gui.center is already centered // Add enhanced title animation with rotation - constant loop function startTitleAnimation() { tween(titleContainer, { scaleX: 1.05, scaleY: 1.05, rotation: 0.02 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { tween(titleContainer, { scaleX: 0.95, scaleY: 0.95, rotation: -0.02 }, { duration: 2000, easing: tween.easeInOut, onFinish: startTitleAnimation }); } }); } startTitleAnimation(); // Add decorative crown above title var crownContainer = new Container(); titleOverlay.addChild(crownContainer); crownContainer.y = -666; // Create pixelart crown var crownBase = crownContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 80, height: 30 }); crownBase.tint = 0xFFD700; var crownLeft = crownContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 20, height: 40 }); crownLeft.x = -25; crownLeft.y = -20; crownLeft.tint = 0xFFD700; var crownCenter = crownContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 25, height: 50 }); crownCenter.x = 0; crownCenter.y = -25; crownCenter.tint = 0xFFD700; var crownRight = crownContainer.attachAsset('king', { anchorX: 0.5, anchorY: 0.5, width: 20, height: 40 }); crownRight.x = 25; crownRight.y = -20; crownRight.tint = 0xFFD700; // Add jewels to crown var jewelLeft = crownContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: 12, height: 12 }); jewelLeft.x = -25; jewelLeft.y = -35; jewelLeft.tint = 0xFF0000; var jewelCenter = crownContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 15 }); jewelCenter.x = 0; jewelCenter.y = -45; jewelCenter.tint = 0x4169E1; var jewelRight = crownContainer.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: 12, height: 12 }); jewelRight.x = 25; jewelRight.y = -35; jewelRight.tint = 0xFF0000; // Add sparkle animations to jewels tween(jewelLeft, { scaleX: 1.3, scaleY: 1.3, alpha: 0.8 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(jewelLeft, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 1000, easing: tween.easeInOut, onFinish: onFinish }); } }); // Offset timing for center jewel LK.setTimeout(function () { tween(jewelCenter, { scaleX: 1.4, scaleY: 1.4, alpha: 0.7 }, { duration: 1200, easing: tween.easeInOut, onFinish: function onFinish() { tween(jewelCenter, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 1200, easing: tween.easeInOut, onFinish: onFinish }); } }); }, 400); // Offset timing for right jewel LK.setTimeout(function () { tween(jewelRight, { scaleX: 1.3, scaleY: 1.3, alpha: 0.8 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(jewelRight, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 1000, easing: tween.easeInOut, onFinish: onFinish }); } }); }, 800); // Animate crown floating with enhanced effects function startCrownAnimation() { tween(crownContainer, { y: -656, rotation: 0.08, scaleX: 1.1, scaleY: 1.1 }, { duration: 2200, easing: tween.easeInOut, onFinish: function onFinish() { tween(crownContainer, { y: -676, rotation: -0.08, scaleX: 0.9, scaleY: 0.9 }, { duration: 2200, easing: tween.easeInOut, onFinish: startCrownAnimation }); } }); } startCrownAnimation(); 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 = 5; 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 kingInfoButton; // Declare kingInfoButton 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 var enemyContainer = new Container(); // Container for enemies to render below fog var topUIContainer = new Container(); // Container for top UI elements var isUIMinimized = false; // Track UI state // 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; // Skip king - it doesn't count towards any limit if (unit.name === "King") { continue; // Don't count king } if (unit.isStructure) { structuresOnField++; } else { unitsOnField++; } } } } // Update the max units text display maxUnitsText.setText('Units: ' + unitsOnField + '/' + maxUnitsOnField + ' | Structures: ' + structuresOnField + '/' + maxStructuresOnField); } // 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.toString()); levelText.setText('King Lvl: ' + playerLevel); countUnitsOnField(); // Update counts and display // 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 // Add enemy container to game first (so it renders below everything else) game.addChild(enemyContainer); // 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 // Calculate exact position for grid slot var slotX = GRID_START_X + x * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; var slotY = GRID_START_Y + y * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; gridSlot.x = slotX; gridSlot.y = slotY; grid[y][x] = gridSlot; } } // Create fog of war containers array to add later (after enemies spawn) var fogOverlays = []; // Create fog of war for top two rows for (var y = 0; y < 2; y++) { for (var x = 0; x < GRID_COLS; x++) { var fogOverlay = new Container(); var fogGraphics = fogOverlay.attachAsset('grid_slot', { anchorX: 0.5, anchorY: 0.5, width: GRID_CELL_SIZE - 10, height: GRID_CELL_SIZE - 10 }); fogGraphics.tint = 0x000000; // Black fog fogGraphics.alpha = 0.3; // More transparent to see enemies underneath fogOverlay.x = GRID_START_X + x * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; fogOverlay.y = GRID_START_Y + y * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; // Add some fog texture variation var fogDetail = fogOverlay.attachAsset('grid_slot', { anchorX: 0.5, anchorY: 0.5, width: GRID_CELL_SIZE * 0.8, height: GRID_CELL_SIZE * 0.8 }); fogDetail.tint = 0x222222; fogDetail.alpha = 0.2; // More transparent detail layer fogDetail.rotation = Math.random() * 0.2 - 0.1; // Store fog overlays to add later fogOverlays.push(fogOverlay); } } // 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; // Get the exact grid slot and place king there var kingSlot = grid[kingGridY][kingGridX]; kingSlot.unit = king; king.gridX = kingGridX; king.gridY = kingGridY; king.lane = 0; // Set king position to match grid slot exactly king.x = kingSlot.x; king.y = kingSlot.y; // King already has baseScale from Unit class, ensure it's applied king.updateUnitScale(); king.showHealthBar(); // Show health bar for king from start kings.push(king); // Register king position registerEntityPosition(king); // Create info button for king var kingInfoButton = new Container(); var kingInfoButtonBg = kingInfoButton.attachAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 80, height: 40 }); kingInfoButtonBg.tint = 0x4444FF; var kingInfoButtonText = new Text2('Info', { size: 32, fill: 0xFFFFFF }); kingInfoButtonText.anchor.set(0.5, 0.5); kingInfoButton.addChild(kingInfoButtonText); kingInfoButton.x = king.x; kingInfoButton.y = king.y + 100; // Position below king game.addChild(kingInfoButton); kingInfoButton.up = function () { showUnitInfoModal(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 = 9 * 120 + 8 * 40; // 9 slots of 120px width with 40px spacing var benchStartX = (2048 - benchTotalWidth) / 2 + 60; // Center horizontally for (var i = 0; i < 9; 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.toString()); 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(); // Add fog overlays on top of everything to ensure they render above enemies for (var i = 0; i < fogOverlays.length; i++) { game.addChild(fogOverlays[i]); } // Create top UI container topUIContainer = game.addChild(topUIContainer); topUIContainer.x = 1024; // Center of screen topUIContainer.y = 10; // Top of screen with small margin // Create top UI container background - bigger initially for planning phase var topUIBg = topUIContainer.addChild(LK.getAsset('bench_area_bg', { anchorX: 0.5, anchorY: 0, width: 1800, height: 240 // Bigger height for planning phase })); topUIBg.alpha = 0.7; topUIBg.tint = 0x3a3a3a; // Darker tint for top section // Create top row elements (Coin, Level, Max Units) var goldContainer = new Container(); goldContainer.x = 650; // Relative to topUIContainer goldContainer.y = 60; // Adjusted for bigger UI topUIContainer.addChild(goldContainer); // Create detailed pixelart coin var coinContainer = new Container(); goldContainer.addChild(coinContainer); coinContainer.x = -50; // Move coin further left to increase spacing // Coin base (outer ring) var coinBase = coinContainer.attachAsset('coin_base', { anchorX: 0.5, anchorY: 0.5 }); // Coin inner ring var coinInner = coinContainer.attachAsset('coin_inner', { anchorX: 0.5, anchorY: 0.5 }); // Coin center var coinCenter = coinContainer.attachAsset('coin_center', { anchorX: 0.5, anchorY: 0.5 }); // Coin highlight for shine effect var coinHighlight = coinContainer.attachAsset('coin_highlight', { anchorX: 0.5, anchorY: 0.5 }); coinHighlight.x = -6; coinHighlight.y = -6; // Add subtle rotation animation to make coin feel alive tween(coinContainer, { rotation: 0.1 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { tween(coinContainer, { rotation: -0.1 }, { duration: 2000, easing: tween.easeInOut, onFinish: onFinish }); } }); goldText = new Text2(gold.toString(), { size: 50, fill: 0xFFD700 }); goldText.anchor.set(0.5, 0.5); goldText.x = 40; // Move number further right to create more space goldContainer.addChild(goldText); var levelText = new Text2('King Lvl: ' + playerLevel, { size: 50, fill: 0x44FF44 }); levelText.anchor.set(0.5, 0.5); levelText.x = 0; // Center relative to topUIContainer levelText.y = 60; // Adjusted for bigger UI topUIContainer.addChild(levelText); var maxUnitsText = new Text2('Units: 0/' + maxUnitsOnField + ' | Structures: 0/' + maxStructuresOnField, { size: 50, fill: 0xFFFFFF }); maxUnitsText.anchor.set(0.5, 0.5); maxUnitsText.x = -500; // Left of center relative to topUIContainer maxUnitsText.y = 60; // Adjusted for bigger UI topUIContainer.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 = -300; // Left of center relative to topUIContainer stateText.y = 140; // Adjusted for bigger UI topUIContainer.addChild(stateText); var waveText = new Text2('Wave: ' + wave, { size: 40, fill: 0xFFFFFF }); waveText.anchor.set(0.5, 0.5); waveText.x = 300; // Right of center relative to topUIContainer waveText.y = 140; // Adjusted for bigger UI topUIContainer.addChild(waveText); // Function to minimize UI during battle function minimizeUI() { // Tween background to smaller size tween(topUIBg, { height: 120 }, { duration: 400, easing: tween.easeInOut }); // Move ready button up tween(readyButton, { y: 90 }, { duration: 400, easing: tween.easeInOut }); // Move state and wave text up tween(stateText, { y: 90 }, { duration: 400, easing: tween.easeInOut }); tween(waveText, { y: 90 }, { duration: 400, easing: tween.easeInOut }); } // Function to maximize UI during planning function maximizeUI() { // Tween background to bigger size tween(topUIBg, { height: 240 }, { duration: 400, easing: tween.easeInOut }); // Move ready button down tween(readyButton, { y: 190 }, { duration: 400, easing: tween.easeInOut }); // Move state and wave text down tween(stateText, { y: 140 }, { duration: 400, easing: tween.easeInOut }); tween(waveText, { y: 140 }, { duration: 400, easing: tween.easeInOut }); } // 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!'); // Show king info button during planning if (kingInfoButton) { kingInfoButton.visible = true; } // Maximize UI for planning phase if (isUIMinimized) { isUIMinimized = false; maximizeUI(); } } 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'); // Hide king info button during battle if (kingInfoButton) { kingInfoButton.visible = false; } // Minimize UI for battle phase if (!isUIMinimized) { isUIMinimized = true; minimizeUI(); } } } var readyButton = new Container(); var readyButtonBg = LK.getAsset('shop_button', { anchorX: 0.5, anchorY: 0.5, width: 300, height: 100 }); readyButtonBg.tint = 0x44AA44; var readyButtonText = new Text2('Ready!', { size: 72, fill: 0xFFFFFF }); readyButtonText.anchor.set(0.5, 0.5); readyButton.addChild(readyButtonBg); readyButton.addChild(readyButtonText); topUIContainer.addChild(readyButton); readyButton.x = 0; // Center relative to topUIContainer readyButton.y = 190; // Position in bigger UI 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 move units to closest grid spots after battle function moveUnitsToClosestGridSpots() { 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.isStructure && gridSlot.unit.name !== "King") { var unit = gridSlot.unit; var currentGridX = unit.gridX; var currentGridY = unit.gridY; // Find closest valid grid position var closestGridX = currentGridX; var closestGridY = currentGridY; var minDistance = 999; for (var checkY = 2; checkY < GRID_ROWS; checkY++) { // Start from row 2 to avoid fog of war for (var checkX = 0; checkX < GRID_COLS; checkX++) { var checkSlot = grid[checkY][checkX]; // Check if position is valid (not occupied or same unit) if (!checkSlot.unit || checkSlot.unit === unit) { var distance = Math.abs(checkX - currentGridX) + Math.abs(checkY - currentGridY); if (distance < minDistance) { minDistance = distance; closestGridX = checkX; closestGridY = checkY; } } } } // Only move if we found a different position if (closestGridX !== currentGridX || closestGridY !== currentGridY) { var targetSlot = grid[closestGridY][closestGridX]; var targetX = targetSlot.x; var targetY = targetSlot.y; // Clear old position gridSlot.unit = null; unregisterEntityPosition(unit, currentGridX, currentGridY); // Set new position targetSlot.unit = unit; unit.gridX = closestGridX; unit.gridY = closestGridY; registerEntityPosition(unit); // Animate unit to new position tween(unit, { x: targetX, y: targetY }, { duration: 800, easing: tween.easeInOut }); } } } } } // 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); var enemy; // Spawn boss monster every 5 waves if (wave % 5 === 0 && enemiesSpawned === Math.floor(enemiesPerWave / 2)) { enemy = new Boss(); // Scale boss stats based on wave if (wave > 1) { var waveMultiplier = 1 + (wave - 1) * 0.2; enemy.health = Math.floor(enemy.health * waveMultiplier); enemy.maxHealth = enemy.health; enemy.damage = Math.floor(enemy.damage * waveMultiplier); enemy.armor = Math.floor(enemy.armor * (1 + (wave - 1) * 0.1)); enemy.magicResist = Math.floor(enemy.magicResist * (1 + (wave - 1) * 0.1)); enemy.goldValue = Math.floor(enemy.goldValue * waveMultiplier); } } else { // Increase ranged enemy chance as waves progress var rangedChance = Math.min(0.3 + wave * 0.05, 0.7); if (Math.random() < rangedChance) { enemy = new RangedEnemy(); } else { enemy = new Enemy(); } // 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)); } // Try to spawn in a strategic position var bestSpawnX = spawnX; var minPlayerUnits = 999; // Check each column for player units for (var x = 0; x < GRID_COLS; x++) { var playerUnitsInColumn = 0; for (var y = 0; y < GRID_ROWS; y++) { if (grid[y] && grid[y][x] && grid[y][x].unit) { playerUnitsInColumn++; } } // Boss prefers columns with fewer player units if (enemy.name === "Boss Monster" && playerUnitsInColumn < minPlayerUnits) { minPlayerUnits = playerUnitsInColumn; bestSpawnX = x; } } // Position enemy at the first row (top) of the grid enemy.gridX = enemy.name === "Boss Monster" ? bestSpawnX : spawnX; enemy.gridY = 0; // Set visual position to match grid position var topCell = grid[0][enemy.gridX]; enemy.x = topCell.x; enemy.y = topCell.y; enemy.lane = 0; // Single grid, so lane is always 0 // Apply base scale if (enemy.name !== "Boss Monster") { // Scale up enemy by 30% enemy.scaleX = 1.3; enemy.scaleY = 1.3; } enemy.showHealthBar(); // Show health bar when enemy is spawned enemy.updateHealthBar(); // Initialize GridMovement immediately for the enemy enemy.gridMovement = new GridMovement(enemy, grid); enemies.push(enemy); enemyContainer.addChild(enemy); // Add spawn animation enemy.alpha = 0; var baseScale = enemy.baseScale || 1.3; enemy.scaleX = baseScale * 0.5; enemy.scaleY = baseScale * 1.5; tween(enemy, { alpha: 1, scaleX: baseScale, scaleY: baseScale }, { 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) { // Prevent King from being moved to bench if (draggedUnit.name !== 'King') { 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(); // Add bounce effect when unit is moved to bench var targetScale = draggedUnit.baseScale || 1.3; if (draggedUnit.tier === 2) { targetScale = draggedUnit.baseScale * 1.2; } else if (draggedUnit.tier === 3) { targetScale = draggedUnit.baseScale * 1.3; } draggedUnit.scaleX = targetScale * 0.8; draggedUnit.scaleY = targetScale * 0.8; // Capture unit reference before clearing draggedUnit var unitToAnimate = draggedUnit; tween(unitToAnimate, { scaleX: targetScale * 1.2, scaleY: targetScale * 1.2 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(unitToAnimate, { scaleX: targetScale, scaleY: targetScale }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { // Clear bounce animation flag when bounce is complete unitToAnimate._isBounceAnimating = false; } }); } }); break; } } } } // If not dropped on valid slot, return to original position if (!dropped) { // Handle King separately - must return to grid if (draggedUnit.name === 'King') { // King must return to original grid position if (draggedFromGrid && draggedUnit.originalGridSlot) { draggedUnit.x = draggedUnit.originalX; draggedUnit.y = draggedUnit.originalY; draggedUnit.originalGridSlot.unit = draggedUnit; // Restore unit to original grid slot } } else { // 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(); // Add bounce effect when unit is returned to bench var targetScale = draggedUnit.baseScale || 1.3; if (draggedUnit.tier === 2) { targetScale = draggedUnit.baseScale * 1.2; } else if (draggedUnit.tier === 3) { targetScale = draggedUnit.baseScale * 1.3; } draggedUnit.scaleX = targetScale * 0.8; draggedUnit.scaleY = targetScale * 0.8; // Capture unit reference before clearing draggedUnit var unitToAnimate = draggedUnit; tween(unitToAnimate, { scaleX: targetScale * 1.2, scaleY: targetScale * 1.2 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(unitToAnimate, { scaleX: targetScale, scaleY: targetScale }, { duration: 150, easing: tween.easeIn }); } }); 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.toString()); // 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(); // Add bounce effect when unit is added to bench newUnit.scaleX = 0; newUnit.scaleY = 0; // Capture unit reference for animation var unitToAnimate = newUnit; var targetScale = unitToAnimate.baseScale || 1.3; tween(unitToAnimate, { scaleX: targetScale * 1.2, scaleY: targetScale * 1.2 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(unitToAnimate, { scaleX: targetScale, scaleY: targetScale }, { duration: 150, easing: tween.easeIn }); } }); // 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 - move units to closest grid spots first moveUnitsToClosestGridSpots(); // 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.toString()); // Check for unit upgrades when entering planning phase checkAndUpgradeUnits(); // Show reward message var rewardText = new Text2('Wave Complete! +' + waveReward + ' Gold (5 + ' + interest + ' interest)', { size: 96, fill: 0xFFD700 }); rewardText.anchor.set(0.5, 0.5); rewardText.x = 1024; rewardText.y = 800; game.addChild(rewardText); // Fade out reward text with longer duration tween(rewardText, { alpha: 0, y: 700 }, { duration: 4000, 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 } // Update unit counts in case any units were destroyed countUnitsOnField(); // 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(); } // Update king info button position if this is the king if (gridSlot.unit.name === 'King' && kingInfoButton) { kingInfoButton.x = gridSlot.unit.x; kingInfoButton.y = gridSlot.unit.y + 100; // Position below king } } } } // 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(); } };
/****
* 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) {
// Trigger death animation instead of immediate destroy
enemies[i].deathAnimation();
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.deathAnimation = function () {
// Hide health bar immediately
if (self.healthBar) {
self.healthBar.destroy();
self.healthBar = null;
}
// Create particle explosion effect
var particleContainer = new Container();
particleContainer.x = self.x;
particleContainer.y = self.y;
self.parent.addChild(particleContainer);
// Number of particles based on enemy type
var particleCount = 12; // Default for regular enemies
var particleColors = [0xFF4444, 0xFF6666, 0xFF8888, 0xFFAAAA]; // Red shades for regular enemies
var particleSize = 15;
var explosionRadius = 80;
// Customize based on enemy type
if (self.name === "Ranged Enemy") {
particleColors = [0x8A2BE2, 0x9370DB, 0xBA55D3, 0xDDA0DD]; // Purple shades
particleCount = 10;
} else if (self.name === "Boss Monster") {
particleColors = [0x800000, 0xA52A2A, 0xDC143C, 0xFF0000]; // Dark red to bright red
particleCount = 20; // More particles for boss
particleSize = 20;
explosionRadius = 120;
}
// Create particles
var particles = [];
for (var i = 0; i < particleCount; i++) {
var particle = particleContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: particleSize + Math.random() * 10,
height: particleSize + Math.random() * 10
});
// Set particle color
particle.tint = particleColors[Math.floor(Math.random() * particleColors.length)];
particle.alpha = 0.8 + Math.random() * 0.2;
// Calculate random direction
var angle = Math.PI * 2 * i / particleCount + (Math.random() - 0.5) * 0.5;
var speed = explosionRadius * (0.7 + Math.random() * 0.3);
// Store particle data
particles.push({
sprite: particle,
targetX: Math.cos(angle) * speed,
targetY: Math.sin(angle) * speed,
rotationSpeed: (Math.random() - 0.5) * 0.2
});
}
// Animate enemy shrinking and fading
tween(self, {
alpha: 0,
scaleX: self.scaleX * 0.5,
scaleY: self.scaleY * 0.5
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
// Destroy the enemy
self.destroy();
}
});
// Animate particles bursting outward
for (var i = 0; i < particles.length; i++) {
var particleData = particles[i];
var particle = particleData.sprite;
// Burst outward animation
tween(particle, {
x: particleData.targetX,
y: particleData.targetY,
scaleX: 0.1,
scaleY: 0.1,
alpha: 0,
rotation: particle.rotation + particleData.rotationSpeed * 10
}, {
duration: 600 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Clean up after last particle
if (i === particles.length - 1) {
particleContainer.destroy();
}
}
});
}
// Add a brief flash effect at the center
var flash = particleContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: particleSize * 3,
height: particleSize * 3
});
flash.tint = 0xFFFFFF;
flash.alpha = 0.8;
tween(flash, {
scaleX: 3,
scaleY: 3,
alpha: 0
}, {
duration: 300,
easing: tween.easeOut
});
};
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
// First check if king exists and is in range
if (kings.length > 0 && kings[0]) {
var king = kings[0];
if (typeof king.gridX === "number" && typeof king.gridY === "number") {
var kingCellDist = Math.abs(self.gridX - king.gridX) + Math.abs(self.gridY - king.gridY);
if (kingCellDist <= self.range && kingCellDist < closestCellDistance && self.gridMovement.isMoving === false) {
closestCellDistance = kingCellDist;
closestUnit = king;
}
}
}
// Then check other units on the grid
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;
// Trigger death animation instead of immediate destroy
closestUnit.deathAnimation();
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
// First check if king exists and is in range
if (kings.length > 0 && kings[0]) {
var king = kings[0];
if (typeof king.gridX === "number" && typeof king.gridY === "number") {
var kingCellDist = Math.abs(self.gridX - king.gridX) + Math.abs(self.gridY - king.gridY);
if (kingCellDist <= self.range && kingCellDist < closestCellDistance) {
closestCellDistance = kingCellDist;
closestUnit = king;
}
}
}
// Then check other units on the grid
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);
enemyContainer.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 Boss = Enemy.expand(function () {
var self = Enemy.call(this);
// Remove default enemy graphics
self.removeChildren();
// Create massive boss container
var bossContainer = new Container();
self.addChild(bossContainer);
// Main body (much larger and darker)
var body = bossContainer.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
body.width = 120;
body.height = 120;
body.y = 15;
body.tint = 0x800000; // Dark red
// Boss head (menacing)
var head = bossContainer.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5,
width: 80,
height: 70
});
head.y = -60;
head.tint = 0x4a0000; // Very dark red
// Glowing eyes (intimidating)
var leftEye = bossContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: 15,
height: 15
});
leftEye.x = -20;
leftEye.y = -60;
leftEye.tint = 0xFF0000; // Bright red glowing eyes
var rightEye = bossContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: 15,
height: 15
});
rightEye.x = 20;
rightEye.y = -60;
rightEye.tint = 0xFF0000;
// Massive claws (larger and more threatening)
var leftClaw = bossContainer.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5,
width: 40,
height: 30
});
leftClaw.x = -50;
leftClaw.y = 10;
leftClaw.rotation = 0.7;
leftClaw.tint = 0x2a0000; // Very dark
var rightClaw = bossContainer.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5,
width: 40,
height: 30
});
rightClaw.x = 50;
rightClaw.y = 10;
rightClaw.rotation = -0.7;
rightClaw.tint = 0x2a0000;
// Boss spikes on shoulders
var leftSpike = bossContainer.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5,
width: 15,
height: 40
});
leftSpike.x = -40;
leftSpike.y = -20;
leftSpike.rotation = 0.3;
leftSpike.tint = 0x1a1a1a; // Dark spikes
var rightSpike = bossContainer.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5,
width: 15,
height: 40
});
rightSpike.x = 40;
rightSpike.y = -20;
rightSpike.rotation = -0.3;
rightSpike.tint = 0x1a1a1a;
// Boss crown/horns
var horn1 = bossContainer.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5,
width: 12,
height: 35
});
horn1.x = -15;
horn1.y = -85;
horn1.rotation = 0.2;
horn1.tint = 0x000000; // Black horns
var horn2 = bossContainer.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5,
width: 12,
height: 35
});
horn2.x = 15;
horn2.y = -85;
horn2.rotation = -0.2;
horn2.tint = 0x000000;
// Override boss attributes - much more powerful
self.health = 200;
self.maxHealth = 200;
self.damage = 60;
self.armor = 15;
self.criticalChance = 0.35;
self.magicResist = 15;
self.mana = 100;
self.speed = 1.2; // Slightly slower but hits harder
self.range = 3; // Even longer reach than normal enemies
self.goldValue = 25; // Very high reward for defeating boss
self.name = "Boss Monster";
self.fireRate = 75; // Faster attack rate with devastating damage
self.baseScale = 2.0; // Much larger than regular enemies (100% larger)
// Boss special abilities
self.lastSpecialAttack = 0;
self.specialAttackCooldown = 300; // 5 seconds
self.isEnraged = false;
self.rageThreshold = 0.3; // Enrage when below 30% health
// Apply base scale
self.scaleX = self.baseScale;
self.scaleY = self.baseScale;
// Add menacing glow effect to eyes
tween(leftEye, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(leftEye, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
tween(rightEye, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(rightEye, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// Boss special attack - area damage
self.performAreaAttack = function () {
// Get all units within 2 cells
var affectedUnits = [];
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;
var cellDist = Math.abs(self.gridX - x) + Math.abs(self.gridY - y);
if (cellDist <= 2) {
affectedUnits.push(unit);
}
}
}
}
// Check king separately
if (kings.length > 0 && kings[0]) {
var king = kings[0];
if (typeof king.gridX === "number" && typeof king.gridY === "number") {
var kingCellDist = Math.abs(self.gridX - king.gridX) + Math.abs(self.gridY - king.gridY);
if (kingCellDist <= 2) {
affectedUnits.push(king);
}
}
}
// Visual warning effect
LK.effects.flashScreen(0x660000, 200);
// Perform ground slam animation
var originalScale = self.baseScale * (self.isEnraged ? 1.2 : 1.0);
tween(self, {
scaleX: originalScale * 0.8,
scaleY: originalScale * 1.3
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Damage all nearby units
for (var i = 0; i < affectedUnits.length; i++) {
var unit = affectedUnits[i];
var areaDamage = Math.floor(self.damage * 0.5); // Half damage for area attack
var killed = unit.takeDamage(areaDamage);
if (killed) {
// Handle unit death
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 === unit) {
grid[y][x].unit = null;
// Trigger death animation instead of immediate destroy
unit.deathAnimation();
for (var j = bench.length - 1; j >= 0; j--) {
if (bench[j] === unit) {
bench.splice(j, 1);
break;
}
}
break;
}
}
}
}
// Flash effect on damaged unit
LK.effects.flashObject(unit, 0xFF0000, 300);
}
// Return to normal size
tween(self, {
scaleX: originalScale,
scaleY: originalScale
}, {
duration: 150,
easing: tween.easeIn
});
}
});
};
// Override takeDamage to handle enrage mechanic
self.takeDamage = function (damage) {
self.health -= damage;
self.updateHealthBar();
// Check for enrage
if (!self.isEnraged && self.health / self.maxHealth <= self.rageThreshold) {
self.isEnraged = true;
// Visual enrage effect
body.tint = 0xFF0000; // Bright red when enraged
head.tint = 0x990000; // Dark red head
// Increase stats when enraged
self.damage = Math.floor(self.damage * 1.5);
self.fireRate = Math.floor(self.fireRate * 0.7); // Attack faster
self.speed = self.speed * 1.3;
// Scale up slightly
var enragedScale = self.baseScale * 1.2;
tween(self, {
scaleX: enragedScale,
scaleY: enragedScale
}, {
duration: 500,
easing: tween.easeOut
});
// Flash effect
LK.effects.flashObject(self, 0xFF0000, 1000);
LK.effects.flashScreen(0x660000, 500);
}
if (self.health <= 0) {
self.health = 0;
return true;
}
return false;
};
// Override attack with more devastating effects
self.update = function () {
if (!self.gridMovement) {
self.gridMovement = new GridMovement(self, grid);
}
self.gridMovement.update();
// Check for special area attack
if (LK.ticks - self.lastSpecialAttack >= self.specialAttackCooldown) {
// Check if there are multiple units nearby
var nearbyUnits = 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 cellDist = Math.abs(self.gridX - x) + Math.abs(self.gridY - y);
if (cellDist <= 2) {
nearbyUnits++;
}
}
}
}
// Perform area attack if 2+ units are nearby
if (nearbyUnits >= 2) {
self.lastSpecialAttack = LK.ticks;
self.performAreaAttack();
return; // Skip normal attack this frame
}
}
// Find and attack units within range (cell-based)
var closestUnit = null;
var closestCellDistance = self.range + 1; // Only units within range (cell count) are valid
// Smart target prioritization
var targets = [];
// First check if king exists and is in range
if (kings.length > 0 && kings[0]) {
var king = kings[0];
if (typeof king.gridX === "number" && typeof king.gridY === "number") {
var kingCellDist = Math.abs(self.gridX - king.gridX) + Math.abs(self.gridY - king.gridY);
if (kingCellDist <= self.range && self.gridMovement.isMoving === false) {
targets.push({
unit: king,
distance: kingCellDist,
priority: 2 // Medium priority for king
});
}
}
}
// Then check other units on the grid
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 && self.gridMovement.isMoving === false) {
var unit = gridSlot.unit;
var priority = 3; // Default priority
// Prioritize high-damage units
if (unit.name === "Wizard" || unit.name === "Ranger") {
priority = 1; // Highest priority
} else if (unit.name === "Wall") {
priority = 4; // Lowest priority
}
targets.push({
unit: unit,
distance: cellDist,
priority: priority
});
}
}
}
}
}
// Sort targets by priority, then by distance
targets.sort(function (a, b) {
if (a.priority !== b.priority) {
return a.priority - b.priority;
}
return a.distance - b.distance;
});
// Select best target
if (targets.length > 0) {
closestUnit = targets[0].unit;
}
// Attack the closest unit if found
if (closestUnit) {
if (!self.lastAttack) {
self.lastAttack = 0;
}
if (LK.ticks - self.lastAttack >= self.fireRate) {
self.lastAttack = LK.ticks;
// Add devastating boss attack animation
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(60, dist * 0.5); // Even larger lunge for boss
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 boss with intense red and shake screen
LK.effects.flashObject(self, 0xFF0000, 500);
if (self.isEnraged) {
LK.effects.flashScreen(0x880000, 400); // Darker red screen flash when enraged
} else {
LK.effects.flashScreen(0x440000, 300); // Dark red screen flash
}
// Lunge towards target with more force
tween(self, {
x: lungeX,
y: lungeY,
rotation: self.rotation + (Math.random() > 0.5 ? 0.1 : -0.1)
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
// Apply massive damage after lunge
var actualDamage = self.damage;
// Critical hit chance
if (Math.random() < self.criticalChance) {
actualDamage = Math.floor(actualDamage * 2);
LK.effects.flashObject(closestUnit, 0xFFFF00, 200); // Yellow flash for crit
}
var killed = closestUnit.takeDamage(actualDamage);
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;
// Trigger death animation instead of immediate destroy
closestUnit.deathAnimation();
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,
rotation: 0
}, {
duration: 100,
easing: tween.easeIn,
onFinish: function onFinish() {
self._isAttackTweening = false;
self.isAttacking = false; // Clear attacking flag after animation
}
});
}
});
}
}
}
};
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;
// Trigger death animation instead of immediate destroy
self.target.deathAnimation();
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
];
// Check if this entity is a player unit
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" || self.entity.name === "Wall" || self.entity.name === "Crossbow Tower");
for (var i = 0; i < directions.length; i++) {
var newX = x + directions[i].x;
var newY = y + directions[i].y;
// Player units cannot path into fog of war (top 2 rows)
if (isPlayerUnit && newY < 2) {
continue;
}
// 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 {
// Smarter enemy movement logic
// First, check if enemy is currently in combat (attacking or being attacked)
if (self.entity.isAttacking) {
// Stay in place while attacking
return false;
}
// Find closest player unit (excluding king initially)
var closestUnit = null;
var closestDist = 99999;
var kingUnit = null;
var kingDist = 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 unit = 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 (unit.name === "King") {
// Track king separately
if (dist < kingDist) {
kingDist = dist;
kingUnit = unit;
}
} else {
// Track other player units
if (dist < closestDist) {
closestDist = dist;
closestUnit = unit;
targetGridX = x;
targetGridY = y;
}
}
}
}
}
// Determine target based on smart AI logic
if (closestUnit) {
// Found a non-king player unit
var entityRange = self.entity.range || 1;
// If enemy is within range, stay and fight
if (closestDist <= entityRange) {
// Stay in current position - engage in combat
return false;
}
// Move towards the closest player unit
shouldMove = true;
} else if (kingUnit) {
// No other player units remaining, go for the king
targetGridX = kingUnit.gridX;
targetGridY = kingUnit.gridY;
var entityRange = self.entity.range || 1;
// If enemy is within range of king, stay and fight
if (kingDist <= entityRange) {
// Stay in current position - engage king in combat
return false;
}
// Move towards the king
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]) {
// Prevent player units from moving into fog of war (top 2 rows)
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" || self.entity.name === "Wall" || self.entity.name === "Crossbow Tower");
if (isPlayerUnit && nextY < 2) {
// Player units cannot move into fog of war
self.pathToTarget = [];
self.pathIndex = 0;
return false;
}
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 - ensure exact grid alignment
var finalGridX = self.currentCell.x;
var finalGridY = self.currentCell.y;
var finalX = GRID_START_X + finalGridX * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
var finalY = GRID_START_Y + finalGridY * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
self.entity.x = finalX;
self.entity.y = finalY;
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;
}
// Prevent placement in fog of war (top 2 rows)
if (self.gridY < 2) {
// Show message about fog of war
var fogMessage = new Text2('Cannot place units in the fog of war!', {
size: 72,
fill: 0xFF4444
});
fogMessage.anchor.set(0.5, 0.5);
fogMessage.x = 1024; // Center of screen
fogMessage.y = 800; // Middle of screen
game.addChild(fogMessage);
// Flash the message and fade it out
LK.effects.flashObject(fogMessage, 0xFFFFFF, 300);
tween(fogMessage, {
alpha: 0,
y: 700
}, {
duration: 3000,
easing: tween.easeOut,
onFinish: function onFinish() {
fogMessage.destroy();
}
});
// Return unit to bench if it was being dragged
if (unit.originalSlot) {
unit.originalSlot.unit = unit;
unit.x = unit.originalSlot.x;
unit.y = unit.originalSlot.y;
} else {
// 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;
}
// 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: 72,
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: 5000,
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);
}
// Calculate exact grid cell center position
var gridCenterX = GRID_START_X + self.gridX * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
var gridCenterY = GRID_START_Y + self.gridY * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
// Update unit's grid coordinates
unit.gridX = self.gridX;
unit.gridY = self.gridY;
unit.lane = self.lane;
// Force unit to exact grid position (snap to grid)
unit.x = gridCenterX;
unit.y = gridCenterY;
// Ensure the grid slot also has the correct position
self.x = gridCenterX;
self.y = gridCenterY;
// Add placement animation with bounce effect
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;
}
// Stop any ongoing scale tweens before starting new bounce
tween.stop(unit, {
scaleX: true,
scaleY: true
});
// Always reset scale to 0 before bounce (fixes bug where scale is not reset)
unit.scaleX = 0;
unit.scaleY = 0;
// Capture unit reference before any async operations
var unitToAnimate = unit;
// Start bounce animation immediately
// Set bounce animation flag to prevent updateUnitScale interference
unitToAnimate._isBounceAnimating = true;
tween(unitToAnimate, {
scaleX: targetScale * 1.2,
scaleY: targetScale * 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(unitToAnimate, {
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
// Delay showStarIndicator until after bounce animation completes
LK.setTimeout(function () {
if (unitToAnimate && unitToAnimate.parent) {
// Check if unit still exists
unitToAnimate.showStarIndicator(); // Show star indicator after bounce animation
}
}, 400); // Wait for bounce animation to complete (200ms + 150ms + small buffer)
// Don't remove from bench - units stay in both places
updateBenchDisplay();
countUnitsOnField(); // Update unit counts
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 () {
// Check if bounce animation is in progress - if so, don't interfere
if (self._isBounceAnimating) {
return; // Skip scale update during bounce animation
}
// 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.deathAnimation = function () {
// Hide health bar and star indicator immediately
if (self.healthBar) {
self.healthBar.destroy();
self.healthBar = null;
}
if (self.starIndicator) {
self.starIndicator.destroy();
self.starIndicator = null;
}
// Create particle explosion effect
var particleContainer = new Container();
particleContainer.x = self.x;
particleContainer.y = self.y;
self.parent.addChild(particleContainer);
// Number of particles based on unit type
var particleCount = 10; // Default for regular units
var particleColors = [];
var particleSize = 12;
var explosionRadius = 60;
// Customize particle colors based on unit type
if (self.name === "Soldier" || self.name === "Knight") {
particleColors = [0x32CD32, 0x228B22, 0x66ff66, 0x4da24d]; // Green shades
} else if (self.name === "Wizard") {
particleColors = [0x191970, 0x000066, 0x00FFFF, 0x4444FF]; // Blue/cyan shades
} else if (self.name === "Ranger") {
particleColors = [0x87CEEB, 0x9d9dff, 0x4169E1, 0x6495ED]; // Light blue shades
} else if (self.name === "Paladin") {
particleColors = [0x4169E1, 0x4444FF, 0xFFD700, 0x1E90FF]; // Royal blue with gold
} else if (self.name === "Wall") {
particleColors = [0x808080, 0x696969, 0x606060, 0x505050]; // Grey stone shades
particleSize = 15;
} else if (self.name === "Crossbow Tower") {
particleColors = [0x8B4513, 0x654321, 0x696969, 0x5C4033]; // Brown/grey shades
particleSize = 15;
} else if (self.name === "King") {
// King gets special treatment
particleColors = [0xFFD700, 0xFFFF00, 0xFF0000, 0x4B0082]; // Gold, yellow, red, purple
particleCount = 20;
particleSize = 18;
explosionRadius = 100;
} else {
// Default colors
particleColors = [0xFFFFFF, 0xCCCCCC, 0x999999, 0x666666];
}
// Create particles
var particles = [];
for (var i = 0; i < particleCount; i++) {
var particle = particleContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: particleSize + Math.random() * 8,
height: particleSize + Math.random() * 8
});
// Set particle color
particle.tint = particleColors[Math.floor(Math.random() * particleColors.length)];
particle.alpha = 0.8 + Math.random() * 0.2;
// Calculate random direction
var angle = Math.PI * 2 * i / particleCount + (Math.random() - 0.5) * 0.5;
var speed = explosionRadius * (0.7 + Math.random() * 0.3);
// Store particle data
particles.push({
sprite: particle,
targetX: Math.cos(angle) * speed,
targetY: Math.sin(angle) * speed,
rotationSpeed: (Math.random() - 0.5) * 0.2
});
}
// Animate unit shrinking and fading
tween(self, {
alpha: 0,
scaleX: self.scaleX * 0.5,
scaleY: self.scaleY * 0.5
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
// Destroy the unit
self.destroy();
}
});
// Animate particles bursting outward
for (var i = 0; i < particles.length; i++) {
var particleData = particles[i];
var particle = particleData.sprite;
// Burst outward animation
tween(particle, {
x: particleData.targetX,
y: particleData.targetY,
scaleX: 0.1,
scaleY: 0.1,
alpha: 0,
rotation: particle.rotation + particleData.rotationSpeed * 10
}, {
duration: 600 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Clean up after last particle
if (i === particles.length - 1) {
particleContainer.destroy();
}
}
});
}
// Add a brief flash effect at the center
var flash = particleContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: particleSize * 3,
height: particleSize * 3
});
flash.tint = 0xFFFFFF;
flash.alpha = 0.8;
tween(flash, {
scaleX: 3,
scaleY: 3,
alpha: 0
}, {
duration: 300,
easing: tween.easeOut
});
};
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) {
// Trigger death animation instead of immediate destroy
enemies[i].deathAnimation();
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 showStarIndicator to prevent King from showing stars
self.showStarIndicator = function () {
// King doesn't show star indicators
};
// 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);
};
// Override hideStarIndicator to handle the case where it might be called
self.hideStarIndicator = function () {
// King doesn't have star indicators, so nothing to hide
};
// Override updateUnitScale to prevent tier-based scaling for King
self.updateUnitScale = function () {
// King doesn't scale based on tier, only through levelUpKing function
};
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
****/
// Create game title overlay that appears on top of everything
var titleOverlay = new Container();
// Create gradient-like background effect with multiple layers
var titleOverlayBg = titleOverlay.attachAsset('shop_area_bg', {
anchorX: 0.5,
anchorY: 0.5,
width: 2048,
height: 2732
});
titleOverlayBg.alpha = 0.95;
titleOverlayBg.tint = 0x0a0a1a; // Deep dark blue instead of pure black
titleOverlayBg.x = 0; // Center relative to parent
titleOverlayBg.y = 0; // Center relative to parent
// Add subtle vignette effect
var vignette = titleOverlay.attachAsset('shop_area_bg', {
anchorX: 0.5,
anchorY: 0.5,
width: 2200,
height: 2900
});
vignette.alpha = 0.3;
vignette.tint = 0x000000;
vignette.x = 0;
vignette.y = 0;
// Create magical particle system for background
var particleContainer = new Container();
titleOverlay.addChild(particleContainer);
// Create floating magical particles
function createMagicalParticle() {
var particle = particleContainer.attachAsset('star_dot', {
anchorX: 0.5,
anchorY: 0.5,
width: 6 + Math.random() * 8,
height: 6 + Math.random() * 8
});
// Random colors for magical effect
var colors = [0xFFD700, 0x4169E1, 0xFF69B4, 0x00CED1, 0xFFA500];
particle.tint = colors[Math.floor(Math.random() * colors.length)];
particle.alpha = 0;
// Random starting position
particle.x = (Math.random() - 0.5) * 2048;
particle.y = 1366 + Math.random() * 800;
// Animate particle floating up with glow
tween(particle, {
y: particle.y - 2000,
alpha: 0.8
}, {
duration: 2000,
easing: tween.easeIn,
onFinish: function onFinish() {
tween(particle, {
alpha: 0
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
});
// Add gentle horizontal sway
tween(particle, {
x: particle.x + (Math.random() - 0.5) * 200
}, {
duration: 4000,
easing: tween.easeInOut
});
}
// Create particles periodically
var particleTimer = LK.setInterval(function () {
if (titleOverlay.parent) {
createMagicalParticle();
} else {
LK.clearInterval(particleTimer);
}
}, 200);
// Create title container for effects
var titleContainer = new Container();
titleOverlay.addChild(titleContainer);
titleContainer.y = -566;
// Add golden glow behind title
var titleGlow = titleContainer.attachAsset('shop_area_bg', {
anchorX: 0.5,
anchorY: 0.5,
width: 800,
height: 200
});
titleGlow.alpha = 0.3;
titleGlow.tint = 0xFFD700;
// Animate glow pulse
tween(titleGlow, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.5
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(titleGlow, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.3
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// Game title with shadow effect
var gameTitleShadow = new Text2('Protect Your King!', {
size: 120,
fill: 0x000000
});
gameTitleShadow.anchor.set(0.5, 0.5);
gameTitleShadow.x = 4;
gameTitleShadow.y = 4;
gameTitleShadow.alpha = 0.5;
titleContainer.addChild(gameTitleShadow);
// Add subtle animation to shadow
tween(gameTitleShadow, {
x: 6,
y: 6,
alpha: 0.3
}, {
duration: 3000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(gameTitleShadow, {
x: 2,
y: 2,
alpha: 0.6
}, {
duration: 3000,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// Main game title
var gameTitle = new Text2('Protect Your King!', {
size: 120,
fill: 0xFFD700
});
gameTitle.anchor.set(0.5, 0.5);
gameTitle.x = 0;
gameTitle.y = 0;
titleContainer.addChild(gameTitle);
// Add sparkle effects around title
function createTitleSparkle() {
var sparkle = titleContainer.attachAsset('star_dot', {
anchorX: 0.5,
anchorY: 0.5,
width: 10,
height: 10
});
sparkle.tint = 0xFFFFFF;
sparkle.alpha = 0;
sparkle.x = (Math.random() - 0.5) * 600;
sparkle.y = (Math.random() - 0.5) * 150;
tween(sparkle, {
alpha: 1,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(sparkle, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
sparkle.destroy();
}
});
}
});
}
// Create sparkles periodically
var sparkleTimer = LK.setInterval(function () {
if (titleOverlay.parent) {
createTitleSparkle();
} else {
LK.clearInterval(sparkleTimer);
}
}, 300);
// Subtitle with shadow
var subtitleShadow = new Text2('Tower Defense', {
size: 60,
fill: 0x000000
});
subtitleShadow.anchor.set(0.5, 0.5);
subtitleShadow.x = 2;
subtitleShadow.y = -464;
subtitleShadow.alpha = 0.5;
titleOverlay.addChild(subtitleShadow);
// Subtitle
var gameSubtitle = new Text2('Tower Defense', {
size: 60,
fill: 0x87CEEB
});
gameSubtitle.anchor.set(0.5, 0.5);
gameSubtitle.x = 0; // Center relative to overlay
gameSubtitle.y = -466; // Adjusted position relative to center
titleOverlay.addChild(gameSubtitle);
// Add continuous floating animation to subtitle
function startSubtitleAnimation() {
tween(gameSubtitle, {
scaleX: 1.05,
scaleY: 1.05,
y: -456,
alpha: 0.9
}, {
duration: 2500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(gameSubtitle, {
scaleX: 0.95,
scaleY: 0.95,
y: -476,
alpha: 1.0
}, {
duration: 2500,
easing: tween.easeInOut,
onFinish: startSubtitleAnimation
});
}
});
}
startSubtitleAnimation();
// Start Game button container
var startGameButton = new Container();
// Button shadow
var startButtonShadow = startGameButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 100
});
startButtonShadow.tint = 0x000000;
startButtonShadow.alpha = 0.3;
startButtonShadow.x = 4;
startButtonShadow.y = 4;
// Button gradient effect background
var startButtonGlow = startGameButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 420,
height: 120
});
startButtonGlow.tint = 0x66FF66;
startButtonGlow.alpha = 0;
// Main button background
var startGameButtonBg = startGameButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 100
});
startGameButtonBg.tint = 0x4169E1; // Royal blue to match theme better
// Button text with shadow
var startButtonTextShadow = new Text2('Start Game', {
size: 60,
fill: 0x000000
});
startButtonTextShadow.anchor.set(0.5, 0.5);
startButtonTextShadow.x = 2;
startButtonTextShadow.y = 2;
startButtonTextShadow.alpha = 0.5;
startGameButton.addChild(startButtonTextShadow);
var startGameButtonText = new Text2('Start Game', {
size: 60,
fill: 0xFFFFFF
});
startGameButtonText.anchor.set(0.5, 0.5);
startGameButton.addChild(startGameButtonText);
startGameButton.x = 0; // Center relative to overlay
startGameButton.y = -66; // Move lower, more centered
titleOverlay.addChild(startGameButton);
// Add floating animation to start button
tween(startGameButton, {
y: -56,
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(startGameButton, {
y: -66,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// Add hover animation
startGameButton.down = function () {
tween(startGameButtonBg, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 100,
easing: tween.easeOut
});
tween(startButtonGlow, {
alpha: 0.7,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeOut
});
};
// How to Play button container
var howToPlayButton = new Container();
// Button shadow
var howToPlayShadow = howToPlayButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 100
});
howToPlayShadow.tint = 0x000000;
howToPlayShadow.alpha = 0.3;
howToPlayShadow.x = 4;
howToPlayShadow.y = 4;
// Button glow effect
var howToPlayGlow = howToPlayButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 420,
height: 120
});
howToPlayGlow.tint = 0x6666FF;
howToPlayGlow.alpha = 0;
// Main button background
var howToPlayButtonBg = howToPlayButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 100
});
howToPlayButtonBg.tint = 0x8A2BE2; // Blue violet for distinction
// Button text with shadow
var howToPlayTextShadow = new Text2('How to Play', {
size: 60,
fill: 0x000000
});
howToPlayTextShadow.anchor.set(0.5, 0.5);
howToPlayTextShadow.x = 2;
howToPlayTextShadow.y = 2;
howToPlayTextShadow.alpha = 0.5;
howToPlayButton.addChild(howToPlayTextShadow);
var howToPlayButtonText = new Text2('How to Play', {
size: 60,
fill: 0xFFFFFF
});
howToPlayButtonText.anchor.set(0.5, 0.5);
howToPlayButton.addChild(howToPlayButtonText);
howToPlayButton.x = 0; // Center relative to overlay
howToPlayButton.y = 84; // Move lower, more centered
titleOverlay.addChild(howToPlayButton);
// Add floating animation to how to play button with slight offset
tween(howToPlayButton, {
y: 94,
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 2300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(howToPlayButton, {
y: 84,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 2300,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// Add hover animation
howToPlayButton.down = function () {
tween(howToPlayButtonBg, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 100,
easing: tween.easeOut
});
tween(howToPlayGlow, {
alpha: 0.7,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeOut
});
};
// How to Play button click handler
howToPlayButton.up = function () {
// Reset button scale
tween(howToPlayButtonBg, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
tween(howToPlayGlow, {
alpha: 0
}, {
duration: 200,
easing: tween.easeIn
});
// Play sound effect
LK.getSound('purchase').play();
// Create how to play modal
var howToPlayModal = new Container();
var howToPlayModalBg = howToPlayModal.attachAsset('shop_area_bg', {
anchorX: 0.5,
anchorY: 0.5,
width: 1600,
height: 2000
});
howToPlayModalBg.alpha = 0.95;
howToPlayModalBg.tint = 0x1a1a2e; // Dark blue theme
howToPlayModalBg.x = 0;
howToPlayModalBg.y = 0;
// Modal title with fun subtitle
var modalTitle = new Text2('How to Play', {
size: 80,
fill: 0xFFD700
});
modalTitle.anchor.set(0.5, 0.5);
modalTitle.x = 0; // Center relative to modal
modalTitle.y = -900; // Adjusted position relative to center
howToPlayModal.addChild(modalTitle);
// Fun subtitle
var subtitle = new Text2('Master the Art of Tower Defense!', {
size: 40,
fill: 0x44FF44
});
subtitle.anchor.set(0.5, 0.5);
subtitle.x = 0;
subtitle.y = -820;
howToPlayModal.addChild(subtitle);
// Game mechanics section
var mechanicsTitle = new Text2('— Game Basics —', {
size: 48,
fill: 0xFFD700
});
mechanicsTitle.anchor.set(0.5, 0.5);
mechanicsTitle.x = 0;
mechanicsTitle.y = -700;
howToPlayModal.addChild(mechanicsTitle);
// Instructions with better spacing
var basicInstructions = ['💰 Purchase units from the shop using gold', '🎯 Drag units to the battlefield grid', '⚔️ Units auto-battle enemies in range', '⭐ Merge 3 identical units for upgrades', '👑 Level up your King for more capacity', '🛡️ Protect your King at all costs!', '🌊 Survive 10 waves to claim victory!'];
for (var i = 0; i < basicInstructions.length; i++) {
var instructionText = new Text2(basicInstructions[i], {
size: 36,
fill: 0xFFFFFF
});
instructionText.anchor.set(0.5, 0.5);
instructionText.x = 0; // Horizontally centered
instructionText.y = -550 + i * 60; // Better spacing
howToPlayModal.addChild(instructionText);
}
// Unit showcase title
var unitsTitle = new Text2('— Meet Your Units —', {
size: 48,
fill: 0xFFD700
});
unitsTitle.anchor.set(0.5, 0.5);
unitsTitle.x = 0;
unitsTitle.y = -50;
howToPlayModal.addChild(unitsTitle);
// Create unit showcase with actual unit graphics
var unitShowcase = [{
unit: new Soldier(),
desc: 'Soldier: Swift melee warrior',
x: -600,
y: 100
}, {
unit: new Knight(),
desc: 'Knight: Tanky defender',
x: -200,
y: 100
}, {
unit: new Wizard(),
desc: 'Wizard: Magic damage dealer',
x: 200,
y: 100
}, {
unit: new Ranger(),
desc: 'Ranger: Long-range archer',
x: 600,
y: 100
}, {
unit: new Paladin(),
desc: 'Paladin: Elite warrior',
x: -400,
y: 350
}, {
unit: new Wall(),
desc: 'Wall: Defensive structure',
x: 0,
y: 350
}, {
unit: new CrossbowTower(),
desc: 'Tower: Area controller',
x: 400,
y: 350
}];
// Add units with animations
for (var i = 0; i < unitShowcase.length; i++) {
var showcase = unitShowcase[i];
var unitDisplay = showcase.unit;
unitDisplay.x = showcase.x;
unitDisplay.y = showcase.y;
unitDisplay.scaleX = 0.8;
unitDisplay.scaleY = 0.8;
howToPlayModal.addChild(unitDisplay);
// Add floating animation
var baseY = unitDisplay.y;
tween(unitDisplay, {
y: baseY - 15,
scaleX: 0.85,
scaleY: 0.85
}, {
duration: 1500 + Math.random() * 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(unitDisplay, {
y: baseY,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 1500 + Math.random() * 500,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// Add unit description
var unitDesc = new Text2(showcase.desc, {
size: 28,
fill: 0xFFFFFF
});
unitDesc.anchor.set(0.5, 0);
unitDesc.x = showcase.x;
unitDesc.y = showcase.y + 60;
howToPlayModal.addChild(unitDesc);
}
// Enemy warning section
var enemyWarning = new Text2('⚠️ Enemies spawn from the fog of war above! ⚠️', {
size: 40,
fill: 0xFF4444
});
enemyWarning.anchor.set(0.5, 0.5);
enemyWarning.x = 0;
enemyWarning.y = 600;
howToPlayModal.addChild(enemyWarning);
// Tips section
var tipsText = new Text2('💡 Pro Tip: Save gold for interest bonus!', {
size: 36,
fill: 0x44FF44
});
tipsText.anchor.set(0.5, 0.5);
tipsText.x = 0;
tipsText.y = 700;
howToPlayModal.addChild(tipsText);
// Close button
var closeButton = new Container();
var closeButtonBg = closeButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 300,
height: 100
});
closeButtonBg.tint = 0xFF4444;
var closeButtonText = new Text2('Got it!', {
size: 60,
fill: 0xFFFFFF
});
closeButtonText.anchor.set(0.5, 0.5);
closeButton.addChild(closeButtonText);
closeButton.x = 0; // Center relative to modal
closeButton.y = 850; // Bottom of modal
howToPlayModal.addChild(closeButton);
closeButton.up = function () {
howToPlayModal.destroy();
};
titleOverlay.addChild(howToPlayModal);
};
// Start Game button click handler
startGameButton.up = function () {
// Reset button scale
tween(startGameButtonBg, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
tween(startButtonGlow, {
alpha: 0
}, {
duration: 200,
easing: tween.easeIn
});
// Play purchase sound for feedback
LK.getSound('purchase').play();
// Create expanding circle transition effect
var transitionCircle = titleOverlay.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: 50,
height: 50
});
transitionCircle.tint = 0xFFD700;
transitionCircle.x = 0;
transitionCircle.y = -266; // Start from button position
tween(transitionCircle, {
scaleX: 100,
scaleY: 100,
alpha: 0.8
}, {
duration: 600,
easing: tween.easeOut
});
// Hide title overlay with fade animation
tween(titleOverlay, {
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
// Clear timers
LK.clearInterval(particleTimer);
LK.clearInterval(sparkleTimer);
titleOverlay.destroy();
}
});
};
// Add title overlay to LK.gui.center to ensure it renders above all game elements
LK.gui.center.addChild(titleOverlay);
// No need to set x,y as gui.center is already centered
// Add enhanced title animation with rotation - constant loop
function startTitleAnimation() {
tween(titleContainer, {
scaleX: 1.05,
scaleY: 1.05,
rotation: 0.02
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(titleContainer, {
scaleX: 0.95,
scaleY: 0.95,
rotation: -0.02
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: startTitleAnimation
});
}
});
}
startTitleAnimation();
// Add decorative crown above title
var crownContainer = new Container();
titleOverlay.addChild(crownContainer);
crownContainer.y = -666;
// Create pixelart crown
var crownBase = crownContainer.attachAsset('king', {
anchorX: 0.5,
anchorY: 0.5,
width: 80,
height: 30
});
crownBase.tint = 0xFFD700;
var crownLeft = crownContainer.attachAsset('king', {
anchorX: 0.5,
anchorY: 0.5,
width: 20,
height: 40
});
crownLeft.x = -25;
crownLeft.y = -20;
crownLeft.tint = 0xFFD700;
var crownCenter = crownContainer.attachAsset('king', {
anchorX: 0.5,
anchorY: 0.5,
width: 25,
height: 50
});
crownCenter.x = 0;
crownCenter.y = -25;
crownCenter.tint = 0xFFD700;
var crownRight = crownContainer.attachAsset('king', {
anchorX: 0.5,
anchorY: 0.5,
width: 20,
height: 40
});
crownRight.x = 25;
crownRight.y = -20;
crownRight.tint = 0xFFD700;
// Add jewels to crown
var jewelLeft = crownContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: 12,
height: 12
});
jewelLeft.x = -25;
jewelLeft.y = -35;
jewelLeft.tint = 0xFF0000;
var jewelCenter = crownContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: 15,
height: 15
});
jewelCenter.x = 0;
jewelCenter.y = -45;
jewelCenter.tint = 0x4169E1;
var jewelRight = crownContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: 12,
height: 12
});
jewelRight.x = 25;
jewelRight.y = -35;
jewelRight.tint = 0xFF0000;
// Add sparkle animations to jewels
tween(jewelLeft, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(jewelLeft, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// Offset timing for center jewel
LK.setTimeout(function () {
tween(jewelCenter, {
scaleX: 1.4,
scaleY: 1.4,
alpha: 0.7
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(jewelCenter, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
}, 400);
// Offset timing for right jewel
LK.setTimeout(function () {
tween(jewelRight, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(jewelRight, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
}, 800);
// Animate crown floating with enhanced effects
function startCrownAnimation() {
tween(crownContainer, {
y: -656,
rotation: 0.08,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 2200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(crownContainer, {
y: -676,
rotation: -0.08,
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 2200,
easing: tween.easeInOut,
onFinish: startCrownAnimation
});
}
});
}
startCrownAnimation();
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 = 5;
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 kingInfoButton; // Declare kingInfoButton 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
var enemyContainer = new Container(); // Container for enemies to render below fog
var topUIContainer = new Container(); // Container for top UI elements
var isUIMinimized = false; // Track UI state
// 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;
// Skip king - it doesn't count towards any limit
if (unit.name === "King") {
continue; // Don't count king
}
if (unit.isStructure) {
structuresOnField++;
} else {
unitsOnField++;
}
}
}
}
// Update the max units text display
maxUnitsText.setText('Units: ' + unitsOnField + '/' + maxUnitsOnField + ' | Structures: ' + structuresOnField + '/' + maxStructuresOnField);
}
// 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.toString());
levelText.setText('King Lvl: ' + playerLevel);
countUnitsOnField(); // Update counts and display
// 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
// Add enemy container to game first (so it renders below everything else)
game.addChild(enemyContainer);
// 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
// Calculate exact position for grid slot
var slotX = GRID_START_X + x * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
var slotY = GRID_START_Y + y * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
gridSlot.x = slotX;
gridSlot.y = slotY;
grid[y][x] = gridSlot;
}
}
// Create fog of war containers array to add later (after enemies spawn)
var fogOverlays = [];
// Create fog of war for top two rows
for (var y = 0; y < 2; y++) {
for (var x = 0; x < GRID_COLS; x++) {
var fogOverlay = new Container();
var fogGraphics = fogOverlay.attachAsset('grid_slot', {
anchorX: 0.5,
anchorY: 0.5,
width: GRID_CELL_SIZE - 10,
height: GRID_CELL_SIZE - 10
});
fogGraphics.tint = 0x000000; // Black fog
fogGraphics.alpha = 0.3; // More transparent to see enemies underneath
fogOverlay.x = GRID_START_X + x * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
fogOverlay.y = GRID_START_Y + y * GRID_CELL_SIZE + GRID_CELL_SIZE / 2;
// Add some fog texture variation
var fogDetail = fogOverlay.attachAsset('grid_slot', {
anchorX: 0.5,
anchorY: 0.5,
width: GRID_CELL_SIZE * 0.8,
height: GRID_CELL_SIZE * 0.8
});
fogDetail.tint = 0x222222;
fogDetail.alpha = 0.2; // More transparent detail layer
fogDetail.rotation = Math.random() * 0.2 - 0.1;
// Store fog overlays to add later
fogOverlays.push(fogOverlay);
}
}
// 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;
// Get the exact grid slot and place king there
var kingSlot = grid[kingGridY][kingGridX];
kingSlot.unit = king;
king.gridX = kingGridX;
king.gridY = kingGridY;
king.lane = 0;
// Set king position to match grid slot exactly
king.x = kingSlot.x;
king.y = kingSlot.y;
// King already has baseScale from Unit class, ensure it's applied
king.updateUnitScale();
king.showHealthBar(); // Show health bar for king from start
kings.push(king);
// Register king position
registerEntityPosition(king);
// Create info button for king
var kingInfoButton = new Container();
var kingInfoButtonBg = kingInfoButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 80,
height: 40
});
kingInfoButtonBg.tint = 0x4444FF;
var kingInfoButtonText = new Text2('Info', {
size: 32,
fill: 0xFFFFFF
});
kingInfoButtonText.anchor.set(0.5, 0.5);
kingInfoButton.addChild(kingInfoButtonText);
kingInfoButton.x = king.x;
kingInfoButton.y = king.y + 100; // Position below king
game.addChild(kingInfoButton);
kingInfoButton.up = function () {
showUnitInfoModal(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 = 9 * 120 + 8 * 40; // 9 slots of 120px width with 40px spacing
var benchStartX = (2048 - benchTotalWidth) / 2 + 60; // Center horizontally
for (var i = 0; i < 9; 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.toString());
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();
// Add fog overlays on top of everything to ensure they render above enemies
for (var i = 0; i < fogOverlays.length; i++) {
game.addChild(fogOverlays[i]);
}
// Create top UI container
topUIContainer = game.addChild(topUIContainer);
topUIContainer.x = 1024; // Center of screen
topUIContainer.y = 10; // Top of screen with small margin
// Create top UI container background - bigger initially for planning phase
var topUIBg = topUIContainer.addChild(LK.getAsset('bench_area_bg', {
anchorX: 0.5,
anchorY: 0,
width: 1800,
height: 240 // Bigger height for planning phase
}));
topUIBg.alpha = 0.7;
topUIBg.tint = 0x3a3a3a; // Darker tint for top section
// Create top row elements (Coin, Level, Max Units)
var goldContainer = new Container();
goldContainer.x = 650; // Relative to topUIContainer
goldContainer.y = 60; // Adjusted for bigger UI
topUIContainer.addChild(goldContainer);
// Create detailed pixelart coin
var coinContainer = new Container();
goldContainer.addChild(coinContainer);
coinContainer.x = -50; // Move coin further left to increase spacing
// Coin base (outer ring)
var coinBase = coinContainer.attachAsset('coin_base', {
anchorX: 0.5,
anchorY: 0.5
});
// Coin inner ring
var coinInner = coinContainer.attachAsset('coin_inner', {
anchorX: 0.5,
anchorY: 0.5
});
// Coin center
var coinCenter = coinContainer.attachAsset('coin_center', {
anchorX: 0.5,
anchorY: 0.5
});
// Coin highlight for shine effect
var coinHighlight = coinContainer.attachAsset('coin_highlight', {
anchorX: 0.5,
anchorY: 0.5
});
coinHighlight.x = -6;
coinHighlight.y = -6;
// Add subtle rotation animation to make coin feel alive
tween(coinContainer, {
rotation: 0.1
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(coinContainer, {
rotation: -0.1
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
goldText = new Text2(gold.toString(), {
size: 50,
fill: 0xFFD700
});
goldText.anchor.set(0.5, 0.5);
goldText.x = 40; // Move number further right to create more space
goldContainer.addChild(goldText);
var levelText = new Text2('King Lvl: ' + playerLevel, {
size: 50,
fill: 0x44FF44
});
levelText.anchor.set(0.5, 0.5);
levelText.x = 0; // Center relative to topUIContainer
levelText.y = 60; // Adjusted for bigger UI
topUIContainer.addChild(levelText);
var maxUnitsText = new Text2('Units: 0/' + maxUnitsOnField + ' | Structures: 0/' + maxStructuresOnField, {
size: 50,
fill: 0xFFFFFF
});
maxUnitsText.anchor.set(0.5, 0.5);
maxUnitsText.x = -500; // Left of center relative to topUIContainer
maxUnitsText.y = 60; // Adjusted for bigger UI
topUIContainer.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 = -300; // Left of center relative to topUIContainer
stateText.y = 140; // Adjusted for bigger UI
topUIContainer.addChild(stateText);
var waveText = new Text2('Wave: ' + wave, {
size: 40,
fill: 0xFFFFFF
});
waveText.anchor.set(0.5, 0.5);
waveText.x = 300; // Right of center relative to topUIContainer
waveText.y = 140; // Adjusted for bigger UI
topUIContainer.addChild(waveText);
// Function to minimize UI during battle
function minimizeUI() {
// Tween background to smaller size
tween(topUIBg, {
height: 120
}, {
duration: 400,
easing: tween.easeInOut
});
// Move ready button up
tween(readyButton, {
y: 90
}, {
duration: 400,
easing: tween.easeInOut
});
// Move state and wave text up
tween(stateText, {
y: 90
}, {
duration: 400,
easing: tween.easeInOut
});
tween(waveText, {
y: 90
}, {
duration: 400,
easing: tween.easeInOut
});
}
// Function to maximize UI during planning
function maximizeUI() {
// Tween background to bigger size
tween(topUIBg, {
height: 240
}, {
duration: 400,
easing: tween.easeInOut
});
// Move ready button down
tween(readyButton, {
y: 190
}, {
duration: 400,
easing: tween.easeInOut
});
// Move state and wave text down
tween(stateText, {
y: 140
}, {
duration: 400,
easing: tween.easeInOut
});
tween(waveText, {
y: 140
}, {
duration: 400,
easing: tween.easeInOut
});
}
// 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!');
// Show king info button during planning
if (kingInfoButton) {
kingInfoButton.visible = true;
}
// Maximize UI for planning phase
if (isUIMinimized) {
isUIMinimized = false;
maximizeUI();
}
} 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');
// Hide king info button during battle
if (kingInfoButton) {
kingInfoButton.visible = false;
}
// Minimize UI for battle phase
if (!isUIMinimized) {
isUIMinimized = true;
minimizeUI();
}
}
}
var readyButton = new Container();
var readyButtonBg = LK.getAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 300,
height: 100
});
readyButtonBg.tint = 0x44AA44;
var readyButtonText = new Text2('Ready!', {
size: 72,
fill: 0xFFFFFF
});
readyButtonText.anchor.set(0.5, 0.5);
readyButton.addChild(readyButtonBg);
readyButton.addChild(readyButtonText);
topUIContainer.addChild(readyButton);
readyButton.x = 0; // Center relative to topUIContainer
readyButton.y = 190; // Position in bigger UI
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 move units to closest grid spots after battle
function moveUnitsToClosestGridSpots() {
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.isStructure && gridSlot.unit.name !== "King") {
var unit = gridSlot.unit;
var currentGridX = unit.gridX;
var currentGridY = unit.gridY;
// Find closest valid grid position
var closestGridX = currentGridX;
var closestGridY = currentGridY;
var minDistance = 999;
for (var checkY = 2; checkY < GRID_ROWS; checkY++) {
// Start from row 2 to avoid fog of war
for (var checkX = 0; checkX < GRID_COLS; checkX++) {
var checkSlot = grid[checkY][checkX];
// Check if position is valid (not occupied or same unit)
if (!checkSlot.unit || checkSlot.unit === unit) {
var distance = Math.abs(checkX - currentGridX) + Math.abs(checkY - currentGridY);
if (distance < minDistance) {
minDistance = distance;
closestGridX = checkX;
closestGridY = checkY;
}
}
}
}
// Only move if we found a different position
if (closestGridX !== currentGridX || closestGridY !== currentGridY) {
var targetSlot = grid[closestGridY][closestGridX];
var targetX = targetSlot.x;
var targetY = targetSlot.y;
// Clear old position
gridSlot.unit = null;
unregisterEntityPosition(unit, currentGridX, currentGridY);
// Set new position
targetSlot.unit = unit;
unit.gridX = closestGridX;
unit.gridY = closestGridY;
registerEntityPosition(unit);
// Animate unit to new position
tween(unit, {
x: targetX,
y: targetY
}, {
duration: 800,
easing: tween.easeInOut
});
}
}
}
}
}
// 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);
var enemy;
// Spawn boss monster every 5 waves
if (wave % 5 === 0 && enemiesSpawned === Math.floor(enemiesPerWave / 2)) {
enemy = new Boss();
// Scale boss stats based on wave
if (wave > 1) {
var waveMultiplier = 1 + (wave - 1) * 0.2;
enemy.health = Math.floor(enemy.health * waveMultiplier);
enemy.maxHealth = enemy.health;
enemy.damage = Math.floor(enemy.damage * waveMultiplier);
enemy.armor = Math.floor(enemy.armor * (1 + (wave - 1) * 0.1));
enemy.magicResist = Math.floor(enemy.magicResist * (1 + (wave - 1) * 0.1));
enemy.goldValue = Math.floor(enemy.goldValue * waveMultiplier);
}
} else {
// Increase ranged enemy chance as waves progress
var rangedChance = Math.min(0.3 + wave * 0.05, 0.7);
if (Math.random() < rangedChance) {
enemy = new RangedEnemy();
} else {
enemy = new Enemy();
}
// 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));
}
// Try to spawn in a strategic position
var bestSpawnX = spawnX;
var minPlayerUnits = 999;
// Check each column for player units
for (var x = 0; x < GRID_COLS; x++) {
var playerUnitsInColumn = 0;
for (var y = 0; y < GRID_ROWS; y++) {
if (grid[y] && grid[y][x] && grid[y][x].unit) {
playerUnitsInColumn++;
}
}
// Boss prefers columns with fewer player units
if (enemy.name === "Boss Monster" && playerUnitsInColumn < minPlayerUnits) {
minPlayerUnits = playerUnitsInColumn;
bestSpawnX = x;
}
}
// Position enemy at the first row (top) of the grid
enemy.gridX = enemy.name === "Boss Monster" ? bestSpawnX : spawnX;
enemy.gridY = 0;
// Set visual position to match grid position
var topCell = grid[0][enemy.gridX];
enemy.x = topCell.x;
enemy.y = topCell.y;
enemy.lane = 0; // Single grid, so lane is always 0
// Apply base scale
if (enemy.name !== "Boss Monster") {
// Scale up enemy by 30%
enemy.scaleX = 1.3;
enemy.scaleY = 1.3;
}
enemy.showHealthBar(); // Show health bar when enemy is spawned
enemy.updateHealthBar();
// Initialize GridMovement immediately for the enemy
enemy.gridMovement = new GridMovement(enemy, grid);
enemies.push(enemy);
enemyContainer.addChild(enemy);
// Add spawn animation
enemy.alpha = 0;
var baseScale = enemy.baseScale || 1.3;
enemy.scaleX = baseScale * 0.5;
enemy.scaleY = baseScale * 1.5;
tween(enemy, {
alpha: 1,
scaleX: baseScale,
scaleY: baseScale
}, {
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) {
// Prevent King from being moved to bench
if (draggedUnit.name !== 'King') {
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();
// Add bounce effect when unit is moved to bench
var targetScale = draggedUnit.baseScale || 1.3;
if (draggedUnit.tier === 2) {
targetScale = draggedUnit.baseScale * 1.2;
} else if (draggedUnit.tier === 3) {
targetScale = draggedUnit.baseScale * 1.3;
}
draggedUnit.scaleX = targetScale * 0.8;
draggedUnit.scaleY = targetScale * 0.8;
// Capture unit reference before clearing draggedUnit
var unitToAnimate = draggedUnit;
tween(unitToAnimate, {
scaleX: targetScale * 1.2,
scaleY: targetScale * 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(unitToAnimate, {
scaleX: targetScale,
scaleY: targetScale
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
// Clear bounce animation flag when bounce is complete
unitToAnimate._isBounceAnimating = false;
}
});
}
});
break;
}
}
}
}
// If not dropped on valid slot, return to original position
if (!dropped) {
// Handle King separately - must return to grid
if (draggedUnit.name === 'King') {
// King must return to original grid position
if (draggedFromGrid && draggedUnit.originalGridSlot) {
draggedUnit.x = draggedUnit.originalX;
draggedUnit.y = draggedUnit.originalY;
draggedUnit.originalGridSlot.unit = draggedUnit; // Restore unit to original grid slot
}
} else {
// 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();
// Add bounce effect when unit is returned to bench
var targetScale = draggedUnit.baseScale || 1.3;
if (draggedUnit.tier === 2) {
targetScale = draggedUnit.baseScale * 1.2;
} else if (draggedUnit.tier === 3) {
targetScale = draggedUnit.baseScale * 1.3;
}
draggedUnit.scaleX = targetScale * 0.8;
draggedUnit.scaleY = targetScale * 0.8;
// Capture unit reference before clearing draggedUnit
var unitToAnimate = draggedUnit;
tween(unitToAnimate, {
scaleX: targetScale * 1.2,
scaleY: targetScale * 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(unitToAnimate, {
scaleX: targetScale,
scaleY: targetScale
}, {
duration: 150,
easing: tween.easeIn
});
}
});
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.toString());
// 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();
// Add bounce effect when unit is added to bench
newUnit.scaleX = 0;
newUnit.scaleY = 0;
// Capture unit reference for animation
var unitToAnimate = newUnit;
var targetScale = unitToAnimate.baseScale || 1.3;
tween(unitToAnimate, {
scaleX: targetScale * 1.2,
scaleY: targetScale * 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(unitToAnimate, {
scaleX: targetScale,
scaleY: targetScale
}, {
duration: 150,
easing: tween.easeIn
});
}
});
// 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 - move units to closest grid spots first
moveUnitsToClosestGridSpots();
// 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.toString());
// Check for unit upgrades when entering planning phase
checkAndUpgradeUnits();
// Show reward message
var rewardText = new Text2('Wave Complete! +' + waveReward + ' Gold (5 + ' + interest + ' interest)', {
size: 96,
fill: 0xFFD700
});
rewardText.anchor.set(0.5, 0.5);
rewardText.x = 1024;
rewardText.y = 800;
game.addChild(rewardText);
// Fade out reward text with longer duration
tween(rewardText, {
alpha: 0,
y: 700
}, {
duration: 4000,
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
}
// Update unit counts in case any units were destroyed
countUnitsOnField();
// 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();
}
// Update king info button position if this is the king
if (gridSlot.unit.name === 'King' && kingInfoButton) {
kingInfoButton.x = gridSlot.unit.x;
kingInfoButton.y = gridSlot.unit.y + 100; // Position below king
}
}
}
}
// 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();
}
};