/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var AreaDamageBullet = Container.expand(function (startX, startY, target, damage, isFromPlayer, areaRadius) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.target = target;
self.damage = damage;
self.speed = 6;
self.isFromPlayer = isFromPlayer;
self.areaRadius = areaRadius || 100; // Default area damage radius
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = isFromPlayer ? 0xFF9800 : 0xFF5722; // Orange tint for area damage bullets
bulletGraphics.scaleX = 1.3;
bulletGraphics.scaleY = 1.3;
self.update = function () {
if (!self.target || self.target.isDead) {
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 < 30) {
// Apply direct damage to the target first
self.target.takeDamage(self.damage);
// Then area damage explosion
self.explode();
self.destroy();
return;
}
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
};
self.explode = function () {
// Visual explosion effect (removed screen flash to prevent bugs)
LK.getSound('explosion').play();
// Create visual circle around the hit target
var explosionCircle = LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 13,
scaleY: 13
});
explosionCircle.x = self.target.x;
explosionCircle.y = self.target.y;
explosionCircle.tint = 0xFF9800;
explosionCircle.alpha = 0.6;
game.addChild(explosionCircle);
// Animate the circle to fade out
tween(explosionCircle, {
alpha: 0,
scaleX: 15,
scaleY: 15
}, {
duration: 500,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Get all possible targets for area damage
var allTargets = [];
if (self.isFromPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage to all units within area radius (100 units as requested)
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead && targetUnit !== self.target) {
// Exclude the initial target from area damage
var distance = Math.sqrt(Math.pow(self.target.x - targetUnit.x, 2) + Math.pow(self.target.y - targetUnit.y, 2));
if (distance <= 100) {
targetUnit.takeDamage(140);
// Flash each affected unit
tween(targetUnit, {
tint: 0xFF9800
}, {
duration: 300,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
};
return self;
});
var Ballesta = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'ballesta';
self.maxHealth = 900;
self.currentHealth = 900;
self.isDead = false;
self.damage = 18;
self.range = 2500;
self.attackSpeed = 18; // Every 0.3 seconds (60fps * 0.3 = 18 ticks)
self.lastAttackTime = 0;
self.target = null;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 240; // 4 seconds (60 * 4 = 240 ticks)
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var ballestaGraphics = self.attachAsset('ballesta', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = 0.25;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Ballesta cannot attack aerial units - only ground units
if (targetUnits[i].isAerial) {
continue;
}
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range, but only if touching middle line (y around 1366)
var canAttackTowers = Math.abs(self.y - 1366) <= 50; // Must be within 50 units of middle line
if (canAttackTowers) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.range && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullet.speed = 16; // 100% faster than default speed of 8
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't attack while stunned
}
// Lose 4% of total health per second (60 ticks = 1 second)
if (LK.ticks % 15 === 0) {
// Every 15 ticks = 4 times per second = 1% each time = 4% per second
self.currentHealth -= self.maxHealth * 0.01;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar without flash effect for natural decay
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
}
self.findTarget();
self.attack();
};
return self;
});
var Bruja = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'bruja';
self.isAerial = false; // Ground unit
self.maxHealth = 550;
self.currentHealth = 550;
self.damage = 130;
self.attackSpeed = 90; // 90 ticks
self.range = 500; // Detection range
self.attackRange = 425; // Attack range
self.speed = 1.25;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Skeleton summoning properties
self.skeletonSummonTimer = 180; // 3 seconds initial delay (60 ticks = 1 second)
self.skeletonSummonInterval = 600; // 10 seconds interval (60 * 10 = 600 ticks)
self.hasSummonedFirst = false; // Track if first summon has happened
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var brujaGraphics = self.attachAsset('bruja', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.2;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Bruja can attack both air and ground units
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Create purple area damage bullet with 75 units radius
var brujaAreaBullet = new BrujaAreaBullet(self.x, self.y, self.target, self.damage, self.isPlayer, 75);
bullets.push(brujaAreaBullet);
game.addChild(brujaAreaBullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.summonSkeletons = function () {
// Generate 4 skeletons in formation: up, down, left, right at 75 units distance
var skeletonPositions = [{
x: self.x,
y: self.y - 75
},
// Up
{
x: self.x,
y: self.y + 75
},
// Down
{
x: self.x - 75,
y: self.y
},
// Left
{
x: self.x + 75,
y: self.y
} // Right
];
for (var i = 0; i < skeletonPositions.length; i++) {
var pos = skeletonPositions[i];
var skeleton = new Skeleton(self.isPlayer);
skeleton.x = pos.x;
skeleton.y = pos.y;
// Remove spawn delay for Bruja-generated skeletons so they can move and attack immediately
skeleton.spawnTimer = 0;
skeleton.isSpawning = false;
skeleton.spawnTimerText.setText('');
// Add skeleton to appropriate unit array
if (self.isPlayer) {
playerUnits.push(skeleton);
} else {
enemyUnits.push(skeleton);
}
game.addChild(skeleton);
}
// Play deploy sound for skeleton summoning
LK.getSound('deploy').play();
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
// Handle skeleton summoning
self.skeletonSummonTimer--;
if (!self.hasSummonedFirst && self.skeletonSummonTimer <= 0) {
// First summon after 3 seconds
self.summonSkeletons();
self.hasSummonedFirst = true;
self.skeletonSummonTimer = self.skeletonSummonInterval; // Set to 10 second interval
} else if (self.hasSummonedFirst && self.skeletonSummonTimer <= 0) {
// Subsequent summons every 10 seconds
self.summonSkeletons();
self.skeletonSummonTimer = self.skeletonSummonInterval; // Reset to 10 second interval
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var BrujaAreaBullet = Container.expand(function (startX, startY, target, damage, isFromPlayer, areaRadius) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.target = target;
self.damage = damage;
self.speed = 6;
self.isFromPlayer = isFromPlayer;
self.areaRadius = areaRadius || 75; // 75 units radius as requested
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0x9C27B0; // Purple color for bruja bullets
bulletGraphics.scaleX = 1.2;
bulletGraphics.scaleY = 1.2;
self.update = function () {
if (!self.target || self.target.isDead) {
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 < 30) {
// Apply direct damage to the target first
self.target.takeDamage(self.damage);
// Then area damage explosion
self.explode();
self.destroy();
return;
}
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
};
self.explode = function () {
// Play explosion sound
LK.getSound('explosion').play();
// Create blue visual circle around the hit target with 75 units radius
var explosionCircle = LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 10,
// Scale to make it 75 units radius (15 * 10 / 2 = 75)
scaleY: 10
});
explosionCircle.x = self.target.x;
explosionCircle.y = self.target.y;
explosionCircle.tint = 0x0080FF; // Blue color as requested
explosionCircle.alpha = 0.6;
game.addChild(explosionCircle);
// Animate the circle to fade out
tween(explosionCircle, {
alpha: 0,
scaleX: 12,
scaleY: 12
}, {
duration: 500,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Get all possible targets for area damage
var allTargets = [];
if (self.isFromPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage to all units within area radius (75 units as requested)
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead && targetUnit !== self.target) {
// Exclude the initial target from area damage
var distance = Math.sqrt(Math.pow(self.target.x - targetUnit.x, 2) + Math.pow(self.target.y - targetUnit.y, 2));
if (distance <= self.areaRadius) {
targetUnit.takeDamage(self.damage);
// Flash each affected unit with blue color
tween(targetUnit, {
tint: 0x0080FF
}, {
duration: 300,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
};
return self;
});
var Bullet = Container.expand(function (startX, startY, target, damage, isFromPlayer) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.target = target;
self.damage = damage;
self.speed = 8;
self.isFromPlayer = isFromPlayer;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = isFromPlayer ? 0x4CAF50 : 0xF44336;
self.update = function () {
if (!self.target || self.target.isDead) {
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 < 20) {
self.target.takeDamage(self.damage);
self.destroy();
return;
}
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
};
return self;
});
var Cannon = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'cannon';
self.maxHealth = 600;
self.currentHealth = 600;
self.isDead = false;
self.damage = 90;
self.range = 500;
self.attackSpeed = 50;
self.lastAttackTime = 0;
self.target = null;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var cannonGraphics = self.attachAsset('cannon', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = 0.25;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Cannon cannot attack aerial units
if (targetUnits[i].isAerial) {
continue;
}
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
self.target = closestTarget;
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.range && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't attack while stunned
}
// Lose 3% of total health per second (60 ticks = 1 second)
if (LK.ticks % 20 === 0) {
// Every 20 ticks = 3 times per second = 1% each time = 3% per second
self.currentHealth -= self.maxHealth * 0.01;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar without flash effect for natural decay
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
}
self.findTarget();
self.attack();
};
return self;
});
var Card = Container.expand(function (cardType) {
var self = Container.call(this);
self.cardType = cardType;
self.cost = cardType === 'giant' ? 5 : cardType === 'wizard' ? 5 : cardType === 'bruja' ? 5 : cardType === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : cardType === 'montapuercos' ? 4 : cardType === 'hielo' ? 4 : cardType === 'lapida' ? 3 : cardType === 'golem' ? 8 : 3;
var cardBg = self.attachAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5
});
var cardIcon = LK.getAsset(cardType === 'discharge' ? 'descarga_descardina' : cardType === 'maquinaVoladora' ? 'flying_machine' : cardType === 'bruja' ? 'bruja' : cardType, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
self.addChild(cardIcon);
var costText = new Text2(self.cost.toString(), {
size: 30,
fill: 0xFFFFFF
});
costText.anchor.set(0.5, 0.5);
costText.x = 120;
costText.y = -40;
self.addChild(costText);
return self;
});
var Discharge = Container.expand(function (targetX, targetY, isFromPlayer) {
var self = Container.call(this);
self.targetX = targetX;
self.targetY = targetY;
self.isFromPlayer = isFromPlayer;
self.damage = 200;
self.stunDuration = 60; // 1 second stun (60 ticks)
// Discharge appears instantly at target location
self.x = targetX;
self.y = targetY;
// Define explode method BEFORE calling it
self.explode = function () {
// Visual explosion effect - blue circle of 150 units radius
var explosionCircle = LK.getAsset('dischargeExplosion', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
// Scale to make it 150 units radius (250 * 1.2 / 2 = 150)
scaleY: 1.2
});
explosionCircle.x = self.targetX;
explosionCircle.y = self.targetY;
explosionCircle.tint = 0x0080FF; // Blue color
explosionCircle.alpha = 0.9;
game.addChild(explosionCircle);
// Animate explosion
tween(explosionCircle, {
alpha: 0,
scaleX: 1.8,
scaleY: 1.8
}, {
duration: 800,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Play explosion sound
LK.getSound('explosion').play();
// Get all possible targets for damage
var allTargets = [];
if (self.isFromPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage and stun to all units within explosion radius of 150 units
var explosionRadius = 150; // 150 units as requested
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead) {
var distance = Math.sqrt(Math.pow(self.targetX - targetUnit.x, 2) + Math.pow(self.targetY - targetUnit.y, 2));
if (distance <= explosionRadius) {
// Check if target is a tower
var isTower = false;
if (self.isFromPlayer) {
isTower = enemyTowers.indexOf(targetUnit) !== -1;
} else {
isTower = playerTowers.indexOf(targetUnit) !== -1;
}
// Apply damage - 50% less to towers
var damageToApply = isTower ? self.damage * 0.5 : self.damage;
targetUnit.takeDamage(damageToApply);
// Apply stun effect (immobilize for 1 second)
targetUnit.stunTimer = self.stunDuration;
targetUnit.isStunned = true;
// Visual effect for stunned units (blue tint)
tween(targetUnit, {
tint: 0x0080FF
}, {
duration: 1000,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
// Destroy discharge immediately after explosion
self.destroy();
};
// Create instant explosion effect - NOW we can call explode since it's defined
self.explode();
return self;
});
var Fireball = Container.expand(function (targetX, targetY, isFromPlayer) {
var self = Container.call(this);
self.targetX = targetX;
self.targetY = targetY;
self.isFromPlayer = isFromPlayer;
self.speed = 13;
self.damage = 415;
self.explosionRadius = 150;
// Start from king tower position
if (isFromPlayer) {
self.x = 1024; // Player king tower x
self.y = 2600; // Player king tower y
} else {
self.x = 1024; // Enemy king tower x
self.y = 200; // Enemy king tower y
}
var fireballGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
fireballGraphics.tint = 0xFF6600;
self.update = function () {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 30) {
// Explode at target location
self.explode();
self.destroy();
return;
}
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
};
self.explode = function () {
// Visual explosion effect
var explosionCircle = LK.getAsset('fireballExplosion', {
anchorX: 0.5,
anchorY: 0.5
});
explosionCircle.x = self.targetX;
explosionCircle.y = self.targetY;
explosionCircle.tint = 0xFF3300;
explosionCircle.alpha = 0.8;
game.addChild(explosionCircle);
// Animate explosion
tween(explosionCircle, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Play explosion sound
LK.getSound('explosion').play();
// Get all possible targets for damage
var allTargets = [];
if (self.isFromPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage and knockback to all units within explosion radius
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead) {
var distance = Math.sqrt(Math.pow(self.targetX - targetUnit.x, 2) + Math.pow(self.targetY - targetUnit.y, 2));
if (distance <= self.explosionRadius) {
// Check if target is a tower
var isTower = false;
if (self.isFromPlayer) {
isTower = enemyTowers.indexOf(targetUnit) !== -1;
} else {
isTower = playerTowers.indexOf(targetUnit) !== -1;
}
// Apply damage - 50% less to towers
var damageToApply = isTower ? self.damage * 0.5 : self.damage;
targetUnit.takeDamage(damageToApply);
// Apply knockback only to non-tower units
if (!isTower) {
var knockbackDistance = 100;
var knockbackDx = targetUnit.x - self.targetX;
var knockbackDy = targetUnit.y - self.targetY;
var knockbackLength = Math.sqrt(knockbackDx * knockbackDx + knockbackDy * knockbackDy);
if (knockbackLength > 0) {
var normalizedDx = knockbackDx / knockbackLength;
var normalizedDy = knockbackDy / knockbackLength;
var newX = targetUnit.x + normalizedDx * knockbackDistance;
var newY = targetUnit.y + normalizedDy * knockbackDistance;
// Animate knockback
tween(targetUnit, {
x: newX,
y: newY
}, {
duration: 300,
easing: tween.easeOut
});
}
}
// Flash effect on hit units
tween(targetUnit, {
tint: 0xFF6600
}, {
duration: 300,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
};
return self;
});
var Golem = Container.expand(function (isPlayer, isMini) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'golem';
self.isMini = isMini || false;
self.isAerial = false; // Ground unit
self.maxHealth = self.isMini ? 1822 : 4455; // 10% less health for golem (4455), mini-golem accordingly (1822)
self.currentHealth = self.maxHealth;
self.damage = self.isMini ? 100 : 200; // 50% less damage for mini-golems
self.attackSpeed = 115; // Every 115 ticks
self.range = 500; // Detection range
self.attackRange = 30; // Melee attack range (reduced from 150 to 30)
self.speed = 0.65;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
self.hasExploded = false; // Prevent multiple explosions
// Spawn delay properties
self.spawnDelay = 240; // 4 seconds (60 * 4 = 240 ticks)
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var golemGraphics = self.attachAsset('golem', {
anchorX: 0.5,
anchorY: 0.5
});
// Scale mini-golems smaller (50% as requested)
if (self.isMini) {
golemGraphics.scaleX = 0.5;
golemGraphics.scaleY = 0.5;
}
// Health bar background
var healthBarScale = self.isMini ? 0.3 : 0.4;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -60
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -60
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 35,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 40;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
// Explode immediately when golem dies
if (!self.hasExploded) {
self.explodeOnDeath();
}
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Golem only attacks structures and towers (wincondition behavior)
// First check for structures (cannons, ballesta, lapida) within detection range
var targetStructures = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetStructures.length; i++) {
if (!targetStructures[i].isDead && (targetStructures[i].unitType === 'cannon' || targetStructures[i].unitType === 'ballesta' || targetStructures[i].unitType === 'lapida')) {
var distance = Math.sqrt(Math.pow(self.x - targetStructures[i].x, 2) + Math.pow(self.y - targetStructures[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetStructures[i];
}
}
}
// If no structures found, target closest tower
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Melee attack - direct damage
self.target.takeDamage(self.damage);
// Visual effect for melee strike
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Flash target when hit
tween(self.target, {
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(self.target, {
tint: 0xFFFFFF
}, {
duration: 100
});
}
});
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.explodeOnDeath = function () {
if (self.hasExploded) {
return; // Prevent multiple explosions
}
self.hasExploded = true;
// Calculate explosion parameters based on mini-golem status
var explosionRadius = self.isMini ? 85 : 170; // 50% less radius for mini-golems, reduced by 15% (170 vs 85)
var explosionDamage = self.isMini ? 50 : 100; // 50% less damage for mini-golems (50 vs 100)
var explosionScale = self.isMini ? 1.0 : 2.0; // Scale visual effect accordingly
// Visual explosion effect using golemExplosion asset
var explosionCircle = LK.getAsset('golemExplosion', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: explosionScale,
scaleY: explosionScale
});
explosionCircle.x = self.x;
explosionCircle.y = self.y;
explosionCircle.tint = 0x8B4513; // Brown color for golem explosion
explosionCircle.alpha = 0.8;
game.addChild(explosionCircle);
// Animate explosion
tween(explosionCircle, {
alpha: 0,
scaleX: explosionScale * 1.5,
scaleY: explosionScale * 1.5
}, {
duration: 1000,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Play explosion sound
LK.getSound('explosion').play();
// Get all possible targets for damage
var allTargets = [];
if (self.isPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage to all units within explosion radius
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetUnit.x, 2) + Math.pow(self.y - targetUnit.y, 2));
if (distance <= explosionRadius) {
targetUnit.takeDamage(explosionDamage);
// Flash effect on hit units
tween(targetUnit, {
tint: 0x8B4513
}, {
duration: 300,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
// Generate 2 mini-golems if this is not already a mini-golem
if (!self.isMini) {
for (var m = 0; m < 2; m++) {
var miniGolem = new Golem(self.isPlayer, true); // Create mini-golem
miniGolem.x = self.x + (m === 0 ? -80 : 80); // Position left and right
miniGolem.y = self.y;
// Remove spawn delay for generated mini-golems
miniGolem.spawnTimer = 0;
miniGolem.isSpawning = false;
miniGolem.spawnTimerText.setText('');
// Add mini-golem to appropriate unit array
if (self.isPlayer) {
playerUnits.push(miniGolem);
} else {
enemyUnits.push(miniGolem);
}
game.addChild(miniGolem);
}
// Play deploy sound for mini-golem generation
LK.getSound('deploy').play();
}
};
self.update = function () {
if (self.isDead) {
// Explode on death if hasn't exploded yet
if (!self.hasExploded) {
self.explodeOnDeath();
}
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Hielo = Container.expand(function (targetX, targetY, isFromPlayer) {
var self = Container.call(this);
self.targetX = targetX;
self.targetY = targetY;
self.isFromPlayer = isFromPlayer;
self.damage = 50;
self.freezeDuration = 300; // 5 seconds freeze (60 ticks * 5 = 300)
self.freezeRadius = 400; // 400 units radius as requested
// Hielo appears instantly at target location
self.x = targetX;
self.y = targetY;
// Define explode method BEFORE calling it
self.explode = function () {
// Visual explosion effect - blue circle of 400 units radius
var explosionCircle = LK.getAsset('dischargeExplosion', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3.2,
// Scale to make it 400 units radius (250 * 3.2 / 2 = 400)
scaleY: 3.2
});
explosionCircle.x = self.targetX;
explosionCircle.y = self.targetY;
explosionCircle.tint = 0x0080FF; // Blue color as requested
explosionCircle.alpha = 0.7;
game.addChild(explosionCircle);
// Keep circle static for duration, then destroy after freeze effect ends
LK.setTimeout(function () {
explosionCircle.destroy();
}, 5000); // Destroy after 5 seconds (freeze duration)
// Play explosion sound
LK.getSound('explosion').play();
// Get all possible targets for damage and freeze
var allTargets = [];
if (self.isFromPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage and freeze to all units within explosion radius of 400 units
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead) {
var distance = Math.sqrt(Math.pow(self.targetX - targetUnit.x, 2) + Math.pow(self.targetY - targetUnit.y, 2));
if (distance <= self.freezeRadius) {
// Apply 50 damage
targetUnit.takeDamage(self.damage);
// Apply freeze effect (immobilize for 5 seconds) - works on towers too
targetUnit.stunTimer = self.freezeDuration;
targetUnit.isStunned = true;
// Visual effect for frozen units and towers (blue tint)
tween(targetUnit, {
tint: 0x0080FF
}, {
duration: 5000,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
// Destroy hielo immediately after explosion
self.destroy();
};
// Create instant explosion effect - NOW we can call explode since it's defined
self.explode();
return self;
});
var Lapida = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'lapida';
self.maxHealth = 700;
self.currentHealth = 700;
self.isDead = false;
self.isStructure = true; // Mark as structure
// Skeleton generation properties
self.skeletonGenerationTime = 60; // 1 second generation time
self.skeletonProductionTime = 300; // 5 seconds to produce skeletons
self.skeletonTimer = 0; // Start immediately for first generation
self.isGenerating = false;
// Health decay properties - loses 3% per second
self.healthDecayRate = 20; // Every 20 ticks = 3 times per second = 1% each time = 3% per second
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var lapidaGraphics = self.attachAsset('lapida', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.3;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
// Generation progress text removed - no longer displayed
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.generateSkeletons = function () {
// Generate 2 skeletons at positions around the lapida
var skeletonPositions = [{
x: self.x - 50,
y: self.y
},
// Left
{
x: self.x + 50,
y: self.y
} // Right
];
for (var i = 0; i < skeletonPositions.length; i++) {
var pos = skeletonPositions[i];
var skeleton = new Skeleton(self.isPlayer);
skeleton.x = pos.x;
skeleton.y = pos.y;
// Remove spawn delay for Lapida-generated skeletons so they can move and attack immediately
skeleton.spawnTimer = 0;
skeleton.isSpawning = false;
skeleton.spawnTimerText.setText('');
// Add skeleton to appropriate unit array
if (self.isPlayer) {
playerUnits.push(skeleton);
} else {
enemyUnits.push(skeleton);
}
game.addChild(skeleton);
}
// Play deploy sound for skeleton generation
LK.getSound('deploy').play();
};
// Method called when lapida is destroyed - generates 4 skeletons
self.onDestroy = function () {
// Generate 4 skeletons at positions around the lapida when destroyed
var destructionSkeletonPositions = [{
x: self.x,
y: self.y - 75
},
// Up
{
x: self.x,
y: self.y + 75
},
// Down
{
x: self.x - 75,
y: self.y
},
// Left
{
x: self.x + 75,
y: self.y
} // Right
];
for (var i = 0; i < destructionSkeletonPositions.length; i++) {
var pos = destructionSkeletonPositions[i];
var skeleton = new Skeleton(self.isPlayer);
skeleton.x = pos.x;
skeleton.y = pos.y;
// Remove spawn delay for destruction-generated skeletons
skeleton.spawnTimer = 0;
skeleton.isSpawning = false;
skeleton.spawnTimerText.setText('');
// Add skeleton to appropriate unit array
if (self.isPlayer) {
playerUnits.push(skeleton);
} else {
enemyUnits.push(skeleton);
}
game.addChild(skeleton);
}
// Play deploy sound for skeleton generation from destruction
LK.getSound('deploy').play();
};
self.update = function () {
if (self.isDead) {
// Call onDestroy when lapida dies - regardless of how it died
if (!self.hasBeenDestroyed) {
self.hasBeenDestroyed = true;
self.onDestroy();
}
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
// Generate first 2 skeletons immediately after spawn delay ends
self.generateSkeletons();
self.skeletonTimer = self.skeletonProductionTime; // Start 6 second timer for next generation
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't do other actions while spawning
}
// Handle stun effect (structures can be stunned too)
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
// Don't generate skeletons while stunned, but still decay health
}
// Handle health decay - loses 3% of total health per second
if (LK.ticks % self.healthDecayRate === 0) {
self.currentHealth -= self.maxHealth * 0.01;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar without flash effect for natural decay
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
}
// Handle skeleton generation (only if not stunned)
if (!self.isStunned) {
if (self.skeletonTimer > 0) {
self.skeletonTimer--;
if (self.skeletonTimer <= 0) {
// Generate 2 skeletons
self.generateSkeletons();
// Reset timer for next generation (5 seconds)
self.skeletonTimer = self.skeletonProductionTime;
}
}
}
};
return self;
});
var MaquinaVoladora = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'maquinaVoladora';
self.isAerial = true; // Mark as aerial unit
self.maxHealth = 500;
self.currentHealth = 500;
self.damage = 85;
self.attackSpeed = 90; // 90 ticks
self.range = 500; // Detection range
self.attackRange = 475; // Attack range
self.speed = 1.5;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var maquinaGraphics = self.attachAsset('flying_machine', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.2;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Create purple bullet
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullet.children[0].tint = 0x9C27B0; // Purple color
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Megaesbirro = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'megaesbirro';
self.isAerial = true; // Mark as aerial unit
self.maxHealth = 650; // 500 + 150
self.currentHealth = 650;
self.damage = 135; // 85 + 50
self.attackSpeed = 90; // 90 ticks
self.range = 500; // Detection range
self.attackRange = 150; // Attack range
self.speed = 1.75;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var megaesbirroGraphics = self.attachAsset('megaesbirro', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.2;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Create purple bullet
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullet.children[0].tint = 0x9C27B0; // Purple color
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Minero = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'minero';
self.isAerial = false; // Ground unit
self.maxHealth = 600;
self.currentHealth = 600;
self.damage = 200;
self.attackSpeed = 90; // 90 ticks
self.range = 450; // Detection range
self.attackRange = 75; // Attack range
self.speed = 1.25;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Tunnel travel properties
self.isTunneling = false;
self.tunnelStartX = 0;
self.tunnelStartY = 0;
self.tunnelTargetX = 0;
self.tunnelTargetY = 0;
self.tunnelCurrentX = 0;
self.tunnelCurrentY = 0;
self.tunnelTimer = 0;
self.tunnelDuration = 120; // 2 seconds (120 ticks at 60fps)
self.trailSegments = [];
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var mineroGraphics = self.attachAsset('minero', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.25;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first (only ground units)
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Skip minero if it's tunneling
if (targetUnits[i].unitType === 'minero' && targetUnits[i].isTunneling) {
continue;
}
// Minero can only attack ground units
if (targetUnits[i].isAerial) {
continue;
}
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Calculate damage - fixed 35 damage against towers, full 200 against units
var damageToApply = self.damage;
var isTower = false;
if (self.isPlayer) {
isTower = enemyTowers.indexOf(self.target) !== -1;
} else {
isTower = playerTowers.indexOf(self.target) !== -1;
}
if (isTower) {
damageToApply = 35; // Fixed 35 damage to towers
} else {
damageToApply = self.damage; // Full 200 damage to units
}
// Melee attack - direct damage
self.target.takeDamage(damageToApply);
// Visual effect for melee strike
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Flash target when hit
tween(self.target, {
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(self.target, {
tint: 0xFFFFFF
}, {
duration: 100
});
}
});
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.startTunneling = function (targetX, targetY) {
self.isTunneling = true;
// Set tunnel origin to king tower position
if (self.isPlayer) {
self.tunnelStartX = 1024; // Player king tower x
self.tunnelStartY = 2600; // Player king tower y
} else {
self.tunnelStartX = 1024; // Enemy king tower x
self.tunnelStartY = 200; // Enemy king tower y
}
self.tunnelTargetX = targetX;
self.tunnelTargetY = targetY;
self.tunnelTimer = self.tunnelDuration;
self.trailSegments = [];
self.tunnelCurrentX = self.tunnelStartX;
self.tunnelCurrentY = self.tunnelStartY;
// Hide minero during tunneling
self.alpha = 0;
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle tunneling
if (self.isTunneling) {
// Calculate tunnel progress
var dx = self.tunnelTargetX - self.tunnelStartX;
var dy = self.tunnelTargetY - self.tunnelStartY;
var totalDistance = Math.sqrt(dx * dx + dy * dy);
var tunnelSpeed = 8; // Speed of tunnel creation increased to 8
var moveX = dx / totalDistance * tunnelSpeed;
var moveY = dy / totalDistance * tunnelSpeed;
// Update tunnel current position
self.tunnelCurrentX += moveX;
self.tunnelCurrentY += moveY;
// Create trail segment every 50 units traveled
var currentDistance = Math.sqrt(Math.pow(self.tunnelCurrentX - self.tunnelStartX, 2) + Math.pow(self.tunnelCurrentY - self.tunnelStartY, 2));
if (Math.floor(currentDistance / 50) > self.trailSegments.length) {
// Create new vertical trail segment
var trailSegment = LK.getAsset('tunnelTrail', {
anchorX: 0.5,
anchorY: 0.5
});
trailSegment.x = self.tunnelCurrentX;
trailSegment.y = self.tunnelCurrentY;
trailSegment.tint = 0x8B4513; // Brown color for dirt trail
trailSegment.alpha = 0.7;
game.addChild(trailSegment);
self.trailSegments.push(trailSegment);
// Schedule trail segment destruction after 3 seconds
LK.setTimeout(function () {
if (trailSegment && trailSegment.destroy) {
trailSegment.destroy();
}
}, 3000);
}
// Check if tunnel reached target
var distanceToTarget = Math.sqrt(Math.pow(self.tunnelTargetX - self.tunnelCurrentX, 2) + Math.pow(self.tunnelTargetY - self.tunnelCurrentY, 2));
if (distanceToTarget < 30) {
// Tunneling complete - now minero appears at target location with 1 second spawn delay
self.x = self.tunnelTargetX;
self.y = self.tunnelTargetY;
self.isTunneling = false;
// Start 1 second spawn delay now that tunnel is complete
self.isSpawning = true;
self.spawnTimer = 60; // 1 second delay
self.alpha = 1; // Make visible but still spawning
}
return; // Don't do anything else while tunneling
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
// Spawn delay complete - minero can now function normally
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Minion = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'minion';
self.isAerial = true; // Mark as aerial unit
self.maxHealth = 400;
self.currentHealth = 400;
self.damage = 85;
self.attackSpeed = 90; // 90 ticks
self.range = 500; // Detection range
self.attackRange = 150; // Attack range
self.speed = 1.75;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var minionGraphics = self.attachAsset('minion', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.2;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Create purple bullet
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullet.children[0].tint = 0x9C27B0; // Purple color
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Montapuercos = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'montapuercos';
self.isAerial = false; // Ground unit
self.maxHealth = 935; // 10% increase from 850
self.currentHealth = 935;
self.damage = 165; // 10% increase from 150
self.attackSpeed = 120; // 120 ticks
self.range = 60; // Melee range
self.speed = 2.25;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var montapuercosGraphics = self.attachAsset('montapuercos', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.25;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Montapuercos only attacks structures and towers (wincondition behavior)
// First check for structures (cannons, ballesta, lapida) within detection range
var targetStructures = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetStructures.length; i++) {
if (!targetStructures[i].isDead && (targetStructures[i].unitType === 'cannon' || targetStructures[i].unitType === 'ballesta' || targetStructures[i].unitType === 'lapida')) {
var distance = Math.sqrt(Math.pow(self.x - targetStructures[i].x, 2) + Math.pow(self.y - targetStructures[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetStructures[i];
}
}
}
// If no structures found, target closest tower
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.range) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.range && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Melee attack - direct damage
self.target.takeDamage(self.damage);
// Visual effect for melee strike
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Flash target red when hit
tween(self.target, {
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(self.target, {
tint: 0xFFFFFF
}, {
duration: 100
});
}
});
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Rompemuros = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'rompemuros';
self.isAerial = false; // Ground unit
self.maxHealth = 200;
self.currentHealth = 200;
self.damage = 275;
self.attackSpeed = 60; // Attack speed doesn't matter much since it dies after attacking
self.range = 400; // Detection range
self.attackRange = 200; // Attack range
self.speed = 2.75;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
self.hasExploded = false; // Prevent multiple explosions
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var rompemuroGraphics = self.attachAsset('rompemuros', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.25;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Rompemuros targets structures and towers (wincondition behavior)
// First check for structures (cannons, ballesta, lapida) within detection range
var targetStructures = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetStructures.length; i++) {
if (!targetStructures[i].isDead && (targetStructures[i].unitType === 'cannon' || targetStructures[i].unitType === 'ballesta' || targetStructures[i].unitType === 'lapida')) {
var distance = Math.sqrt(Math.pow(self.x - targetStructures[i].x, 2) + Math.pow(self.y - targetStructures[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetStructures[i];
}
}
}
// If no structures found, target closest tower
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Apply damage to target
self.target.takeDamage(self.damage);
// Kamikaze explosion - rompemuros dies after attacking
if (!self.hasExploded) {
self.explodeOnAttack();
}
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.explodeOnAttack = function () {
if (self.hasExploded) {
return; // Prevent multiple explosions
}
self.hasExploded = true;
// Visual explosion effect using golemExplosion asset
var explosionCircle = LK.getAsset('golemExplosion', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
explosionCircle.x = self.x;
explosionCircle.y = self.y;
explosionCircle.tint = 0xFF6600; // Orange color for rompemuros explosion
explosionCircle.alpha = 0.8;
game.addChild(explosionCircle);
// Animate explosion
tween(explosionCircle, {
alpha: 0,
scaleX: 2.0,
scaleY: 2.0
}, {
duration: 800,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Play explosion sound
LK.getSound('explosion').play();
// Rompemuros dies after exploding
self.isDead = true;
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Tower = Container.expand(function (isPlayer, isKing) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.isKing = isKing;
self.maxHealth = isKing ? 3700 : 2400;
self.currentHealth = self.maxHealth;
self.isDead = false;
self.range = isKing ? 800 : 900;
self.damage = 75;
self.attackSpeed = 67; // 50% slower than 45 ticks (45 * 1.5 = 67.5, rounded to 67)
self.lastAttackTime = 0;
// Initialize stun properties for freeze compatibility
self.stunTimer = 0;
self.isStunned = false;
var towerGraphics = self.attachAsset(isKing ? 'kingTower' : 'tower', {
anchorX: 0.5,
anchorY: 1
});
towerGraphics.tint = self.isPlayer ? 0x4CAF50 : 0xF44336;
// Health display
var healthText = new Text2(self.currentHealth.toString(), {
size: 40,
fill: 0xFFFFFF
});
healthText.anchor.set(0.5, 0.5);
healthText.y = -75;
self.addChild(healthText);
self.healthText = healthText;
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
towersDestroyed++;
LK.effects.flashObject(self, 0xFF0000, 1000);
LK.getSound('towerDestroy').play();
if (self.isPlayer) {
enemyTowersDestroyed++;
} else {
playerTowersDestroyed++;
}
}
self.healthText.setText(self.currentHealth.toString());
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.attack = function () {
if (self.isDead) {
return;
}
// Handle stun effect - towers can't attack while stunned/frozen
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't attack while stunned/frozen
}
// King towers cannot attack until at least one Princess tower is destroyed OR king tower has taken damage
if (self.isKing) {
var teamTowers = self.isPlayer ? playerTowers : enemyTowers;
var princessTowersAlive = 0;
for (var i = 0; i < teamTowers.length; i++) {
if (!teamTowers[i].isKing && !teamTowers[i].isDead) {
princessTowersAlive++;
}
}
// King tower can attack if either: 1) Princess tower destroyed OR 2) King tower has taken damage
var kingTowerHasTakenDamage = self.currentHealth < self.maxHealth;
if (princessTowersAlive === 2 && !kingTowerHasTakenDamage) {
return; // King tower cannot attack while both Princess towers are alive AND king tower is undamaged
}
}
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
var closestTarget = null;
var closestDistance = Infinity;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Skip minero if it's tunneling
if (targetUnits[i].unitType === 'minero' && targetUnits[i].isTunneling) {
continue;
}
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
if (closestTarget && LK.ticks - self.lastAttackTime > self.attackSpeed) {
var bullet = new Bullet(self.x, self.y - 50, closestTarget, self.damage, self.isPlayer);
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
self.attack();
};
return self;
});
var Unit = Container.expand(function (isPlayer, unitType, health, damage, attackSpeed, range, speed) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = unitType;
self.maxHealth = health;
self.currentHealth = health;
self.damage = damage;
self.attackSpeed = attackSpeed;
self.range = range;
self.speed = speed;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false; // Track if unit has started attacking current target
// Spawn delay properties
self.spawnDelay = unitType === 'giant' ? 180 : 60; // 3 seconds for giant, 1 second for others (60 ticks = 1 second)
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var unitGraphics = self.attachAsset(unitType, {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = unitType === 'giant' ? 0.4 : 0.2;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false; // Reset attacking state when target dies
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return; // Keep current target while attacking
}
var closestDistance = Infinity;
var closestTarget = null;
// Giant (wincondition unit) targets structures within detection range, then towers
if (self.unitType === 'giant') {
// First check for structures (cannons, ballesta, lapida) within detection range (538)
var targetStructures = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetStructures.length; i++) {
if (!targetStructures[i].isDead && (targetStructures[i].unitType === 'cannon' || targetStructures[i].unitType === 'ballesta' || targetStructures[i].unitType === 'lapida')) {
var distance = Math.sqrt(Math.pow(self.x - targetStructures[i].x, 2) + Math.pow(self.y - targetStructures[i].y, 2));
if (distance <= 538 && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetStructures[i];
}
}
}
// If no structures in detection range, target closest tower
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
} else {
// Normal units: First priority - Find closest enemy units and structures (they are more immediate threats)
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Skip aerial units if this unit cannot attack air (archers, wizards, and towers can attack air)
if (targetUnits[i].isAerial && self.unitType !== 'archer' && self.unitType !== 'wizard') {
continue;
}
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Second priority: Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
return;
}
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Use proper attack range for movement positioning
var attackRange = self.attackRange || (self.unitType === 'knight' ? 168 : self.unitType === 'giant' ? 60 : self.range);
if (distance > attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
// Use different ranges for detection vs attack
var attackRange = self.attackRange || (self.unitType === 'knight' ? 168 : self.unitType === 'giant' ? 60 : self.range); // Use attackRange property if defined, otherwise use unit-specific ranges
if (distance <= attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
// Set attacking flag when first attack begins
self.isAttacking = true;
// Knight, Giant, and Skeleton use melee attack - direct damage without bullet
if (self.unitType === 'knight' || self.unitType === 'giant' || self.unitType === 'skeleton') {
self.target.takeDamage(self.damage);
// Visual effect for melee strike
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Flash target red when hit by Giant
if (self.unitType === 'giant') {
tween(self.target, {
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(self.target, {
tint: 0xFFFFFF
}, {
duration: 100
});
}
});
}
// Knight and Skeleton no longer apply knockback to units
} else {
// Other units (like archers, wizard) still use bullets
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullets.push(bullet);
game.addChild(bullet);
}
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
// Always find targets every frame to ensure units never ignore enemies
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Wizard = Unit.expand(function (isPlayer) {
var self = Unit.call(this, isPlayer, 'wizard', 550, 140, 100, 530, 1.25);
// Override the attack function to use area damage
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
// Wizard uses ranged attack with area damage projectile (use detection range for attacking)
if (distance <= self.range && LK.ticks - self.lastAttackTime > self.attackSpeed) {
// Set attacking flag when first attack begins
self.isAttacking = true;
// Create area damage bullet with 100 unit area radius
var areaBullet = new AreaDamageBullet(self.x, self.y, self.target, self.damage, self.isPlayer, 100);
bullets.push(areaBullet);
game.addChild(areaBullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
return self;
});
var Skeleton = Unit.expand(function (isPlayer) {
var self = Unit.call(this, isPlayer, 'skeleton', 25, 45, 60, 500, 2);
// Skeleton: 25 HP, 45 damage, 60 tick attack speed, 500 detection range, speed 2
// Attacks ground units and structures only (same targeting as other units)
// Override attack range to be 150 instead of detection range
self.attackRange = 150;
return self;
});
var Knight = Unit.expand(function (isPlayer) {
var self = Unit.call(this, isPlayer, 'knight', 825, 160, 60, 538, 1.75);
// Detection range set to 538 (same as archer for consistent detection)
// Attack range remains at melee distance in attack function
return self;
});
var Giant = Unit.expand(function (isPlayer) {
var self = Unit.call(this, isPlayer, 'giant', 4000, 175, 90, 538, 0.85);
// Giant is a wincondition unit that only attacks structures and towers
// Detection range set to 538 (same as archer), attack range remains at 60 in attack function
return self;
});
var Archer = Unit.expand(function (isPlayer) {
var self = Unit.call(this, isPlayer, 'archer', 400, 65, 90, 525, 1.5);
// Range reduced and stats adjusted
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x228B22
});
/****
* Game Code
****/
// Game state variables
/*
CARD AND TOWER STATS:
TOWERS:
- Princess Towers: 2400 HP, 75 damage, attacks every 1.1s (67 ticks), range 800 units
- King Tower: 3700 HP, 75 damage, attacks every 1.1s (67 ticks), range 800 units
* King tower cannot attack until at least one Princess tower is destroyed
CARDS:
- Archers: 3 elixir cost, 425 HP each, 75 damage, attacks every 1.5s (90 ticks), range 567 units, speed 1.5
* Deploys 2 archers, can attack air and ground targets
- Knight: 3 elixir cost, 825 HP, 160 damage, attacks every 1s (60 ticks), range 168 units, speed 2
* Single melee unit, ground targets only
- Giant: 5 elixir cost, 4000 HP, 175 damage, attacks every 1.5s (90 ticks), range 300 units, speed 1
* Wincondition unit that only attacks structures and towers, ground unit
UNIT SPEEDS:
- Knight: speed 2 (medium)
- Archer: speed 1.5 (medium-slow)
- Giant: speed 1 (slow)
ELIXIR SYSTEM:
- Maximum: 10 elixir
- Regeneration: 1 elixir every 2.0 seconds (120 ticks at 60fps)
- Starting amount: 5 elixir
GAME RULES:
- Match duration: 2 minutes (120 seconds)
- Victory: Most towers destroyed wins
- Tiebreaker: Remaining tower health
- Draw: Equal towers destroyed and equal remaining health
*/
var gameStarted = false;
var gameTime = 250; // 250 seconds total duration
var elixir = 5; // Starting elixir
var maxElixir = 10;
var elixirRegenRate = 114; // Ticks for 1.9 seconds at 60fps (1.9 * 60 = 114)
var doubleElixirRegenRate = 57; // Half the time for double speed (1.9 / 2 = 0.95 seconds = 57 ticks)
var lastElixirRegen = 0;
var aiElixir = 5; // AI starting elixir
var aiMaxElixir = 10;
var aiElixirRegenRate = 120; // Ticks for 2 seconds at 60fps
var doubleAiElixirRegenRate = 60; // Half the time for double speed (2 / 2 = 1 second = 60 ticks)
var lastAiElixirRegen = 0;
var doubleElixirActive = false;
var doubleElixirMessageShown = false;
var doubleElixirMessageText = null;
var doubleElixirMessageTimer = 0;
var playerUnits = [];
var enemyUnits = [];
var bullets = [];
var playerTowers = [];
var enemyTowers = [];
var playerTowersDestroyed = 0;
var enemyTowersDestroyed = 0;
var towersDestroyed = 0;
var gameEnded = false;
var aiLastDeploy = 0;
var aiDeployInterval = 240; // 4 seconds
// Card and UI variables
var draggedCard = null;
var menuElements = [];
var deckMenuElements = [];
var deckCards = [];
var archerCard, knightCard, giantCard, wizardCard, cannonCard;
var timerText, elixirText;
var battlefieldLine;
var showingDeckSelection = false;
var selectedDeck = ['archer', 'knight', 'giant', 'wizard', 'cannon']; // Default deck (minimum 5 cards)
var availableCards = ['archer', 'knight', 'giant', 'wizard', 'cannon', 'skeleton', 'skeletonArmy', 'fireball', 'minion', 'maquinaVoladora', 'megaesbirro', 'discharge', 'bruja', 'ballesta', 'montapuercos', 'hielo', 'lapida', 'golem', 'rompemuros', 'minero'];
// Card system variables
var activeCards = []; // Currently displayed 4 cards
var cardQueue = []; // Queue of remaining cards
var maxActiveCards = 4;
// Create main menu
function createMenu() {
// Title
var titleText = new Text2('CLASH ROYALE', {
size: 120,
fill: 0xFFD700
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 1024;
titleText.y = 800;
game.addChild(titleText);
menuElements.push(titleText);
// Play button background
var playButton = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 0.8,
x: 1024,
y: 1400
});
playButton.tint = 0x4CAF50;
game.addChild(playButton);
menuElements.push(playButton);
// Play button text
var playText = new Text2('JUGAR', {
size: 80,
fill: 0xFFFFFF
});
playText.anchor.set(0.5, 0.5);
playText.x = 1024;
playText.y = 1400;
game.addChild(playText);
menuElements.push(playText);
// Mazo button background
var mazoButton = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 0.8,
x: 1024,
y: 1600
});
mazoButton.tint = 0x9C27B0;
game.addChild(mazoButton);
menuElements.push(mazoButton);
// Mazo button text
var mazoText = new Text2('MAZO', {
size: 80,
fill: 0xFFFFFF
});
mazoText.anchor.set(0.5, 0.5);
mazoText.x = 1024;
mazoText.y = 1600;
game.addChild(mazoText);
menuElements.push(mazoText);
// Instructions
var instructionsText = new Text2('Arrastra las cartas al campo de batalla', {
size: 40,
fill: 0xFFFFFF
});
instructionsText.anchor.set(0.5, 0.5);
instructionsText.x = 1024;
instructionsText.y = 1800;
game.addChild(instructionsText);
menuElements.push(instructionsText);
// Make play button clickable
playButton.down = function (x, y, obj) {
startGame();
};
playText.down = function (x, y, obj) {
startGame();
};
// Make mazo button clickable
mazoButton.down = function (x, y, obj) {
showDeckSelection();
};
mazoText.down = function (x, y, obj) {
showDeckSelection();
};
}
function showDeckSelection() {
// Hide main menu
for (var i = 0; i < menuElements.length; i++) {
menuElements[i].alpha = 0.3;
}
showingDeckSelection = true;
createDeckMenu();
}
function createDeckMenu() {
// Title
var deckTitle = new Text2('SELECCIONA TU MAZO', {
size: 80,
fill: 0xFFD700
});
deckTitle.anchor.set(0.5, 0.5);
deckTitle.x = 1024;
deckTitle.y = 600;
game.addChild(deckTitle);
deckMenuElements.push(deckTitle);
// Instructions
var instructions = new Text2('Toca las cartas para añadir/quitar del mazo (min 5, max 12)', {
size: 35,
fill: 0xFFFFFF
});
instructions.anchor.set(0.5, 0.5);
instructions.x = 1024;
instructions.y = 700;
game.addChild(instructions);
deckMenuElements.push(instructions);
// Available cards display
var cardStartX = 300;
var cardSpacing = 350;
for (var i = 0; i < availableCards.length; i++) {
var cardType = availableCards[i];
var cardContainer = new Container();
cardContainer.x = cardStartX + i * cardSpacing;
// Position skeleton card 100px below archer, skeleton army 100px below knight, fireball 100px below giant, minion 100px below wizard, maquina voladora 100px below skeleton, discharge 100px below cannon
if (cardType === 'skeleton') {
var archerIndex = availableCards.indexOf('archer');
cardContainer.x = cardStartX + archerIndex * cardSpacing;
cardContainer.y = 1000; // 100px below archer
} else if (cardType === 'skeletonArmy') {
var knightIndex = availableCards.indexOf('knight');
cardContainer.x = cardStartX + knightIndex * cardSpacing;
cardContainer.y = 1000; // 100px below knight
} else if (cardType === 'fireball') {
var giantIndex = availableCards.indexOf('giant');
cardContainer.x = cardStartX + giantIndex * cardSpacing;
cardContainer.y = 1000; // 100px below giant
} else if (cardType === 'minion') {
var wizardIndex = availableCards.indexOf('wizard');
cardContainer.x = cardStartX + wizardIndex * cardSpacing;
cardContainer.y = 1000; // 100px below wizard
} else if (cardType === 'maquinaVoladora') {
var skeletonIndex = availableCards.indexOf('skeleton');
var archerIndex = availableCards.indexOf('archer');
cardContainer.x = cardStartX + archerIndex * cardSpacing;
cardContainer.y = 1100; // 100px below skeleton (200px total below archer)
} else if (cardType === 'megaesbirro') {
var skeletonArmyIndex = availableCards.indexOf('skeletonArmy');
var knightIndex = availableCards.indexOf('knight');
cardContainer.x = cardStartX + knightIndex * cardSpacing;
cardContainer.y = 1100; // 100px below skeleton army (200px total below knight)
} else if (cardType === 'discharge') {
var cannonIndex = availableCards.indexOf('cannon');
cardContainer.x = cardStartX + cannonIndex * cardSpacing;
cardContainer.y = 1000; // 100px below cannon
} else if (cardType === 'bruja') {
var fireballIndex = availableCards.indexOf('fireball');
cardContainer.x = cardStartX + fireballIndex * cardSpacing;
cardContainer.y = 1100; // 100px below fireball
} else if (cardType === 'ballesta') {
var minionIndex = availableCards.indexOf('minion');
var wizardIndex = availableCards.indexOf('wizard');
cardContainer.x = cardStartX + wizardIndex * cardSpacing;
cardContainer.y = 1100; // 100px below minion (200px total below wizard)
} else if (cardType === 'montapuercos') {
var dischargeIndex = availableCards.indexOf('discharge');
var cannonIndex = availableCards.indexOf('cannon');
cardContainer.x = cardStartX + cannonIndex * cardSpacing;
cardContainer.y = 1100; // 100px below discharge (200px total below cannon)
} else if (cardType === 'hielo') {
var montapuercosIndex = availableCards.indexOf('montapuercos');
var cannonIndex = availableCards.indexOf('cannon');
cardContainer.x = cardStartX + cannonIndex * cardSpacing;
cardContainer.y = 1200; // 100px below montapuercos (300px total below cannon)
} else if (cardType === 'lapida') {
var ballestaIndex = availableCards.indexOf('ballesta');
var wizardIndex = availableCards.indexOf('wizard');
cardContainer.x = cardStartX + wizardIndex * cardSpacing;
cardContainer.y = 1200; // 100px below ballesta (300px total below wizard)
} else if (cardType === 'golem') {
var brujaIndex = availableCards.indexOf('bruja');
var giantIndex = availableCards.indexOf('giant');
cardContainer.x = cardStartX + giantIndex * cardSpacing;
cardContainer.y = 1200; // 100px below bruja (300px total below giant)
} else if (cardType === 'rompemuros') {
var megaesbirroIndex = availableCards.indexOf('megaesbirro');
var knightIndex = availableCards.indexOf('knight');
cardContainer.x = cardStartX + knightIndex * cardSpacing;
cardContainer.y = 1200; // 100px below megaesbirro (300px total below knight)
} else if (cardType === 'minero') {
var maquinaVoladoraIndex = availableCards.indexOf('maquinaVoladora');
var archerIndex = availableCards.indexOf('archer');
cardContainer.x = cardStartX + archerIndex * cardSpacing;
cardContainer.y = 1200; // 100px below maquina voladora (300px total below archer)
} else {
cardContainer.y = 900;
}
// Card background
var cardBg = cardContainer.attachAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5
});
// Check if card is in selected deck
var isSelected = selectedDeck.indexOf(cardType) !== -1;
cardBg.tint = isSelected ? 0x4CAF50 : 0x607d8b;
// Create special cardslot container for bruja below fireball
if (cardType === 'fireball') {
// Create bruja cardslot container
var brujaCardSlot = new Container();
brujaCardSlot.x = 0;
brujaCardSlot.y = 100; // 100px below fireball
// Bruja cardslot background
var brujaCardBg = brujaCardSlot.attachAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5
});
brujaCardBg.tint = selectedDeck.indexOf('bruja') !== -1 ? 0x4CAF50 : 0x607d8b;
// Bruja icon
var brujaIcon = LK.getAsset('bruja', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.6,
scaleY: 0.6
});
brujaCardSlot.addChild(brujaIcon);
// Bruja cost
var brujaCost = new Text2('5', {
size: 25,
fill: 0xFFFFFF
});
brujaCost.anchor.set(0.5, 0.5);
brujaCost.x = 120;
brujaCost.y = -40;
brujaCardSlot.addChild(brujaCost);
// Selection indicator for bruja
if (selectedDeck.indexOf('bruja') !== -1) {
var brujaCheckmark = new Text2('✓', {
size: 40,
fill: 0xFFFFFF
});
brujaCheckmark.anchor.set(0.5, 0.5);
brujaCheckmark.x = 0;
brujaCheckmark.y = 80;
brujaCardSlot.addChild(brujaCheckmark);
}
// Make bruja cardslot clickable
brujaCardSlot.cardType = 'bruja';
brujaCardSlot.down = function (x, y, obj) {
toggleCardInDeck('bruja');
// Return true to stop event propagation to parent containers
return true;
};
cardContainer.addChild(brujaCardSlot);
deckMenuElements.push(brujaCardSlot);
}
// Card icon
var cardIcon = LK.getAsset(cardType === 'discharge' ? 'descarga_descardina' : cardType === 'maquinaVoladora' ? 'flying_machine' : cardType === 'bruja' ? 'bruja' : cardType, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.6,
scaleY: 0.6
});
cardContainer.addChild(cardIcon);
// Card cost
var cost = cardType === 'giant' ? 5 : cardType === 'wizard' ? 5 : cardType === 'bruja' ? 5 : cardType === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : cardType === 'ballesta' ? 6 : cardType === 'montapuercos' ? 4 : cardType === 'hielo' ? 4 : cardType === 'lapida' ? 3 : cardType === 'golem' ? 8 : cardType === 'rompemuros' ? 2 : cardType === 'minero' ? 3 : 3;
var costText = new Text2(cost.toString(), {
size: 25,
fill: 0xFFFFFF
});
costText.anchor.set(0.5, 0.5);
costText.x = 120;
costText.y = -40;
cardContainer.addChild(costText);
// Selection indicator
if (isSelected) {
var checkmark = new Text2('✓', {
size: 40,
fill: 0xFFFFFF
});
checkmark.anchor.set(0.5, 0.5);
checkmark.x = 0;
checkmark.y = 80;
cardContainer.addChild(checkmark);
}
// Make card clickable
cardContainer.cardType = cardType;
cardContainer.down = function (x, y, obj) {
// For fireball, check if click is in the bruja area to prevent double selection
if (this.cardType === 'fireball' && y > 50) {
// Click is in the lower area where bruja cardslot is, ignore it
return true;
}
toggleCardInDeck(this.cardType);
};
game.addChild(cardContainer);
deckMenuElements.push(cardContainer);
}
// Selected deck display
var deckTitle2 = new Text2('MAZO ACTUAL (' + selectedDeck.length + '/12)', {
size: 60,
fill: 0x4CAF50
});
deckTitle2.anchor.set(0.5, 0.5);
deckTitle2.x = 1024;
deckTitle2.y = 1200;
game.addChild(deckTitle2);
deckMenuElements.push(deckTitle2);
// Display selected cards
var selectedStartX = 224;
var selectedSpacing = 150;
for (var i = 0; i < selectedDeck.length && i < 12; i++) {
var cardType = selectedDeck[i];
var selectedCardContainer = new Container();
// Display cards in two rows if more than 8
if (i < 8) {
selectedCardContainer.x = selectedStartX + i * selectedSpacing;
selectedCardContainer.y = 1400;
} else {
selectedCardContainer.x = selectedStartX + (i - 8) * selectedSpacing;
selectedCardContainer.y = 1500;
}
// Small card background
var selectedCardBg = selectedCardContainer.attachAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.6,
scaleY: 0.6
});
selectedCardBg.tint = 0x2196F3;
// Small card icon
var selectedCardIcon = LK.getAsset(cardType === 'discharge' ? 'descarga_descardina' : cardType === 'maquinaVoladora' ? 'flying_machine' : cardType === 'bruja' ? 'bruja' : cardType, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.35,
scaleY: 0.35
});
selectedCardContainer.addChild(selectedCardIcon);
game.addChild(selectedCardContainer);
deckMenuElements.push(selectedCardContainer);
}
// Back button
var backButton = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 0.6,
x: 1024,
y: 1650
});
backButton.tint = 0xFF9800;
game.addChild(backButton);
deckMenuElements.push(backButton);
var backText = new Text2('VOLVER', {
size: 50,
fill: 0xFFFFFF
});
backText.anchor.set(0.5, 0.5);
backText.x = 1024;
backText.y = 1650;
game.addChild(backText);
deckMenuElements.push(backText);
// Make back button clickable
backButton.down = function (x, y, obj) {
hideDeckSelection();
};
backText.down = function (x, y, obj) {
hideDeckSelection();
};
}
function toggleCardInDeck(cardType) {
var cardIndex = selectedDeck.indexOf(cardType);
if (cardIndex !== -1) {
// Only allow removal if deck has more than 5 cards
if (selectedDeck.length > 5) {
selectedDeck.splice(cardIndex, 1);
}
} else if (selectedDeck.length < 12) {
// Add card to deck if not full (max 12 cards)
selectedDeck.push(cardType);
}
// Refresh deck menu
refreshDeckMenu();
}
function refreshDeckMenu() {
// Remove current deck menu elements
for (var i = 0; i < deckMenuElements.length; i++) {
deckMenuElements[i].destroy();
}
deckMenuElements = [];
// Recreate deck menu
createDeckMenu();
}
function hideDeckSelection() {
// Remove deck menu elements
for (var i = 0; i < deckMenuElements.length; i++) {
deckMenuElements[i].destroy();
}
deckMenuElements = [];
// Show main menu again
for (var i = 0; i < menuElements.length; i++) {
menuElements[i].alpha = 1;
}
showingDeckSelection = false;
}
function startGame() {
// Remove menu elements
for (var i = 0; i < menuElements.length; i++) {
menuElements[i].destroy();
}
menuElements = [];
// Start the actual game
gameStarted = true;
initializeGame();
}
function initializeGame() {
// Generate AI's randomized deck for this match
generateAIDeck();
// Create battlefield divider
battlefieldLine = game.addChild(LK.getAsset('battlefield', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
battlefieldLine.tint = 0x000000;
battlefieldLine.alpha = 0.3;
// Create towers
// Player towers (bottom)
var playerLeftTower = game.addChild(new Tower(true, false));
playerLeftTower.x = 512;
playerLeftTower.y = 2400;
playerTowers.push(playerLeftTower);
var playerRightTower = game.addChild(new Tower(true, false));
playerRightTower.x = 1536;
playerRightTower.y = 2400;
playerTowers.push(playerRightTower);
var playerKingTower = game.addChild(new Tower(true, true));
playerKingTower.x = 1024;
playerKingTower.y = 2600;
playerTowers.push(playerKingTower);
// Enemy towers (top)
var enemyLeftTower = game.addChild(new Tower(false, false));
enemyLeftTower.x = 512;
enemyLeftTower.y = 400;
enemyTowers.push(enemyLeftTower);
var enemyRightTower = game.addChild(new Tower(false, false));
enemyRightTower.x = 1536;
enemyRightTower.y = 400;
enemyTowers.push(enemyRightTower);
var enemyKingTower = game.addChild(new Tower(false, true));
enemyKingTower.x = 1024;
enemyKingTower.y = 200;
enemyTowers.push(enemyKingTower);
// UI Elements
timerText = new Text2(gameTime.toString(), {
size: 60,
fill: 0xFFFFFF
});
timerText.anchor.set(0.5, 0);
LK.gui.top.addChild(timerText);
elixirText = new Text2(elixir + "/" + maxElixir, {
size: 40,
fill: 0xE91E63
});
elixirText.anchor.set(0, 1);
elixirText.x = 50;
LK.gui.bottomLeft.addChild(elixirText);
// Initialize card system
activeCards = [];
cardQueue = [];
deckCards = [];
// Setup active cards (first 4) and queue (remaining)
for (var i = 0; i < selectedDeck.length; i++) {
if (i < maxActiveCards) {
activeCards.push(selectedDeck[i]);
} else {
cardQueue.push(selectedDeck[i]);
}
}
// Create visual cards for active cards only
var cardSpacing = 300;
var startX = -(maxActiveCards - 1) * cardSpacing / 2;
for (var i = 0; i < activeCards.length; i++) {
var cardType = activeCards[i];
var newCard = LK.gui.bottom.addChild(new Card(cardType));
newCard.x = startX + i * cardSpacing;
newCard.y = -100;
newCard.cardType = cardType; // Store card type for event handling
newCard.cardIndex = i; // Store index for replacement
deckCards.push(newCard);
}
// Store references for backward compatibility
if (activeCards.indexOf('archer') !== -1) {
archerCard = deckCards[activeCards.indexOf('archer')];
}
if (activeCards.indexOf('knight') !== -1) {
knightCard = deckCards[activeCards.indexOf('knight')];
}
if (activeCards.indexOf('giant') !== -1) {
giantCard = deckCards[activeCards.indexOf('giant')];
}
if (activeCards.indexOf('wizard') !== -1) {
wizardCard = deckCards[activeCards.indexOf('wizard')];
}
if (activeCards.indexOf('cannon') !== -1) {
cannonCard = deckCards[activeCards.indexOf('cannon')];
}
// Setup event handlers
setupEventHandlers();
}
function rotateCard(usedCardIndex) {
if (cardQueue.length === 0) {
return;
} // No cards in queue to rotate
// Get the next card from queue
var nextCard = cardQueue.shift(); // Remove first card from queue
var usedCard = activeCards[usedCardIndex]; // Get the used card
// Add used card to end of queue
cardQueue.push(usedCard);
// Replace active card with next card
activeCards[usedCardIndex] = nextCard;
// Update visual card
var cardToUpdate = deckCards[usedCardIndex];
if (cardToUpdate) {
// Store position and index
var cardX = cardToUpdate.x;
var cardY = cardToUpdate.y;
var cardIndex = cardToUpdate.cardIndex;
// Remove old card
cardToUpdate.destroy();
// Create new card with next card type
var newCard = LK.gui.bottom.addChild(new Card(nextCard));
newCard.x = cardX;
newCard.y = cardY;
newCard.cardType = nextCard;
newCard.cardIndex = cardIndex;
// Update references
deckCards[usedCardIndex] = newCard;
// Update event handler for new card
var cardType = newCard.cardType;
var cost = cardType === 'giant' ? 5 : cardType === 'wizard' ? 5 : cardType === 'bruja' ? 5 : cardType === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : 3;
newCard.down = function (x, y, obj) {
if (elixir >= cost) {
draggedCard = cardType;
}
};
}
}
function deployUnit(cardType, x, y, isPlayer) {
var cost = cardType === 'giant' ? 5 : cardType === 'wizard' ? 5 : cardType === 'bruja' ? 5 : cardType === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : cardType === 'ballesta' ? 6 : cardType === 'montapuercos' ? 4 : cardType === 'hielo' ? 4 : cardType === 'lapida' ? 3 : cardType === 'golem' ? 8 : cardType === 'rompemuros' ? 2 : cardType === 'minero' ? 3 : 3;
if (isPlayer && elixir < cost) {
return false;
}
if (!isPlayer && aiElixir < cost) {
return false;
} // AI also needs elixir
// Check AI card usage restriction
if (!isPlayer && !canAIUseCard(cardType)) {
return false; // AI can't use this card yet
}
// Check if position is in correct half (except for spells and minero)
if (cardType !== 'fireball' && cardType !== 'hielo' && cardType !== 'discharge' && cardType !== 'minero') {
if (isPlayer && y < 1366) {
return false;
} // Player can only deploy in bottom half
if (!isPlayer && y > 1366) {
return false;
} // AI can only deploy in top half
}
var unit = null;
if (cardType === 'archer') {
// Deploy 2 archers
var archer1 = new Archer(isPlayer);
var archer2 = new Archer(isPlayer);
archer1.x = x - 40; // Left archer
archer1.y = y;
archer2.x = x + 40; // Right archer
archer2.y = y;
if (isPlayer) {
playerUnits.push(archer1);
playerUnits.push(archer2);
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
enemyUnits.push(archer1);
enemyUnits.push(archer2);
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
game.addChild(archer1);
game.addChild(archer2);
LK.getSound('deploy').play();
return true;
} else if (cardType === 'knight') {
unit = new Knight(isPlayer);
} else if (cardType === 'giant') {
unit = new Giant(isPlayer);
} else if (cardType === 'wizard') {
unit = new Wizard(isPlayer);
} else if (cardType === 'skeleton') {
// Deploy 3 skeletons in formation: front, back-right, back-left
var skeleton1 = new Skeleton(isPlayer); // Front skeleton
var skeleton2 = new Skeleton(isPlayer); // Back-right skeleton
var skeleton3 = new Skeleton(isPlayer); // Back-left skeleton
skeleton1.x = x; // Front skeleton at deployment position
skeleton1.y = y;
skeleton2.x = x + 60; // Back-right skeleton
skeleton2.y = isPlayer ? y + 60 : y - 60; // Adjust based on player side
skeleton3.x = x - 60; // Back-left skeleton
skeleton3.y = isPlayer ? y + 60 : y - 60; // Adjust based on player side
if (isPlayer) {
playerUnits.push(skeleton1);
playerUnits.push(skeleton2);
playerUnits.push(skeleton3);
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
enemyUnits.push(skeleton1);
enemyUnits.push(skeleton2);
enemyUnits.push(skeleton3);
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
game.addChild(skeleton1);
game.addChild(skeleton2);
game.addChild(skeleton3);
LK.getSound('deploy').play();
return true;
} else if (cardType === 'skeletonArmy') {
// Deploy 15 skeletons in a 3x5 formation
var skeletons = [];
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 5; col++) {
var skeleton = new Skeleton(isPlayer);
skeleton.x = x + (col - 2) * 40; // Spread horizontally (adjusted for 5 columns)
skeleton.y = y + (row - 1) * 40; // Spread vertically
skeletons.push(skeleton);
game.addChild(skeleton);
}
}
if (isPlayer) {
for (var s = 0; s < skeletons.length; s++) {
playerUnits.push(skeletons[s]);
}
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
for (var s = 0; s < skeletons.length; s++) {
enemyUnits.push(skeletons[s]);
}
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
LK.getSound('deploy').play();
return true;
} else if (cardType === 'fireball') {
// Fireball is a spell - can be deployed anywhere on the map
var fireball = new Fireball(x, y, isPlayer);
bullets.push(fireball); // Add to bullets array for cleanup
game.addChild(fireball);
if (isPlayer) {
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
LK.getSound('deploy').play();
return true;
} else if (cardType === 'discharge') {
// Discharge is a spell - can be deployed anywhere on the map, appears instantly
var discharge = new Discharge(x, y, isPlayer);
// Don't add to bullets array since it destroys itself immediately
game.addChild(discharge);
if (isPlayer) {
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
LK.getSound('deploy').play();
return true;
} else if (cardType === 'minion') {
// Deploy 3 minions in formation like skeletons: front, back-right, back-left
var minion1 = new Minion(isPlayer); // Front minion
var minion2 = new Minion(isPlayer); // Back-right minion
var minion3 = new Minion(isPlayer); // Back-left minion
minion1.x = x; // Front minion at deployment position
minion1.y = y;
minion2.x = x + 60; // Back-right minion
minion2.y = isPlayer ? y + 60 : y - 60; // Adjust based on player side
minion3.x = x - 60; // Back-left minion
minion3.y = isPlayer ? y + 60 : y - 60; // Adjust based on player side
if (isPlayer) {
playerUnits.push(minion1);
playerUnits.push(minion2);
playerUnits.push(minion3);
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
enemyUnits.push(minion1);
enemyUnits.push(minion2);
enemyUnits.push(minion3);
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
game.addChild(minion1);
game.addChild(minion2);
game.addChild(minion3);
LK.getSound('deploy').play();
return true;
} else if (cardType === 'maquinaVoladora') {
// Deploy single maquina voladora
unit = new MaquinaVoladora(isPlayer);
} else if (cardType === 'megaesbirro') {
// Deploy single megaesbirro
unit = new Megaesbirro(isPlayer);
} else if (cardType === 'bruja') {
// Deploy single bruja
unit = new Bruja(isPlayer);
} else if (cardType === 'ballesta') {
unit = new Ballesta(isPlayer);
} else if (cardType === 'montapuercos') {
unit = new Montapuercos(isPlayer);
} else if (cardType === 'lapida') {
unit = new Lapida(isPlayer);
} else if (cardType === 'golem') {
unit = new Golem(isPlayer, false); // false for regular golem (not mini)
} else if (cardType === 'rompemuros') {
// Deploy 2 rompemuros units - one left, one right
var rompemuros1 = new Rompemuros(isPlayer);
var rompemuros2 = new Rompemuros(isPlayer);
rompemuros1.x = x - 40; // Left rompemuros
rompemuros1.y = y;
rompemuros2.x = x + 40; // Right rompemuros
rompemuros2.y = y;
if (isPlayer) {
playerUnits.push(rompemuros1);
playerUnits.push(rompemuros2);
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
enemyUnits.push(rompemuros1);
enemyUnits.push(rompemuros2);
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
game.addChild(rompemuros1);
game.addChild(rompemuros2);
LK.getSound('deploy').play();
return true;
} else if (cardType === 'hielo') {
// Hielo is a spell - can be deployed anywhere on the map, appears instantly
var hielo = new Hielo(x, y, isPlayer);
// Don't add to bullets array since it destroys itself immediately
game.addChild(hielo);
if (isPlayer) {
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
LK.getSound('deploy').play();
return true;
} else if (cardType === 'minero') {
unit = new Minero(isPlayer);
// Special placement logic for minero - can be placed anywhere but needs tunneling
unit.tunnelTargetX = x;
unit.tunnelTargetY = y;
// Don't set initial position here - let minero handle its own tunneling logic
// Remove spawn delay for minero since tunneling acts as the delay
unit.spawnTimer = 0;
unit.isSpawning = false;
unit.spawnTimerText.setText('');
// Start tunneling immediately
unit.startTunneling(x, y);
} else if (cardType === 'cannon') {
unit = new Cannon(isPlayer);
}
if (unit) {
if (cardType !== 'minero') {
unit.x = x;
unit.y = y;
}
if (isPlayer) {
playerUnits.push(unit);
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
enemyUnits.push(unit);
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
game.addChild(unit);
LK.getSound('deploy').play();
return true;
}
return false;
}
// AI deployment variables
var aiLastUsedCard = null;
var aiCardCooldown = {};
var aiDeck = []; // AI's randomized deck for the match
var aiUsedCards = []; // Track recently used cards
var aiCardUsageRestriction = {}; // Track cards that can't be used yet
// Generate AI deck with required constraints
function generateAIDeck() {
aiDeck = [];
// Required cards (must have)
var requiredCards = {
aerial: Math.random() < 0.33 ? 'archer' : Math.random() < 0.5 ? 'bruja' : 'wizard',
// Card that can attack aerial units (randomly choose between archer, bruja, and wizard)
wincondition: Math.random() < 0.25 ? 'giant' : Math.random() < 0.33 ? 'montapuercos' : Math.random() < 0.5 ? 'golem' : 'rompemuros',
// Wincondition card (randomly choose between giant, montapuercos, and golem)
spell: Math.random() < 0.33 ? 'fireball' : Math.random() < 0.5 ? 'discharge' : 'hielo',
// Spell card (randomly choose between fireball, discharge, and hielo)
structure: Math.random() < 0.33 ? 'cannon' : Math.random() < 0.5 ? 'ballesta' : 'lapida' // Structure card (randomly choose between cannon, ballesta, and lapida)
};
// Add required cards to AI deck
aiDeck.push(requiredCards.aerial);
aiDeck.push(requiredCards.wincondition);
aiDeck.push(requiredCards.spell);
aiDeck.push(requiredCards.structure);
// Remaining card pool (excluding already added required cards)
var remainingCards = ['knight', 'wizard', 'bruja', 'skeleton', 'skeletonArmy', 'minion', 'maquinaVoladora', 'megaesbirro', 'ballesta', 'montapuercos', 'hielo', 'lapida', 'golem', 'minero'];
// Fill remaining 8 slots with random cards from remaining pool
while (aiDeck.length < 12) {
var randomIndex = Math.floor(Math.random() * remainingCards.length);
var randomCard = remainingCards[randomIndex];
aiDeck.push(randomCard);
// Don't remove from remainingCards to allow duplicates
}
// Shuffle the deck to randomize order
for (var i = aiDeck.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = aiDeck[i];
aiDeck[i] = aiDeck[j];
aiDeck[j] = temp;
}
}
// AI deployment function
function aiDeploy() {
// Check if AI is being attacked (player units in AI territory)
var isUnderAttack = false;
var playerUnitsInAITerritory = 0;
for (var i = 0; i < playerUnits.length; i++) {
if (playerUnits[i].y < 1366) {
// Player units in AI half
isUnderAttack = true;
playerUnitsInAITerritory++;
}
}
// Check if AI towers are taking damage
var aiTowersUnderAttack = false;
for (var i = 0; i < enemyTowers.length; i++) {
if (enemyTowers[i].currentHealth < enemyTowers[i].maxHealth) {
aiTowersUnderAttack = true;
break;
}
}
if (isUnderAttack || aiTowersUnderAttack) {
// DEFENSE MODE: Deploy defensive units quickly if being attacked
if (LK.ticks - aiLastDeploy > 60 && aiElixir >= 3) {
// Deploy faster when defending
var defensiveCards = [];
// Check if player has used wincondition (giant) or if towers are damaged
var playerHasWincondition = false;
for (var i = 0; i < playerUnits.length; i++) {
if (playerUnits[i].unitType === 'giant') {
playerHasWincondition = true;
break;
}
}
if (aiElixir >= 3) {
// Use cards from AI's deck for defense
for (var d = 0; d < aiDeck.length; d++) {
var deckCard = aiDeck[d];
var cardCost = deckCard === 'giant' ? 5 : deckCard === 'wizard' ? 5 : deckCard === 'bruja' ? 5 : deckCard === 'skeleton' ? 1 : deckCard === 'skeletonArmy' ? 3 : deckCard === 'fireball' ? 4 : deckCard === 'discharge' ? 2 : deckCard === 'minion' ? 3 : deckCard === 'maquinaVoladora' ? 4 : deckCard === 'megaesbirro' ? 3 : deckCard === 'ballesta' ? 6 : deckCard === 'montapuercos' ? 4 : deckCard === 'hielo' ? 4 : deckCard === 'lapida' ? 3 : deckCard === 'golem' ? 8 : deckCard === 'rompemuros' ? 2 : deckCard === 'minero' ? 3 : 3;
if (aiElixir >= cardCost && canAIUseCard(deckCard)) {
// Add defensive units (not spells for defense)
if (deckCard !== 'fireball' && deckCard !== 'discharge') {
defensiveCards.push(deckCard);
}
}
}
}
// Check for spell usage opportunities (discharge, fireball, and hielo for defense)
var spellCards = [];
for (var s = 0; s < aiDeck.length; s++) {
var spellCard = aiDeck[s];
if (spellCard === 'discharge' || spellCard === 'fireball' || spellCard === 'hielo') {
var spellCost = spellCard === 'discharge' ? 2 : spellCard === 'hielo' ? 4 : 4;
if (aiElixir >= spellCost) {
spellCards.push(spellCard);
}
}
}
if (spellCards.length > 0 && playerUnits.length > 0) {
// Find player unit with most enemies nearby for spell targeting
var bestTarget = null;
var maxNearbyUnits = 0;
for (var p = 0; p < playerUnits.length; p++) {
var playerUnit = playerUnits[p];
if (!playerUnit.isDead) {
var nearbyCount = 0;
// Count nearby player units
for (var n = 0; n < playerUnits.length; n++) {
var otherUnit = playerUnits[n];
if (!otherUnit.isDead && otherUnit !== playerUnit) {
var distance = Math.sqrt(Math.pow(playerUnit.x - otherUnit.x, 2) + Math.pow(playerUnit.y - otherUnit.y, 2));
if (distance <= 200) {
// Within spell effect range
nearbyCount++;
}
}
}
if (nearbyCount > maxNearbyUnits) {
maxNearbyUnits = nearbyCount;
bestTarget = playerUnit;
}
}
}
// Use spell if we found a good target (at least 1 nearby unit or any unit if no clusters)
if (bestTarget && (maxNearbyUnits > 0 || playerUnits.length > 0)) {
var chosenSpell = spellCards[Math.floor(Math.random() * spellCards.length)];
if (deployUnit(chosenSpell, bestTarget.x, bestTarget.y, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 120 + Math.random() * 60;
return; // Exit early after using spell
}
}
}
if (defensiveCards.length > 0) {
var randomCard = defensiveCards[Math.floor(Math.random() * defensiveCards.length)];
var deployX, deployY;
// Special placement logic for minero - place near player towers/structures
if (randomCard === 'minero') {
// Find closest player tower or structure to place minero near it
var closestTarget = null;
var closestDistance = Infinity;
var allPlayerTargets = playerTowers.concat(playerUnits.filter(function (unit) {
return unit.unitType === 'cannon' || unit.unitType === 'ballesta' || unit.unitType === 'lapida';
}));
for (var t = 0; t < allPlayerTargets.length; t++) {
var target = allPlayerTargets[t];
if (!target.isDead) {
var distance = Math.sqrt(Math.pow(1024 - target.x, 2) + Math.pow(1366 - target.y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = target;
}
}
}
if (closestTarget) {
// Place minero within 50 units of the closest target
var angle = Math.random() * Math.PI * 2;
deployX = closestTarget.x + Math.cos(angle) * (30 + Math.random() * 20);
deployY = closestTarget.y + Math.sin(angle) * (30 + Math.random() * 20);
} else {
// Fallback to random position if no target found
deployX = 300 + Math.random() * 1448;
deployY = 800 + Math.random() * 400;
}
} else {
// Deploy near threatened area for other cards
deployX = 300 + Math.random() * 1448;
deployY = 800 + Math.random() * 400; // Deploy in middle-upper area
}
if (deployUnit(randomCard, deployX, deployY, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 120 + Math.random() * 60; // Quick deploy interval when defending
}
}
}
} else {
// ATTACK MODE: Use wincondition strategy when not under attack
// Check for spell usage opportunities in attack mode (hielo excluded - defensive only)
if (aiElixir >= 2 && playerUnits.length > 0 && Math.random() < 0.3) {
// 30% chance to use spells in attack
var attackSpellCards = [];
for (var s = 0; s < aiDeck.length; s++) {
var spellCard = aiDeck[s];
if (spellCard === 'discharge' || spellCard === 'fireball') {
var spellCost = spellCard === 'discharge' ? 2 : 4;
if (aiElixir >= spellCost) {
attackSpellCards.push(spellCard);
}
}
}
if (attackSpellCards.length > 0) {
// Target strongest or most clustered player units
var bestAttackTarget = null;
var maxValue = 0;
for (var p = 0; p < playerUnits.length; p++) {
var playerUnit = playerUnits[p];
if (!playerUnit.isDead) {
// Calculate value based on unit health + nearby units
var unitValue = playerUnit.currentHealth;
// Count nearby player units for cluster bonus
for (var n = 0; n < playerUnits.length; n++) {
var otherUnit = playerUnits[n];
if (!otherUnit.isDead && otherUnit !== playerUnit) {
var distance = Math.sqrt(Math.pow(playerUnit.x - otherUnit.x, 2) + Math.pow(playerUnit.y - otherUnit.y, 2));
if (distance <= 200) {
unitValue += otherUnit.currentHealth * 0.5; // Bonus for clustered units
}
}
}
if (unitValue > maxValue) {
maxValue = unitValue;
bestAttackTarget = playerUnit;
}
}
}
if (bestAttackTarget) {
var chosenAttackSpell = attackSpellCards[Math.floor(Math.random() * attackSpellCards.length)];
if (deployUnit(chosenAttackSpell, bestAttackTarget.x, bestAttackTarget.y, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 180 + Math.random() * 120;
return; // Exit after using spell
}
}
}
}
// Check if AI already has giants on the field
var giantsOnField = 0;
for (var i = 0; i < enemyUnits.length; i++) {
if (enemyUnits[i].unitType === 'giant' && !enemyUnits[i].isDead) {
giantsOnField++;
}
}
if (aiElixir >= 8 && LK.ticks - aiLastDeploy > aiDeployInterval && giantsOnField < 2) {
// Big push with wincondition + support (max 2 giants on field)
if (aiElixir >= 8 && Math.random() < 0.9) {
// 90% chance for big attack when having 8+ elixir
// Find wincondition card in AI deck
var winconditionCard = null;
for (var w = 0; w < aiDeck.length; w++) {
if (aiDeck[w] === 'giant' || aiDeck[w] === 'montapuercos' || aiDeck[w] === 'golem' || aiDeck[w] === 'rompemuros') {
winconditionCard = aiDeck[w];
break;
}
}
if (winconditionCard) {
// Deploy wincondition first
var deployX = 300 + Math.random() * 1448;
var deployY = 200 + Math.random() * 600;
if (deployUnit(winconditionCard, deployX, deployY, false)) {
var winconditionCost = winconditionCard === 'giant' ? 5 : winconditionCard === 'montapuercos' ? 4 : winconditionCard === 'golem' ? 8 : 3;
aiElixir -= winconditionCost;
// Wait a bit then deploy support unit behind wincondition
LK.setTimeout(function () {
if (aiElixir >= 3) {
var supportCards = [];
// Get support cards from AI deck
for (var s = 0; s < aiDeck.length; s++) {
var supportCard = aiDeck[s];
var supportCost = supportCard === 'giant' ? 5 : supportCard === 'wizard' ? 5 : supportCard === 'skeleton' ? 1 : supportCard === 'skeletonArmy' ? 3 : supportCard === 'fireball' ? 4 : supportCard === 'discharge' ? 2 : supportCard === 'minion' ? 3 : supportCard === 'montapuercos' ? 4 : supportCard === 'hielo' ? 4 : supportCard === 'ballesta' ? 6 : supportCard === 'lapida' ? 3 : supportCard === 'golem' ? 8 : supportCard === 'rompemuros' ? 2 : supportCard === 'minero' ? 3 : 3;
if (aiElixir >= supportCost && supportCard !== 'fireball' && supportCard !== 'discharge' && supportCard !== 'hielo' && supportCard !== winconditionCard) {
supportCards.push(supportCard);
}
}
// Check for Ballesta offensive deployment near middle line
if (aiElixir >= 6 && Math.random() < 0.3) {
// 30% chance when AI has enough elixir
var hasBallesta = false;
for (var b = 0; b < aiDeck.length; b++) {
if (aiDeck[b] === 'ballesta') {
hasBallesta = true;
break;
}
}
if (hasBallesta) {
// Deploy Ballesta near middle line for cross-field tower attacks
var ballestaX = 300 + Math.random() * 1448;
var ballestaY = 1316 + Math.random() * 100; // Near middle line (1366 ± 50)
if (deployUnit('ballesta', ballestaX, ballestaY, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 300 + Math.random() * 120;
return; // Exit after deploying Ballesta
}
}
}
if (supportCards.length > 0) {
var chosenSupport = supportCards[Math.floor(Math.random() * supportCards.length)];
var supportX = deployX + (Math.random() - 0.5) * 200; // Near wincondition
var supportY = deployY + 100 + Math.random() * 200; // Behind wincondition
deployUnit(chosenSupport, supportX, supportY, false);
}
}
}, 1000); // 1 second delay
aiLastDeploy = LK.ticks;
aiDeployInterval = 300 + Math.random() * 180; // Longer interval after big push
}
}
}
} else if (aiElixir >= 5 && aiElixir < 8 && LK.ticks - aiLastDeploy > aiDeployInterval && giantsOnField < 2) {
// Single wincondition deploy when having 5-7 elixir (more likely to save)
if (Math.random() < 0.4) {
// 40% chance to use wincondition (reduced from 60% to encourage saving)
var winconditionCard = null;
for (var w = 0; w < aiDeck.length; w++) {
if (aiDeck[w] === 'giant' || aiDeck[w] === 'montapuercos' || aiDeck[w] === 'golem' || aiDeck[w] === 'rompemuros') {
winconditionCard = aiDeck[w];
break;
}
}
if (winconditionCard) {
var deployX = 300 + Math.random() * 1448;
var deployY = 200 + Math.random() * 600;
if (deployUnit(winconditionCard, deployX, deployY, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 240 + Math.random() * 120;
}
}
} else {
// Regular unit deployment (less likely when saving)
if (aiElixir >= 3 && Math.random() < 0.3) {
var availableCards = [];
for (var a = 0; a < aiDeck.length; a++) {
var deckCard = aiDeck[a];
var cardCost = deckCard === 'giant' ? 5 : deckCard === 'wizard' ? 5 : deckCard === 'bruja' ? 5 : deckCard === 'skeleton' ? 1 : deckCard === 'skeletonArmy' ? 3 : deckCard === 'fireball' ? 4 : deckCard === 'discharge' ? 2 : deckCard === 'minion' ? 3 : deckCard === 'maquinaVoladora' ? 4 : deckCard === 'megaesbirro' ? 3 : deckCard === 'ballesta' ? 6 : deckCard === 'hielo' ? 4 : deckCard === 'lapida' ? 3 : deckCard === 'golem' ? 8 : deckCard === 'rompemuros' ? 2 : deckCard === 'minero' ? 3 : 3;
if (aiElixir >= cardCost && deckCard !== 'fireball' && deckCard !== 'discharge' && deckCard !== 'hielo') {
availableCards.push(deckCard);
}
}
if (availableCards.length > 0) {
var randomCard = availableCards[Math.floor(Math.random() * availableCards.length)];
var deployX = 300 + Math.random() * 1448;
var deployY = 200 + Math.random() * 1000;
if (deployUnit(randomCard, deployX, deployY, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 180 + Math.random() * 120;
}
}
}
}
} else if (aiElixir >= 3 && aiElixir < 5 && LK.ticks - aiLastDeploy > aiDeployInterval * 3) {
// Wait even longer when having low elixir (3-4), prioritize saving for wincondition
// Only deploy if waiting for too long or in emergency
if (Math.random() < 0.15) {
// Only 15% chance to deploy regular units when saving (reduced from 30%)
var lowCostCards = [];
for (var l = 0; l < aiDeck.length; l++) {
var deckCard = aiDeck[l];
var cardCost = deckCard === 'giant' ? 5 : deckCard === 'wizard' ? 5 : deckCard === 'bruja' ? 5 : deckCard === 'skeleton' ? 1 : deckCard === 'skeletonArmy' ? 3 : deckCard === 'fireball' ? 4 : deckCard === 'discharge' ? 2 : deckCard === 'minion' ? 3 : deckCard === 'maquinaVoladora' ? 4 : deckCard === 'megaesbirro' ? 3 : deckCard === 'ballesta' ? 6 : deckCard === 'hielo' ? 4 : deckCard === 'lapida' ? 3 : deckCard === 'golem' ? 8 : deckCard === 'rompemuros' ? 2 : deckCard === 'minero' ? 3 : 3;
if (aiElixir >= cardCost && deckCard !== 'fireball' && deckCard !== 'discharge' && deckCard !== 'hielo' && cardCost <= 4) {
lowCostCards.push(deckCard);
}
}
if (lowCostCards.length > 0) {
var randomCard = lowCostCards[Math.floor(Math.random() * lowCostCards.length)];
var deployX = 300 + Math.random() * 1448;
var deployY = 200 + Math.random() * 1000;
if (deployUnit(randomCard, deployX, deployY, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 240 + Math.random() * 120;
}
}
}
}
}
}
function checkGameEnd() {
if (gameTime <= 0 && !gameEnded) {
gameEnded = true;
if (playerTowersDestroyed > enemyTowersDestroyed) {
LK.showGameOver();
} else if (enemyTowersDestroyed > playerTowersDestroyed) {
LK.showYouWin();
} else {
// Tie-breaker: check tower health
var playerTotalHealth = 0;
var enemyTotalHealth = 0;
for (var i = 0; i < playerTowers.length; i++) {
playerTotalHealth += playerTowers[i].currentHealth;
}
for (var i = 0; i < enemyTowers.length; i++) {
enemyTotalHealth += enemyTowers[i].currentHealth;
}
if (playerTotalHealth > enemyTotalHealth) {
LK.showYouWin();
} else if (enemyTotalHealth > playerTotalHealth) {
LK.showGameOver();
} else {
LK.showGameOver(); // Draw counts as loss
}
}
}
}
function setupEventHandlers() {
// Event handlers for active deck cards only
for (var i = 0; i < deckCards.length; i++) {
var card = deckCards[i];
var cardType = card.cardType;
var cost = cardType === 'giant' ? 5 : cardType === 'wizard' ? 5 : cardType === 'bruja' ? 5 : cardType === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : cardType === 'ballesta' ? 6 : cardType === 'montapuercos' ? 4 : cardType === 'hielo' ? 4 : cardType === 'lapida' ? 3 : cardType === 'golem' ? 8 : cardType === 'rompemuros' ? 2 : cardType === 'minero' ? 3 : 3;
// Create closure to capture cardType and cost
(function (type, cardCost, index) {
card.down = function (x, y, obj) {
if (elixir >= cardCost) {
draggedCard = type;
}
};
})(cardType, cost, i);
}
game.down = function (x, y, obj) {
// Only handle game events if game has started
if (!gameStarted) {
return;
}
// This will handle deployment when clicking on the battlefield
};
game.up = function (x, y, obj) {
// Only handle game events if game has started
if (!gameStarted) {
return;
}
if (draggedCard) {
// Convert coordinates to game space
var gamePos = {
x: x,
y: y
};
if (obj && obj.parent) {
var globalPos = obj.parent.toGlobal ? obj.parent.toGlobal(obj.position) : {
x: x,
y: y
};
gamePos = game.toLocal ? game.toLocal(globalPos) : {
x: x,
y: y
};
}
if (deployUnit(draggedCard, gamePos.x, gamePos.y, true)) {
// Unit deployed successfully
}
draggedCard = null;
}
};
}
// Initialize menu
createMenu();
game.update = function () {
// Only run game logic if game has started
if (!gameStarted) {
return;
}
if (gameEnded) {
return;
}
// Update timer
if (LK.ticks % 60 === 0 && gameTime > 0) {
gameTime--;
timerText.setText(gameTime.toString());
}
// Check for double elixir activation at 120 seconds remaining (130 seconds elapsed)
if (gameTime === 130 && !doubleElixirActive) {
doubleElixirActive = true;
doubleElixirMessageShown = false;
// Create double elixir message
doubleElixirMessageText = new Text2('¡ELIXIR DOBLE ACTIVADO!', {
size: 80,
fill: 0xFFD700
});
doubleElixirMessageText.anchor.set(0.5, 0.5);
doubleElixirMessageText.x = 1024;
doubleElixirMessageText.y = 300;
game.addChild(doubleElixirMessageText);
// Set message timer for 3 seconds
doubleElixirMessageTimer = 180; // 3 seconds at 60fps
// Flash effect for the message
tween(doubleElixirMessageText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
onFinish: function onFinish() {
tween(doubleElixirMessageText, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 500
});
}
});
}
// Handle double elixir message timer
if (doubleElixirMessageTimer > 0) {
doubleElixirMessageTimer--;
if (doubleElixirMessageTimer <= 0 && doubleElixirMessageText) {
doubleElixirMessageText.destroy();
doubleElixirMessageText = null;
}
}
// Regenerate elixir (double speed after 120 seconds passed)
var currentElixirRegenRate = doubleElixirActive ? doubleElixirRegenRate : elixirRegenRate;
if (LK.ticks - lastElixirRegen >= currentElixirRegenRate && elixir < maxElixir) {
elixir++;
elixirText.setText(elixir + "/" + maxElixir);
lastElixirRegen = LK.ticks;
}
// Regenerate AI elixir (double speed after 120 seconds passed)
var currentAiElixirRegenRate = doubleElixirActive ? doubleAiElixirRegenRate : aiElixirRegenRate;
if (LK.ticks - lastAiElixirRegen >= currentAiElixirRegenRate && aiElixir < aiMaxElixir) {
aiElixir++;
lastAiElixirRegen = LK.ticks;
}
// AI deployment
aiDeploy();
// Clean up dead units
for (var i = playerUnits.length - 1; i >= 0; i--) {
if (playerUnits[i].isDead) {
playerUnits[i].destroy();
playerUnits.splice(i, 1);
}
}
for (var i = enemyUnits.length - 1; i >= 0; i--) {
if (enemyUnits[i].isDead) {
enemyUnits[i].destroy();
enemyUnits.splice(i, 1);
}
}
// Clean up bullets
for (var i = bullets.length - 1; i >= 0; i--) {
if (bullets[i].destroyed || bullets[i].y < -100 || bullets[i].y > 2800) {
bullets[i].destroy();
bullets.splice(i, 1);
}
}
// Check for immediate game end conditions
var allPlayerTowersDead = true;
var allEnemyTowersDead = true;
for (var i = 0; i < playerTowers.length; i++) {
if (!playerTowers[i].isDead) {
allPlayerTowersDead = false;
break;
}
}
for (var i = 0; i < enemyTowers.length; i++) {
if (!enemyTowers[i].isDead) {
allEnemyTowersDead = false;
break;
}
}
if (allPlayerTowersDead && !gameEnded) {
gameEnded = true;
LK.showGameOver();
} else if (allEnemyTowersDead && !gameEnded) {
gameEnded = true;
LK.showYouWin();
}
checkGameEnd();
};
// Check if AI can use a specific card based on usage restriction
function canAIUseCard(cardType) {
// If card hasn't been used yet, it can be used
if (aiCardUsageRestriction[cardType] === undefined) {
return true;
}
// If card is restricted, check if 4 different cards have been used since
return aiCardUsageRestriction[cardType] <= 0;
}
// Update AI card usage tracking when a card is used
function updateAICardUsage(usedCardType) {
// Add the used card to recently used list
if (aiUsedCards.indexOf(usedCardType) === -1) {
aiUsedCards.push(usedCardType);
}
// Set restriction for the used card (needs 4 different cards before reuse)
aiCardUsageRestriction[usedCardType] = 4;
// Reduce restriction counter for all other restricted cards
for (var cardType in aiCardUsageRestriction) {
if (cardType !== usedCardType && aiCardUsageRestriction[cardType] > 0) {
aiCardUsageRestriction[cardType]--;
}
}
} /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var AreaDamageBullet = Container.expand(function (startX, startY, target, damage, isFromPlayer, areaRadius) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.target = target;
self.damage = damage;
self.speed = 6;
self.isFromPlayer = isFromPlayer;
self.areaRadius = areaRadius || 100; // Default area damage radius
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = isFromPlayer ? 0xFF9800 : 0xFF5722; // Orange tint for area damage bullets
bulletGraphics.scaleX = 1.3;
bulletGraphics.scaleY = 1.3;
self.update = function () {
if (!self.target || self.target.isDead) {
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 < 30) {
// Apply direct damage to the target first
self.target.takeDamage(self.damage);
// Then area damage explosion
self.explode();
self.destroy();
return;
}
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
};
self.explode = function () {
// Visual explosion effect (removed screen flash to prevent bugs)
LK.getSound('explosion').play();
// Create visual circle around the hit target
var explosionCircle = LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 13,
scaleY: 13
});
explosionCircle.x = self.target.x;
explosionCircle.y = self.target.y;
explosionCircle.tint = 0xFF9800;
explosionCircle.alpha = 0.6;
game.addChild(explosionCircle);
// Animate the circle to fade out
tween(explosionCircle, {
alpha: 0,
scaleX: 15,
scaleY: 15
}, {
duration: 500,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Get all possible targets for area damage
var allTargets = [];
if (self.isFromPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage to all units within area radius (100 units as requested)
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead && targetUnit !== self.target) {
// Exclude the initial target from area damage
var distance = Math.sqrt(Math.pow(self.target.x - targetUnit.x, 2) + Math.pow(self.target.y - targetUnit.y, 2));
if (distance <= 100) {
targetUnit.takeDamage(140);
// Flash each affected unit
tween(targetUnit, {
tint: 0xFF9800
}, {
duration: 300,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
};
return self;
});
var Ballesta = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'ballesta';
self.maxHealth = 900;
self.currentHealth = 900;
self.isDead = false;
self.damage = 18;
self.range = 2500;
self.attackSpeed = 18; // Every 0.3 seconds (60fps * 0.3 = 18 ticks)
self.lastAttackTime = 0;
self.target = null;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 240; // 4 seconds (60 * 4 = 240 ticks)
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var ballestaGraphics = self.attachAsset('ballesta', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = 0.25;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Ballesta cannot attack aerial units - only ground units
if (targetUnits[i].isAerial) {
continue;
}
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range, but only if touching middle line (y around 1366)
var canAttackTowers = Math.abs(self.y - 1366) <= 50; // Must be within 50 units of middle line
if (canAttackTowers) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.range && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullet.speed = 16; // 100% faster than default speed of 8
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't attack while stunned
}
// Lose 4% of total health per second (60 ticks = 1 second)
if (LK.ticks % 15 === 0) {
// Every 15 ticks = 4 times per second = 1% each time = 4% per second
self.currentHealth -= self.maxHealth * 0.01;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar without flash effect for natural decay
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
}
self.findTarget();
self.attack();
};
return self;
});
var Bruja = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'bruja';
self.isAerial = false; // Ground unit
self.maxHealth = 550;
self.currentHealth = 550;
self.damage = 130;
self.attackSpeed = 90; // 90 ticks
self.range = 500; // Detection range
self.attackRange = 425; // Attack range
self.speed = 1.25;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Skeleton summoning properties
self.skeletonSummonTimer = 180; // 3 seconds initial delay (60 ticks = 1 second)
self.skeletonSummonInterval = 600; // 10 seconds interval (60 * 10 = 600 ticks)
self.hasSummonedFirst = false; // Track if first summon has happened
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var brujaGraphics = self.attachAsset('bruja', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.2;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Bruja can attack both air and ground units
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Create purple area damage bullet with 75 units radius
var brujaAreaBullet = new BrujaAreaBullet(self.x, self.y, self.target, self.damage, self.isPlayer, 75);
bullets.push(brujaAreaBullet);
game.addChild(brujaAreaBullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.summonSkeletons = function () {
// Generate 4 skeletons in formation: up, down, left, right at 75 units distance
var skeletonPositions = [{
x: self.x,
y: self.y - 75
},
// Up
{
x: self.x,
y: self.y + 75
},
// Down
{
x: self.x - 75,
y: self.y
},
// Left
{
x: self.x + 75,
y: self.y
} // Right
];
for (var i = 0; i < skeletonPositions.length; i++) {
var pos = skeletonPositions[i];
var skeleton = new Skeleton(self.isPlayer);
skeleton.x = pos.x;
skeleton.y = pos.y;
// Remove spawn delay for Bruja-generated skeletons so they can move and attack immediately
skeleton.spawnTimer = 0;
skeleton.isSpawning = false;
skeleton.spawnTimerText.setText('');
// Add skeleton to appropriate unit array
if (self.isPlayer) {
playerUnits.push(skeleton);
} else {
enemyUnits.push(skeleton);
}
game.addChild(skeleton);
}
// Play deploy sound for skeleton summoning
LK.getSound('deploy').play();
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
// Handle skeleton summoning
self.skeletonSummonTimer--;
if (!self.hasSummonedFirst && self.skeletonSummonTimer <= 0) {
// First summon after 3 seconds
self.summonSkeletons();
self.hasSummonedFirst = true;
self.skeletonSummonTimer = self.skeletonSummonInterval; // Set to 10 second interval
} else if (self.hasSummonedFirst && self.skeletonSummonTimer <= 0) {
// Subsequent summons every 10 seconds
self.summonSkeletons();
self.skeletonSummonTimer = self.skeletonSummonInterval; // Reset to 10 second interval
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var BrujaAreaBullet = Container.expand(function (startX, startY, target, damage, isFromPlayer, areaRadius) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.target = target;
self.damage = damage;
self.speed = 6;
self.isFromPlayer = isFromPlayer;
self.areaRadius = areaRadius || 75; // 75 units radius as requested
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0x9C27B0; // Purple color for bruja bullets
bulletGraphics.scaleX = 1.2;
bulletGraphics.scaleY = 1.2;
self.update = function () {
if (!self.target || self.target.isDead) {
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 < 30) {
// Apply direct damage to the target first
self.target.takeDamage(self.damage);
// Then area damage explosion
self.explode();
self.destroy();
return;
}
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
};
self.explode = function () {
// Play explosion sound
LK.getSound('explosion').play();
// Create blue visual circle around the hit target with 75 units radius
var explosionCircle = LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 10,
// Scale to make it 75 units radius (15 * 10 / 2 = 75)
scaleY: 10
});
explosionCircle.x = self.target.x;
explosionCircle.y = self.target.y;
explosionCircle.tint = 0x0080FF; // Blue color as requested
explosionCircle.alpha = 0.6;
game.addChild(explosionCircle);
// Animate the circle to fade out
tween(explosionCircle, {
alpha: 0,
scaleX: 12,
scaleY: 12
}, {
duration: 500,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Get all possible targets for area damage
var allTargets = [];
if (self.isFromPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage to all units within area radius (75 units as requested)
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead && targetUnit !== self.target) {
// Exclude the initial target from area damage
var distance = Math.sqrt(Math.pow(self.target.x - targetUnit.x, 2) + Math.pow(self.target.y - targetUnit.y, 2));
if (distance <= self.areaRadius) {
targetUnit.takeDamage(self.damage);
// Flash each affected unit with blue color
tween(targetUnit, {
tint: 0x0080FF
}, {
duration: 300,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
};
return self;
});
var Bullet = Container.expand(function (startX, startY, target, damage, isFromPlayer) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.target = target;
self.damage = damage;
self.speed = 8;
self.isFromPlayer = isFromPlayer;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = isFromPlayer ? 0x4CAF50 : 0xF44336;
self.update = function () {
if (!self.target || self.target.isDead) {
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 < 20) {
self.target.takeDamage(self.damage);
self.destroy();
return;
}
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
};
return self;
});
var Cannon = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'cannon';
self.maxHealth = 600;
self.currentHealth = 600;
self.isDead = false;
self.damage = 90;
self.range = 500;
self.attackSpeed = 50;
self.lastAttackTime = 0;
self.target = null;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var cannonGraphics = self.attachAsset('cannon', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = 0.25;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Cannon cannot attack aerial units
if (targetUnits[i].isAerial) {
continue;
}
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
self.target = closestTarget;
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.range && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't attack while stunned
}
// Lose 3% of total health per second (60 ticks = 1 second)
if (LK.ticks % 20 === 0) {
// Every 20 ticks = 3 times per second = 1% each time = 3% per second
self.currentHealth -= self.maxHealth * 0.01;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar without flash effect for natural decay
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
}
self.findTarget();
self.attack();
};
return self;
});
var Card = Container.expand(function (cardType) {
var self = Container.call(this);
self.cardType = cardType;
self.cost = cardType === 'giant' ? 5 : cardType === 'wizard' ? 5 : cardType === 'bruja' ? 5 : cardType === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : cardType === 'montapuercos' ? 4 : cardType === 'hielo' ? 4 : cardType === 'lapida' ? 3 : cardType === 'golem' ? 8 : 3;
var cardBg = self.attachAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5
});
var cardIcon = LK.getAsset(cardType === 'discharge' ? 'descarga_descardina' : cardType === 'maquinaVoladora' ? 'flying_machine' : cardType === 'bruja' ? 'bruja' : cardType, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
self.addChild(cardIcon);
var costText = new Text2(self.cost.toString(), {
size: 30,
fill: 0xFFFFFF
});
costText.anchor.set(0.5, 0.5);
costText.x = 120;
costText.y = -40;
self.addChild(costText);
return self;
});
var Discharge = Container.expand(function (targetX, targetY, isFromPlayer) {
var self = Container.call(this);
self.targetX = targetX;
self.targetY = targetY;
self.isFromPlayer = isFromPlayer;
self.damage = 200;
self.stunDuration = 60; // 1 second stun (60 ticks)
// Discharge appears instantly at target location
self.x = targetX;
self.y = targetY;
// Define explode method BEFORE calling it
self.explode = function () {
// Visual explosion effect - blue circle of 150 units radius
var explosionCircle = LK.getAsset('dischargeExplosion', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
// Scale to make it 150 units radius (250 * 1.2 / 2 = 150)
scaleY: 1.2
});
explosionCircle.x = self.targetX;
explosionCircle.y = self.targetY;
explosionCircle.tint = 0x0080FF; // Blue color
explosionCircle.alpha = 0.9;
game.addChild(explosionCircle);
// Animate explosion
tween(explosionCircle, {
alpha: 0,
scaleX: 1.8,
scaleY: 1.8
}, {
duration: 800,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Play explosion sound
LK.getSound('explosion').play();
// Get all possible targets for damage
var allTargets = [];
if (self.isFromPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage and stun to all units within explosion radius of 150 units
var explosionRadius = 150; // 150 units as requested
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead) {
var distance = Math.sqrt(Math.pow(self.targetX - targetUnit.x, 2) + Math.pow(self.targetY - targetUnit.y, 2));
if (distance <= explosionRadius) {
// Check if target is a tower
var isTower = false;
if (self.isFromPlayer) {
isTower = enemyTowers.indexOf(targetUnit) !== -1;
} else {
isTower = playerTowers.indexOf(targetUnit) !== -1;
}
// Apply damage - 50% less to towers
var damageToApply = isTower ? self.damage * 0.5 : self.damage;
targetUnit.takeDamage(damageToApply);
// Apply stun effect (immobilize for 1 second)
targetUnit.stunTimer = self.stunDuration;
targetUnit.isStunned = true;
// Visual effect for stunned units (blue tint)
tween(targetUnit, {
tint: 0x0080FF
}, {
duration: 1000,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
// Destroy discharge immediately after explosion
self.destroy();
};
// Create instant explosion effect - NOW we can call explode since it's defined
self.explode();
return self;
});
var Fireball = Container.expand(function (targetX, targetY, isFromPlayer) {
var self = Container.call(this);
self.targetX = targetX;
self.targetY = targetY;
self.isFromPlayer = isFromPlayer;
self.speed = 13;
self.damage = 415;
self.explosionRadius = 150;
// Start from king tower position
if (isFromPlayer) {
self.x = 1024; // Player king tower x
self.y = 2600; // Player king tower y
} else {
self.x = 1024; // Enemy king tower x
self.y = 200; // Enemy king tower y
}
var fireballGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
fireballGraphics.tint = 0xFF6600;
self.update = function () {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 30) {
// Explode at target location
self.explode();
self.destroy();
return;
}
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
};
self.explode = function () {
// Visual explosion effect
var explosionCircle = LK.getAsset('fireballExplosion', {
anchorX: 0.5,
anchorY: 0.5
});
explosionCircle.x = self.targetX;
explosionCircle.y = self.targetY;
explosionCircle.tint = 0xFF3300;
explosionCircle.alpha = 0.8;
game.addChild(explosionCircle);
// Animate explosion
tween(explosionCircle, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Play explosion sound
LK.getSound('explosion').play();
// Get all possible targets for damage
var allTargets = [];
if (self.isFromPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage and knockback to all units within explosion radius
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead) {
var distance = Math.sqrt(Math.pow(self.targetX - targetUnit.x, 2) + Math.pow(self.targetY - targetUnit.y, 2));
if (distance <= self.explosionRadius) {
// Check if target is a tower
var isTower = false;
if (self.isFromPlayer) {
isTower = enemyTowers.indexOf(targetUnit) !== -1;
} else {
isTower = playerTowers.indexOf(targetUnit) !== -1;
}
// Apply damage - 50% less to towers
var damageToApply = isTower ? self.damage * 0.5 : self.damage;
targetUnit.takeDamage(damageToApply);
// Apply knockback only to non-tower units
if (!isTower) {
var knockbackDistance = 100;
var knockbackDx = targetUnit.x - self.targetX;
var knockbackDy = targetUnit.y - self.targetY;
var knockbackLength = Math.sqrt(knockbackDx * knockbackDx + knockbackDy * knockbackDy);
if (knockbackLength > 0) {
var normalizedDx = knockbackDx / knockbackLength;
var normalizedDy = knockbackDy / knockbackLength;
var newX = targetUnit.x + normalizedDx * knockbackDistance;
var newY = targetUnit.y + normalizedDy * knockbackDistance;
// Animate knockback
tween(targetUnit, {
x: newX,
y: newY
}, {
duration: 300,
easing: tween.easeOut
});
}
}
// Flash effect on hit units
tween(targetUnit, {
tint: 0xFF6600
}, {
duration: 300,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
};
return self;
});
var Golem = Container.expand(function (isPlayer, isMini) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'golem';
self.isMini = isMini || false;
self.isAerial = false; // Ground unit
self.maxHealth = self.isMini ? 1822 : 4455; // 10% less health for golem (4455), mini-golem accordingly (1822)
self.currentHealth = self.maxHealth;
self.damage = self.isMini ? 100 : 200; // 50% less damage for mini-golems
self.attackSpeed = 115; // Every 115 ticks
self.range = 500; // Detection range
self.attackRange = 30; // Melee attack range (reduced from 150 to 30)
self.speed = 0.65;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
self.hasExploded = false; // Prevent multiple explosions
// Spawn delay properties
self.spawnDelay = 240; // 4 seconds (60 * 4 = 240 ticks)
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var golemGraphics = self.attachAsset('golem', {
anchorX: 0.5,
anchorY: 0.5
});
// Scale mini-golems smaller (50% as requested)
if (self.isMini) {
golemGraphics.scaleX = 0.5;
golemGraphics.scaleY = 0.5;
}
// Health bar background
var healthBarScale = self.isMini ? 0.3 : 0.4;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -60
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -60
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 35,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 40;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
// Explode immediately when golem dies
if (!self.hasExploded) {
self.explodeOnDeath();
}
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Golem only attacks structures and towers (wincondition behavior)
// First check for structures (cannons, ballesta, lapida) within detection range
var targetStructures = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetStructures.length; i++) {
if (!targetStructures[i].isDead && (targetStructures[i].unitType === 'cannon' || targetStructures[i].unitType === 'ballesta' || targetStructures[i].unitType === 'lapida')) {
var distance = Math.sqrt(Math.pow(self.x - targetStructures[i].x, 2) + Math.pow(self.y - targetStructures[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetStructures[i];
}
}
}
// If no structures found, target closest tower
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Melee attack - direct damage
self.target.takeDamage(self.damage);
// Visual effect for melee strike
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Flash target when hit
tween(self.target, {
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(self.target, {
tint: 0xFFFFFF
}, {
duration: 100
});
}
});
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.explodeOnDeath = function () {
if (self.hasExploded) {
return; // Prevent multiple explosions
}
self.hasExploded = true;
// Calculate explosion parameters based on mini-golem status
var explosionRadius = self.isMini ? 85 : 170; // 50% less radius for mini-golems, reduced by 15% (170 vs 85)
var explosionDamage = self.isMini ? 50 : 100; // 50% less damage for mini-golems (50 vs 100)
var explosionScale = self.isMini ? 1.0 : 2.0; // Scale visual effect accordingly
// Visual explosion effect using golemExplosion asset
var explosionCircle = LK.getAsset('golemExplosion', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: explosionScale,
scaleY: explosionScale
});
explosionCircle.x = self.x;
explosionCircle.y = self.y;
explosionCircle.tint = 0x8B4513; // Brown color for golem explosion
explosionCircle.alpha = 0.8;
game.addChild(explosionCircle);
// Animate explosion
tween(explosionCircle, {
alpha: 0,
scaleX: explosionScale * 1.5,
scaleY: explosionScale * 1.5
}, {
duration: 1000,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Play explosion sound
LK.getSound('explosion').play();
// Get all possible targets for damage
var allTargets = [];
if (self.isPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage to all units within explosion radius
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetUnit.x, 2) + Math.pow(self.y - targetUnit.y, 2));
if (distance <= explosionRadius) {
targetUnit.takeDamage(explosionDamage);
// Flash effect on hit units
tween(targetUnit, {
tint: 0x8B4513
}, {
duration: 300,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
// Generate 2 mini-golems if this is not already a mini-golem
if (!self.isMini) {
for (var m = 0; m < 2; m++) {
var miniGolem = new Golem(self.isPlayer, true); // Create mini-golem
miniGolem.x = self.x + (m === 0 ? -80 : 80); // Position left and right
miniGolem.y = self.y;
// Remove spawn delay for generated mini-golems
miniGolem.spawnTimer = 0;
miniGolem.isSpawning = false;
miniGolem.spawnTimerText.setText('');
// Add mini-golem to appropriate unit array
if (self.isPlayer) {
playerUnits.push(miniGolem);
} else {
enemyUnits.push(miniGolem);
}
game.addChild(miniGolem);
}
// Play deploy sound for mini-golem generation
LK.getSound('deploy').play();
}
};
self.update = function () {
if (self.isDead) {
// Explode on death if hasn't exploded yet
if (!self.hasExploded) {
self.explodeOnDeath();
}
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Hielo = Container.expand(function (targetX, targetY, isFromPlayer) {
var self = Container.call(this);
self.targetX = targetX;
self.targetY = targetY;
self.isFromPlayer = isFromPlayer;
self.damage = 50;
self.freezeDuration = 300; // 5 seconds freeze (60 ticks * 5 = 300)
self.freezeRadius = 400; // 400 units radius as requested
// Hielo appears instantly at target location
self.x = targetX;
self.y = targetY;
// Define explode method BEFORE calling it
self.explode = function () {
// Visual explosion effect - blue circle of 400 units radius
var explosionCircle = LK.getAsset('dischargeExplosion', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3.2,
// Scale to make it 400 units radius (250 * 3.2 / 2 = 400)
scaleY: 3.2
});
explosionCircle.x = self.targetX;
explosionCircle.y = self.targetY;
explosionCircle.tint = 0x0080FF; // Blue color as requested
explosionCircle.alpha = 0.7;
game.addChild(explosionCircle);
// Keep circle static for duration, then destroy after freeze effect ends
LK.setTimeout(function () {
explosionCircle.destroy();
}, 5000); // Destroy after 5 seconds (freeze duration)
// Play explosion sound
LK.getSound('explosion').play();
// Get all possible targets for damage and freeze
var allTargets = [];
if (self.isFromPlayer) {
allTargets = allTargets.concat(enemyUnits, enemyTowers);
} else {
allTargets = allTargets.concat(playerUnits, playerTowers);
}
// Apply damage and freeze to all units within explosion radius of 400 units
for (var i = 0; i < allTargets.length; i++) {
var targetUnit = allTargets[i];
if (!targetUnit.isDead) {
var distance = Math.sqrt(Math.pow(self.targetX - targetUnit.x, 2) + Math.pow(self.targetY - targetUnit.y, 2));
if (distance <= self.freezeRadius) {
// Apply 50 damage
targetUnit.takeDamage(self.damage);
// Apply freeze effect (immobilize for 5 seconds) - works on towers too
targetUnit.stunTimer = self.freezeDuration;
targetUnit.isStunned = true;
// Visual effect for frozen units and towers (blue tint)
tween(targetUnit, {
tint: 0x0080FF
}, {
duration: 5000,
onFinish: function onFinish() {
tween(targetUnit, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
}
}
// Destroy hielo immediately after explosion
self.destroy();
};
// Create instant explosion effect - NOW we can call explode since it's defined
self.explode();
return self;
});
var Lapida = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'lapida';
self.maxHealth = 700;
self.currentHealth = 700;
self.isDead = false;
self.isStructure = true; // Mark as structure
// Skeleton generation properties
self.skeletonGenerationTime = 60; // 1 second generation time
self.skeletonProductionTime = 300; // 5 seconds to produce skeletons
self.skeletonTimer = 0; // Start immediately for first generation
self.isGenerating = false;
// Health decay properties - loses 3% per second
self.healthDecayRate = 20; // Every 20 ticks = 3 times per second = 1% each time = 3% per second
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var lapidaGraphics = self.attachAsset('lapida', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.3;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
// Generation progress text removed - no longer displayed
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.generateSkeletons = function () {
// Generate 2 skeletons at positions around the lapida
var skeletonPositions = [{
x: self.x - 50,
y: self.y
},
// Left
{
x: self.x + 50,
y: self.y
} // Right
];
for (var i = 0; i < skeletonPositions.length; i++) {
var pos = skeletonPositions[i];
var skeleton = new Skeleton(self.isPlayer);
skeleton.x = pos.x;
skeleton.y = pos.y;
// Remove spawn delay for Lapida-generated skeletons so they can move and attack immediately
skeleton.spawnTimer = 0;
skeleton.isSpawning = false;
skeleton.spawnTimerText.setText('');
// Add skeleton to appropriate unit array
if (self.isPlayer) {
playerUnits.push(skeleton);
} else {
enemyUnits.push(skeleton);
}
game.addChild(skeleton);
}
// Play deploy sound for skeleton generation
LK.getSound('deploy').play();
};
// Method called when lapida is destroyed - generates 4 skeletons
self.onDestroy = function () {
// Generate 4 skeletons at positions around the lapida when destroyed
var destructionSkeletonPositions = [{
x: self.x,
y: self.y - 75
},
// Up
{
x: self.x,
y: self.y + 75
},
// Down
{
x: self.x - 75,
y: self.y
},
// Left
{
x: self.x + 75,
y: self.y
} // Right
];
for (var i = 0; i < destructionSkeletonPositions.length; i++) {
var pos = destructionSkeletonPositions[i];
var skeleton = new Skeleton(self.isPlayer);
skeleton.x = pos.x;
skeleton.y = pos.y;
// Remove spawn delay for destruction-generated skeletons
skeleton.spawnTimer = 0;
skeleton.isSpawning = false;
skeleton.spawnTimerText.setText('');
// Add skeleton to appropriate unit array
if (self.isPlayer) {
playerUnits.push(skeleton);
} else {
enemyUnits.push(skeleton);
}
game.addChild(skeleton);
}
// Play deploy sound for skeleton generation from destruction
LK.getSound('deploy').play();
};
self.update = function () {
if (self.isDead) {
// Call onDestroy when lapida dies - regardless of how it died
if (!self.hasBeenDestroyed) {
self.hasBeenDestroyed = true;
self.onDestroy();
}
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
// Generate first 2 skeletons immediately after spawn delay ends
self.generateSkeletons();
self.skeletonTimer = self.skeletonProductionTime; // Start 6 second timer for next generation
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't do other actions while spawning
}
// Handle stun effect (structures can be stunned too)
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
// Don't generate skeletons while stunned, but still decay health
}
// Handle health decay - loses 3% of total health per second
if (LK.ticks % self.healthDecayRate === 0) {
self.currentHealth -= self.maxHealth * 0.01;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar without flash effect for natural decay
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
}
// Handle skeleton generation (only if not stunned)
if (!self.isStunned) {
if (self.skeletonTimer > 0) {
self.skeletonTimer--;
if (self.skeletonTimer <= 0) {
// Generate 2 skeletons
self.generateSkeletons();
// Reset timer for next generation (5 seconds)
self.skeletonTimer = self.skeletonProductionTime;
}
}
}
};
return self;
});
var MaquinaVoladora = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'maquinaVoladora';
self.isAerial = true; // Mark as aerial unit
self.maxHealth = 500;
self.currentHealth = 500;
self.damage = 85;
self.attackSpeed = 90; // 90 ticks
self.range = 500; // Detection range
self.attackRange = 475; // Attack range
self.speed = 1.5;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var maquinaGraphics = self.attachAsset('flying_machine', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.2;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Create purple bullet
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullet.children[0].tint = 0x9C27B0; // Purple color
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Megaesbirro = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'megaesbirro';
self.isAerial = true; // Mark as aerial unit
self.maxHealth = 650; // 500 + 150
self.currentHealth = 650;
self.damage = 135; // 85 + 50
self.attackSpeed = 90; // 90 ticks
self.range = 500; // Detection range
self.attackRange = 150; // Attack range
self.speed = 1.75;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var megaesbirroGraphics = self.attachAsset('megaesbirro', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.2;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Create purple bullet
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullet.children[0].tint = 0x9C27B0; // Purple color
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Minero = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'minero';
self.isAerial = false; // Ground unit
self.maxHealth = 600;
self.currentHealth = 600;
self.damage = 200;
self.attackSpeed = 90; // 90 ticks
self.range = 450; // Detection range
self.attackRange = 75; // Attack range
self.speed = 1.25;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Tunnel travel properties
self.isTunneling = false;
self.tunnelStartX = 0;
self.tunnelStartY = 0;
self.tunnelTargetX = 0;
self.tunnelTargetY = 0;
self.tunnelCurrentX = 0;
self.tunnelCurrentY = 0;
self.tunnelTimer = 0;
self.tunnelDuration = 120; // 2 seconds (120 ticks at 60fps)
self.trailSegments = [];
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var mineroGraphics = self.attachAsset('minero', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.25;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first (only ground units)
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Skip minero if it's tunneling
if (targetUnits[i].unitType === 'minero' && targetUnits[i].isTunneling) {
continue;
}
// Minero can only attack ground units
if (targetUnits[i].isAerial) {
continue;
}
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Calculate damage - fixed 35 damage against towers, full 200 against units
var damageToApply = self.damage;
var isTower = false;
if (self.isPlayer) {
isTower = enemyTowers.indexOf(self.target) !== -1;
} else {
isTower = playerTowers.indexOf(self.target) !== -1;
}
if (isTower) {
damageToApply = 35; // Fixed 35 damage to towers
} else {
damageToApply = self.damage; // Full 200 damage to units
}
// Melee attack - direct damage
self.target.takeDamage(damageToApply);
// Visual effect for melee strike
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Flash target when hit
tween(self.target, {
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(self.target, {
tint: 0xFFFFFF
}, {
duration: 100
});
}
});
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.startTunneling = function (targetX, targetY) {
self.isTunneling = true;
// Set tunnel origin to king tower position
if (self.isPlayer) {
self.tunnelStartX = 1024; // Player king tower x
self.tunnelStartY = 2600; // Player king tower y
} else {
self.tunnelStartX = 1024; // Enemy king tower x
self.tunnelStartY = 200; // Enemy king tower y
}
self.tunnelTargetX = targetX;
self.tunnelTargetY = targetY;
self.tunnelTimer = self.tunnelDuration;
self.trailSegments = [];
self.tunnelCurrentX = self.tunnelStartX;
self.tunnelCurrentY = self.tunnelStartY;
// Hide minero during tunneling
self.alpha = 0;
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle tunneling
if (self.isTunneling) {
// Calculate tunnel progress
var dx = self.tunnelTargetX - self.tunnelStartX;
var dy = self.tunnelTargetY - self.tunnelStartY;
var totalDistance = Math.sqrt(dx * dx + dy * dy);
var tunnelSpeed = 8; // Speed of tunnel creation increased to 8
var moveX = dx / totalDistance * tunnelSpeed;
var moveY = dy / totalDistance * tunnelSpeed;
// Update tunnel current position
self.tunnelCurrentX += moveX;
self.tunnelCurrentY += moveY;
// Create trail segment every 50 units traveled
var currentDistance = Math.sqrt(Math.pow(self.tunnelCurrentX - self.tunnelStartX, 2) + Math.pow(self.tunnelCurrentY - self.tunnelStartY, 2));
if (Math.floor(currentDistance / 50) > self.trailSegments.length) {
// Create new vertical trail segment
var trailSegment = LK.getAsset('tunnelTrail', {
anchorX: 0.5,
anchorY: 0.5
});
trailSegment.x = self.tunnelCurrentX;
trailSegment.y = self.tunnelCurrentY;
trailSegment.tint = 0x8B4513; // Brown color for dirt trail
trailSegment.alpha = 0.7;
game.addChild(trailSegment);
self.trailSegments.push(trailSegment);
// Schedule trail segment destruction after 3 seconds
LK.setTimeout(function () {
if (trailSegment && trailSegment.destroy) {
trailSegment.destroy();
}
}, 3000);
}
// Check if tunnel reached target
var distanceToTarget = Math.sqrt(Math.pow(self.tunnelTargetX - self.tunnelCurrentX, 2) + Math.pow(self.tunnelTargetY - self.tunnelCurrentY, 2));
if (distanceToTarget < 30) {
// Tunneling complete - now minero appears at target location with 1 second spawn delay
self.x = self.tunnelTargetX;
self.y = self.tunnelTargetY;
self.isTunneling = false;
// Start 1 second spawn delay now that tunnel is complete
self.isSpawning = true;
self.spawnTimer = 60; // 1 second delay
self.alpha = 1; // Make visible but still spawning
}
return; // Don't do anything else while tunneling
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
// Spawn delay complete - minero can now function normally
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Minion = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'minion';
self.isAerial = true; // Mark as aerial unit
self.maxHealth = 400;
self.currentHealth = 400;
self.damage = 85;
self.attackSpeed = 90; // 90 ticks
self.range = 500; // Detection range
self.attackRange = 150; // Attack range
self.speed = 1.75;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var minionGraphics = self.attachAsset('minion', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.2;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Find closest enemy units first
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Create purple bullet
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullet.children[0].tint = 0x9C27B0; // Purple color
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Montapuercos = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'montapuercos';
self.isAerial = false; // Ground unit
self.maxHealth = 935; // 10% increase from 850
self.currentHealth = 935;
self.damage = 165; // 10% increase from 150
self.attackSpeed = 120; // 120 ticks
self.range = 60; // Melee range
self.speed = 2.25;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var montapuercosGraphics = self.attachAsset('montapuercos', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.25;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Montapuercos only attacks structures and towers (wincondition behavior)
// First check for structures (cannons, ballesta, lapida) within detection range
var targetStructures = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetStructures.length; i++) {
if (!targetStructures[i].isDead && (targetStructures[i].unitType === 'cannon' || targetStructures[i].unitType === 'ballesta' || targetStructures[i].unitType === 'lapida')) {
var distance = Math.sqrt(Math.pow(self.x - targetStructures[i].x, 2) + Math.pow(self.y - targetStructures[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetStructures[i];
}
}
}
// If no structures found, target closest tower
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.range) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.range && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Melee attack - direct damage
self.target.takeDamage(self.damage);
// Visual effect for melee strike
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Flash target red when hit
tween(self.target, {
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(self.target, {
tint: 0xFFFFFF
}, {
duration: 100
});
}
});
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Rompemuros = Container.expand(function (isPlayer) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = 'rompemuros';
self.isAerial = false; // Ground unit
self.maxHealth = 200;
self.currentHealth = 200;
self.damage = 275;
self.attackSpeed = 60; // Attack speed doesn't matter much since it dies after attacking
self.range = 400; // Detection range
self.attackRange = 200; // Attack range
self.speed = 2.75;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;
self.hasExploded = false; // Prevent multiple explosions
// Spawn delay properties
self.spawnDelay = 60; // 1 second
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var rompemuroGraphics = self.attachAsset('rompemuros', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = 0.25;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false;
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return;
}
var closestDistance = Infinity;
var closestTarget = null;
// Rompemuros targets structures and towers (wincondition behavior)
// First check for structures (cannons, ballesta, lapida) within detection range
var targetStructures = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetStructures.length; i++) {
if (!targetStructures[i].isDead && (targetStructures[i].unitType === 'cannon' || targetStructures[i].unitType === 'ballesta' || targetStructures[i].unitType === 'lapida')) {
var distance = Math.sqrt(Math.pow(self.x - targetStructures[i].x, 2) + Math.pow(self.y - targetStructures[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetStructures[i];
}
}
}
// If no structures found, target closest tower
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
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 > self.attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
if (distance <= self.attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
self.isAttacking = true;
// Apply damage to target
self.target.takeDamage(self.damage);
// Kamikaze explosion - rompemuros dies after attacking
if (!self.hasExploded) {
self.explodeOnAttack();
}
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.explodeOnAttack = function () {
if (self.hasExploded) {
return; // Prevent multiple explosions
}
self.hasExploded = true;
// Visual explosion effect using golemExplosion asset
var explosionCircle = LK.getAsset('golemExplosion', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
explosionCircle.x = self.x;
explosionCircle.y = self.y;
explosionCircle.tint = 0xFF6600; // Orange color for rompemuros explosion
explosionCircle.alpha = 0.8;
game.addChild(explosionCircle);
// Animate explosion
tween(explosionCircle, {
alpha: 0,
scaleX: 2.0,
scaleY: 2.0
}, {
duration: 800,
onFinish: function onFinish() {
explosionCircle.destroy();
}
});
// Play explosion sound
LK.getSound('explosion').play();
// Rompemuros dies after exploding
self.isDead = true;
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Tower = Container.expand(function (isPlayer, isKing) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.isKing = isKing;
self.maxHealth = isKing ? 3700 : 2400;
self.currentHealth = self.maxHealth;
self.isDead = false;
self.range = isKing ? 800 : 900;
self.damage = 75;
self.attackSpeed = 67; // 50% slower than 45 ticks (45 * 1.5 = 67.5, rounded to 67)
self.lastAttackTime = 0;
// Initialize stun properties for freeze compatibility
self.stunTimer = 0;
self.isStunned = false;
var towerGraphics = self.attachAsset(isKing ? 'kingTower' : 'tower', {
anchorX: 0.5,
anchorY: 1
});
towerGraphics.tint = self.isPlayer ? 0x4CAF50 : 0xF44336;
// Health display
var healthText = new Text2(self.currentHealth.toString(), {
size: 40,
fill: 0xFFFFFF
});
healthText.anchor.set(0.5, 0.5);
healthText.y = -75;
self.addChild(healthText);
self.healthText = healthText;
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
towersDestroyed++;
LK.effects.flashObject(self, 0xFF0000, 1000);
LK.getSound('towerDestroy').play();
if (self.isPlayer) {
enemyTowersDestroyed++;
} else {
playerTowersDestroyed++;
}
}
self.healthText.setText(self.currentHealth.toString());
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.attack = function () {
if (self.isDead) {
return;
}
// Handle stun effect - towers can't attack while stunned/frozen
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't attack while stunned/frozen
}
// King towers cannot attack until at least one Princess tower is destroyed OR king tower has taken damage
if (self.isKing) {
var teamTowers = self.isPlayer ? playerTowers : enemyTowers;
var princessTowersAlive = 0;
for (var i = 0; i < teamTowers.length; i++) {
if (!teamTowers[i].isKing && !teamTowers[i].isDead) {
princessTowersAlive++;
}
}
// King tower can attack if either: 1) Princess tower destroyed OR 2) King tower has taken damage
var kingTowerHasTakenDamage = self.currentHealth < self.maxHealth;
if (princessTowersAlive === 2 && !kingTowerHasTakenDamage) {
return; // King tower cannot attack while both Princess towers are alive AND king tower is undamaged
}
}
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
var closestTarget = null;
var closestDistance = Infinity;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Skip minero if it's tunneling
if (targetUnits[i].unitType === 'minero' && targetUnits[i].isTunneling) {
continue;
}
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
if (closestTarget && LK.ticks - self.lastAttackTime > self.attackSpeed) {
var bullet = new Bullet(self.x, self.y - 50, closestTarget, self.damage, self.isPlayer);
bullets.push(bullet);
game.addChild(bullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
self.attack();
};
return self;
});
var Unit = Container.expand(function (isPlayer, unitType, health, damage, attackSpeed, range, speed) {
var self = Container.call(this);
self.isPlayer = isPlayer;
self.unitType = unitType;
self.maxHealth = health;
self.currentHealth = health;
self.damage = damage;
self.attackSpeed = attackSpeed;
self.range = range;
self.speed = speed;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false; // Track if unit has started attacking current target
// Spawn delay properties
self.spawnDelay = unitType === 'giant' ? 180 : 60; // 3 seconds for giant, 1 second for others (60 ticks = 1 second)
self.spawnTimer = self.spawnDelay;
self.isSpawning = true;
var unitGraphics = self.attachAsset(unitType, {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var healthBarScale = unitType === 'giant' ? 0.4 : 0.2;
var healthBarBg = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBarBg.tint = 0x000000;
self.addChild(healthBarBg);
// Health bar
var healthBar = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: healthBarScale,
scaleY: 0.1,
y: -50
});
healthBar.tint = self.isPlayer ? 0x2196F3 : 0xF44336;
self.addChild(healthBar);
self.healthBar = healthBar;
self.healthBarMaxScale = healthBarScale;
// Spawn timer text
self.spawnTimerText = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
self.spawnTimerText.anchor.set(0.5, 0.5);
self.spawnTimerText.y = 30;
self.addChild(self.spawnTimerText);
self.takeDamage = function (damage) {
self.currentHealth -= damage;
if (self.currentHealth <= 0) {
self.currentHealth = 0;
self.isDead = true;
}
// Update health bar
var healthPercent = self.currentHealth / self.maxHealth;
self.healthBar.scaleX = self.healthBarMaxScale * healthPercent;
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 300);
};
self.findTarget = function () {
// Reset target if current target is dead or invalid
if (self.target && self.target.isDead) {
self.target = null;
self.isAttacking = false; // Reset attacking state when target dies
}
// If already attacking a target, don't change targets
if (self.isAttacking && self.target && !self.target.isDead) {
return; // Keep current target while attacking
}
var closestDistance = Infinity;
var closestTarget = null;
// Giant (wincondition unit) targets structures within detection range, then towers
if (self.unitType === 'giant') {
// First check for structures (cannons, ballesta, lapida) within detection range (538)
var targetStructures = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetStructures.length; i++) {
if (!targetStructures[i].isDead && (targetStructures[i].unitType === 'cannon' || targetStructures[i].unitType === 'ballesta' || targetStructures[i].unitType === 'lapida')) {
var distance = Math.sqrt(Math.pow(self.x - targetStructures[i].x, 2) + Math.pow(self.y - targetStructures[i].y, 2));
if (distance <= 538 && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetStructures[i];
}
}
}
// If no structures in detection range, target closest tower
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
} else {
// Normal units: First priority - Find closest enemy units and structures (they are more immediate threats)
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
for (var i = 0; i < targetUnits.length; i++) {
if (!targetUnits[i].isDead) {
// Skip aerial units if this unit cannot attack air (archers, wizards, and towers can attack air)
if (targetUnits[i].isAerial && self.unitType !== 'archer' && self.unitType !== 'wizard') {
continue;
}
var distance = Math.sqrt(Math.pow(self.x - targetUnits[i].x, 2) + Math.pow(self.y - targetUnits[i].y, 2));
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestTarget = targetUnits[i];
}
}
}
// Second priority: Find closest enemy towers if no units in range
if (!closestTarget) {
var targetTowers = self.isPlayer ? enemyTowers : playerTowers;
for (var i = 0; i < targetTowers.length; i++) {
if (!targetTowers[i].isDead) {
var distance = Math.sqrt(Math.pow(self.x - targetTowers[i].x, 2) + Math.pow(self.y - targetTowers[i].y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = targetTowers[i];
}
}
}
}
}
self.target = closestTarget;
};
self.moveToTarget = function () {
if (!self.target || self.target.isDead) {
return;
}
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Use proper attack range for movement positioning
var attackRange = self.attackRange || (self.unitType === 'knight' ? 168 : self.unitType === 'giant' ? 60 : self.range);
if (distance > attackRange) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
};
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
// Use different ranges for detection vs attack
var attackRange = self.attackRange || (self.unitType === 'knight' ? 168 : self.unitType === 'giant' ? 60 : self.range); // Use attackRange property if defined, otherwise use unit-specific ranges
if (distance <= attackRange && LK.ticks - self.lastAttackTime > self.attackSpeed) {
// Set attacking flag when first attack begins
self.isAttacking = true;
// Knight, Giant, and Skeleton use melee attack - direct damage without bullet
if (self.unitType === 'knight' || self.unitType === 'giant' || self.unitType === 'skeleton') {
self.target.takeDamage(self.damage);
// Visual effect for melee strike
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Flash target red when hit by Giant
if (self.unitType === 'giant') {
tween(self.target, {
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(self.target, {
tint: 0xFFFFFF
}, {
duration: 100
});
}
});
}
// Knight and Skeleton no longer apply knockback to units
} else {
// Other units (like archers, wizard) still use bullets
var bullet = new Bullet(self.x, self.y, self.target, self.damage, self.isPlayer);
bullets.push(bullet);
game.addChild(bullet);
}
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
self.update = function () {
if (self.isDead) {
return;
}
// Handle spawn delay
if (self.isSpawning) {
self.spawnTimer--;
if (self.spawnTimer <= 0) {
self.isSpawning = false;
self.spawnTimerText.setText('');
} else {
// Show remaining time in seconds
var secondsLeft = Math.ceil(self.spawnTimer / 60);
self.spawnTimerText.setText(secondsLeft.toString());
}
return; // Don't move or attack while spawning
}
// Handle stun effect
if (self.stunTimer > 0) {
self.stunTimer--;
if (self.stunTimer <= 0) {
self.isStunned = false;
}
return; // Don't move or attack while stunned
}
// Always find targets every frame to ensure units never ignore enemies
self.findTarget();
self.moveToTarget();
self.attack();
};
return self;
});
var Wizard = Unit.expand(function (isPlayer) {
var self = Unit.call(this, isPlayer, 'wizard', 550, 140, 100, 530, 1.25);
// Override the attack function to use area damage
self.attack = function () {
if (!self.target || self.target.isDead) {
return;
}
// Verify target is from opposing team before attacking
var isValidTarget = false;
if (self.isPlayer && enemyUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (self.isPlayer && enemyTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerUnits.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!self.isPlayer && playerTowers.indexOf(self.target) !== -1) {
isValidTarget = true;
}
if (!isValidTarget) {
self.target = null;
return;
}
var distance = Math.sqrt(Math.pow(self.x - self.target.x, 2) + Math.pow(self.y - self.target.y, 2));
// Wizard uses ranged attack with area damage projectile (use detection range for attacking)
if (distance <= self.range && LK.ticks - self.lastAttackTime > self.attackSpeed) {
// Set attacking flag when first attack begins
self.isAttacking = true;
// Create area damage bullet with 100 unit area radius
var areaBullet = new AreaDamageBullet(self.x, self.y, self.target, self.damage, self.isPlayer, 100);
bullets.push(areaBullet);
game.addChild(areaBullet);
self.lastAttackTime = LK.ticks;
LK.getSound('attack').play();
}
};
return self;
});
var Skeleton = Unit.expand(function (isPlayer) {
var self = Unit.call(this, isPlayer, 'skeleton', 25, 45, 60, 500, 2);
// Skeleton: 25 HP, 45 damage, 60 tick attack speed, 500 detection range, speed 2
// Attacks ground units and structures only (same targeting as other units)
// Override attack range to be 150 instead of detection range
self.attackRange = 150;
return self;
});
var Knight = Unit.expand(function (isPlayer) {
var self = Unit.call(this, isPlayer, 'knight', 825, 160, 60, 538, 1.75);
// Detection range set to 538 (same as archer for consistent detection)
// Attack range remains at melee distance in attack function
return self;
});
var Giant = Unit.expand(function (isPlayer) {
var self = Unit.call(this, isPlayer, 'giant', 4000, 175, 90, 538, 0.85);
// Giant is a wincondition unit that only attacks structures and towers
// Detection range set to 538 (same as archer), attack range remains at 60 in attack function
return self;
});
var Archer = Unit.expand(function (isPlayer) {
var self = Unit.call(this, isPlayer, 'archer', 400, 65, 90, 525, 1.5);
// Range reduced and stats adjusted
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x228B22
});
/****
* Game Code
****/
// Game state variables
/*
CARD AND TOWER STATS:
TOWERS:
- Princess Towers: 2400 HP, 75 damage, attacks every 1.1s (67 ticks), range 800 units
- King Tower: 3700 HP, 75 damage, attacks every 1.1s (67 ticks), range 800 units
* King tower cannot attack until at least one Princess tower is destroyed
CARDS:
- Archers: 3 elixir cost, 425 HP each, 75 damage, attacks every 1.5s (90 ticks), range 567 units, speed 1.5
* Deploys 2 archers, can attack air and ground targets
- Knight: 3 elixir cost, 825 HP, 160 damage, attacks every 1s (60 ticks), range 168 units, speed 2
* Single melee unit, ground targets only
- Giant: 5 elixir cost, 4000 HP, 175 damage, attacks every 1.5s (90 ticks), range 300 units, speed 1
* Wincondition unit that only attacks structures and towers, ground unit
UNIT SPEEDS:
- Knight: speed 2 (medium)
- Archer: speed 1.5 (medium-slow)
- Giant: speed 1 (slow)
ELIXIR SYSTEM:
- Maximum: 10 elixir
- Regeneration: 1 elixir every 2.0 seconds (120 ticks at 60fps)
- Starting amount: 5 elixir
GAME RULES:
- Match duration: 2 minutes (120 seconds)
- Victory: Most towers destroyed wins
- Tiebreaker: Remaining tower health
- Draw: Equal towers destroyed and equal remaining health
*/
var gameStarted = false;
var gameTime = 250; // 250 seconds total duration
var elixir = 5; // Starting elixir
var maxElixir = 10;
var elixirRegenRate = 114; // Ticks for 1.9 seconds at 60fps (1.9 * 60 = 114)
var doubleElixirRegenRate = 57; // Half the time for double speed (1.9 / 2 = 0.95 seconds = 57 ticks)
var lastElixirRegen = 0;
var aiElixir = 5; // AI starting elixir
var aiMaxElixir = 10;
var aiElixirRegenRate = 120; // Ticks for 2 seconds at 60fps
var doubleAiElixirRegenRate = 60; // Half the time for double speed (2 / 2 = 1 second = 60 ticks)
var lastAiElixirRegen = 0;
var doubleElixirActive = false;
var doubleElixirMessageShown = false;
var doubleElixirMessageText = null;
var doubleElixirMessageTimer = 0;
var playerUnits = [];
var enemyUnits = [];
var bullets = [];
var playerTowers = [];
var enemyTowers = [];
var playerTowersDestroyed = 0;
var enemyTowersDestroyed = 0;
var towersDestroyed = 0;
var gameEnded = false;
var aiLastDeploy = 0;
var aiDeployInterval = 240; // 4 seconds
// Card and UI variables
var draggedCard = null;
var menuElements = [];
var deckMenuElements = [];
var deckCards = [];
var archerCard, knightCard, giantCard, wizardCard, cannonCard;
var timerText, elixirText;
var battlefieldLine;
var showingDeckSelection = false;
var selectedDeck = ['archer', 'knight', 'giant', 'wizard', 'cannon']; // Default deck (minimum 5 cards)
var availableCards = ['archer', 'knight', 'giant', 'wizard', 'cannon', 'skeleton', 'skeletonArmy', 'fireball', 'minion', 'maquinaVoladora', 'megaesbirro', 'discharge', 'bruja', 'ballesta', 'montapuercos', 'hielo', 'lapida', 'golem', 'rompemuros', 'minero'];
// Card system variables
var activeCards = []; // Currently displayed 4 cards
var cardQueue = []; // Queue of remaining cards
var maxActiveCards = 4;
// Create main menu
function createMenu() {
// Title
var titleText = new Text2('CLASH ROYALE', {
size: 120,
fill: 0xFFD700
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 1024;
titleText.y = 800;
game.addChild(titleText);
menuElements.push(titleText);
// Play button background
var playButton = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 0.8,
x: 1024,
y: 1400
});
playButton.tint = 0x4CAF50;
game.addChild(playButton);
menuElements.push(playButton);
// Play button text
var playText = new Text2('JUGAR', {
size: 80,
fill: 0xFFFFFF
});
playText.anchor.set(0.5, 0.5);
playText.x = 1024;
playText.y = 1400;
game.addChild(playText);
menuElements.push(playText);
// Mazo button background
var mazoButton = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 0.8,
x: 1024,
y: 1600
});
mazoButton.tint = 0x9C27B0;
game.addChild(mazoButton);
menuElements.push(mazoButton);
// Mazo button text
var mazoText = new Text2('MAZO', {
size: 80,
fill: 0xFFFFFF
});
mazoText.anchor.set(0.5, 0.5);
mazoText.x = 1024;
mazoText.y = 1600;
game.addChild(mazoText);
menuElements.push(mazoText);
// Instructions
var instructionsText = new Text2('Arrastra las cartas al campo de batalla', {
size: 40,
fill: 0xFFFFFF
});
instructionsText.anchor.set(0.5, 0.5);
instructionsText.x = 1024;
instructionsText.y = 1800;
game.addChild(instructionsText);
menuElements.push(instructionsText);
// Make play button clickable
playButton.down = function (x, y, obj) {
startGame();
};
playText.down = function (x, y, obj) {
startGame();
};
// Make mazo button clickable
mazoButton.down = function (x, y, obj) {
showDeckSelection();
};
mazoText.down = function (x, y, obj) {
showDeckSelection();
};
}
function showDeckSelection() {
// Hide main menu
for (var i = 0; i < menuElements.length; i++) {
menuElements[i].alpha = 0.3;
}
showingDeckSelection = true;
createDeckMenu();
}
function createDeckMenu() {
// Title
var deckTitle = new Text2('SELECCIONA TU MAZO', {
size: 80,
fill: 0xFFD700
});
deckTitle.anchor.set(0.5, 0.5);
deckTitle.x = 1024;
deckTitle.y = 600;
game.addChild(deckTitle);
deckMenuElements.push(deckTitle);
// Instructions
var instructions = new Text2('Toca las cartas para añadir/quitar del mazo (min 5, max 12)', {
size: 35,
fill: 0xFFFFFF
});
instructions.anchor.set(0.5, 0.5);
instructions.x = 1024;
instructions.y = 700;
game.addChild(instructions);
deckMenuElements.push(instructions);
// Available cards display
var cardStartX = 300;
var cardSpacing = 350;
for (var i = 0; i < availableCards.length; i++) {
var cardType = availableCards[i];
var cardContainer = new Container();
cardContainer.x = cardStartX + i * cardSpacing;
// Position skeleton card 100px below archer, skeleton army 100px below knight, fireball 100px below giant, minion 100px below wizard, maquina voladora 100px below skeleton, discharge 100px below cannon
if (cardType === 'skeleton') {
var archerIndex = availableCards.indexOf('archer');
cardContainer.x = cardStartX + archerIndex * cardSpacing;
cardContainer.y = 1000; // 100px below archer
} else if (cardType === 'skeletonArmy') {
var knightIndex = availableCards.indexOf('knight');
cardContainer.x = cardStartX + knightIndex * cardSpacing;
cardContainer.y = 1000; // 100px below knight
} else if (cardType === 'fireball') {
var giantIndex = availableCards.indexOf('giant');
cardContainer.x = cardStartX + giantIndex * cardSpacing;
cardContainer.y = 1000; // 100px below giant
} else if (cardType === 'minion') {
var wizardIndex = availableCards.indexOf('wizard');
cardContainer.x = cardStartX + wizardIndex * cardSpacing;
cardContainer.y = 1000; // 100px below wizard
} else if (cardType === 'maquinaVoladora') {
var skeletonIndex = availableCards.indexOf('skeleton');
var archerIndex = availableCards.indexOf('archer');
cardContainer.x = cardStartX + archerIndex * cardSpacing;
cardContainer.y = 1100; // 100px below skeleton (200px total below archer)
} else if (cardType === 'megaesbirro') {
var skeletonArmyIndex = availableCards.indexOf('skeletonArmy');
var knightIndex = availableCards.indexOf('knight');
cardContainer.x = cardStartX + knightIndex * cardSpacing;
cardContainer.y = 1100; // 100px below skeleton army (200px total below knight)
} else if (cardType === 'discharge') {
var cannonIndex = availableCards.indexOf('cannon');
cardContainer.x = cardStartX + cannonIndex * cardSpacing;
cardContainer.y = 1000; // 100px below cannon
} else if (cardType === 'bruja') {
var fireballIndex = availableCards.indexOf('fireball');
cardContainer.x = cardStartX + fireballIndex * cardSpacing;
cardContainer.y = 1100; // 100px below fireball
} else if (cardType === 'ballesta') {
var minionIndex = availableCards.indexOf('minion');
var wizardIndex = availableCards.indexOf('wizard');
cardContainer.x = cardStartX + wizardIndex * cardSpacing;
cardContainer.y = 1100; // 100px below minion (200px total below wizard)
} else if (cardType === 'montapuercos') {
var dischargeIndex = availableCards.indexOf('discharge');
var cannonIndex = availableCards.indexOf('cannon');
cardContainer.x = cardStartX + cannonIndex * cardSpacing;
cardContainer.y = 1100; // 100px below discharge (200px total below cannon)
} else if (cardType === 'hielo') {
var montapuercosIndex = availableCards.indexOf('montapuercos');
var cannonIndex = availableCards.indexOf('cannon');
cardContainer.x = cardStartX + cannonIndex * cardSpacing;
cardContainer.y = 1200; // 100px below montapuercos (300px total below cannon)
} else if (cardType === 'lapida') {
var ballestaIndex = availableCards.indexOf('ballesta');
var wizardIndex = availableCards.indexOf('wizard');
cardContainer.x = cardStartX + wizardIndex * cardSpacing;
cardContainer.y = 1200; // 100px below ballesta (300px total below wizard)
} else if (cardType === 'golem') {
var brujaIndex = availableCards.indexOf('bruja');
var giantIndex = availableCards.indexOf('giant');
cardContainer.x = cardStartX + giantIndex * cardSpacing;
cardContainer.y = 1200; // 100px below bruja (300px total below giant)
} else if (cardType === 'rompemuros') {
var megaesbirroIndex = availableCards.indexOf('megaesbirro');
var knightIndex = availableCards.indexOf('knight');
cardContainer.x = cardStartX + knightIndex * cardSpacing;
cardContainer.y = 1200; // 100px below megaesbirro (300px total below knight)
} else if (cardType === 'minero') {
var maquinaVoladoraIndex = availableCards.indexOf('maquinaVoladora');
var archerIndex = availableCards.indexOf('archer');
cardContainer.x = cardStartX + archerIndex * cardSpacing;
cardContainer.y = 1200; // 100px below maquina voladora (300px total below archer)
} else {
cardContainer.y = 900;
}
// Card background
var cardBg = cardContainer.attachAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5
});
// Check if card is in selected deck
var isSelected = selectedDeck.indexOf(cardType) !== -1;
cardBg.tint = isSelected ? 0x4CAF50 : 0x607d8b;
// Create special cardslot container for bruja below fireball
if (cardType === 'fireball') {
// Create bruja cardslot container
var brujaCardSlot = new Container();
brujaCardSlot.x = 0;
brujaCardSlot.y = 100; // 100px below fireball
// Bruja cardslot background
var brujaCardBg = brujaCardSlot.attachAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5
});
brujaCardBg.tint = selectedDeck.indexOf('bruja') !== -1 ? 0x4CAF50 : 0x607d8b;
// Bruja icon
var brujaIcon = LK.getAsset('bruja', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.6,
scaleY: 0.6
});
brujaCardSlot.addChild(brujaIcon);
// Bruja cost
var brujaCost = new Text2('5', {
size: 25,
fill: 0xFFFFFF
});
brujaCost.anchor.set(0.5, 0.5);
brujaCost.x = 120;
brujaCost.y = -40;
brujaCardSlot.addChild(brujaCost);
// Selection indicator for bruja
if (selectedDeck.indexOf('bruja') !== -1) {
var brujaCheckmark = new Text2('✓', {
size: 40,
fill: 0xFFFFFF
});
brujaCheckmark.anchor.set(0.5, 0.5);
brujaCheckmark.x = 0;
brujaCheckmark.y = 80;
brujaCardSlot.addChild(brujaCheckmark);
}
// Make bruja cardslot clickable
brujaCardSlot.cardType = 'bruja';
brujaCardSlot.down = function (x, y, obj) {
toggleCardInDeck('bruja');
// Return true to stop event propagation to parent containers
return true;
};
cardContainer.addChild(brujaCardSlot);
deckMenuElements.push(brujaCardSlot);
}
// Card icon
var cardIcon = LK.getAsset(cardType === 'discharge' ? 'descarga_descardina' : cardType === 'maquinaVoladora' ? 'flying_machine' : cardType === 'bruja' ? 'bruja' : cardType, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.6,
scaleY: 0.6
});
cardContainer.addChild(cardIcon);
// Card cost
var cost = cardType === 'giant' ? 5 : cardType === 'wizard' ? 5 : cardType === 'bruja' ? 5 : cardType === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : cardType === 'ballesta' ? 6 : cardType === 'montapuercos' ? 4 : cardType === 'hielo' ? 4 : cardType === 'lapida' ? 3 : cardType === 'golem' ? 8 : cardType === 'rompemuros' ? 2 : cardType === 'minero' ? 3 : 3;
var costText = new Text2(cost.toString(), {
size: 25,
fill: 0xFFFFFF
});
costText.anchor.set(0.5, 0.5);
costText.x = 120;
costText.y = -40;
cardContainer.addChild(costText);
// Selection indicator
if (isSelected) {
var checkmark = new Text2('✓', {
size: 40,
fill: 0xFFFFFF
});
checkmark.anchor.set(0.5, 0.5);
checkmark.x = 0;
checkmark.y = 80;
cardContainer.addChild(checkmark);
}
// Make card clickable
cardContainer.cardType = cardType;
cardContainer.down = function (x, y, obj) {
// For fireball, check if click is in the bruja area to prevent double selection
if (this.cardType === 'fireball' && y > 50) {
// Click is in the lower area where bruja cardslot is, ignore it
return true;
}
toggleCardInDeck(this.cardType);
};
game.addChild(cardContainer);
deckMenuElements.push(cardContainer);
}
// Selected deck display
var deckTitle2 = new Text2('MAZO ACTUAL (' + selectedDeck.length + '/12)', {
size: 60,
fill: 0x4CAF50
});
deckTitle2.anchor.set(0.5, 0.5);
deckTitle2.x = 1024;
deckTitle2.y = 1200;
game.addChild(deckTitle2);
deckMenuElements.push(deckTitle2);
// Display selected cards
var selectedStartX = 224;
var selectedSpacing = 150;
for (var i = 0; i < selectedDeck.length && i < 12; i++) {
var cardType = selectedDeck[i];
var selectedCardContainer = new Container();
// Display cards in two rows if more than 8
if (i < 8) {
selectedCardContainer.x = selectedStartX + i * selectedSpacing;
selectedCardContainer.y = 1400;
} else {
selectedCardContainer.x = selectedStartX + (i - 8) * selectedSpacing;
selectedCardContainer.y = 1500;
}
// Small card background
var selectedCardBg = selectedCardContainer.attachAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.6,
scaleY: 0.6
});
selectedCardBg.tint = 0x2196F3;
// Small card icon
var selectedCardIcon = LK.getAsset(cardType === 'discharge' ? 'descarga_descardina' : cardType === 'maquinaVoladora' ? 'flying_machine' : cardType === 'bruja' ? 'bruja' : cardType, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.35,
scaleY: 0.35
});
selectedCardContainer.addChild(selectedCardIcon);
game.addChild(selectedCardContainer);
deckMenuElements.push(selectedCardContainer);
}
// Back button
var backButton = LK.getAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 0.6,
x: 1024,
y: 1650
});
backButton.tint = 0xFF9800;
game.addChild(backButton);
deckMenuElements.push(backButton);
var backText = new Text2('VOLVER', {
size: 50,
fill: 0xFFFFFF
});
backText.anchor.set(0.5, 0.5);
backText.x = 1024;
backText.y = 1650;
game.addChild(backText);
deckMenuElements.push(backText);
// Make back button clickable
backButton.down = function (x, y, obj) {
hideDeckSelection();
};
backText.down = function (x, y, obj) {
hideDeckSelection();
};
}
function toggleCardInDeck(cardType) {
var cardIndex = selectedDeck.indexOf(cardType);
if (cardIndex !== -1) {
// Only allow removal if deck has more than 5 cards
if (selectedDeck.length > 5) {
selectedDeck.splice(cardIndex, 1);
}
} else if (selectedDeck.length < 12) {
// Add card to deck if not full (max 12 cards)
selectedDeck.push(cardType);
}
// Refresh deck menu
refreshDeckMenu();
}
function refreshDeckMenu() {
// Remove current deck menu elements
for (var i = 0; i < deckMenuElements.length; i++) {
deckMenuElements[i].destroy();
}
deckMenuElements = [];
// Recreate deck menu
createDeckMenu();
}
function hideDeckSelection() {
// Remove deck menu elements
for (var i = 0; i < deckMenuElements.length; i++) {
deckMenuElements[i].destroy();
}
deckMenuElements = [];
// Show main menu again
for (var i = 0; i < menuElements.length; i++) {
menuElements[i].alpha = 1;
}
showingDeckSelection = false;
}
function startGame() {
// Remove menu elements
for (var i = 0; i < menuElements.length; i++) {
menuElements[i].destroy();
}
menuElements = [];
// Start the actual game
gameStarted = true;
initializeGame();
}
function initializeGame() {
// Generate AI's randomized deck for this match
generateAIDeck();
// Create battlefield divider
battlefieldLine = game.addChild(LK.getAsset('battlefield', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
battlefieldLine.tint = 0x000000;
battlefieldLine.alpha = 0.3;
// Create towers
// Player towers (bottom)
var playerLeftTower = game.addChild(new Tower(true, false));
playerLeftTower.x = 512;
playerLeftTower.y = 2400;
playerTowers.push(playerLeftTower);
var playerRightTower = game.addChild(new Tower(true, false));
playerRightTower.x = 1536;
playerRightTower.y = 2400;
playerTowers.push(playerRightTower);
var playerKingTower = game.addChild(new Tower(true, true));
playerKingTower.x = 1024;
playerKingTower.y = 2600;
playerTowers.push(playerKingTower);
// Enemy towers (top)
var enemyLeftTower = game.addChild(new Tower(false, false));
enemyLeftTower.x = 512;
enemyLeftTower.y = 400;
enemyTowers.push(enemyLeftTower);
var enemyRightTower = game.addChild(new Tower(false, false));
enemyRightTower.x = 1536;
enemyRightTower.y = 400;
enemyTowers.push(enemyRightTower);
var enemyKingTower = game.addChild(new Tower(false, true));
enemyKingTower.x = 1024;
enemyKingTower.y = 200;
enemyTowers.push(enemyKingTower);
// UI Elements
timerText = new Text2(gameTime.toString(), {
size: 60,
fill: 0xFFFFFF
});
timerText.anchor.set(0.5, 0);
LK.gui.top.addChild(timerText);
elixirText = new Text2(elixir + "/" + maxElixir, {
size: 40,
fill: 0xE91E63
});
elixirText.anchor.set(0, 1);
elixirText.x = 50;
LK.gui.bottomLeft.addChild(elixirText);
// Initialize card system
activeCards = [];
cardQueue = [];
deckCards = [];
// Setup active cards (first 4) and queue (remaining)
for (var i = 0; i < selectedDeck.length; i++) {
if (i < maxActiveCards) {
activeCards.push(selectedDeck[i]);
} else {
cardQueue.push(selectedDeck[i]);
}
}
// Create visual cards for active cards only
var cardSpacing = 300;
var startX = -(maxActiveCards - 1) * cardSpacing / 2;
for (var i = 0; i < activeCards.length; i++) {
var cardType = activeCards[i];
var newCard = LK.gui.bottom.addChild(new Card(cardType));
newCard.x = startX + i * cardSpacing;
newCard.y = -100;
newCard.cardType = cardType; // Store card type for event handling
newCard.cardIndex = i; // Store index for replacement
deckCards.push(newCard);
}
// Store references for backward compatibility
if (activeCards.indexOf('archer') !== -1) {
archerCard = deckCards[activeCards.indexOf('archer')];
}
if (activeCards.indexOf('knight') !== -1) {
knightCard = deckCards[activeCards.indexOf('knight')];
}
if (activeCards.indexOf('giant') !== -1) {
giantCard = deckCards[activeCards.indexOf('giant')];
}
if (activeCards.indexOf('wizard') !== -1) {
wizardCard = deckCards[activeCards.indexOf('wizard')];
}
if (activeCards.indexOf('cannon') !== -1) {
cannonCard = deckCards[activeCards.indexOf('cannon')];
}
// Setup event handlers
setupEventHandlers();
}
function rotateCard(usedCardIndex) {
if (cardQueue.length === 0) {
return;
} // No cards in queue to rotate
// Get the next card from queue
var nextCard = cardQueue.shift(); // Remove first card from queue
var usedCard = activeCards[usedCardIndex]; // Get the used card
// Add used card to end of queue
cardQueue.push(usedCard);
// Replace active card with next card
activeCards[usedCardIndex] = nextCard;
// Update visual card
var cardToUpdate = deckCards[usedCardIndex];
if (cardToUpdate) {
// Store position and index
var cardX = cardToUpdate.x;
var cardY = cardToUpdate.y;
var cardIndex = cardToUpdate.cardIndex;
// Remove old card
cardToUpdate.destroy();
// Create new card with next card type
var newCard = LK.gui.bottom.addChild(new Card(nextCard));
newCard.x = cardX;
newCard.y = cardY;
newCard.cardType = nextCard;
newCard.cardIndex = cardIndex;
// Update references
deckCards[usedCardIndex] = newCard;
// Update event handler for new card
var cardType = newCard.cardType;
var cost = cardType === 'giant' ? 5 : cardType === 'wizard' ? 5 : cardType === 'bruja' ? 5 : cardType === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : 3;
newCard.down = function (x, y, obj) {
if (elixir >= cost) {
draggedCard = cardType;
}
};
}
}
function deployUnit(cardType, x, y, isPlayer) {
var cost = cardType === 'giant' ? 5 : cardType === 'wizard' ? 5 : cardType === 'bruja' ? 5 : cardType === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : cardType === 'ballesta' ? 6 : cardType === 'montapuercos' ? 4 : cardType === 'hielo' ? 4 : cardType === 'lapida' ? 3 : cardType === 'golem' ? 8 : cardType === 'rompemuros' ? 2 : cardType === 'minero' ? 3 : 3;
if (isPlayer && elixir < cost) {
return false;
}
if (!isPlayer && aiElixir < cost) {
return false;
} // AI also needs elixir
// Check AI card usage restriction
if (!isPlayer && !canAIUseCard(cardType)) {
return false; // AI can't use this card yet
}
// Check if position is in correct half (except for spells and minero)
if (cardType !== 'fireball' && cardType !== 'hielo' && cardType !== 'discharge' && cardType !== 'minero') {
if (isPlayer && y < 1366) {
return false;
} // Player can only deploy in bottom half
if (!isPlayer && y > 1366) {
return false;
} // AI can only deploy in top half
}
var unit = null;
if (cardType === 'archer') {
// Deploy 2 archers
var archer1 = new Archer(isPlayer);
var archer2 = new Archer(isPlayer);
archer1.x = x - 40; // Left archer
archer1.y = y;
archer2.x = x + 40; // Right archer
archer2.y = y;
if (isPlayer) {
playerUnits.push(archer1);
playerUnits.push(archer2);
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
enemyUnits.push(archer1);
enemyUnits.push(archer2);
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
game.addChild(archer1);
game.addChild(archer2);
LK.getSound('deploy').play();
return true;
} else if (cardType === 'knight') {
unit = new Knight(isPlayer);
} else if (cardType === 'giant') {
unit = new Giant(isPlayer);
} else if (cardType === 'wizard') {
unit = new Wizard(isPlayer);
} else if (cardType === 'skeleton') {
// Deploy 3 skeletons in formation: front, back-right, back-left
var skeleton1 = new Skeleton(isPlayer); // Front skeleton
var skeleton2 = new Skeleton(isPlayer); // Back-right skeleton
var skeleton3 = new Skeleton(isPlayer); // Back-left skeleton
skeleton1.x = x; // Front skeleton at deployment position
skeleton1.y = y;
skeleton2.x = x + 60; // Back-right skeleton
skeleton2.y = isPlayer ? y + 60 : y - 60; // Adjust based on player side
skeleton3.x = x - 60; // Back-left skeleton
skeleton3.y = isPlayer ? y + 60 : y - 60; // Adjust based on player side
if (isPlayer) {
playerUnits.push(skeleton1);
playerUnits.push(skeleton2);
playerUnits.push(skeleton3);
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
enemyUnits.push(skeleton1);
enemyUnits.push(skeleton2);
enemyUnits.push(skeleton3);
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
game.addChild(skeleton1);
game.addChild(skeleton2);
game.addChild(skeleton3);
LK.getSound('deploy').play();
return true;
} else if (cardType === 'skeletonArmy') {
// Deploy 15 skeletons in a 3x5 formation
var skeletons = [];
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 5; col++) {
var skeleton = new Skeleton(isPlayer);
skeleton.x = x + (col - 2) * 40; // Spread horizontally (adjusted for 5 columns)
skeleton.y = y + (row - 1) * 40; // Spread vertically
skeletons.push(skeleton);
game.addChild(skeleton);
}
}
if (isPlayer) {
for (var s = 0; s < skeletons.length; s++) {
playerUnits.push(skeletons[s]);
}
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
for (var s = 0; s < skeletons.length; s++) {
enemyUnits.push(skeletons[s]);
}
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
LK.getSound('deploy').play();
return true;
} else if (cardType === 'fireball') {
// Fireball is a spell - can be deployed anywhere on the map
var fireball = new Fireball(x, y, isPlayer);
bullets.push(fireball); // Add to bullets array for cleanup
game.addChild(fireball);
if (isPlayer) {
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
LK.getSound('deploy').play();
return true;
} else if (cardType === 'discharge') {
// Discharge is a spell - can be deployed anywhere on the map, appears instantly
var discharge = new Discharge(x, y, isPlayer);
// Don't add to bullets array since it destroys itself immediately
game.addChild(discharge);
if (isPlayer) {
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
LK.getSound('deploy').play();
return true;
} else if (cardType === 'minion') {
// Deploy 3 minions in formation like skeletons: front, back-right, back-left
var minion1 = new Minion(isPlayer); // Front minion
var minion2 = new Minion(isPlayer); // Back-right minion
var minion3 = new Minion(isPlayer); // Back-left minion
minion1.x = x; // Front minion at deployment position
minion1.y = y;
minion2.x = x + 60; // Back-right minion
minion2.y = isPlayer ? y + 60 : y - 60; // Adjust based on player side
minion3.x = x - 60; // Back-left minion
minion3.y = isPlayer ? y + 60 : y - 60; // Adjust based on player side
if (isPlayer) {
playerUnits.push(minion1);
playerUnits.push(minion2);
playerUnits.push(minion3);
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
enemyUnits.push(minion1);
enemyUnits.push(minion2);
enemyUnits.push(minion3);
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
game.addChild(minion1);
game.addChild(minion2);
game.addChild(minion3);
LK.getSound('deploy').play();
return true;
} else if (cardType === 'maquinaVoladora') {
// Deploy single maquina voladora
unit = new MaquinaVoladora(isPlayer);
} else if (cardType === 'megaesbirro') {
// Deploy single megaesbirro
unit = new Megaesbirro(isPlayer);
} else if (cardType === 'bruja') {
// Deploy single bruja
unit = new Bruja(isPlayer);
} else if (cardType === 'ballesta') {
unit = new Ballesta(isPlayer);
} else if (cardType === 'montapuercos') {
unit = new Montapuercos(isPlayer);
} else if (cardType === 'lapida') {
unit = new Lapida(isPlayer);
} else if (cardType === 'golem') {
unit = new Golem(isPlayer, false); // false for regular golem (not mini)
} else if (cardType === 'rompemuros') {
// Deploy 2 rompemuros units - one left, one right
var rompemuros1 = new Rompemuros(isPlayer);
var rompemuros2 = new Rompemuros(isPlayer);
rompemuros1.x = x - 40; // Left rompemuros
rompemuros1.y = y;
rompemuros2.x = x + 40; // Right rompemuros
rompemuros2.y = y;
if (isPlayer) {
playerUnits.push(rompemuros1);
playerUnits.push(rompemuros2);
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
enemyUnits.push(rompemuros1);
enemyUnits.push(rompemuros2);
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
game.addChild(rompemuros1);
game.addChild(rompemuros2);
LK.getSound('deploy').play();
return true;
} else if (cardType === 'hielo') {
// Hielo is a spell - can be deployed anywhere on the map, appears instantly
var hielo = new Hielo(x, y, isPlayer);
// Don't add to bullets array since it destroys itself immediately
game.addChild(hielo);
if (isPlayer) {
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
LK.getSound('deploy').play();
return true;
} else if (cardType === 'minero') {
unit = new Minero(isPlayer);
// Special placement logic for minero - can be placed anywhere but needs tunneling
unit.tunnelTargetX = x;
unit.tunnelTargetY = y;
// Don't set initial position here - let minero handle its own tunneling logic
// Remove spawn delay for minero since tunneling acts as the delay
unit.spawnTimer = 0;
unit.isSpawning = false;
unit.spawnTimerText.setText('');
// Start tunneling immediately
unit.startTunneling(x, y);
} else if (cardType === 'cannon') {
unit = new Cannon(isPlayer);
}
if (unit) {
if (cardType !== 'minero') {
unit.x = x;
unit.y = y;
}
if (isPlayer) {
playerUnits.push(unit);
elixir -= cost;
elixirText.setText(elixir + "/" + maxElixir);
// Rotate card if player used it
var cardIndex = activeCards.indexOf(cardType);
if (cardIndex !== -1) {
rotateCard(cardIndex);
}
} else {
enemyUnits.push(unit);
aiElixir -= cost; // Deduct AI elixir
updateAICardUsage(cardType); // Track AI card usage
}
game.addChild(unit);
LK.getSound('deploy').play();
return true;
}
return false;
}
// AI deployment variables
var aiLastUsedCard = null;
var aiCardCooldown = {};
var aiDeck = []; // AI's randomized deck for the match
var aiUsedCards = []; // Track recently used cards
var aiCardUsageRestriction = {}; // Track cards that can't be used yet
// Generate AI deck with required constraints
function generateAIDeck() {
aiDeck = [];
// Required cards (must have)
var requiredCards = {
aerial: Math.random() < 0.33 ? 'archer' : Math.random() < 0.5 ? 'bruja' : 'wizard',
// Card that can attack aerial units (randomly choose between archer, bruja, and wizard)
wincondition: Math.random() < 0.25 ? 'giant' : Math.random() < 0.33 ? 'montapuercos' : Math.random() < 0.5 ? 'golem' : 'rompemuros',
// Wincondition card (randomly choose between giant, montapuercos, and golem)
spell: Math.random() < 0.33 ? 'fireball' : Math.random() < 0.5 ? 'discharge' : 'hielo',
// Spell card (randomly choose between fireball, discharge, and hielo)
structure: Math.random() < 0.33 ? 'cannon' : Math.random() < 0.5 ? 'ballesta' : 'lapida' // Structure card (randomly choose between cannon, ballesta, and lapida)
};
// Add required cards to AI deck
aiDeck.push(requiredCards.aerial);
aiDeck.push(requiredCards.wincondition);
aiDeck.push(requiredCards.spell);
aiDeck.push(requiredCards.structure);
// Remaining card pool (excluding already added required cards)
var remainingCards = ['knight', 'wizard', 'bruja', 'skeleton', 'skeletonArmy', 'minion', 'maquinaVoladora', 'megaesbirro', 'ballesta', 'montapuercos', 'hielo', 'lapida', 'golem', 'minero'];
// Fill remaining 8 slots with random cards from remaining pool
while (aiDeck.length < 12) {
var randomIndex = Math.floor(Math.random() * remainingCards.length);
var randomCard = remainingCards[randomIndex];
aiDeck.push(randomCard);
// Don't remove from remainingCards to allow duplicates
}
// Shuffle the deck to randomize order
for (var i = aiDeck.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = aiDeck[i];
aiDeck[i] = aiDeck[j];
aiDeck[j] = temp;
}
}
// AI deployment function
function aiDeploy() {
// Check if AI is being attacked (player units in AI territory)
var isUnderAttack = false;
var playerUnitsInAITerritory = 0;
for (var i = 0; i < playerUnits.length; i++) {
if (playerUnits[i].y < 1366) {
// Player units in AI half
isUnderAttack = true;
playerUnitsInAITerritory++;
}
}
// Check if AI towers are taking damage
var aiTowersUnderAttack = false;
for (var i = 0; i < enemyTowers.length; i++) {
if (enemyTowers[i].currentHealth < enemyTowers[i].maxHealth) {
aiTowersUnderAttack = true;
break;
}
}
if (isUnderAttack || aiTowersUnderAttack) {
// DEFENSE MODE: Deploy defensive units quickly if being attacked
if (LK.ticks - aiLastDeploy > 60 && aiElixir >= 3) {
// Deploy faster when defending
var defensiveCards = [];
// Check if player has used wincondition (giant) or if towers are damaged
var playerHasWincondition = false;
for (var i = 0; i < playerUnits.length; i++) {
if (playerUnits[i].unitType === 'giant') {
playerHasWincondition = true;
break;
}
}
if (aiElixir >= 3) {
// Use cards from AI's deck for defense
for (var d = 0; d < aiDeck.length; d++) {
var deckCard = aiDeck[d];
var cardCost = deckCard === 'giant' ? 5 : deckCard === 'wizard' ? 5 : deckCard === 'bruja' ? 5 : deckCard === 'skeleton' ? 1 : deckCard === 'skeletonArmy' ? 3 : deckCard === 'fireball' ? 4 : deckCard === 'discharge' ? 2 : deckCard === 'minion' ? 3 : deckCard === 'maquinaVoladora' ? 4 : deckCard === 'megaesbirro' ? 3 : deckCard === 'ballesta' ? 6 : deckCard === 'montapuercos' ? 4 : deckCard === 'hielo' ? 4 : deckCard === 'lapida' ? 3 : deckCard === 'golem' ? 8 : deckCard === 'rompemuros' ? 2 : deckCard === 'minero' ? 3 : 3;
if (aiElixir >= cardCost && canAIUseCard(deckCard)) {
// Add defensive units (not spells for defense)
if (deckCard !== 'fireball' && deckCard !== 'discharge') {
defensiveCards.push(deckCard);
}
}
}
}
// Check for spell usage opportunities (discharge, fireball, and hielo for defense)
var spellCards = [];
for (var s = 0; s < aiDeck.length; s++) {
var spellCard = aiDeck[s];
if (spellCard === 'discharge' || spellCard === 'fireball' || spellCard === 'hielo') {
var spellCost = spellCard === 'discharge' ? 2 : spellCard === 'hielo' ? 4 : 4;
if (aiElixir >= spellCost) {
spellCards.push(spellCard);
}
}
}
if (spellCards.length > 0 && playerUnits.length > 0) {
// Find player unit with most enemies nearby for spell targeting
var bestTarget = null;
var maxNearbyUnits = 0;
for (var p = 0; p < playerUnits.length; p++) {
var playerUnit = playerUnits[p];
if (!playerUnit.isDead) {
var nearbyCount = 0;
// Count nearby player units
for (var n = 0; n < playerUnits.length; n++) {
var otherUnit = playerUnits[n];
if (!otherUnit.isDead && otherUnit !== playerUnit) {
var distance = Math.sqrt(Math.pow(playerUnit.x - otherUnit.x, 2) + Math.pow(playerUnit.y - otherUnit.y, 2));
if (distance <= 200) {
// Within spell effect range
nearbyCount++;
}
}
}
if (nearbyCount > maxNearbyUnits) {
maxNearbyUnits = nearbyCount;
bestTarget = playerUnit;
}
}
}
// Use spell if we found a good target (at least 1 nearby unit or any unit if no clusters)
if (bestTarget && (maxNearbyUnits > 0 || playerUnits.length > 0)) {
var chosenSpell = spellCards[Math.floor(Math.random() * spellCards.length)];
if (deployUnit(chosenSpell, bestTarget.x, bestTarget.y, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 120 + Math.random() * 60;
return; // Exit early after using spell
}
}
}
if (defensiveCards.length > 0) {
var randomCard = defensiveCards[Math.floor(Math.random() * defensiveCards.length)];
var deployX, deployY;
// Special placement logic for minero - place near player towers/structures
if (randomCard === 'minero') {
// Find closest player tower or structure to place minero near it
var closestTarget = null;
var closestDistance = Infinity;
var allPlayerTargets = playerTowers.concat(playerUnits.filter(function (unit) {
return unit.unitType === 'cannon' || unit.unitType === 'ballesta' || unit.unitType === 'lapida';
}));
for (var t = 0; t < allPlayerTargets.length; t++) {
var target = allPlayerTargets[t];
if (!target.isDead) {
var distance = Math.sqrt(Math.pow(1024 - target.x, 2) + Math.pow(1366 - target.y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestTarget = target;
}
}
}
if (closestTarget) {
// Place minero within 50 units of the closest target
var angle = Math.random() * Math.PI * 2;
deployX = closestTarget.x + Math.cos(angle) * (30 + Math.random() * 20);
deployY = closestTarget.y + Math.sin(angle) * (30 + Math.random() * 20);
} else {
// Fallback to random position if no target found
deployX = 300 + Math.random() * 1448;
deployY = 800 + Math.random() * 400;
}
} else {
// Deploy near threatened area for other cards
deployX = 300 + Math.random() * 1448;
deployY = 800 + Math.random() * 400; // Deploy in middle-upper area
}
if (deployUnit(randomCard, deployX, deployY, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 120 + Math.random() * 60; // Quick deploy interval when defending
}
}
}
} else {
// ATTACK MODE: Use wincondition strategy when not under attack
// Check for spell usage opportunities in attack mode (hielo excluded - defensive only)
if (aiElixir >= 2 && playerUnits.length > 0 && Math.random() < 0.3) {
// 30% chance to use spells in attack
var attackSpellCards = [];
for (var s = 0; s < aiDeck.length; s++) {
var spellCard = aiDeck[s];
if (spellCard === 'discharge' || spellCard === 'fireball') {
var spellCost = spellCard === 'discharge' ? 2 : 4;
if (aiElixir >= spellCost) {
attackSpellCards.push(spellCard);
}
}
}
if (attackSpellCards.length > 0) {
// Target strongest or most clustered player units
var bestAttackTarget = null;
var maxValue = 0;
for (var p = 0; p < playerUnits.length; p++) {
var playerUnit = playerUnits[p];
if (!playerUnit.isDead) {
// Calculate value based on unit health + nearby units
var unitValue = playerUnit.currentHealth;
// Count nearby player units for cluster bonus
for (var n = 0; n < playerUnits.length; n++) {
var otherUnit = playerUnits[n];
if (!otherUnit.isDead && otherUnit !== playerUnit) {
var distance = Math.sqrt(Math.pow(playerUnit.x - otherUnit.x, 2) + Math.pow(playerUnit.y - otherUnit.y, 2));
if (distance <= 200) {
unitValue += otherUnit.currentHealth * 0.5; // Bonus for clustered units
}
}
}
if (unitValue > maxValue) {
maxValue = unitValue;
bestAttackTarget = playerUnit;
}
}
}
if (bestAttackTarget) {
var chosenAttackSpell = attackSpellCards[Math.floor(Math.random() * attackSpellCards.length)];
if (deployUnit(chosenAttackSpell, bestAttackTarget.x, bestAttackTarget.y, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 180 + Math.random() * 120;
return; // Exit after using spell
}
}
}
}
// Check if AI already has giants on the field
var giantsOnField = 0;
for (var i = 0; i < enemyUnits.length; i++) {
if (enemyUnits[i].unitType === 'giant' && !enemyUnits[i].isDead) {
giantsOnField++;
}
}
if (aiElixir >= 8 && LK.ticks - aiLastDeploy > aiDeployInterval && giantsOnField < 2) {
// Big push with wincondition + support (max 2 giants on field)
if (aiElixir >= 8 && Math.random() < 0.9) {
// 90% chance for big attack when having 8+ elixir
// Find wincondition card in AI deck
var winconditionCard = null;
for (var w = 0; w < aiDeck.length; w++) {
if (aiDeck[w] === 'giant' || aiDeck[w] === 'montapuercos' || aiDeck[w] === 'golem' || aiDeck[w] === 'rompemuros') {
winconditionCard = aiDeck[w];
break;
}
}
if (winconditionCard) {
// Deploy wincondition first
var deployX = 300 + Math.random() * 1448;
var deployY = 200 + Math.random() * 600;
if (deployUnit(winconditionCard, deployX, deployY, false)) {
var winconditionCost = winconditionCard === 'giant' ? 5 : winconditionCard === 'montapuercos' ? 4 : winconditionCard === 'golem' ? 8 : 3;
aiElixir -= winconditionCost;
// Wait a bit then deploy support unit behind wincondition
LK.setTimeout(function () {
if (aiElixir >= 3) {
var supportCards = [];
// Get support cards from AI deck
for (var s = 0; s < aiDeck.length; s++) {
var supportCard = aiDeck[s];
var supportCost = supportCard === 'giant' ? 5 : supportCard === 'wizard' ? 5 : supportCard === 'skeleton' ? 1 : supportCard === 'skeletonArmy' ? 3 : supportCard === 'fireball' ? 4 : supportCard === 'discharge' ? 2 : supportCard === 'minion' ? 3 : supportCard === 'montapuercos' ? 4 : supportCard === 'hielo' ? 4 : supportCard === 'ballesta' ? 6 : supportCard === 'lapida' ? 3 : supportCard === 'golem' ? 8 : supportCard === 'rompemuros' ? 2 : supportCard === 'minero' ? 3 : 3;
if (aiElixir >= supportCost && supportCard !== 'fireball' && supportCard !== 'discharge' && supportCard !== 'hielo' && supportCard !== winconditionCard) {
supportCards.push(supportCard);
}
}
// Check for Ballesta offensive deployment near middle line
if (aiElixir >= 6 && Math.random() < 0.3) {
// 30% chance when AI has enough elixir
var hasBallesta = false;
for (var b = 0; b < aiDeck.length; b++) {
if (aiDeck[b] === 'ballesta') {
hasBallesta = true;
break;
}
}
if (hasBallesta) {
// Deploy Ballesta near middle line for cross-field tower attacks
var ballestaX = 300 + Math.random() * 1448;
var ballestaY = 1316 + Math.random() * 100; // Near middle line (1366 ± 50)
if (deployUnit('ballesta', ballestaX, ballestaY, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 300 + Math.random() * 120;
return; // Exit after deploying Ballesta
}
}
}
if (supportCards.length > 0) {
var chosenSupport = supportCards[Math.floor(Math.random() * supportCards.length)];
var supportX = deployX + (Math.random() - 0.5) * 200; // Near wincondition
var supportY = deployY + 100 + Math.random() * 200; // Behind wincondition
deployUnit(chosenSupport, supportX, supportY, false);
}
}
}, 1000); // 1 second delay
aiLastDeploy = LK.ticks;
aiDeployInterval = 300 + Math.random() * 180; // Longer interval after big push
}
}
}
} else if (aiElixir >= 5 && aiElixir < 8 && LK.ticks - aiLastDeploy > aiDeployInterval && giantsOnField < 2) {
// Single wincondition deploy when having 5-7 elixir (more likely to save)
if (Math.random() < 0.4) {
// 40% chance to use wincondition (reduced from 60% to encourage saving)
var winconditionCard = null;
for (var w = 0; w < aiDeck.length; w++) {
if (aiDeck[w] === 'giant' || aiDeck[w] === 'montapuercos' || aiDeck[w] === 'golem' || aiDeck[w] === 'rompemuros') {
winconditionCard = aiDeck[w];
break;
}
}
if (winconditionCard) {
var deployX = 300 + Math.random() * 1448;
var deployY = 200 + Math.random() * 600;
if (deployUnit(winconditionCard, deployX, deployY, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 240 + Math.random() * 120;
}
}
} else {
// Regular unit deployment (less likely when saving)
if (aiElixir >= 3 && Math.random() < 0.3) {
var availableCards = [];
for (var a = 0; a < aiDeck.length; a++) {
var deckCard = aiDeck[a];
var cardCost = deckCard === 'giant' ? 5 : deckCard === 'wizard' ? 5 : deckCard === 'bruja' ? 5 : deckCard === 'skeleton' ? 1 : deckCard === 'skeletonArmy' ? 3 : deckCard === 'fireball' ? 4 : deckCard === 'discharge' ? 2 : deckCard === 'minion' ? 3 : deckCard === 'maquinaVoladora' ? 4 : deckCard === 'megaesbirro' ? 3 : deckCard === 'ballesta' ? 6 : deckCard === 'hielo' ? 4 : deckCard === 'lapida' ? 3 : deckCard === 'golem' ? 8 : deckCard === 'rompemuros' ? 2 : deckCard === 'minero' ? 3 : 3;
if (aiElixir >= cardCost && deckCard !== 'fireball' && deckCard !== 'discharge' && deckCard !== 'hielo') {
availableCards.push(deckCard);
}
}
if (availableCards.length > 0) {
var randomCard = availableCards[Math.floor(Math.random() * availableCards.length)];
var deployX = 300 + Math.random() * 1448;
var deployY = 200 + Math.random() * 1000;
if (deployUnit(randomCard, deployX, deployY, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 180 + Math.random() * 120;
}
}
}
}
} else if (aiElixir >= 3 && aiElixir < 5 && LK.ticks - aiLastDeploy > aiDeployInterval * 3) {
// Wait even longer when having low elixir (3-4), prioritize saving for wincondition
// Only deploy if waiting for too long or in emergency
if (Math.random() < 0.15) {
// Only 15% chance to deploy regular units when saving (reduced from 30%)
var lowCostCards = [];
for (var l = 0; l < aiDeck.length; l++) {
var deckCard = aiDeck[l];
var cardCost = deckCard === 'giant' ? 5 : deckCard === 'wizard' ? 5 : deckCard === 'bruja' ? 5 : deckCard === 'skeleton' ? 1 : deckCard === 'skeletonArmy' ? 3 : deckCard === 'fireball' ? 4 : deckCard === 'discharge' ? 2 : deckCard === 'minion' ? 3 : deckCard === 'maquinaVoladora' ? 4 : deckCard === 'megaesbirro' ? 3 : deckCard === 'ballesta' ? 6 : deckCard === 'hielo' ? 4 : deckCard === 'lapida' ? 3 : deckCard === 'golem' ? 8 : deckCard === 'rompemuros' ? 2 : deckCard === 'minero' ? 3 : 3;
if (aiElixir >= cardCost && deckCard !== 'fireball' && deckCard !== 'discharge' && deckCard !== 'hielo' && cardCost <= 4) {
lowCostCards.push(deckCard);
}
}
if (lowCostCards.length > 0) {
var randomCard = lowCostCards[Math.floor(Math.random() * lowCostCards.length)];
var deployX = 300 + Math.random() * 1448;
var deployY = 200 + Math.random() * 1000;
if (deployUnit(randomCard, deployX, deployY, false)) {
aiLastDeploy = LK.ticks;
aiDeployInterval = 240 + Math.random() * 120;
}
}
}
}
}
}
function checkGameEnd() {
if (gameTime <= 0 && !gameEnded) {
gameEnded = true;
if (playerTowersDestroyed > enemyTowersDestroyed) {
LK.showGameOver();
} else if (enemyTowersDestroyed > playerTowersDestroyed) {
LK.showYouWin();
} else {
// Tie-breaker: check tower health
var playerTotalHealth = 0;
var enemyTotalHealth = 0;
for (var i = 0; i < playerTowers.length; i++) {
playerTotalHealth += playerTowers[i].currentHealth;
}
for (var i = 0; i < enemyTowers.length; i++) {
enemyTotalHealth += enemyTowers[i].currentHealth;
}
if (playerTotalHealth > enemyTotalHealth) {
LK.showYouWin();
} else if (enemyTotalHealth > playerTotalHealth) {
LK.showGameOver();
} else {
LK.showGameOver(); // Draw counts as loss
}
}
}
}
function setupEventHandlers() {
// Event handlers for active deck cards only
for (var i = 0; i < deckCards.length; i++) {
var card = deckCards[i];
var cardType = card.cardType;
var cost = cardType === 'giant' ? 5 : cardType === 'wizard' ? 5 : cardType === 'bruja' ? 5 : cardType === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : cardType === 'ballesta' ? 6 : cardType === 'montapuercos' ? 4 : cardType === 'hielo' ? 4 : cardType === 'lapida' ? 3 : cardType === 'golem' ? 8 : cardType === 'rompemuros' ? 2 : cardType === 'minero' ? 3 : 3;
// Create closure to capture cardType and cost
(function (type, cardCost, index) {
card.down = function (x, y, obj) {
if (elixir >= cardCost) {
draggedCard = type;
}
};
})(cardType, cost, i);
}
game.down = function (x, y, obj) {
// Only handle game events if game has started
if (!gameStarted) {
return;
}
// This will handle deployment when clicking on the battlefield
};
game.up = function (x, y, obj) {
// Only handle game events if game has started
if (!gameStarted) {
return;
}
if (draggedCard) {
// Convert coordinates to game space
var gamePos = {
x: x,
y: y
};
if (obj && obj.parent) {
var globalPos = obj.parent.toGlobal ? obj.parent.toGlobal(obj.position) : {
x: x,
y: y
};
gamePos = game.toLocal ? game.toLocal(globalPos) : {
x: x,
y: y
};
}
if (deployUnit(draggedCard, gamePos.x, gamePos.y, true)) {
// Unit deployed successfully
}
draggedCard = null;
}
};
}
// Initialize menu
createMenu();
game.update = function () {
// Only run game logic if game has started
if (!gameStarted) {
return;
}
if (gameEnded) {
return;
}
// Update timer
if (LK.ticks % 60 === 0 && gameTime > 0) {
gameTime--;
timerText.setText(gameTime.toString());
}
// Check for double elixir activation at 120 seconds remaining (130 seconds elapsed)
if (gameTime === 130 && !doubleElixirActive) {
doubleElixirActive = true;
doubleElixirMessageShown = false;
// Create double elixir message
doubleElixirMessageText = new Text2('¡ELIXIR DOBLE ACTIVADO!', {
size: 80,
fill: 0xFFD700
});
doubleElixirMessageText.anchor.set(0.5, 0.5);
doubleElixirMessageText.x = 1024;
doubleElixirMessageText.y = 300;
game.addChild(doubleElixirMessageText);
// Set message timer for 3 seconds
doubleElixirMessageTimer = 180; // 3 seconds at 60fps
// Flash effect for the message
tween(doubleElixirMessageText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
onFinish: function onFinish() {
tween(doubleElixirMessageText, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 500
});
}
});
}
// Handle double elixir message timer
if (doubleElixirMessageTimer > 0) {
doubleElixirMessageTimer--;
if (doubleElixirMessageTimer <= 0 && doubleElixirMessageText) {
doubleElixirMessageText.destroy();
doubleElixirMessageText = null;
}
}
// Regenerate elixir (double speed after 120 seconds passed)
var currentElixirRegenRate = doubleElixirActive ? doubleElixirRegenRate : elixirRegenRate;
if (LK.ticks - lastElixirRegen >= currentElixirRegenRate && elixir < maxElixir) {
elixir++;
elixirText.setText(elixir + "/" + maxElixir);
lastElixirRegen = LK.ticks;
}
// Regenerate AI elixir (double speed after 120 seconds passed)
var currentAiElixirRegenRate = doubleElixirActive ? doubleAiElixirRegenRate : aiElixirRegenRate;
if (LK.ticks - lastAiElixirRegen >= currentAiElixirRegenRate && aiElixir < aiMaxElixir) {
aiElixir++;
lastAiElixirRegen = LK.ticks;
}
// AI deployment
aiDeploy();
// Clean up dead units
for (var i = playerUnits.length - 1; i >= 0; i--) {
if (playerUnits[i].isDead) {
playerUnits[i].destroy();
playerUnits.splice(i, 1);
}
}
for (var i = enemyUnits.length - 1; i >= 0; i--) {
if (enemyUnits[i].isDead) {
enemyUnits[i].destroy();
enemyUnits.splice(i, 1);
}
}
// Clean up bullets
for (var i = bullets.length - 1; i >= 0; i--) {
if (bullets[i].destroyed || bullets[i].y < -100 || bullets[i].y > 2800) {
bullets[i].destroy();
bullets.splice(i, 1);
}
}
// Check for immediate game end conditions
var allPlayerTowersDead = true;
var allEnemyTowersDead = true;
for (var i = 0; i < playerTowers.length; i++) {
if (!playerTowers[i].isDead) {
allPlayerTowersDead = false;
break;
}
}
for (var i = 0; i < enemyTowers.length; i++) {
if (!enemyTowers[i].isDead) {
allEnemyTowersDead = false;
break;
}
}
if (allPlayerTowersDead && !gameEnded) {
gameEnded = true;
LK.showGameOver();
} else if (allEnemyTowersDead && !gameEnded) {
gameEnded = true;
LK.showYouWin();
}
checkGameEnd();
};
// Check if AI can use a specific card based on usage restriction
function canAIUseCard(cardType) {
// If card hasn't been used yet, it can be used
if (aiCardUsageRestriction[cardType] === undefined) {
return true;
}
// If card is restricted, check if 4 different cards have been used since
return aiCardUsageRestriction[cardType] <= 0;
}
// Update AI card usage tracking when a card is used
function updateAICardUsage(usedCardType) {
// Add the used card to recently used list
if (aiUsedCards.indexOf(usedCardType) === -1) {
aiUsedCards.push(usedCardType);
}
// Set restriction for the used card (needs 4 different cards before reuse)
aiCardUsageRestriction[usedCardType] = 4;
// Reduce restriction counter for all other restricted cards
for (var cardType in aiCardUsageRestriction) {
if (cardType !== usedCardType && aiCardUsageRestriction[cardType] > 0) {
aiCardUsageRestriction[cardType]--;
}
}
}