User prompt
haz que el icono de la maquina voladora tambien sea el de "flying_machine" y haz que tenga la maquina voladora velocidad 1.5 y radio de ataque 475
User prompt
cambiale el icono de la descarga al icono llamado "descarga_descardina" y recuerda ponerle a la maquina voladora el de "flying_machine". aclaremos, está la maquina voladora, los 3 esbirros, y el mega esbirro eh. ademas el icono del megaesbirro debe estar un 100% debajo de el ejercito de esqueletos
User prompt
haz que la maquina voladora tenga el asset llamado "flying_machine". y crea una carta exactamente igual que flying machine exactamente igual, pero que tenga su propio asset, que tenga 125 de hp mas, que se llame "megaesbirro", cuesta 3 de elixir, y que haga 35 mas de daño por golpe
User prompt
añadamos una nueva carta, funcionará exactamente igual que los esbirros, pero tiene su propio asset, es solo 1, cuesta 4 de elixir, se llama "maquina voladora", que tenga 500hp, y su icono estará un 100% debajo del de esqueletos. cambia la ubicacion del icono de la descarga para que esté un 100% debajo del cañon
User prompt
arregla por completo la descarga, haz que al ser colocada hará 200 de daño en un area de circulo de color azul de 150 unidades y dejará inmovil a las cartas que haya dañado durante 1 segundo ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'Uncaught TypeError: self.explode is not a function' in or related to this line: 'self.explode();' Line Number: 347
User prompt
Please fix the bug: 'Uncaught TypeError: self.explode is not a function' in or related to this line: 'self.explode();' Line Number: 347
User prompt
Please fix the bug: 'Uncaught TypeError: self.explode is not a function' in or related to this line: 'self.explode();' Line Number: 347
User prompt
Please fix the bug: 'Uncaught TypeError: self.explode is not a function' in or related to this line: 'self.explode();' Line Number: 347
Code edit (1 edits merged)
Please save this source code
User prompt
vamos a crear una nueva tropa, pero antes quiero aclarar algo, hay 3 tipos de carta, están los hechizos (aun no hemos creado ninguno), las tropas y las estructuras (aun no hemos creado ninguno). las tropas atacan a las tropas y a las estructuras (y a las torres) sin embargo dentro de ellos hay distintas clases, estan las wincondition, que son tropas que solo atacan a las estructuras y a las torres. luego estan las tropas normales, que dentro de ellas se distinguen en terrestres y aereos, y cada tropa puede atacar a terrestres y aereos o solo a terrestres, depende de la tropa, pero todas pueden atacar a estructuras y a torres. por ejemplo: el caballero es tropa terrestre, y solo ataca a terrestres. la arquera es tropa terrestre y ataca a aereas y terrestres.
User prompt
haz que si una carta está atacando a otra aunque haya una carta mas cerca si ya ha empezado a atacarla no cambiará de objetivo, sino que seguirá atacando a la misma hasta que la mate, una vez la mate cambiará nuevamente el objetivo a la mas cercana, sin embargo, si se está dirigiendo hacia una carta porque ha detectado que está en su rango y de repente una nueva se mete en el rango y está mas cerca que la anterior, la atacará siempre y cuando aun no haya atacado a la primera
User prompt
haz que el jugador recargue elexir cada 2 segundos
User prompt
arregla al caballero, sigue detectando cartas enemigas delante y detras, haz que tambien los detecte en todas las direcciones, como las arqueras. y bajale el rango de disparo a la arqueras en un 5%. vamos a aclarar una cosa, una cosa es el rango de ataque, y otro el de detectar, cuando una carta enemiga o mas estan en el rango de deteccion de una carta, esta se acerca a la carta enemiga mas cercana de su radio de deteccion y se acerca hasta que entre en el rango de ataque y la comienze a atacar. el rango de deteccion siempre es mas grande que el de ataque
User prompt
haz que el jugador recargue elexir cada 2.3 segundos. haz que la arquera tenga un 25% menos de rango. y haz que le caballero detecte enemigos en todas las direcciones, derecha, izquierda y diagonales, no solo delante y atras
User prompt
pues haz que el cpu tenga elixir limitado, y que consiga 1 de elixir el cpu cada 2 segundos. por cierto no se porque ocurre pero de vez en cuando los caballeros se ignoran entre ellos. y otra cosa haz que el caballero no puede mover la torre, entiendo que al atacar mueva a las tropas, pero no debe poder mover a las torres
User prompt
de vez en cuando por alguna razon las cartas se ignoran entre ellas, haz que siempre se detecten entre ellas aunque seanel mismo tipo de carta si son de diferentes bandos deben atacarse y no ignorarse por ninguna razon.
User prompt
por alguna razon el cpu cuando usa una carta, se obsesiona con esa carta y tira esa carta de forma seguida y tarda en decidir tirar la otra carta, haz que no se obsesione con una. tambien haz al cpu mas inteligente, si detecta muchas cartas del jugador en un lado del mapa el tambien lo colocará en ese lado para defender las del rival, pero tambien de vez en cuando las tirara al otro lado. y el caballero no dispara, sino que golpea con su espada, no hagas que dispare una bala, haz algo para que simule que los golpea con una espada
Remix started
Copy Tower Defense Royale
/****
* 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 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 === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : 3;
var cardBg = self.attachAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5
});
var cardIcon = LK.getAsset(cardType === 'discharge' ? 'descarga_descardina' : 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) {
// Apply 200 damage
targetUnit.takeDamage(self.damage);
// 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 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 = 625; // 500 + 125
self.currentHealth = 625;
self.damage = 120; // 85 + 35
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 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 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 = 800;
self.damage = 75;
self.attackSpeed = 67; // 50% slower than 45 ticks (45 * 1.5 = 67.5, rounded to 67)
self.lastAttackTime = 0;
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;
}
// King towers cannot attack until at least one Princess tower is destroyed
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++;
}
}
if (princessTowersAlive === 2) {
return; // King tower cannot attack while both Princess towers are alive
}
}
var targetUnits = self.isPlayer ? enemyUnits : playerUnits;
var closestTarget = null;
var closestDistance = Infinity;
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];
}
}
}
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) 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') {
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 (only archers and towers can attack air)
if (targetUnits[i].isAerial && self.unitType !== 'archer') {
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', 425, 75, 90, 538, 1.5);
// Range reduced by 30% from 756 to 538 (25% + 5%)
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 = 120; // 2 minutes in seconds
var elixir = 5; // Starting elixir
var maxElixir = 10;
var elixirRegenRate = 120; // Ticks for 2.0 seconds at 60fps
var lastElixirRegen = 0;
var aiElixir = 5; // AI starting elixir
var aiMaxElixir = 10;
var aiElixirRegenRate = 120; // Ticks for 2 seconds at 60fps (faster than player)
var lastAiElixirRegen = 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'];
// 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 8)', {
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 {
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;
// Card icon
var cardIcon = LK.getAsset(cardType === 'discharge' ? 'descarga_descardina' : 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 === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 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) {
toggleCardInDeck(this.cardType);
};
game.addChild(cardContainer);
deckMenuElements.push(cardContainer);
}
// Selected deck display
var deckTitle2 = new Text2('MAZO ACTUAL (' + selectedDeck.length + '/8)', {
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 = 424;
var selectedSpacing = 240;
for (var i = 0; i < selectedDeck.length && i < 8; i++) {
var cardType = selectedDeck[i];
var selectedCardContainer = new Container();
selectedCardContainer.x = selectedStartX + i * selectedSpacing;
selectedCardContainer.y = 1400;
// Small card background
var selectedCardBg = selectedCardContainer.attachAsset('cardSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.7,
scaleY: 0.7
});
selectedCardBg.tint = 0x2196F3;
// Small card icon
var selectedCardIcon = LK.getAsset(cardType, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.4,
scaleY: 0.4
});
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 < 8) {
// Add card to deck if not full (max 8 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 === '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 === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 3 : 3;
if (isPlayer && elixir < cost) {
return false;
}
if (!isPlayer && aiElixir < cost) {
return false;
} // AI also needs elixir
// Check if position is in correct half (except for spells)
if (cardType !== 'fireball') {
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
}
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
}
game.addChild(skeleton1);
game.addChild(skeleton2);
game.addChild(skeleton3);
LK.getSound('deploy').play();
return true;
} else if (cardType === 'skeletonArmy') {
// Deploy 12 skeletons in a 3x4 formation
var skeletons = [];
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 4; col++) {
var skeleton = new Skeleton(isPlayer);
skeleton.x = x + (col - 1.5) * 40; // Spread horizontally
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
}
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
}
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
}
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
}
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 === 'cannon') {
unit = new Cannon(isPlayer);
}
if (unit) {
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
}
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
// Generate AI deck with required constraints
function generateAIDeck() {
aiDeck = [];
// Required cards (must have)
var requiredCards = {
aerial: 'archer',
// Card that can attack aerial units
wincondition: 'giant',
// Wincondition card
spell: Math.random() < 0.5 ? 'fireball' : 'discharge',
// Spell card (randomly choose between fireball and discharge)
structure: 'cannon' // Structure card
};
// 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', 'skeleton', 'skeletonArmy', 'minion', 'maquinaVoladora', 'megaesbirro'];
// Fill remaining 4 slots with random cards from remaining pool
while (aiDeck.length < 8) {
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 === 'skeleton' ? 1 : deckCard === 'skeletonArmy' ? 3 : deckCard === 'fireball' ? 4 : deckCard === 'discharge' ? 2 : deckCard === 'minion' ? 3 : deckCard === 'maquinaVoladora' ? 4 : deckCard === 'megaesbirro' ? 3 : 3;
if (aiElixir >= cardCost) {
// Add defensive units (not spells for defense)
if (deckCard !== 'fireball' && deckCard !== 'discharge') {
defensiveCards.push(deckCard);
}
}
}
}
// Check for spell usage opportunities (discharge and fireball)
var spellCards = [];
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) {
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)];
// Deploy near threatened area
var deployX = 300 + Math.random() * 1448;
var 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
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') {
winconditionCard = 'giant';
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 : 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 : 3;
if (aiElixir >= supportCost && supportCard !== 'fireball' && supportCard !== 'discharge' && supportCard !== winconditionCard) {
supportCards.push(supportCard);
}
}
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') {
winconditionCard = 'giant';
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 === 'skeleton' ? 1 : deckCard === 'skeletonArmy' ? 3 : deckCard === 'fireball' ? 4 : deckCard === 'discharge' ? 2 : deckCard === 'minion' ? 3 : deckCard === 'maquinaVoladora' ? 4 : deckCard === 'megaesbirro' ? 3 : 3;
if (aiElixir >= cardCost && deckCard !== 'fireball' && deckCard !== 'discharge') {
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 === 'skeleton' ? 1 : deckCard === 'skeletonArmy' ? 3 : deckCard === 'fireball' ? 4 : deckCard === 'discharge' ? 2 : deckCard === 'minion' ? 3 : deckCard === 'maquinaVoladora' ? 4 : deckCard === 'megaesbirro' ? 3 : 3;
if (aiElixir >= cardCost && deckCard !== 'fireball' && deckCard !== 'discharge' && 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 === 'skeleton' ? 1 : cardType === 'skeletonArmy' ? 3 : cardType === 'fireball' ? 4 : cardType === 'discharge' ? 2 : cardType === 'minion' ? 3 : cardType === 'maquinaVoladora' ? 4 : cardType === 'megaesbirro' ? 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());
}
// Regenerate elixir
if (LK.ticks - lastElixirRegen >= elixirRegenRate && elixir < maxElixir) {
elixir++;
elixirText.setText(elixir + "/" + maxElixir);
lastElixirRegen = LK.ticks;
}
// Regenerate AI elixir
if (LK.ticks - lastAiElixirRegen >= aiElixirRegenRate && 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();
}; ===================================================================
--- original.js
+++ change.js
@@ -521,10 +521,10 @@
self.currentHealth = 500;
self.damage = 85;
self.attackSpeed = 90; // 90 ticks
self.range = 500; // Detection range
- self.attackRange = 150; // Attack range
- self.speed = 1.75;
+ self.attackRange = 475; // Attack range
+ self.speed = 1.5;
self.target = null;
self.lastAttackTime = 0;
self.isDead = false;
self.isAttacking = false;