/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.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
// Update enemies defeated count in storage
storage.enemiesDefeated = (storage.enemiesDefeated || 0) + 1;
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
// Only recalculate targets every 15 ticks or if position changed
if (!self._targetCache || LK.ticks % 15 === 0 || self._lastTargetX !== self.gridX || self._lastTargetY !== self.gridY) {
self._lastTargetX = self.gridX;
self._lastTargetY = self.gridY;
// Smart targeting - collect all units in range
var targetsInRange = [];
// 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) {
targetsInRange.push({
unit: king,
distance: kingCellDist,
priority: 3,
// Medium priority for king
healthRatio: king.health / king.maxHealth
});
}
}
}
// Then check other units on the grid
for (var y = Math.max(0, self.gridY - self.range); y <= Math.min(GRID_ROWS - 1, self.gridY + self.range); y++) {
for (var x = Math.max(0, self.gridX - self.range); x <= Math.min(GRID_COLS - 1, self.gridX + self.range); 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 = 5; // Default priority
var healthRatio = unit.health / unit.maxHealth;
// Smart prioritization
if (healthRatio <= 0.3) {
priority = 1; // Highest priority - can finish off quickly
} else if (unit.name === "Wizard" || unit.name === "Ranger") {
priority = 2; // High priority - dangerous units
} else if (unit.name === "Wall") {
priority = 8; // Lowest priority - defensive units
} else if (unit.name === "Paladin" || unit.name === "Knight") {
priority = 6; // Low priority - tanky units
}
targetsInRange.push({
unit: unit,
distance: cellDist,
priority: priority,
healthRatio: healthRatio
});
}
}
}
}
}
self._targetCache = targetsInRange;
} else {
// Use cached targets
var targetsInRange = self._targetCache || [];
}
// Sort targets by priority first, then by health ratio, then by distance
targetsInRange.sort(function (a, b) {
if (a.priority !== b.priority) {
return a.priority - b.priority;
}
if (Math.abs(a.healthRatio - b.healthRatio) > 0.1) {
return a.healthRatio - b.healthRatio; // Lower health first
}
return a.distance - b.distance;
});
// Select best target
if (targetsInRange.length > 0) {
closestUnit = targetsInRange[0].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
// Play melee attack sound
LK.getSound('melee_attack').play();
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();
// Smart positioning - try to stay behind melee enemies
var meleeFrontline = GRID_ROWS;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy !== self && enemy.range <= 1) {
// Melee enemy
if (enemy.gridY > self.gridY && enemy.gridY < meleeFrontline) {
meleeFrontline = enemy.gridY;
}
}
}
// If we're ahead of melee units, consider retreating
if (self.gridY > meleeFrontline && !self.isAttacking && Math.random() < 0.5) {
// Try to move back if possible
var backY = self.gridY - 1;
if (backY >= 0 && !self.gridMovement.isCellOccupied(self.gridX, backY)) {
// Override normal movement to retreat
self.gridMovement.targetCell.y = backY;
}
}
// 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);
// Play ranged attack sound
LK.getSound('shoot').play();
// 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
// Play melee attack sound
LK.getSound('melee_attack').play();
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);
}
// Cache directions array at class level to avoid recreation
if (!self.directions) {
self.directions = [{
x: 0,
y: -1
},
// Up
{
x: 1,
y: 0
},
// Right
{
x: 0,
y: 1
},
// Down
{
x: -1,
y: 0
} // Left
];
}
// Get neighbors of a cell
function getNeighbors(x, y) {
var neighbors = [];
// Check if this entity is a player unit
var isPlayerUnit = self.entity.isPlayerUnit !== undefined ? self.entity.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");
// Cache this value on entity for future use
if (self.entity.isPlayerUnit === undefined) {
self.entity.isPlayerUnit = isPlayerUnit;
}
for (var i = 0; i < self.directions.length; i++) {
var newX = x + self.directions[i].x;
var newY = y + self.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;
}
// Enemy coordination - wait for nearby allies before advancing
var nearbyAllies = 0;
var shouldWait = false;
for (var i = 0; i < enemies.length; i++) {
var ally = enemies[i];
if (ally !== self.entity && ally.gridY === self.entity.gridY) {
// Check if ally is in same row
var allyDist = Math.abs(ally.gridX - self.entity.gridX);
if (allyDist <= 2) {
nearbyAllies++;
// If ally is behind us, wait for them
if (ally.gridY < self.entity.gridY) {
shouldWait = true;
}
}
}
}
// Wait if we're too far ahead of our allies
if (shouldWait && nearbyAllies < 2 && Math.random() < 0.7) {
return false; // 70% chance to wait for allies
}
// 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') {
// 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) {
// Swap units if the slot is occupied
var tempUnit = self.unit;
self.unit = unit;
unit.originalGridSlot.unit = tempUnit;
tempUnit.x = unit.originalGridSlot.x;
tempUnit.y = unit.originalGridSlot.y;
tempUnit.gridX = unit.originalGridSlot.gridX;
tempUnit.gridY = unit.originalGridSlot.gridY;
tempUnit.lane = unit.originalGridSlot.lane;
return true;
}
// 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._lastVisible = false; // Track visibility state
}
// Only update if visibility changed
if (!self.healthBar._lastVisible) {
self.healthBar.visible = true; // Ensure health bar is visible
self.healthBar._lastVisible = true;
}
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 = [];
// Pre-calculate common values
var angleStep = Math.PI * 2 / particleCount;
var colorCount = particleColors.length;
for (var i = 0; i < particleCount; i++) {
var size = particleSize + Math.random() * 8;
var particle = particleContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: size,
height: size
});
// Set particle color
particle.tint = particleColors[i % colorCount];
particle.alpha = 0.8 + Math.random() * 0.2;
// Calculate random direction
var angle = angleStep * i + (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
// Play melee attack sound
LK.getSound('melee_attack').play();
var killed = target.takeDamage(self.damage);
if (killed) {
self.isAttacking = false; // Clear attacking flag if target dies
// Don't give gold for killing enemies
// Update enemies defeated count in storage
storage.enemiesDefeated = (storage.enemiesDefeated || 0) + 1;
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
var targetsInRange = [];
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) {
var healthRatio = enemy.health / enemy.maxHealth;
var priority = 5; // Default priority
// Smart target prioritization
if (healthRatio <= 0.2) {
priority = 1; // Highest priority - almost dead
} else if (enemy.name === "Boss Monster") {
priority = 2; // High priority - dangerous
} else if (enemy.name === "Ranged Enemy") {
priority = 3; // Medium-high priority
} else if (healthRatio <= 0.5) {
priority = 4; // Medium priority - damaged
}
targetsInRange.push({
enemy: enemy,
distance: cellDist,
priority: priority,
healthRatio: healthRatio,
threat: enemy.damage // Consider enemy threat level
});
}
}
// Sort targets by priority, then by health ratio, then by threat level
targetsInRange.sort(function (a, b) {
if (a.priority !== b.priority) {
return a.priority - b.priority;
}
if (Math.abs(a.healthRatio - b.healthRatio) > 0.1) {
return a.healthRatio - b.healthRatio; // Lower health first
}
if (a.threat !== b.threat) {
return b.threat - a.threat; // Higher threat first
}
return a.distance - b.distance;
});
// Select best target
if (targetsInRange.length > 0) {
closestEnemy = targetsInRange[0].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();
// Add decorative statistics container
var statsContainer = new Container();
titleOverlay.addChild(statsContainer);
statsContainer.y = 450;
// Best wave text
var bestWaveText = new Text2('Best Wave: ' + (storage.bestWave || 0), {
size: 36,
fill: 0xFFD700
});
bestWaveText.anchor.set(0.5, 0.5);
bestWaveText.x = -300;
statsContainer.addChild(bestWaveText);
// Units defeated text
var unitsDefeatedText = new Text2('Enemies Defeated: ' + (storage.enemiesDefeated || 0), {
size: 36,
fill: 0xFF6666
});
unitsDefeatedText.anchor.set(0.5, 0.5);
unitsDefeatedText.x = 300;
statsContainer.addChild(unitsDefeatedText);
// Add subtle animation to stats
tween(statsContainer, {
alpha: 0.8
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(statsContainer, {
alpha: 1.0
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// 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 continuous pulsing glow effect to start button
function pulseStartButton() {
tween(startButtonGlow, {
alpha: 0.3,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(startButtonGlow, {
alpha: 0,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: pulseStartButton
});
}
});
}
pulseStartButton();
// 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
});
}
});
// Create settings button
var settingsButton = new Container();
var settingsShadow = settingsButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 100,
height: 100
});
settingsShadow.tint = 0x000000;
settingsShadow.alpha = 0.3;
settingsShadow.x = 4;
settingsShadow.y = 4;
var settingsGlow = settingsButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 120,
height: 120
});
settingsGlow.tint = 0x6666FF;
settingsGlow.alpha = 0;
var settingsButtonBg = settingsButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 100,
height: 100
});
settingsButtonBg.tint = 0x4B0082;
// Create gear icon using shapes
var gearIcon = new Container();
settingsButton.addChild(gearIcon);
// Gear center
var gearCenter = gearIcon.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: 30,
height: 30
});
gearCenter.tint = 0xFFFFFF;
// Gear teeth
for (var i = 0; i < 8; i++) {
var tooth = gearIcon.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 10,
height: 20
});
var angle = i * Math.PI / 4;
tooth.x = Math.cos(angle) * 20;
tooth.y = Math.sin(angle) * 20;
tooth.rotation = angle;
tooth.tint = 0xFFFFFF;
}
settingsButton.x = -800;
settingsButton.y = -1100;
titleOverlay.addChild(settingsButton);
// Animate gear rotation
tween(gearIcon, {
rotation: Math.PI * 2
}, {
duration: 10000,
easing: tween.linear,
onFinish: function onFinish() {
gearIcon.rotation = 0;
tween(gearIcon, {
rotation: Math.PI * 2
}, {
duration: 10000,
easing: tween.linear,
onFinish: onFinish
});
}
});
settingsButton.down = function () {
tween(settingsButtonBg, {
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 100,
easing: tween.easeOut
});
};
settingsButton.up = function () {
tween(settingsButtonBg, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
LK.getSound('purchase').play();
};
// 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
});
};
// Add version and credits text
var versionText = new Text2('v1.0', {
size: 24,
fill: 0x666666
});
versionText.anchor.set(1, 1);
versionText.x = 900;
versionText.y = 1300;
titleOverlay.addChild(versionText);
// Add credits text
var creditsText = new Text2('Made with FRVR', {
size: 24,
fill: 0x666666
});
creditsText.anchor.set(0, 1);
creditsText.x = -900;
creditsText.y = 1300;
titleOverlay.addChild(creditsText);
// 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: 0x87CEEB // Sky blue for planning
});
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();
// Stop main menu music and play background music when battle actually starts
// 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);
// Play main menu music when title overlay is shown
LK.playMusic('mainmenu_music');
// Create animated castle silhouette in background
var castleContainer = new Container();
titleOverlay.addChild(castleContainer);
castleContainer.y = 300;
castleContainer.alpha = 0.3;
// Create castle towers
var leftTower = castleContainer.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 300
});
leftTower.x = -400;
leftTower.tint = 0x1a1a2e;
var centerTower = castleContainer.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5,
width: 200,
height: 400
});
centerTower.x = 0;
centerTower.tint = 0x16213e;
var rightTower = castleContainer.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 300
});
rightTower.x = 400;
rightTower.tint = 0x1a1a2e;
// Animate castle floating
tween(castleContainer, {
y: 320,
alpha: 0.25
}, {
duration: 4000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(castleContainer, {
y: 300,
alpha: 0.3
}, {
duration: 4000,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// 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();
// Add enemy preview showcase
var enemyShowcase = new Container();
titleOverlay.addChild(enemyShowcase);
enemyShowcase.y = 700;
enemyShowcase.alpha = 0.6;
// Create actual enemy units for showcase
var showcaseEnemies = [];
var enemyTypes = [{
type: 'regular',
x: -400,
constructor: Enemy
}, {
type: 'ranged',
x: 0,
constructor: RangedEnemy
}, {
type: 'boss',
x: 400,
constructor: Boss
}];
var _loop = function _loop() {
enemyData = enemyTypes[i]; // Create actual enemy unit
showcaseEnemy = new enemyData.constructor();
showcaseEnemy.x = enemyData.x;
showcaseEnemy.y = 0;
// Scale down the enemy for showcase
showcaseEnemy.scaleX = 0.6;
showcaseEnemy.scaleY = 0.6;
enemyShowcase.addChild(showcaseEnemy);
showcaseEnemies.push(showcaseEnemy);
// Add floating animation with different timing for each enemy
baseY = showcaseEnemy.y;
floatDelay = i * 300; // Stagger the animations
floatDuration = 2000 + Math.random() * 1000; // Randomize duration slightly
function startFloatingAnimation(enemy, baseY, delay) {
LK.setTimeout(function () {
function floatUp() {
tween(enemy, {
y: baseY - 20,
scaleX: 0.66,
scaleY: 0.66,
rotation: 0.08
}, {
duration: floatDuration,
easing: tween.easeInOut,
onFinish: floatDown
});
}
function floatDown() {
tween(enemy, {
y: baseY + 10,
scaleX: 0.54,
scaleY: 0.54,
rotation: -0.08
}, {
duration: floatDuration,
easing: tween.easeInOut,
onFinish: floatUp
});
}
floatUp(); // Start the animation cycle
}, delay);
}
// Start floating animation for this enemy
startFloatingAnimation(showcaseEnemy, baseY, floatDelay);
// Add breathing/idle animation - slight scale pulsing
function startBreathingAnimation(enemy, delay) {
LK.setTimeout(function () {
function breatheIn() {
tween(enemy, {
scaleX: enemy.scaleX * 1.05,
scaleY: enemy.scaleY * 1.05
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: breatheOut
});
}
function breatheOut() {
tween(enemy, {
scaleX: enemy.scaleX / 1.05,
scaleY: enemy.scaleY / 1.05
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: breatheIn
});
}
breatheIn(); // Start the breathing cycle
}, delay + 400); // Offset from floating animation
}
// Start breathing animation for this enemy
startBreathingAnimation(showcaseEnemy, floatDelay);
// Add menacing glow effect for boss enemy
if (enemyData.type === 'boss') {
var startGlowAnimation = function startGlowAnimation(enemy) {
function glowUp() {
LK.effects.flashObject(enemy, 0xFF0000, 300); // Red flash
LK.setTimeout(glowDown, 300);
}
function glowDown() {
LK.setTimeout(glowUp, 2000 + Math.random() * 1000); // Random interval
}
LK.setTimeout(glowUp, 1000); // Start after 1 second
};
startGlowAnimation(showcaseEnemy);
}
// Add special effect for ranged enemy - slight weapon sway
if (enemyData.type === 'ranged') {
var startWeaponSway = function startWeaponSway(enemy) {
function swayLeft() {
tween(enemy, {
rotation: enemy.rotation - 0.05
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: swayRight
});
}
function swayRight() {
tween(enemy, {
rotation: enemy.rotation + 0.1
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: swayLeft
});
}
LK.setTimeout(swayLeft, floatDelay + 600);
};
startWeaponSway(showcaseEnemy);
}
},
enemyData,
showcaseEnemy,
baseY,
floatDelay,
floatDuration;
for (var i = 0; i < enemyTypes.length; i++) {
_loop();
}
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 stateText = new Text2('', {
size: 40,
fill: 0x44FF44 // Default to green for planning phase
});
stateText.anchor.set(0.5, 0.5);
stateText.x = 0; // Center relative to topUIContainer
stateText.y = 140; // Adjusted for bigger UI
topUIContainer = new Container(); // Container for top UI elements
topUIContainer.addChild(stateText);
var waveText = new Text2('Wave: 1', {
size: 40,
fill: 0xFFFFFF
});
waveText.anchor.set(0.5, 0.5);
waveText.x = 0; // Center relative to topUIContainer
waveText.y = 140; // Adjusted for bigger UI
topUIContainer.addChild(waveText);
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
topUIContainer = new Container(); // Container for top UI elements
var isUIMinimized = false; // Track UI state
// Helper function to register entity position
function registerEntityPosition(entity) {
if (entity.gridX >= 0 && entity.gridY >= 0) {
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: 280,
height: 120
});
refreshButtonBg.tint = 0xFF8800;
var refreshText1 = new Text2('Refresh', {
size: 48,
fill: 0xFFFFFF
});
refreshText1.anchor.set(0.5, 0.5);
refreshText1.y = -20;
refreshButton.addChild(refreshButtonBg);
refreshButton.addChild(refreshText1);
var refreshText2 = new Text2('Shop', {
size: 48,
fill: 0xFFFFFF
});
refreshText2.anchor.set(0.5, 0.5);
refreshText2.y = 20;
refreshButton.addChild(refreshText2);
refreshButton.x = 2048 - 200;
refreshButton.y = PLAYABLE_HEIGHT + 100; // Move button higher
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: 280,
height: 120
});
levelUpButtonBg.tint = 0xFF8800;
var levelUpText = new Text2('Level Up', {
size: 48,
fill: 0xFFFFFF
});
levelUpText.anchor.set(0.5, 0.5);
levelUpButton.addChild(levelUpText);
levelUpButton.x = 200;
levelUpButton.y = PLAYABLE_HEIGHT + 100; // Move button higher
levelUpButton.up = function () {
levelUpPlayer();
};
// Create level up cost text
var levelUpCostText = new Text2('Cost: ' + getLevelUpCost(playerLevel) + 'g', {
size: 42,
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: 42,
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 = 0x0a0a1a; // Match the deep dark blue of the title screen
// 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: 0x87CEEB
});
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: 0xFFD700
});
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)
// 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!');
// Allow unit movement during planning phase
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.interactive = true;
}
}
}
// 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');
// Disable unit movement during battle phase
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.interactive = false;
}
}
}
// 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
// Stop main menu music and play background music when battle actually starts
LK.stopMusic();
LK.playMusic('backgroundmusic');
}
// 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;
}
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
if (timeDiff < doubleTapDelay && distance < doubleTapDistance) {
// This is a double-tap - check if we can purchase
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
}
}
// Reset tap tracking after double-tap to prevent triple-tap
lastTapTime = 0;
lastTapX = 0;
lastTapY = 0;
break;
} else {
// Not a double-tap: just update last tap info for next potential double-tap
lastTapTime = currentTime;
lastTapX = x;
lastTapY = y;
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();
// Update best wave in storage
if (wave > (storage.bestWave || 0)) {
storage.bestWave = wave;
}
// 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();
// Switch to main menu music when no enemies remain (planning phase)
LK.stopMusic();
LK.playMusic('mainmenu_music');
// 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
var unitsToUpdate = [];
for (var y = 0; y < GRID_ROWS; y++) {
for (var x = 0; x < GRID_COLS; x++) {
var gridSlot = grid[y][x];
if (gridSlot.unit) {
unitsToUpdate.push(gridSlot.unit);
}
}
}
// Process all units in a single loop
for (var i = 0; i < unitsToUpdate.length; i++) {
var unit = unitsToUpdate[i];
unit.update();
if (gameState === 'battle') {
unit.showHealthBar(); // Ensure health bar is shown during battle
unit.updateHealthBar();
unit.showStarIndicator(); // Ensure star indicator is shown during battle
} else {
// Hide health bars during planning phase
if (unit.healthBar) {
unit.healthBar.visible = false;
}
// Show star indicators during planning phase for easy tier identification
unit.showStarIndicator();
}
// Update king info button position if this is the king
if (unit.name === 'King' && kingInfoButton) {
kingInfoButton.x = unit.x;
kingInfoButton.y = unit.y + 100; // Position below king
}
}
// Unit shooting (only during battle)
if (gameState === 'battle') {
for (var y = 0; y < GRID_ROWS; y++) {
for (var x = 0; x < GRID_COLS; x++) {
var gridSlot = grid[y][x];
if (gridSlot.unit) {
var target = gridSlot.unit.findTarget();
if (target) {
var bullet = gridSlot.unit.shoot(target);
if (bullet) {
bullets.push(bullet);
game.addChild(bullet);
LK.getSound('shoot').play();
} else if (gridSlot.unit.range <= 100) {
// Melee attack sound effect (no bullet created)
if (gridSlot.unit.canShoot()) {
LK.getSound('shoot').play();
}
}
}
}
}
}
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
if (bullet && bullet.update) {
bullet.update();
}
if (!bullet.parent) {
bullets.splice(i, 1);
}
}
// Update enemy projectiles
for (var i = enemyProjectiles.length - 1; i >= 0; i--) {
var projectile = enemyProjectiles[i];
if (projectile && projectile.update) {
projectile.update();
}
if (!projectile.parent) {
enemyProjectiles.splice(i, 1);
}
}
// Update level up cost text
levelUpCostText.setText('Cost: ' + getLevelUpCost(playerLevel) + 'g');
// Check win condition
if (wave >= 10 && enemies.length === 0 && enemiesSpawned >= enemiesPerWave) {
LK.showYouWin();
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.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
// Update enemies defeated count in storage
storage.enemiesDefeated = (storage.enemiesDefeated || 0) + 1;
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
// Only recalculate targets every 15 ticks or if position changed
if (!self._targetCache || LK.ticks % 15 === 0 || self._lastTargetX !== self.gridX || self._lastTargetY !== self.gridY) {
self._lastTargetX = self.gridX;
self._lastTargetY = self.gridY;
// Smart targeting - collect all units in range
var targetsInRange = [];
// 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) {
targetsInRange.push({
unit: king,
distance: kingCellDist,
priority: 3,
// Medium priority for king
healthRatio: king.health / king.maxHealth
});
}
}
}
// Then check other units on the grid
for (var y = Math.max(0, self.gridY - self.range); y <= Math.min(GRID_ROWS - 1, self.gridY + self.range); y++) {
for (var x = Math.max(0, self.gridX - self.range); x <= Math.min(GRID_COLS - 1, self.gridX + self.range); 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 = 5; // Default priority
var healthRatio = unit.health / unit.maxHealth;
// Smart prioritization
if (healthRatio <= 0.3) {
priority = 1; // Highest priority - can finish off quickly
} else if (unit.name === "Wizard" || unit.name === "Ranger") {
priority = 2; // High priority - dangerous units
} else if (unit.name === "Wall") {
priority = 8; // Lowest priority - defensive units
} else if (unit.name === "Paladin" || unit.name === "Knight") {
priority = 6; // Low priority - tanky units
}
targetsInRange.push({
unit: unit,
distance: cellDist,
priority: priority,
healthRatio: healthRatio
});
}
}
}
}
}
self._targetCache = targetsInRange;
} else {
// Use cached targets
var targetsInRange = self._targetCache || [];
}
// Sort targets by priority first, then by health ratio, then by distance
targetsInRange.sort(function (a, b) {
if (a.priority !== b.priority) {
return a.priority - b.priority;
}
if (Math.abs(a.healthRatio - b.healthRatio) > 0.1) {
return a.healthRatio - b.healthRatio; // Lower health first
}
return a.distance - b.distance;
});
// Select best target
if (targetsInRange.length > 0) {
closestUnit = targetsInRange[0].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
// Play melee attack sound
LK.getSound('melee_attack').play();
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();
// Smart positioning - try to stay behind melee enemies
var meleeFrontline = GRID_ROWS;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy !== self && enemy.range <= 1) {
// Melee enemy
if (enemy.gridY > self.gridY && enemy.gridY < meleeFrontline) {
meleeFrontline = enemy.gridY;
}
}
}
// If we're ahead of melee units, consider retreating
if (self.gridY > meleeFrontline && !self.isAttacking && Math.random() < 0.5) {
// Try to move back if possible
var backY = self.gridY - 1;
if (backY >= 0 && !self.gridMovement.isCellOccupied(self.gridX, backY)) {
// Override normal movement to retreat
self.gridMovement.targetCell.y = backY;
}
}
// 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);
// Play ranged attack sound
LK.getSound('shoot').play();
// 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
// Play melee attack sound
LK.getSound('melee_attack').play();
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);
}
// Cache directions array at class level to avoid recreation
if (!self.directions) {
self.directions = [{
x: 0,
y: -1
},
// Up
{
x: 1,
y: 0
},
// Right
{
x: 0,
y: 1
},
// Down
{
x: -1,
y: 0
} // Left
];
}
// Get neighbors of a cell
function getNeighbors(x, y) {
var neighbors = [];
// Check if this entity is a player unit
var isPlayerUnit = self.entity.isPlayerUnit !== undefined ? self.entity.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");
// Cache this value on entity for future use
if (self.entity.isPlayerUnit === undefined) {
self.entity.isPlayerUnit = isPlayerUnit;
}
for (var i = 0; i < self.directions.length; i++) {
var newX = x + self.directions[i].x;
var newY = y + self.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;
}
// Enemy coordination - wait for nearby allies before advancing
var nearbyAllies = 0;
var shouldWait = false;
for (var i = 0; i < enemies.length; i++) {
var ally = enemies[i];
if (ally !== self.entity && ally.gridY === self.entity.gridY) {
// Check if ally is in same row
var allyDist = Math.abs(ally.gridX - self.entity.gridX);
if (allyDist <= 2) {
nearbyAllies++;
// If ally is behind us, wait for them
if (ally.gridY < self.entity.gridY) {
shouldWait = true;
}
}
}
}
// Wait if we're too far ahead of our allies
if (shouldWait && nearbyAllies < 2 && Math.random() < 0.7) {
return false; // 70% chance to wait for allies
}
// 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') {
// 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) {
// Swap units if the slot is occupied
var tempUnit = self.unit;
self.unit = unit;
unit.originalGridSlot.unit = tempUnit;
tempUnit.x = unit.originalGridSlot.x;
tempUnit.y = unit.originalGridSlot.y;
tempUnit.gridX = unit.originalGridSlot.gridX;
tempUnit.gridY = unit.originalGridSlot.gridY;
tempUnit.lane = unit.originalGridSlot.lane;
return true;
}
// 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._lastVisible = false; // Track visibility state
}
// Only update if visibility changed
if (!self.healthBar._lastVisible) {
self.healthBar.visible = true; // Ensure health bar is visible
self.healthBar._lastVisible = true;
}
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 = [];
// Pre-calculate common values
var angleStep = Math.PI * 2 / particleCount;
var colorCount = particleColors.length;
for (var i = 0; i < particleCount; i++) {
var size = particleSize + Math.random() * 8;
var particle = particleContainer.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: size,
height: size
});
// Set particle color
particle.tint = particleColors[i % colorCount];
particle.alpha = 0.8 + Math.random() * 0.2;
// Calculate random direction
var angle = angleStep * i + (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
// Play melee attack sound
LK.getSound('melee_attack').play();
var killed = target.takeDamage(self.damage);
if (killed) {
self.isAttacking = false; // Clear attacking flag if target dies
// Don't give gold for killing enemies
// Update enemies defeated count in storage
storage.enemiesDefeated = (storage.enemiesDefeated || 0) + 1;
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
var targetsInRange = [];
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) {
var healthRatio = enemy.health / enemy.maxHealth;
var priority = 5; // Default priority
// Smart target prioritization
if (healthRatio <= 0.2) {
priority = 1; // Highest priority - almost dead
} else if (enemy.name === "Boss Monster") {
priority = 2; // High priority - dangerous
} else if (enemy.name === "Ranged Enemy") {
priority = 3; // Medium-high priority
} else if (healthRatio <= 0.5) {
priority = 4; // Medium priority - damaged
}
targetsInRange.push({
enemy: enemy,
distance: cellDist,
priority: priority,
healthRatio: healthRatio,
threat: enemy.damage // Consider enemy threat level
});
}
}
// Sort targets by priority, then by health ratio, then by threat level
targetsInRange.sort(function (a, b) {
if (a.priority !== b.priority) {
return a.priority - b.priority;
}
if (Math.abs(a.healthRatio - b.healthRatio) > 0.1) {
return a.healthRatio - b.healthRatio; // Lower health first
}
if (a.threat !== b.threat) {
return b.threat - a.threat; // Higher threat first
}
return a.distance - b.distance;
});
// Select best target
if (targetsInRange.length > 0) {
closestEnemy = targetsInRange[0].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();
// Add decorative statistics container
var statsContainer = new Container();
titleOverlay.addChild(statsContainer);
statsContainer.y = 450;
// Best wave text
var bestWaveText = new Text2('Best Wave: ' + (storage.bestWave || 0), {
size: 36,
fill: 0xFFD700
});
bestWaveText.anchor.set(0.5, 0.5);
bestWaveText.x = -300;
statsContainer.addChild(bestWaveText);
// Units defeated text
var unitsDefeatedText = new Text2('Enemies Defeated: ' + (storage.enemiesDefeated || 0), {
size: 36,
fill: 0xFF6666
});
unitsDefeatedText.anchor.set(0.5, 0.5);
unitsDefeatedText.x = 300;
statsContainer.addChild(unitsDefeatedText);
// Add subtle animation to stats
tween(statsContainer, {
alpha: 0.8
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(statsContainer, {
alpha: 1.0
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// 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 continuous pulsing glow effect to start button
function pulseStartButton() {
tween(startButtonGlow, {
alpha: 0.3,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(startButtonGlow, {
alpha: 0,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: pulseStartButton
});
}
});
}
pulseStartButton();
// 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
});
}
});
// Create settings button
var settingsButton = new Container();
var settingsShadow = settingsButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 100,
height: 100
});
settingsShadow.tint = 0x000000;
settingsShadow.alpha = 0.3;
settingsShadow.x = 4;
settingsShadow.y = 4;
var settingsGlow = settingsButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 120,
height: 120
});
settingsGlow.tint = 0x6666FF;
settingsGlow.alpha = 0;
var settingsButtonBg = settingsButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 100,
height: 100
});
settingsButtonBg.tint = 0x4B0082;
// Create gear icon using shapes
var gearIcon = new Container();
settingsButton.addChild(gearIcon);
// Gear center
var gearCenter = gearIcon.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: 30,
height: 30
});
gearCenter.tint = 0xFFFFFF;
// Gear teeth
for (var i = 0; i < 8; i++) {
var tooth = gearIcon.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5,
width: 10,
height: 20
});
var angle = i * Math.PI / 4;
tooth.x = Math.cos(angle) * 20;
tooth.y = Math.sin(angle) * 20;
tooth.rotation = angle;
tooth.tint = 0xFFFFFF;
}
settingsButton.x = -800;
settingsButton.y = -1100;
titleOverlay.addChild(settingsButton);
// Animate gear rotation
tween(gearIcon, {
rotation: Math.PI * 2
}, {
duration: 10000,
easing: tween.linear,
onFinish: function onFinish() {
gearIcon.rotation = 0;
tween(gearIcon, {
rotation: Math.PI * 2
}, {
duration: 10000,
easing: tween.linear,
onFinish: onFinish
});
}
});
settingsButton.down = function () {
tween(settingsButtonBg, {
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 100,
easing: tween.easeOut
});
};
settingsButton.up = function () {
tween(settingsButtonBg, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
LK.getSound('purchase').play();
};
// 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
});
};
// Add version and credits text
var versionText = new Text2('v1.0', {
size: 24,
fill: 0x666666
});
versionText.anchor.set(1, 1);
versionText.x = 900;
versionText.y = 1300;
titleOverlay.addChild(versionText);
// Add credits text
var creditsText = new Text2('Made with FRVR', {
size: 24,
fill: 0x666666
});
creditsText.anchor.set(0, 1);
creditsText.x = -900;
creditsText.y = 1300;
titleOverlay.addChild(creditsText);
// 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: 0x87CEEB // Sky blue for planning
});
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();
// Stop main menu music and play background music when battle actually starts
// 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);
// Play main menu music when title overlay is shown
LK.playMusic('mainmenu_music');
// Create animated castle silhouette in background
var castleContainer = new Container();
titleOverlay.addChild(castleContainer);
castleContainer.y = 300;
castleContainer.alpha = 0.3;
// Create castle towers
var leftTower = castleContainer.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 300
});
leftTower.x = -400;
leftTower.tint = 0x1a1a2e;
var centerTower = castleContainer.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5,
width: 200,
height: 400
});
centerTower.x = 0;
centerTower.tint = 0x16213e;
var rightTower = castleContainer.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 300
});
rightTower.x = 400;
rightTower.tint = 0x1a1a2e;
// Animate castle floating
tween(castleContainer, {
y: 320,
alpha: 0.25
}, {
duration: 4000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(castleContainer, {
y: 300,
alpha: 0.3
}, {
duration: 4000,
easing: tween.easeInOut,
onFinish: onFinish
});
}
});
// 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();
// Add enemy preview showcase
var enemyShowcase = new Container();
titleOverlay.addChild(enemyShowcase);
enemyShowcase.y = 700;
enemyShowcase.alpha = 0.6;
// Create actual enemy units for showcase
var showcaseEnemies = [];
var enemyTypes = [{
type: 'regular',
x: -400,
constructor: Enemy
}, {
type: 'ranged',
x: 0,
constructor: RangedEnemy
}, {
type: 'boss',
x: 400,
constructor: Boss
}];
var _loop = function _loop() {
enemyData = enemyTypes[i]; // Create actual enemy unit
showcaseEnemy = new enemyData.constructor();
showcaseEnemy.x = enemyData.x;
showcaseEnemy.y = 0;
// Scale down the enemy for showcase
showcaseEnemy.scaleX = 0.6;
showcaseEnemy.scaleY = 0.6;
enemyShowcase.addChild(showcaseEnemy);
showcaseEnemies.push(showcaseEnemy);
// Add floating animation with different timing for each enemy
baseY = showcaseEnemy.y;
floatDelay = i * 300; // Stagger the animations
floatDuration = 2000 + Math.random() * 1000; // Randomize duration slightly
function startFloatingAnimation(enemy, baseY, delay) {
LK.setTimeout(function () {
function floatUp() {
tween(enemy, {
y: baseY - 20,
scaleX: 0.66,
scaleY: 0.66,
rotation: 0.08
}, {
duration: floatDuration,
easing: tween.easeInOut,
onFinish: floatDown
});
}
function floatDown() {
tween(enemy, {
y: baseY + 10,
scaleX: 0.54,
scaleY: 0.54,
rotation: -0.08
}, {
duration: floatDuration,
easing: tween.easeInOut,
onFinish: floatUp
});
}
floatUp(); // Start the animation cycle
}, delay);
}
// Start floating animation for this enemy
startFloatingAnimation(showcaseEnemy, baseY, floatDelay);
// Add breathing/idle animation - slight scale pulsing
function startBreathingAnimation(enemy, delay) {
LK.setTimeout(function () {
function breatheIn() {
tween(enemy, {
scaleX: enemy.scaleX * 1.05,
scaleY: enemy.scaleY * 1.05
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: breatheOut
});
}
function breatheOut() {
tween(enemy, {
scaleX: enemy.scaleX / 1.05,
scaleY: enemy.scaleY / 1.05
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: breatheIn
});
}
breatheIn(); // Start the breathing cycle
}, delay + 400); // Offset from floating animation
}
// Start breathing animation for this enemy
startBreathingAnimation(showcaseEnemy, floatDelay);
// Add menacing glow effect for boss enemy
if (enemyData.type === 'boss') {
var startGlowAnimation = function startGlowAnimation(enemy) {
function glowUp() {
LK.effects.flashObject(enemy, 0xFF0000, 300); // Red flash
LK.setTimeout(glowDown, 300);
}
function glowDown() {
LK.setTimeout(glowUp, 2000 + Math.random() * 1000); // Random interval
}
LK.setTimeout(glowUp, 1000); // Start after 1 second
};
startGlowAnimation(showcaseEnemy);
}
// Add special effect for ranged enemy - slight weapon sway
if (enemyData.type === 'ranged') {
var startWeaponSway = function startWeaponSway(enemy) {
function swayLeft() {
tween(enemy, {
rotation: enemy.rotation - 0.05
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: swayRight
});
}
function swayRight() {
tween(enemy, {
rotation: enemy.rotation + 0.1
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: swayLeft
});
}
LK.setTimeout(swayLeft, floatDelay + 600);
};
startWeaponSway(showcaseEnemy);
}
},
enemyData,
showcaseEnemy,
baseY,
floatDelay,
floatDuration;
for (var i = 0; i < enemyTypes.length; i++) {
_loop();
}
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 stateText = new Text2('', {
size: 40,
fill: 0x44FF44 // Default to green for planning phase
});
stateText.anchor.set(0.5, 0.5);
stateText.x = 0; // Center relative to topUIContainer
stateText.y = 140; // Adjusted for bigger UI
topUIContainer = new Container(); // Container for top UI elements
topUIContainer.addChild(stateText);
var waveText = new Text2('Wave: 1', {
size: 40,
fill: 0xFFFFFF
});
waveText.anchor.set(0.5, 0.5);
waveText.x = 0; // Center relative to topUIContainer
waveText.y = 140; // Adjusted for bigger UI
topUIContainer.addChild(waveText);
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
topUIContainer = new Container(); // Container for top UI elements
var isUIMinimized = false; // Track UI state
// Helper function to register entity position
function registerEntityPosition(entity) {
if (entity.gridX >= 0 && entity.gridY >= 0) {
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: 280,
height: 120
});
refreshButtonBg.tint = 0xFF8800;
var refreshText1 = new Text2('Refresh', {
size: 48,
fill: 0xFFFFFF
});
refreshText1.anchor.set(0.5, 0.5);
refreshText1.y = -20;
refreshButton.addChild(refreshButtonBg);
refreshButton.addChild(refreshText1);
var refreshText2 = new Text2('Shop', {
size: 48,
fill: 0xFFFFFF
});
refreshText2.anchor.set(0.5, 0.5);
refreshText2.y = 20;
refreshButton.addChild(refreshText2);
refreshButton.x = 2048 - 200;
refreshButton.y = PLAYABLE_HEIGHT + 100; // Move button higher
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: 280,
height: 120
});
levelUpButtonBg.tint = 0xFF8800;
var levelUpText = new Text2('Level Up', {
size: 48,
fill: 0xFFFFFF
});
levelUpText.anchor.set(0.5, 0.5);
levelUpButton.addChild(levelUpText);
levelUpButton.x = 200;
levelUpButton.y = PLAYABLE_HEIGHT + 100; // Move button higher
levelUpButton.up = function () {
levelUpPlayer();
};
// Create level up cost text
var levelUpCostText = new Text2('Cost: ' + getLevelUpCost(playerLevel) + 'g', {
size: 42,
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: 42,
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 = 0x0a0a1a; // Match the deep dark blue of the title screen
// 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: 0x87CEEB
});
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: 0xFFD700
});
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)
// 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!');
// Allow unit movement during planning phase
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.interactive = true;
}
}
}
// 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');
// Disable unit movement during battle phase
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.interactive = false;
}
}
}
// 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
// Stop main menu music and play background music when battle actually starts
LK.stopMusic();
LK.playMusic('backgroundmusic');
}
// 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;
}
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
if (timeDiff < doubleTapDelay && distance < doubleTapDistance) {
// This is a double-tap - check if we can purchase
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
}
}
// Reset tap tracking after double-tap to prevent triple-tap
lastTapTime = 0;
lastTapX = 0;
lastTapY = 0;
break;
} else {
// Not a double-tap: just update last tap info for next potential double-tap
lastTapTime = currentTime;
lastTapX = x;
lastTapY = y;
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();
// Update best wave in storage
if (wave > (storage.bestWave || 0)) {
storage.bestWave = wave;
}
// 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();
// Switch to main menu music when no enemies remain (planning phase)
LK.stopMusic();
LK.playMusic('mainmenu_music');
// 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
var unitsToUpdate = [];
for (var y = 0; y < GRID_ROWS; y++) {
for (var x = 0; x < GRID_COLS; x++) {
var gridSlot = grid[y][x];
if (gridSlot.unit) {
unitsToUpdate.push(gridSlot.unit);
}
}
}
// Process all units in a single loop
for (var i = 0; i < unitsToUpdate.length; i++) {
var unit = unitsToUpdate[i];
unit.update();
if (gameState === 'battle') {
unit.showHealthBar(); // Ensure health bar is shown during battle
unit.updateHealthBar();
unit.showStarIndicator(); // Ensure star indicator is shown during battle
} else {
// Hide health bars during planning phase
if (unit.healthBar) {
unit.healthBar.visible = false;
}
// Show star indicators during planning phase for easy tier identification
unit.showStarIndicator();
}
// Update king info button position if this is the king
if (unit.name === 'King' && kingInfoButton) {
kingInfoButton.x = unit.x;
kingInfoButton.y = 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();
}
};