User prompt
Make the other element of the home screen also have an animation like the tile ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Move start game and howt to playr buttons a little lower, more i the center o the screen, and also change their color and animate them to match the rest of the style of the homepage ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
keep animation for game title constant ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Nie! improve the start game and dhow to play buttons too ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
can we improve the game title overlay style, use some tricks to make it looked better, maybe even change the color of the buttons. add some magic. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
In how to play section, the text below GAme Bascis, should be moved to te right so that it is hoziontaly centered in the screen
User prompt
Nice! add same effect to pplayer unit deaths ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Yes, implment particle explosion effect
User prompt
Add some animation to enemy death. Maybe we can make them turn into an ash pile and then dissapear. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make the how to play text a little more entertaining. Add some graphics, the units. Aand center it better in the screen
/**** * 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(); } };
===================================================================
--- original.js
+++ change.js
@@ -3261,8 +3261,28 @@
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
@@ -3331,26 +3351,33 @@
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 subtle animation to subtitle
-tween(gameSubtitle, {
- scaleX: 1.05,
- scaleY: 1.05
-}, {
- duration: 3000,
- easing: tween.easeInOut,
- onFinish: function onFinish() {
- tween(gameSubtitle, {
- scaleX: 1.0,
- scaleY: 1.0
- }, {
- duration: 3000,
- easing: tween.easeInOut,
- onFinish: onFinish
- });
- }
-});
+// 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', {
@@ -3862,26 +3889,97 @@
});
jewelRight.x = 25;
jewelRight.y = -35;
jewelRight.tint = 0xFF0000;
-// Animate crown floating
-tween(crownContainer, {
- y: -656,
- rotation: 0.05
+// Add sparkle animations to jewels
+tween(jewelLeft, {
+ scaleX: 1.3,
+ scaleY: 1.3,
+ alpha: 0.8
}, {
- duration: 2500,
+ duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
- tween(crownContainer, {
- y: -666,
- rotation: -0.05
+ tween(jewelLeft, {
+ scaleX: 1.0,
+ scaleY: 1.0,
+ alpha: 1.0
}, {
- duration: 2500,
+ 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;