/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Demon = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'basic';
// Base stats that get modified by wave difficulty
var baseHealth = 450;
var baseSpeed = 0.3;
var baseReward = 10;
// Wave difficulty multiplier - increases health and speed
var waveMultiplier = 1 + (wave - 1) * 0.15; // 15% increase per wave
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 2.0); // Cap speed at 2x
self.reward = Math.floor(baseReward * waveMultiplier);
self.biteDamage = 1 + Math.floor((wave - 1) / 3); // Increase bite damage every 3 waves
self.biteDelay = Math.max(15, 30 - wave); // Faster biting as waves progress
self.lastBite = 0;
var assetName = 'basicDemon';
if (self.type === 'fast') {
assetName = 'fastDemon';
baseHealth = 360;
baseSpeed = 1;
baseReward = 15;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 2.5); // Fast demons can go faster
self.reward = Math.floor(baseReward * waveMultiplier);
} else if (self.type === 'tank') {
assetName = 'tankDemon';
baseHealth = 1800;
baseSpeed = 0.25;
baseReward = 25;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 1.5); // Tank demons have speed cap
self.reward = Math.floor(baseReward * waveMultiplier);
} else if (self.type === 'zombieAllStar') {
assetName = 'tankDemon'; // Using tank demon sprite
baseHealth = 1200;
baseSpeed = 0.15;
baseReward = 50;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 1.3);
self.reward = Math.floor(baseReward * waveMultiplier);
self.biteDamage = 3 + Math.floor((wave - 1) / 2); // Extra damage scaling
self.biteDelay = Math.max(10, 20 - wave); // Faster biting
} else if (self.type === 'speedy') {
assetName = 'speedyDemon';
baseHealth = 600; // Reduced resistance
baseSpeed = 1.0; // Reduced speed
baseReward = 40;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 2.5); // Reduced max speed multiplier
self.reward = Math.floor(baseReward * waveMultiplier);
self.biteDamage = 2 + Math.floor((wave - 1) / 3);
self.biteDelay = Math.max(10, 20 - wave); // Fast attacks
} else if (self.type === 'dancing') {
assetName = 'dancingDemon';
baseHealth = 600;
baseSpeed = 0.4;
baseReward = 30;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 2.0);
self.reward = Math.floor(baseReward * waveMultiplier);
self.biteDamage = 2 + Math.floor((wave - 1) / 4);
self.biteDelay = Math.max(15, 25 - wave);
self.isDancing = false;
} else if (self.type === 'nutrient') {
assetName = 'basicDemon';
baseHealth = 400;
baseSpeed = 0.35;
baseReward = 20;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 2.0);
self.reward = Math.floor(baseReward * waveMultiplier);
self.biteDamage = 1 + Math.floor((wave - 1) / 3);
self.biteDelay = Math.max(15, 30 - wave);
self.isNutrientDemon = true;
}
var graphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.3
});
self.update = function () {
// Check for golem collision and bite them
var blocked = false;
var targetGolem = null;
for (var i = 0; i < golems.length; i++) {
var golem = golems[i];
if (golem.type !== 'spike' && self.intersects(golem)) {
blocked = true;
targetGolem = golem;
break;
}
}
if (targetGolem && LK.ticks - self.lastBite > self.biteDelay) {
// Bite the golem
if (!targetGolem.health) {
if (targetGolem.type === 'barrier') {
targetGolem.health = 160; // Barrier golems have 160 health
targetGolem.maxHealth = 160;
} else {
targetGolem.health = 15; // All other golems have 15 health
targetGolem.maxHealth = 15;
}
}
targetGolem.health -= self.biteDamage;
self.lastBite = LK.ticks;
// Visual feedback - flash golem red
tween(targetGolem, {
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(targetGolem, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
if (targetGolem.health <= 0) {
// Remove golem from grid
var golemGridCol = Math.floor((targetGolem.x - gridStartX + cellSize / 2) / cellSize);
var golemGridRow = Math.floor((targetGolem.y - gridStartY + cellSize / 2) / cellSize);
if (golemGridCol >= 0 && golemGridCol < gridCols && golemGridRow >= 0 && golemGridRow < gridRows) {
gridOccupied[golemGridRow][golemGridCol] = false;
}
// Remove from golems array
for (var j = golems.length - 1; j >= 0; j--) {
if (golems[j] === targetGolem) {
targetGolem.destroy();
golems.splice(j, 1);
break;
}
}
blocked = false; // Can continue moving after eating golem
}
}
if (!blocked) {
self.x -= self.speed;
}
// Special dancing demon effects
if (self.type === 'dancing' && !self.isDancing && Math.random() < 0.01) {
// Start dancing animation
self.isDancing = true;
var originalSpeed = self.speed;
self.speed = 0; // Stop moving while dancing
// Dancing rotation animation
var danceRotations = 0;
var maxRotations = 3;
var _danceRotate2 = function _danceRotate() {
tween(graphics, {
rotation: Math.PI * 2
}, {
duration: 400,
easing: tween.easeInOut,
onFinish: function onFinish() {
graphics.rotation = 0; // Reset rotation
danceRotations++;
if (danceRotations < maxRotations) {
_danceRotate2();
} else {
// Dance complete, resume movement with speed boost
self.speed = originalSpeed * 1.5; // 50% speed boost after dance
self.isDancing = false;
// Speed boost lasts for 3 seconds
LK.setTimeout(function () {
if (self.health > 0) {
self.speed = originalSpeed; // Return to normal speed
}
}, 3000);
}
}
});
};
_danceRotate2();
// Color flash during dance
tween(graphics, {
tint: 0xFF00FF
}, {
duration: 200,
onFinish: function onFinish() {
tween(graphics, {
tint: 0x00FFFF
}, {
duration: 200,
onFinish: function onFinish() {
tween(graphics, {
tint: 0xFFFF00
}, {
duration: 200,
onFinish: function onFinish() {
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
});
}
});
}
// Special zombie all star effects
if (self.type === 'zombieAllStar') {
// Scale pulsing effect when at low health
var healthPercent = self.health / self.maxHealth;
if (healthPercent < 0.5 && !self.isPulsing) {
self.isPulsing = true;
var _pulseScale = function pulseScale() {
tween(graphics, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(graphics, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.health > 0 && healthPercent < 0.5) {
_pulseScale();
} else {
self.isPulsing = false;
}
}
});
}
});
};
_pulseScale();
}
}
// Update health bar color
var healthPercent = self.health / self.maxHealth;
if (healthPercent < 0.3) {
graphics.tint = 0xFF0000;
} else if (healthPercent < 0.6) {
graphics.tint = 0xFF6600;
}
};
// Add green glowing effect for nutrient demons
if (self.type === 'nutrient') {
// Pulsing green glow effect
if (!self.isGlowing) {
self.isGlowing = true;
var _glowPulse = function glowPulse() {
tween(graphics, {
tint: 0x00FF00,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(graphics, {
tint: 0x88FF88,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.health > 0) {
_glowPulse();
}
}
});
}
});
};
_glowPulse();
}
}
self.takeDamage = function (damage) {
self.health -= damage;
LK.getSound('hit').play();
if (self.health <= 0) {
self.shouldRemove = true;
score += self.reward;
demonsKilledInWave++;
// Drop purple nutrient stone if this is a nutrient demon
if (self.type === 'nutrient') {
var nutrient = new NutrientStone();
nutrient.x = self.x;
nutrient.y = self.y;
nutrients.push(nutrient);
game.addChild(nutrient);
}
updateUI();
}
};
return self;
});
var Golem = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'basic';
self.lastShot = 0;
self.range = 2048; // Full screen width for entire line attack
self.damage = 25;
self.shootDelay = 60; // frames
var assetName = 'basicGolem';
if (self.type === 'heavy') {
assetName = 'heavyGolem';
self.damage = 75;
self.range = 2048; // Full screen width for entire line attack
self.shootDelay = 120;
} else if (self.type === 'crystal') {
assetName = 'crystalGolem';
self.damage = 20;
self.range = 2048; // Full screen width for entire line attack
self.shootDelay = 30;
} else if (self.type === 'barrier') {
assetName = 'barrierGolem';
self.damage = 0;
self.range = 0;
self.shootDelay = 0;
} else if (self.type === 'spike') {
assetName = 'spikeGolem';
self.damage = 10;
self.range = 0;
self.shootDelay = 0;
} else if (self.type === 'stone') {
assetName = 'stoneGolem';
self.damage = 0;
self.range = 0;
self.shootDelay = 450; // Generate stone every 7.5 seconds
} else if (self.type === 'melee') {
assetName = 'meleeGolem';
self.damage = 40;
self.range = cellSize * 2; // 2 cell radius
self.shootDelay = 0; // No projectiles, direct combat
self.meleeDelay = 30; // frames between melee attacks
self.lastMelee = 0;
} else if (self.type === 'explosive') {
assetName = 'explosiveGolem';
self.damage = 80;
self.range = cellSize * 1.5; // 1.5 cell radius for detection
self.shootDelay = 0; // No projectiles, explodes on contact
self.hasExploded = false;
}
var graphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: self.type === 'spike' ? 0.0 : 0.3
});
self.update = function () {
if (self.type === 'barrier') {
return;
}
if (self.type === 'spike') {
// Check for contact damage with cooldown
if (!self.damageCooldown) {
self.damageCooldown = {};
}
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
if (self.intersects(demon)) {
var demonId = demon.id || i; // Use demon id or index as identifier
var currentTime = LK.ticks;
if (!self.damageCooldown[demonId] || currentTime - self.damageCooldown[demonId] > 60) {
demon.takeDamage(self.damage);
self.damageCooldown[demonId] = currentTime;
}
}
}
return;
}
if (self.type === 'stone') {
// Generate stones periodically
if (LK.ticks - self.lastShot > self.shootDelay) {
var stone = new Stone();
stone.x = self.x + (Math.random() - 0.5) * 100; // Spawn near golem
stone.y = self.y + (Math.random() - 0.5) * 100;
stone.value = 25; // Stone golem generates stones worth 25
stones_collectible.push(stone);
game.addChild(stone);
self.lastShot = LK.ticks;
}
return;
}
if (self.type === 'melee') {
// Melee combat - attack demons in 2 cell radius front and back
if (LK.ticks - self.lastMelee > self.meleeDelay) {
var golemRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
var golemCol = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
var attackedDemons = [];
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
var demonRow = Math.floor((demon.y - gridStartY + cellSize / 2) / cellSize);
var demonCol = Math.floor((demon.x - gridStartX + cellSize / 2) / cellSize);
// Check if demon is in same row and within 2 cells front or back
if (demonRow === golemRow) {
var colDistance = Math.abs(demonCol - golemCol);
if (colDistance <= 2 && colDistance > 0) {
var dx = demon.x - self.x;
var dy = demon.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.range) {
attackedDemons.push(demon);
}
}
}
}
if (attackedDemons.length > 0) {
// Attack all demons in range
for (var k = 0; k < attackedDemons.length; k++) {
var targetDemon = attackedDemons[k];
targetDemon.takeDamage(self.damage);
// Visual melee attack effect
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFF4444
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF
}, {
duration: 150
});
}
});
}
self.lastMelee = LK.ticks;
LK.getSound('hit').play();
}
}
return;
}
if (self.type === 'explosive') {
// Explosive golem - explode when demon gets close
if (!self.hasExploded) {
var golemRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
var golemCol = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
var demonsInRange = [];
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
var dx = demon.x - self.x;
var dy = demon.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.range) {
demonsInRange.push(demon);
}
}
if (demonsInRange.length > 0) {
// Explode in 3x3 area
self.hasExploded = true;
// Visual explosion effect
tween(self, {
scaleX: 2.0,
scaleY: 2.0,
tint: 0xFF4400,
alpha: 0.8
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 3.0,
scaleY: 3.0,
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
// Remove golem from grid after explosion
var golemGridCol = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
var golemGridRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
if (golemGridCol >= 0 && golemGridCol < gridCols && golemGridRow >= 0 && golemGridRow < gridRows) {
gridOccupied[golemGridRow][golemGridCol] = false;
}
// Remove from golems array
for (var j = golems.length - 1; j >= 0; j--) {
if (golems[j] === self) {
self.destroy();
golems.splice(j, 1);
break;
}
}
}
});
}
});
// Instantly kill all demons in 3x3 area
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
var demonRow = Math.floor((demon.y - gridStartY + cellSize / 2) / cellSize);
var demonCol = Math.floor((demon.x - gridStartX + cellSize / 2) / cellSize);
// Check if demon is within 3x3 area centered on golem
var rowDistance = Math.abs(demonRow - golemRow);
var colDistance = Math.abs(demonCol - golemCol);
if (rowDistance <= 1 && colDistance <= 1) {
// Instantly kill the demon by setting health to 0
demon.health = 0;
demon.shouldRemove = true;
stones += demon.reward;
score += demon.reward;
}
}
LK.getSound('hit').play();
}
}
return;
}
// Check if special ability has expired
if (self.hasSpecialAbility && LK.ticks > self.specialAbilityEndTime) {
self.hasSpecialAbility = false;
// Return to normal color
tween(self, {
tint: 0xFFFFFF
}, {
duration: 300
});
}
var currentShootDelay = self.shootDelay;
var currentDamage = self.damage;
// Apply special ability effects
if (self.hasSpecialAbility) {
if (self.type === 'basic' || self.type === 'heavy' || self.type === 'crystal') {
// Shooting golems: double fire rate and +50% damage
currentShootDelay = Math.floor(self.shootDelay * 0.5);
currentDamage = Math.floor(self.damage * 1.5);
} else if (self.type === 'spike') {
// Spike golems: triple damage
currentDamage = self.damage * 3;
} else if (self.type === 'stone') {
// Stone golems: generate stones 3x faster
currentShootDelay = Math.floor(self.shootDelay * 0.33);
} else if (self.type === 'melee') {
// Melee golems: double attack speed and damage
self.meleeDelay = Math.floor(30 * 0.5);
currentDamage = self.damage * 2;
} else if (self.type === 'explosive') {
// Explosive golems: larger explosion range
self.range = cellSize * 2.5;
}
}
if (LK.ticks - self.lastShot > currentShootDelay) {
var target = self.findTarget();
if (target) {
self.shoot(target);
self.lastShot = LK.ticks;
}
}
};
self.findTarget = function () {
var closestDemon = null;
var closestDistance = self.range;
var golemRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
var demonRow = Math.floor((demon.y - gridStartY + cellSize / 2) / cellSize);
// Only target demons in the same row
if (demonRow !== golemRow) {
continue;
}
var dx = demon.x - self.x;
var dy = demon.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestDemon = demon;
}
}
return closestDemon;
};
self.shoot = function (target) {
var projectile = new Projectile(self.type);
projectile.x = self.x;
projectile.y = self.y;
projectile.targetX = target.x;
projectile.targetY = target.y;
// Use enhanced damage if special ability is active
if (self.hasSpecialAbility) {
projectile.damage = Math.floor(self.damage * 1.5);
} else {
projectile.damage = self.damage;
}
projectiles.push(projectile);
game.addChild(projectile);
LK.getSound('shoot').play();
};
return self;
});
var Lawnmower = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('lawnmower', {
anchorX: 0.5,
anchorY: 0.3
});
self.isActive = false;
self.speed = 8;
self.row = 0;
self.update = function () {
if (self.isActive) {
self.x += self.speed;
// Remove lawnmower when it goes off screen
if (self.x > 2200) {
self.shouldRemove = true;
}
// Check collision with demons
for (var i = demons.length - 1; i >= 0; i--) {
var demon = demons[i];
if (self.intersects(demon)) {
// Instantly kill demon
demon.health = 0;
demon.shouldRemove = true;
score += demon.reward;
demonsKilledInWave++;
}
}
}
};
self.activate = function () {
if (!self.isActive) {
self.isActive = true;
LK.getSound('shoot').play();
}
};
return self;
});
var NutrientStone = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('stone', {
anchorX: 0.5,
anchorY: 0.3
});
// Make it purple and larger
graphics.tint = 0x8B008B;
graphics.scaleX = 1.3;
graphics.scaleY = 1.3;
self.value = 1;
self.bobOffset = Math.random() * Math.PI * 2;
self.spawnTime = LK.ticks;
self.lifespan = 600; // 10 seconds at 60 FPS
self.isCollected = false;
self.update = function () {
// Bobbing animation with purple sparkle effect
self.y += Math.sin(LK.ticks * 0.05 + self.bobOffset) * 0.5;
// Purple sparkle effect
if (LK.ticks % 30 === 0) {
tween(graphics, {
tint: 0xFF00FF
}, {
duration: 200,
onFinish: function onFinish() {
tween(graphics, {
tint: 0x8B008B
}, {
duration: 200
});
}
});
}
// Check if stone should disappear
if (LK.ticks - self.spawnTime > self.lifespan) {
self.shouldRemove = true;
}
};
return self;
});
var Projectile = Container.expand(function (golemType) {
var self = Container.call(this);
self.golemType = golemType || 'basic';
var assetName = 'projectile';
if (self.golemType === 'heavy') {
assetName = 'heavyProjectile';
} else if (self.golemType === 'crystal') {
assetName = 'crystalProjectile';
}
var graphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.3
});
self.damage = 25;
self.speed = 8;
self.targetX = 0;
self.targetY = 0;
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 < 10) {
self.shouldRemove = true;
return;
}
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
};
return self;
});
var Stone = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('stone', {
anchorX: 0.5,
anchorY: 0.3
});
self.value = 10;
self.bobOffset = Math.random() * Math.PI * 2;
self.spawnTime = LK.ticks;
self.lifespan = 300; // 5 seconds at 60 FPS
self.isMarkedForCollection = false;
self.collectionMarkTime = 0;
self.collectionDelay = 60; // 1 second at 60 FPS
self.update = function () {
// Bobbing animation
self.y += Math.sin(LK.ticks * 0.05 + self.bobOffset) * 0.3;
// Check if stone should disappear
if (LK.ticks - self.spawnTime > self.lifespan) {
self.shouldRemove = true;
}
};
return self;
});
var miniExplodeGolem = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('potatoMine', {
anchorX: 0.5,
anchorY: 0.0
});
self.isUnderground = true;
self.armingTime = 900; // 15 seconds to arm
self.spawnTime = LK.ticks;
self.isArmed = false;
self.hasExploded = false;
self.explosionDamage = 150; // High damage but less than explosiveGolem
// Start underground (invisible and smaller)
graphics.alpha = 0.3;
graphics.scaleX = 0.5;
graphics.scaleY = 0.5;
graphics.tint = 0x8B4513; // Brown color for underground
self.update = function () {
// Arm the mine after arming time
if (!self.isArmed && LK.ticks - self.spawnTime > self.armingTime) {
self.isArmed = true;
self.isUnderground = false;
// Surface animation - become visible and full size
tween(graphics, {
alpha: 1.0,
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF
}, {
duration: 500,
easing: tween.easeOut
});
}
// Check for demon collision if armed and not exploded
if (self.isArmed && !self.hasExploded) {
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
if (self.intersects(demon)) {
self.explode();
break;
}
}
}
};
self.explode = function () {
if (self.hasExploded) {
return;
}
self.hasExploded = true;
// Visual explosion effect
tween(graphics, {
scaleX: 3.0,
scaleY: 3.0,
tint: 0xFF4400,
alpha: 0.8
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(graphics, {
scaleX: 4.0,
scaleY: 4.0,
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
// Remove from grid and golems array
var mineGridCol = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
var mineGridRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
if (mineGridCol >= 0 && mineGridCol < gridCols && mineGridRow >= 0 && mineGridRow < gridRows) {
gridOccupied[mineGridRow][mineGridCol] = false;
}
// Remove from golems array
for (var j = golems.length - 1; j >= 0; j--) {
if (golems[j] === self) {
self.destroy();
golems.splice(j, 1);
break;
}
}
}
});
}
});
// Kill all demons in a 1x1 area (single cell) around the mine
var mineRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
var mineCol = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
var demonRow = Math.floor((demon.y - gridStartY + cellSize / 2) / cellSize);
var demonCol = Math.floor((demon.x - gridStartX + cellSize / 2) / cellSize);
// Check if demon is within same cell as mine
if (demonRow === mineRow && demonCol === mineCol) {
// Damage the demon with explosion damage
demon.health -= self.explosionDamage;
if (demon.health <= 0) {
demon.shouldRemove = true;
score += demon.reward;
demonsKilledInWave++;
}
}
}
LK.getSound('hit').play();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2F5233
});
/****
* Game Code
****/
// Game variables
var stones = 100;
var score = 0;
var lives = 10;
var wave = 1;
var nextWaveTimer = 0;
var waveDelay = 300; // frames between waves
// Flag wave system variables
var totalDemonsInWave = 0;
var demonsKilledInWave = 0;
var flagWaves = [5, 10, 15, 20]; // Waves with massive demon spawns
var isFlagWave = false;
var flagWaveSpawned = false;
// Game arrays
var golems = [];
var demons = [];
var projectiles = [];
var stones_collectible = [];
var lawnmowers = [];
var nutrients = [];
// Special ability system
var specialAbilityActive = false;
var specialAbilityEndTime = 0;
var specialAbilityDuration = 300; // 5 seconds at 60 FPS
// Nutrient stone inventory system
var nutrientInventory = []; // Max 3 stones
var maxNutrientSlots = 3;
var selectedNutrientSlot = -1; // -1 means no slot selected
// Stone spawning - gets slower as waves progress
var lastStoneSpawn = 0;
var baseStoneSpawnDelay = 150;
var stoneSpawnDelay = baseStoneSpawnDelay + (wave - 1) * 20; // Slower stone spawning as waves progress
// Demon spawn cooldown
var demonSpawnCooldown = 300; // 5 seconds between demon spawns
var lastDemonSpawn = 0;
// Initial game delay - demons don't spawn for first 12 seconds
var gameStartDelay = 720; // 12 seconds at 60 FPS
var gameStartTime = 0;
var demonsCanSpawn = false;
// Grid system
var gridCols = 12;
var gridRows = 5;
var gridStartX = 224;
var gridStartY = 800;
var cellSize = 200;
// Golem costs
var golemCosts = {
basic: 100,
heavy: 250,
crystal: 75,
barrier: 80,
spike: 60,
stone: 50,
melee: 120,
explosive: 150,
miniExplodeGolem: 25,
shovel: 0
};
// Selected golem type
var selectedGolemType = 'basic';
// Double-tap detection variables
var lastClickedButton = null;
var lastClickTime = 0;
var doubleTapThreshold = 500; // 500ms window for double tap
// Golem placement cooldown system
var golemPlacementCooldown = {};
var golemCooldownDuration = 180; // 3 seconds at 60 FPS - base duration
var lastGolemPlacement = 0;
// Function to get cooldown duration based on golem cost
function getGolemCooldownDuration(golemType) {
var cost = golemCosts[golemType];
if (cost === 0) {
return 0;
} // No cooldown for free items like shovel
// More expensive golems have longer cooldowns
// Base formula: cost * 1.5 frames (at 60 FPS)
return Math.floor(cost * 1.5);
}
function updateNutrientUI() {
// Update nutrient title to show current count
nutrientTitle.setText('Nutrientes: ' + nutrientInventory.length + '/3');
// Clear existing nutrient visuals
for (var s = 0; s < maxNutrientSlots; s++) {
// Start with gray/opaque for empty slots, purple for selected
if (s < nutrientInventory.length) {
nutrientSlotBg[s].tint = selectedNutrientSlot === s ? 0xFF00FF : 0x8B008B;
nutrientSlotBg[s].alpha = 1.0;
} else {
nutrientSlotBg[s].tint = 0x666666; // Gray for empty slots
nutrientSlotBg[s].alpha = 0.6; // More opaque
}
// Remove old nutrient stones from slots
for (var c = nutrientSlotBg[s].children.length - 1; c >= 0; c--) {
nutrientSlotBg[s].children[c].destroy();
}
}
// Add nutrient stones to occupied slots
for (var i = 0; i < nutrientInventory.length; i++) {
var nutrientStone = LK.getAsset('stone', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8,
tint: 0x8B008B
});
nutrientStone.x = 0;
nutrientStone.y = 0;
nutrientSlotBg[i].addChild(nutrientStone);
}
}
function applyNutrientToGolem(golem) {
if (golem.type === 'barrier') {
// Barrier golems get permanent durability boost
if (!golem.health) {
golem.health = 160;
golem.maxHealth = 160;
}
golem.health += 80; // Permanent +80 health boost
golem.maxHealth += 80;
// Visual enhancement for barrier
tween(golem, {
tint: 0x4444FF,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 300
});
} else {
// Other golems get temporary special abilities
golem.hasSpecialAbility = true;
golem.specialAbilityEndTime = LK.ticks + specialAbilityDuration;
// Visual enhancement
tween(golem, {
tint: 0xFF8800
}, {
duration: 300
});
}
// Show effect text
var effectText = new Text2('¡POTENCIADO!', {
size: 30,
fill: 0x8B008B
});
effectText.anchor.set(0.5, 0.5);
effectText.x = golem.x;
effectText.y = golem.y - 50;
game.addChild(effectText);
// Animate effect text
tween(effectText, {
y: golem.y - 100,
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
effectText.destroy();
}
});
}
function activateSpecialAbilities() {
specialAbilityActive = true;
specialAbilityEndTime = LK.ticks + specialAbilityDuration;
// Show special ability activation message
var abilityText = new Text2('¡PODER ESPECIAL ACTIVADO!', {
size: 50,
fill: 0x8B008B
});
abilityText.anchor.set(0.5, 0.5);
abilityText.x = 2048 / 2;
abilityText.y = 300;
game.addChild(abilityText);
// Animate ability text
tween(abilityText, {
scaleX: 1.2,
scaleY: 1.2,
tint: 0xFF00FF
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(abilityText, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 1500,
onFinish: function onFinish() {
abilityText.destroy();
}
});
}
});
// Apply special effects to all golems
for (var i = 0; i < golems.length; i++) {
var golem = golems[i];
if (golem.type === 'barrier') {
// Barrier golems get permanent durability boost
if (!golem.health) {
golem.health = 160;
golem.maxHealth = 160;
}
golem.health += 80; // Permanent +80 health boost
golem.maxHealth += 80;
// Visual enhancement for barrier
tween(golem, {
tint: 0x4444FF,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 300
});
} else {
// Other golems get temporary special abilities
golem.hasSpecialAbility = true;
golem.specialAbilityEndTime = specialAbilityEndTime;
// Visual enhancement
tween(golem, {
tint: 0xFF8800
}, {
duration: 300,
onFinish: function onFinish() {
// Keep enhanced color during ability duration
}
});
}
}
}
// UI elements
var stonesText = new Text2('Stones: 100', {
size: 40,
fill: 0xFFFFFF
});
stonesText.anchor.set(0, 0);
LK.gui.topLeft.addChild(stonesText);
stonesText.x = 120;
stonesText.y = 20;
var livesText = new Text2('Lives: 10', {
size: 40,
fill: 0xFFFFFF
});
livesText.anchor.set(0, 0);
LK.gui.topLeft.addChild(livesText);
livesText.x = 120;
livesText.y = 70;
var waveText = new Text2('Wave: 1', {
size: 40,
fill: 0xFFFFFF
});
waveText.anchor.set(0, 0);
LK.gui.topLeft.addChild(waveText);
waveText.x = 120;
waveText.y = 120;
var scoreText = new Text2('Score: 0', {
size: 40,
fill: 0xFFFFFF
});
scoreText.anchor.set(1, 0);
LK.gui.topRight.addChild(scoreText);
scoreText.x = -20;
scoreText.y = 20;
// Wave progress bar
var progressBarBg = LK.getAsset('gridCell', {
scaleX: 12,
scaleY: 0.3,
alpha: 0.3,
tint: 0x444444
});
progressBarBg.x = 2048 / 2;
progressBarBg.y = 50;
game.addChild(progressBarBg);
var progressBarFill = LK.getAsset('gridCell', {
scaleX: 0,
scaleY: 0.3,
tint: 0x00FF00
});
progressBarFill.x = 2048 / 2 - 1200;
progressBarFill.y = 50;
game.addChild(progressBarFill);
var progressText = new Text2('Wave 1 Progress', {
size: 30,
fill: 0xFFFFFF
});
progressText.anchor.set(0.5, 0.5);
progressText.x = 2048 / 2;
progressText.y = 80;
game.addChild(progressText);
// Create nutrient inventory slots
var nutrientSlots = [];
var nutrientSlotBg = [];
for (var s = 0; s < maxNutrientSlots; s++) {
// Background for slot
var slotBg = LK.getAsset('gridCell', {
scaleX: 0.4,
scaleY: 0.4,
alpha: 0.5,
tint: 0x333333
});
slotBg.x = 2048 - 200 - s * 100;
slotBg.y = 200;
LK.gui.topRight.addChild(slotBg);
nutrientSlotBg.push(slotBg);
// Slot for nutrient stone (initially empty)
nutrientSlots.push(null);
}
// Nutrient inventory title
var nutrientTitle = new Text2('Nutrientes: 0/3', {
size: 50,
fill: 0x8B008B
});
nutrientTitle.anchor.set(1, 0);
nutrientTitle.x = -20;
nutrientTitle.y = 140;
LK.gui.topRight.addChild(nutrientTitle);
// Cooldown status text
var cooldownText = new Text2('', {
size: 30,
fill: 0xFF6600
});
cooldownText.anchor.set(0.5, 0.5);
cooldownText.x = 2048 / 2;
cooldownText.y = 200;
game.addChild(cooldownText);
// Golem selection buttons
var buttonY = 200;
var buttonSpacing = 150;
var basicButton = LK.getAsset('basicGolem', {
scaleX: 1.2,
scaleY: 1.2
});
basicButton.x = 50;
basicButton.y = buttonY;
LK.gui.topLeft.addChild(basicButton);
var heavyButton = LK.getAsset('heavyGolem', {
scaleX: 1.0,
scaleY: 1.0
});
heavyButton.x = 50;
heavyButton.y = buttonY + buttonSpacing;
LK.gui.topLeft.addChild(heavyButton);
var crystalButton = LK.getAsset('crystalGolem', {
scaleX: 1.1,
scaleY: 1.1
});
crystalButton.x = 50;
crystalButton.y = buttonY + buttonSpacing * 2;
LK.gui.topLeft.addChild(crystalButton);
var barrierButton = LK.getAsset('barrierGolem', {
scaleX: 0.9,
scaleY: 1.0
});
barrierButton.x = 50;
barrierButton.y = buttonY + buttonSpacing * 3;
LK.gui.topLeft.addChild(barrierButton);
var spikeButton = LK.getAsset('spikeGolem', {
scaleX: 1.2,
scaleY: 1.2
});
spikeButton.x = 50;
spikeButton.y = buttonY + buttonSpacing * 4;
LK.gui.topLeft.addChild(spikeButton);
var stoneButton = LK.getAsset('stoneGolem', {
scaleX: 1.0,
scaleY: 1.0
});
stoneButton.x = 50;
stoneButton.y = buttonY + buttonSpacing * 5;
LK.gui.topLeft.addChild(stoneButton);
var meleeButton = LK.getAsset('meleeGolem', {
scaleX: 1.0,
scaleY: 1.0
});
meleeButton.x = 50;
meleeButton.y = buttonY + buttonSpacing * 6;
LK.gui.topLeft.addChild(meleeButton);
var explosiveButton = LK.getAsset('explosiveGolem', {
scaleX: 1.0,
scaleY: 1.0
});
explosiveButton.x = 50;
explosiveButton.y = buttonY + buttonSpacing * 7;
LK.gui.topLeft.addChild(explosiveButton);
var miniExplodeGolemButton = LK.getAsset('potatoMine', {
scaleX: 1.0,
scaleY: 1.0
});
miniExplodeGolemButton.x = 50;
miniExplodeGolemButton.y = buttonY + buttonSpacing * 8;
LK.gui.topLeft.addChild(miniExplodeGolemButton);
var shovelButton = LK.getAsset('shovel', {
scaleX: 1.0,
scaleY: 1.0
});
shovelButton.x = 50;
shovelButton.y = buttonY + buttonSpacing * 9;
LK.gui.topLeft.addChild(shovelButton);
// Cost labels
var basicCostText = new Text2('100', {
size: 24,
fill: 0xFFFF00
});
basicCostText.x = 150;
basicCostText.y = buttonY + 20;
LK.gui.topLeft.addChild(basicCostText);
var heavyCostText = new Text2('250', {
size: 24,
fill: 0xFFFF00
});
heavyCostText.x = 150;
heavyCostText.y = buttonY + buttonSpacing + 20;
LK.gui.topLeft.addChild(heavyCostText);
var crystalCostText = new Text2('75', {
size: 24,
fill: 0xFFFF00
});
crystalCostText.x = 150;
crystalCostText.y = buttonY + buttonSpacing * 2 + 20;
LK.gui.topLeft.addChild(crystalCostText);
var barrierCostText = new Text2('80', {
size: 24,
fill: 0xFFFF00
});
barrierCostText.x = 150;
barrierCostText.y = buttonY + buttonSpacing * 3 + 20;
LK.gui.topLeft.addChild(barrierCostText);
var spikeCostText = new Text2('60', {
size: 24,
fill: 0xFFFF00
});
spikeCostText.x = 150;
spikeCostText.y = buttonY + buttonSpacing * 4 + 20;
LK.gui.topLeft.addChild(spikeCostText);
var stoneCostText = new Text2('50', {
size: 24,
fill: 0xFFFF00
});
stoneCostText.x = 150;
stoneCostText.y = buttonY + buttonSpacing * 5 + 20;
LK.gui.topLeft.addChild(stoneCostText);
var meleeCostText = new Text2('120', {
size: 24,
fill: 0xFFFF00
});
meleeCostText.x = 150;
meleeCostText.y = buttonY + buttonSpacing * 6 + 20;
LK.gui.topLeft.addChild(meleeCostText);
var explosiveCostText = new Text2('150', {
size: 24,
fill: 0xFFFF00
});
explosiveCostText.x = 150;
explosiveCostText.y = buttonY + buttonSpacing * 7 + 20;
LK.gui.topLeft.addChild(explosiveCostText);
var miniExplodeGolemCostText = new Text2('25', {
size: 24,
fill: 0xFFFF00
});
miniExplodeGolemCostText.x = 150;
miniExplodeGolemCostText.y = buttonY + buttonSpacing * 8 + 20;
LK.gui.topLeft.addChild(miniExplodeGolemCostText);
var shovelCostText = new Text2('Remove', {
size: 20,
fill: 0xFFFF00
});
shovelCostText.x = 150;
shovelCostText.y = buttonY + buttonSpacing * 9 + 20;
LK.gui.topLeft.addChild(shovelCostText);
// Cooldown text displays for buttons
var basicCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
basicCooldownText.anchor.set(0.5, 0.5);
basicCooldownText.x = 50;
basicCooldownText.y = buttonY - 20;
basicCooldownText.alpha = 0;
LK.gui.topLeft.addChild(basicCooldownText);
var heavyCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
heavyCooldownText.anchor.set(0.5, 0.5);
heavyCooldownText.x = 50;
heavyCooldownText.y = buttonY + buttonSpacing - 20;
heavyCooldownText.alpha = 0;
LK.gui.topLeft.addChild(heavyCooldownText);
var crystalCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
crystalCooldownText.anchor.set(0.5, 0.5);
crystalCooldownText.x = 50;
crystalCooldownText.y = buttonY + buttonSpacing * 2 - 20;
crystalCooldownText.alpha = 0;
LK.gui.topLeft.addChild(crystalCooldownText);
var barrierCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
barrierCooldownText.anchor.set(0.5, 0.5);
barrierCooldownText.x = 50;
barrierCooldownText.y = buttonY + buttonSpacing * 3 - 20;
barrierCooldownText.alpha = 0;
LK.gui.topLeft.addChild(barrierCooldownText);
var spikeCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
spikeCooldownText.anchor.set(0.5, 0.5);
spikeCooldownText.x = 50;
spikeCooldownText.y = buttonY + buttonSpacing * 4 - 20;
spikeCooldownText.alpha = 0;
LK.gui.topLeft.addChild(spikeCooldownText);
var stoneCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
stoneCooldownText.anchor.set(0.5, 0.5);
stoneCooldownText.x = 50;
stoneCooldownText.y = buttonY + buttonSpacing * 5 - 20;
stoneCooldownText.alpha = 0;
LK.gui.topLeft.addChild(stoneCooldownText);
var meleeCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
meleeCooldownText.anchor.set(0.5, 0.5);
meleeCooldownText.x = 50;
meleeCooldownText.y = buttonY + buttonSpacing * 6 - 20;
meleeCooldownText.alpha = 0;
LK.gui.topLeft.addChild(meleeCooldownText);
var explosiveCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
explosiveCooldownText.anchor.set(0.5, 0.5);
explosiveCooldownText.x = 50;
explosiveCooldownText.y = buttonY + buttonSpacing * 7 - 20;
explosiveCooldownText.alpha = 0;
LK.gui.topLeft.addChild(explosiveCooldownText);
var miniExplodeGolemCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
miniExplodeGolemCooldownText.anchor.set(0.5, 0.5);
miniExplodeGolemCooldownText.x = 50;
miniExplodeGolemCooldownText.y = buttonY + buttonSpacing * 8 - 20;
miniExplodeGolemCooldownText.alpha = 0;
LK.gui.topLeft.addChild(miniExplodeGolemCooldownText);
// Create grid visualization
var gridCells = [];
for (var row = 0; row < gridRows; row++) {
gridCells[row] = [];
for (var col = 0; col < gridCols; col++) {
var cell = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.3,
alpha: 0.3,
tint: row === 0 ? 0x87CEEB : 0xFFFFFF // Sky blue for first row
});
cell.x = gridStartX + col * cellSize;
cell.y = gridStartY + row * cellSize;
cell.gridX = col;
cell.gridY = row;
gridCells[row][col] = cell;
game.addChild(cell);
}
}
// Grid occupied tracking
var gridOccupied = [];
for (var row = 0; row < gridRows; row++) {
gridOccupied[row] = [];
for (var col = 0; col < gridCols; col++) {
gridOccupied[row][col] = false;
}
}
function updateUI() {
stonesText.setText('Stones: ' + stones);
livesText.setText('Lives: ' + lives);
waveText.setText('Wave: ' + wave);
scoreText.setText('Score: ' + score);
var currentTime = LK.ticks;
// Update button availability based on cost and cooldown
function getButtonAlpha(golemType, cost) {
var hasEnoughStones = stones >= cost;
var cooldownDuration = getGolemCooldownDuration(golemType);
var isOnCooldown = golemPlacementCooldown[golemType] && currentTime - golemPlacementCooldown[golemType] < cooldownDuration;
return hasEnoughStones && !isOnCooldown ? 1.0 : 0.5;
}
basicButton.alpha = getButtonAlpha('basic', golemCosts.basic);
heavyButton.alpha = getButtonAlpha('heavy', golemCosts.heavy);
crystalButton.alpha = getButtonAlpha('crystal', golemCosts.crystal);
barrierButton.alpha = getButtonAlpha('barrier', golemCosts.barrier);
spikeButton.alpha = getButtonAlpha('spike', golemCosts.spike);
stoneButton.alpha = getButtonAlpha('stone', golemCosts.stone);
meleeButton.alpha = getButtonAlpha('melee', golemCosts.melee);
explosiveButton.alpha = getButtonAlpha('explosive', golemCosts.explosive);
miniExplodeGolemButton.alpha = getButtonAlpha('miniExplodeGolem', golemCosts.miniExplodeGolem);
shovelButton.alpha = 1.0; // Shovel is always available
// Update cooldown displays
function updateCooldownDisplay(golemType, cooldownText) {
var cooldownDuration = getGolemCooldownDuration(golemType);
if (golemPlacementCooldown[golemType] && currentTime - golemPlacementCooldown[golemType] < cooldownDuration) {
var remainingTime = Math.ceil((cooldownDuration - (currentTime - golemPlacementCooldown[golemType])) / 60);
cooldownText.setText(remainingTime + 's');
cooldownText.alpha = 1;
} else {
cooldownText.alpha = 0;
}
}
updateCooldownDisplay('basic', basicCooldownText);
updateCooldownDisplay('heavy', heavyCooldownText);
updateCooldownDisplay('crystal', crystalCooldownText);
updateCooldownDisplay('barrier', barrierCooldownText);
updateCooldownDisplay('spike', spikeCooldownText);
updateCooldownDisplay('stone', stoneCooldownText);
updateCooldownDisplay('melee', meleeCooldownText);
updateCooldownDisplay('explosive', explosiveCooldownText);
updateCooldownDisplay('miniExplodeGolem', miniExplodeGolemCooldownText);
}
function spawnWave() {
// Don't spawn demons if initial delay hasn't passed
if (!demonsCanSpawn) {
return;
}
// Check if this is a flag wave
isFlagWave = flagWaves.indexOf(wave) !== -1;
flagWaveSpawned = false;
var demonsToSpawn;
if (isFlagWave) {
// Flag waves spawn 3x more demons
demonsToSpawn = Math.floor((2 + Math.floor(wave * 1.5)) * 3);
} else {
// Dramatically increase demons per wave - starts at 3, increases significantly
demonsToSpawn = Math.max(3, 2 + Math.floor(wave * 1.5)); // More aggressive scaling
}
totalDemonsInWave = demonsToSpawn;
demonsKilledInWave = 0;
var spawnDelay = 300; // 5 seconds delay (300 frames at 60 FPS)
// Decrease spawn delay between demons as waves progress
var delayBetweenSpawns = Math.max(150, 450 - wave * 15); // Faster spawning in later waves
// Flag waves spawn demons much faster
if (isFlagWave) {
delayBetweenSpawns = Math.max(60, delayBetweenSpawns / 2);
}
for (var i = 0; i < demonsToSpawn; i++) {
LK.setTimeout(function () {
// Check if enough time has passed since last demon spawn
if (LK.ticks - lastDemonSpawn >= demonSpawnCooldown) {
var demonType = 'basic';
// More aggressive probability scaling based on wave
var waveScaling = Math.min(wave / 10, 1.0); // Scale from 0 to 1 over 10 waves
// Flag waves have higher chance of special demons
var flagMultiplier = isFlagWave ? 1.5 : 1.0;
if (wave > 2 && Math.random() < (0.2 + waveScaling * 0.4) * flagMultiplier) {
demonType = 'fast';
} // Up to 60% chance
if (wave > 3 && Math.random() < (0.15 + waveScaling * 0.3) * flagMultiplier) {
demonType = 'dancing';
} // Up to 45% chance
if (wave > 4 && Math.random() < (0.1 + waveScaling * 0.4) * flagMultiplier) {
demonType = 'tank';
} // Up to 50% chance
if (wave > 5 && Math.random() < (0.05 + waveScaling * 0.3) * flagMultiplier) {
demonType = 'speedy';
} // Up to 35% chance
// Add elite demon spawning for high waves
if (wave > 7 && Math.random() < (0.05 + waveScaling * 0.15) * flagMultiplier) {
demonType = 'zombieAllStar';
} // Up to 20% chance
// Add nutrient demons starting from wave 3
if (wave > 2 && Math.random() < 0.15) {
demonType = 'nutrient';
} // 15% chance for nutrient demons
var demon = new Demon(demonType);
demon.x = 2048;
demon.y = gridStartY + Math.floor(Math.random() * gridRows) * cellSize;
demons.push(demon);
game.addChild(demon);
lastDemonSpawn = LK.ticks;
}
}, spawnDelay);
spawnDelay += delayBetweenSpawns; // Use variable delay that decreases with wave
}
}
function getGridPosition(x, y) {
var col = Math.floor((x - gridStartX + cellSize / 2) / cellSize);
var row = Math.floor((y - gridStartY + cellSize / 2) / cellSize);
if (col >= 0 && col < gridCols && row >= 0 && row < gridRows) {
return {
col: col,
row: row
};
}
return null;
}
// Button click handlers
basicButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'basic' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.basic) {
selectedGolemType = 'basic';
lastClickedButton = 'basic';
lastClickTime = currentTime;
updateButtonSelection();
}
};
heavyButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'heavy' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.heavy) {
selectedGolemType = 'heavy';
lastClickedButton = 'heavy';
lastClickTime = currentTime;
updateButtonSelection();
}
};
crystalButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'crystal' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.crystal) {
selectedGolemType = 'crystal';
lastClickedButton = 'crystal';
lastClickTime = currentTime;
updateButtonSelection();
}
};
barrierButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'barrier' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.barrier) {
selectedGolemType = 'barrier';
lastClickedButton = 'barrier';
lastClickTime = currentTime;
updateButtonSelection();
}
};
spikeButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'spike' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.spike) {
selectedGolemType = 'spike';
lastClickedButton = 'spike';
lastClickTime = currentTime;
updateButtonSelection();
}
};
stoneButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'stone' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.stone) {
selectedGolemType = 'stone';
lastClickedButton = 'stone';
lastClickTime = currentTime;
updateButtonSelection();
}
};
meleeButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'melee' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.melee) {
selectedGolemType = 'melee';
lastClickedButton = 'melee';
lastClickTime = currentTime;
updateButtonSelection();
}
};
explosiveButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'explosive' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.explosive) {
selectedGolemType = 'explosive';
lastClickedButton = 'explosive';
lastClickTime = currentTime;
updateButtonSelection();
}
};
miniExplodeGolemButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'miniExplodeGolem' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.miniExplodeGolem) {
selectedGolemType = 'miniExplodeGolem';
lastClickedButton = 'miniExplodeGolem';
lastClickTime = currentTime;
updateButtonSelection();
}
};
shovelButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'shovel' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else {
selectedGolemType = 'shovel';
lastClickedButton = 'shovel';
lastClickTime = currentTime;
updateButtonSelection();
}
};
function updateButtonSelection() {
// If no golem is selected, all buttons return to normal color
basicButton.tint = selectedGolemType === 'basic' ? 0x00FF00 : 0xFFFFFF;
heavyButton.tint = selectedGolemType === 'heavy' ? 0x00FF00 : 0xFFFFFF;
crystalButton.tint = selectedGolemType === 'crystal' ? 0x00FF00 : 0xFFFFFF;
barrierButton.tint = selectedGolemType === 'barrier' ? 0x00FF00 : 0xFFFFFF;
spikeButton.tint = selectedGolemType === 'spike' ? 0x00FF00 : 0xFFFFFF;
stoneButton.tint = selectedGolemType === 'stone' ? 0x00FF00 : 0xFFFFFF;
meleeButton.tint = selectedGolemType === 'melee' ? 0x00FF00 : 0xFFFFFF;
explosiveButton.tint = selectedGolemType === 'explosive' ? 0x00FF00 : 0xFFFFFF;
miniExplodeGolemButton.tint = selectedGolemType === 'miniExplodeGolem' ? 0x00FF00 : 0xFFFFFF;
shovelButton.tint = selectedGolemType === 'shovel' ? 0x00FF00 : 0xFFFFFF;
}
// Initialize button selection
updateButtonSelection();
// Initialize lawnmowers
for (var row = 0; row < gridRows; row++) {
var lawnmower = new Lawnmower();
lawnmower.x = 300;
lawnmower.y = gridStartY + row * cellSize;
lawnmower.row = row;
lawnmowers.push(lawnmower);
game.addChild(lawnmower);
}
// Game click handler
game.down = function (x, y, obj) {
// Check if clicking on a nutrient stone first
for (var n = 0; n < nutrients.length; n++) {
var nutrient = nutrients[n];
var dx = x - nutrient.x;
var dy = y - nutrient.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 50 && nutrientInventory.length < maxNutrientSlots) {
// Collect the nutrient stone
tween(nutrient, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
// Add to inventory
nutrientInventory.push(1);
updateNutrientUI();
nutrient.destroy();
}
});
nutrients.splice(n, 1);
return; // Exit early, don't process grid placement
}
}
// Check if clicking on nutrient inventory slots
var slotClicked = -1;
for (var s = 0; s < maxNutrientSlots; s++) {
var slotX = 2048 - 200 - s * 100;
var slotY = 200;
var globalSlotPos = LK.gui.topRight.toGlobal({
x: slotX,
y: slotY
});
var dx = x - globalSlotPos.x;
var dy = y - globalSlotPos.y;
if (Math.abs(dx) < 40 && Math.abs(dy) < 40 && s < nutrientInventory.length) {
slotClicked = s;
break;
}
}
if (slotClicked >= 0) {
selectedNutrientSlot = selectedNutrientSlot === slotClicked ? -1 : slotClicked;
updateNutrientUI();
return;
}
var gridPos = getGridPosition(x, y);
if (gridPos) {
if (selectedGolemType === 'shovel') {
// Remove golem if one exists at this position
if (gridOccupied[gridPos.row][gridPos.col]) {
for (var i = golems.length - 1; i >= 0; i--) {
var golem = golems[i];
var golemGridCol = Math.floor((golem.x - gridStartX + cellSize / 2) / cellSize);
var golemGridRow = Math.floor((golem.y - gridStartY + cellSize / 2) / cellSize);
if (golemGridCol === gridPos.col && golemGridRow === gridPos.row) {
golem.destroy();
golems.splice(i, 1);
gridOccupied[gridPos.row][gridPos.col] = false;
LK.getSound('remove').play();
break;
}
}
}
} else if (!gridOccupied[gridPos.row][gridPos.col]) {
// Check if we have a nutrient selected and there's a golem here
if (selectedNutrientSlot >= 0 && gridOccupied[gridPos.row][gridPos.col]) {
// Find the golem at this position
for (var g = 0; g < golems.length; g++) {
var golem = golems[g];
var golemGridCol = Math.floor((golem.x - gridStartX + cellSize / 2) / cellSize);
var golemGridRow = Math.floor((golem.y - gridStartY + cellSize / 2) / cellSize);
if (golemGridCol === gridPos.col && golemGridRow === gridPos.row) {
// Apply nutrient stone to this golem
applyNutrientToGolem(golem);
// Remove nutrient from inventory
nutrientInventory.splice(selectedNutrientSlot, 1);
selectedNutrientSlot = -1;
updateNutrientUI();
return;
}
}
}
var cost = golemCosts[selectedGolemType];
var currentTime = LK.ticks;
var cooldownKey = selectedGolemType;
var cooldownDuration = getGolemCooldownDuration(selectedGolemType);
// Check if golem type is on cooldown
if (golemPlacementCooldown[cooldownKey] && currentTime - golemPlacementCooldown[cooldownKey] < cooldownDuration) {
var remainingTime = Math.ceil((cooldownDuration - (currentTime - golemPlacementCooldown[cooldownKey])) / 60);
cooldownText.setText('Espera ' + remainingTime + 's para colocar ' + selectedGolemType);
cooldownText.alpha = 1;
tween(cooldownText, {
alpha: 0
}, {
duration: 2000
});
return;
}
if (stones >= cost) {
var golem;
if (selectedGolemType === 'miniExplodeGolem') {
golem = new miniExplodeGolem();
} else {
golem = new Golem(selectedGolemType);
}
golem.x = gridStartX + gridPos.col * cellSize;
golem.y = gridStartY + gridPos.row * cellSize;
golems.push(golem);
game.addChild(golem);
gridOccupied[gridPos.row][gridPos.col] = true;
stones -= cost;
golemPlacementCooldown[cooldownKey] = currentTime;
lastGolemPlacement = currentTime;
updateUI();
LK.getSound('place').play();
}
}
}
};
// Countdown sequence before first wave
var countdownText = new Text2('', {
size: 80,
fill: 0xFFFFFF
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 2048 / 2;
countdownText.y = 2732 / 2;
game.addChild(countdownText);
// Show "preparado..." at 4 seconds
LK.setTimeout(function () {
countdownText.setText('preparado...');
countdownText.alpha = 1;
// Fade in effect
tween(countdownText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(countdownText, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200
});
}
});
}, 4000);
// Show "listo..." at 8 seconds
LK.setTimeout(function () {
countdownText.setText('listo...');
// Scale and color effect
tween(countdownText, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFFFF00
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(countdownText, {
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}, 8000);
// Show "¡PELEA!" at 12 seconds and start wave
LK.setTimeout(function () {
countdownText.setText('¡PELEA!');
// Dramatic entrance effect
tween(countdownText, {
scaleX: 1.5,
scaleY: 1.5,
tint: 0xFF0000
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
// Enable demon spawning after PELEA text is shown
demonsCanSpawn = true;
// Start first wave immediately after PELEA
spawnWave();
// Fade out after showing PELEA
tween(countdownText, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 500,
onFinish: function onFinish() {
countdownText.destroy();
}
});
}
});
}, 12000);
game.update = function () {
// Update golems
for (var i = golems.length - 1; i >= 0; i--) {
var golem = golems[i];
if (golem.update) {
golem.update();
}
}
// Update demons
for (var i = demons.length - 1; i >= 0; i--) {
var demon = demons[i];
demon.update();
// Check if demon reached left side
if (demon.x < 350) {
// Check if there's a lawnmower in this row to activate
var demonRow = Math.floor((demon.y - gridStartY + cellSize / 2) / cellSize);
var lawnmowerActivated = false;
for (var l = 0; l < lawnmowers.length; l++) {
var lawnmower = lawnmowers[l];
if (lawnmower.row === demonRow && !lawnmower.isActive) {
lawnmower.activate();
lawnmowerActivated = true;
break;
}
}
if (!lawnmowerActivated) {
lives--;
demon.destroy();
demons.splice(i, 1);
updateUI();
if (lives <= 0) {
LK.setScore(score);
LK.showGameOver();
return;
}
}
} else if (demon.shouldRemove) {
demon.destroy();
demons.splice(i, 1);
}
}
// Update lawnmowers
for (var i = lawnmowers.length - 1; i >= 0; i--) {
var lawnmower = lawnmowers[i];
lawnmower.update();
if (lawnmower.shouldRemove) {
lawnmower.destroy();
lawnmowers.splice(i, 1);
}
}
// Update progress bar
if (totalDemonsInWave > 0) {
var progress = demonsKilledInWave / totalDemonsInWave;
var targetScale = progress * 12;
tween.stop(progressBarFill, {
scaleX: true
});
tween(progressBarFill, {
scaleX: targetScale
}, {
duration: 200
});
// Update progress text with flag wave indicator
var progressPercentage = Math.floor(progress * 100);
var flagText = isFlagWave ? " (BANDERA!)" : "";
progressText.setText('Wave ' + wave + ': ' + progressPercentage + '%' + flagText);
// Change progress bar color for flag waves
if (isFlagWave) {
progressBarFill.tint = 0xFF4444; // Red for flag waves
progressText.tint = 0xFF4444;
} else {
progressBarFill.tint = 0x00FF00; // Green for normal waves
progressText.tint = 0xFFFFFF;
}
}
// Update projectiles
for (var i = projectiles.length - 1; i >= 0; i--) {
var projectile = projectiles[i];
projectile.update();
if (projectile.shouldRemove) {
projectile.destroy();
projectiles.splice(i, 1);
continue;
}
// Check projectile vs demon collision
for (var j = demons.length - 1; j >= 0; j--) {
var demon = demons[j];
if (projectile.intersects(demon)) {
demon.takeDamage(projectile.damage);
// Heavy golem projectiles have 25% chance to push demons backwards
if (projectile.golemType === 'heavy' && Math.random() < 0.25) {
// Push demon backwards (to the right) by 100 pixels
tween(demon, {
x: demon.x + 100
}, {
duration: 300,
easing: tween.easeOut
});
}
projectile.destroy();
projectiles.splice(i, 1);
break;
}
}
}
// Update nutrients - no auto collection, manual only
for (var i = nutrients.length - 1; i >= 0; i--) {
var nutrient = nutrients[i];
nutrient.update();
if (nutrient.shouldRemove) {
nutrient.destroy();
nutrients.splice(i, 1);
continue;
}
}
// Update stones with automatic collection only
for (var i = stones_collectible.length - 1; i >= 0; i--) {
var stone = stones_collectible[i];
stone.update();
if (stone.shouldRemove) {
stone.destroy();
stones_collectible.splice(i, 1);
continue;
}
// Auto-collect stones after 2 seconds (no manual collection)
if (!stone.isMarkedForCollection && LK.ticks - stone.spawnTime > 120) {
stone.isMarkedForCollection = true;
stone.collectionMarkTime = LK.ticks;
// Visual indicator - slight glow effect
tween(stone, {
tint: 0xFFFF88
}, {
duration: 200
});
}
// Check if marked stone should be collected after delay
if (stone.isMarkedForCollection && LK.ticks - stone.collectionMarkTime >= stone.collectionDelay) {
// Animate stone collection
tween(stone, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
stones += stone.value;
stone.destroy();
updateUI();
}
});
stones_collectible.splice(i, 1);
}
}
// Update stone spawn delay based on current wave
stoneSpawnDelay = baseStoneSpawnDelay + (wave - 1) * 20;
// Spawn stones periodically
if (LK.ticks - lastStoneSpawn > stoneSpawnDelay) {
var stone = new Stone();
stone.x = 400 + Math.random() * (2048 - 800); // Spawn in playable area
stone.y = 600 + Math.random() * 400;
stones_collectible.push(stone);
game.addChild(stone);
lastStoneSpawn = LK.ticks;
}
// Wave management
if (demons.length === 0 && demonsCanSpawn) {
nextWaveTimer++;
if (nextWaveTimer >= waveDelay) {
wave++;
nextWaveTimer = 0;
// Show flag wave announcement
if (flagWaves.indexOf(wave) !== -1) {
var flagText = new Text2('¡OLEADA BANDERA!', {
size: 60,
fill: 0xFF0000
});
flagText.anchor.set(0.5, 0.5);
flagText.x = 2048 / 2;
flagText.y = 400;
game.addChild(flagText);
// Animate flag announcement
tween(flagText, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFFFF00
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(flagText, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 1000,
onFinish: function onFinish() {
flagText.destroy();
}
});
}
});
}
spawnWave();
updateUI();
// Victory condition - now goes to wave 25
if (wave > 25) {
LK.setScore(score);
LK.showYouWin();
return;
}
}
}
};
// Initialize game start time
gameStartTime = LK.ticks;
// Add decorative elements outside garden
// Rocks scattered around the edges with rock texture
for (var i = 0; i < 15; i++) {
var rock = LK.getAsset('rockTexture', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8 + Math.random() * 1.0,
scaleY: 0.8 + Math.random() * 1.0,
tint: 0x8B7355 // Natural rock brown color
});
// Position rocks outside garden area
if (Math.random() < 0.5) {
// Left side decorations
rock.x = 50 + Math.random() * 150;
rock.y = 700 + Math.random() * 800;
} else {
// Right side decorations
rock.x = 2600 + Math.random() * 300;
rock.y = 700 + Math.random() * 800;
}
game.addChild(rock);
}
// Grass patches for decoration with grass texture
for (var i = 0; i < 20; i++) {
var grass = LK.getAsset('grassTexture', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8 + Math.random() * 0.6,
scaleY: 0.6 + Math.random() * 0.4,
alpha: 0.7,
tint: 0x32CD32 // Lime green for natural grass
});
// Scatter grass around the garden
if (Math.random() < 0.3) {
// Above garden
grass.x = 400 + Math.random() * 2000;
grass.y = 600 + Math.random() * 150;
} else if (Math.random() < 0.5) {
// Below garden
grass.x = 400 + Math.random() * 2000;
grass.y = 1800 + Math.random() * 200;
} else {
// Left and right sides
grass.x = Math.random() < 0.5 ? 100 + Math.random() * 100 : 2700 + Math.random() * 200;
grass.y = 700 + Math.random() * 1000;
}
game.addChild(grass);
}
// Small decorative stones near garden edges with rock texture
for (var i = 0; i < 8; i++) {
var smallStone = LK.getAsset('rockTexture', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5,
tint: 0x696969 // Darker gray for small stones
});
// Position near garden borders
smallStone.x = gridStartX - 50 + Math.random() * (gridCols * cellSize + 100);
smallStone.y = Math.random() < 0.5 ? gridStartY - 100 : gridStartY + gridRows * cellSize + 50;
game.addChild(smallStone);
}
// Initialize UI
updateUI();
updateNutrientUI(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Demon = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'basic';
// Base stats that get modified by wave difficulty
var baseHealth = 450;
var baseSpeed = 0.3;
var baseReward = 10;
// Wave difficulty multiplier - increases health and speed
var waveMultiplier = 1 + (wave - 1) * 0.15; // 15% increase per wave
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 2.0); // Cap speed at 2x
self.reward = Math.floor(baseReward * waveMultiplier);
self.biteDamage = 1 + Math.floor((wave - 1) / 3); // Increase bite damage every 3 waves
self.biteDelay = Math.max(15, 30 - wave); // Faster biting as waves progress
self.lastBite = 0;
var assetName = 'basicDemon';
if (self.type === 'fast') {
assetName = 'fastDemon';
baseHealth = 360;
baseSpeed = 1;
baseReward = 15;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 2.5); // Fast demons can go faster
self.reward = Math.floor(baseReward * waveMultiplier);
} else if (self.type === 'tank') {
assetName = 'tankDemon';
baseHealth = 1800;
baseSpeed = 0.25;
baseReward = 25;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 1.5); // Tank demons have speed cap
self.reward = Math.floor(baseReward * waveMultiplier);
} else if (self.type === 'zombieAllStar') {
assetName = 'tankDemon'; // Using tank demon sprite
baseHealth = 1200;
baseSpeed = 0.15;
baseReward = 50;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 1.3);
self.reward = Math.floor(baseReward * waveMultiplier);
self.biteDamage = 3 + Math.floor((wave - 1) / 2); // Extra damage scaling
self.biteDelay = Math.max(10, 20 - wave); // Faster biting
} else if (self.type === 'speedy') {
assetName = 'speedyDemon';
baseHealth = 600; // Reduced resistance
baseSpeed = 1.0; // Reduced speed
baseReward = 40;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 2.5); // Reduced max speed multiplier
self.reward = Math.floor(baseReward * waveMultiplier);
self.biteDamage = 2 + Math.floor((wave - 1) / 3);
self.biteDelay = Math.max(10, 20 - wave); // Fast attacks
} else if (self.type === 'dancing') {
assetName = 'dancingDemon';
baseHealth = 600;
baseSpeed = 0.4;
baseReward = 30;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 2.0);
self.reward = Math.floor(baseReward * waveMultiplier);
self.biteDamage = 2 + Math.floor((wave - 1) / 4);
self.biteDelay = Math.max(15, 25 - wave);
self.isDancing = false;
} else if (self.type === 'nutrient') {
assetName = 'basicDemon';
baseHealth = 400;
baseSpeed = 0.35;
baseReward = 20;
self.health = Math.floor(baseHealth * waveMultiplier);
self.maxHealth = self.health;
self.speed = baseSpeed * Math.min(waveMultiplier, 2.0);
self.reward = Math.floor(baseReward * waveMultiplier);
self.biteDamage = 1 + Math.floor((wave - 1) / 3);
self.biteDelay = Math.max(15, 30 - wave);
self.isNutrientDemon = true;
}
var graphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.3
});
self.update = function () {
// Check for golem collision and bite them
var blocked = false;
var targetGolem = null;
for (var i = 0; i < golems.length; i++) {
var golem = golems[i];
if (golem.type !== 'spike' && self.intersects(golem)) {
blocked = true;
targetGolem = golem;
break;
}
}
if (targetGolem && LK.ticks - self.lastBite > self.biteDelay) {
// Bite the golem
if (!targetGolem.health) {
if (targetGolem.type === 'barrier') {
targetGolem.health = 160; // Barrier golems have 160 health
targetGolem.maxHealth = 160;
} else {
targetGolem.health = 15; // All other golems have 15 health
targetGolem.maxHealth = 15;
}
}
targetGolem.health -= self.biteDamage;
self.lastBite = LK.ticks;
// Visual feedback - flash golem red
tween(targetGolem, {
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(targetGolem, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
if (targetGolem.health <= 0) {
// Remove golem from grid
var golemGridCol = Math.floor((targetGolem.x - gridStartX + cellSize / 2) / cellSize);
var golemGridRow = Math.floor((targetGolem.y - gridStartY + cellSize / 2) / cellSize);
if (golemGridCol >= 0 && golemGridCol < gridCols && golemGridRow >= 0 && golemGridRow < gridRows) {
gridOccupied[golemGridRow][golemGridCol] = false;
}
// Remove from golems array
for (var j = golems.length - 1; j >= 0; j--) {
if (golems[j] === targetGolem) {
targetGolem.destroy();
golems.splice(j, 1);
break;
}
}
blocked = false; // Can continue moving after eating golem
}
}
if (!blocked) {
self.x -= self.speed;
}
// Special dancing demon effects
if (self.type === 'dancing' && !self.isDancing && Math.random() < 0.01) {
// Start dancing animation
self.isDancing = true;
var originalSpeed = self.speed;
self.speed = 0; // Stop moving while dancing
// Dancing rotation animation
var danceRotations = 0;
var maxRotations = 3;
var _danceRotate2 = function _danceRotate() {
tween(graphics, {
rotation: Math.PI * 2
}, {
duration: 400,
easing: tween.easeInOut,
onFinish: function onFinish() {
graphics.rotation = 0; // Reset rotation
danceRotations++;
if (danceRotations < maxRotations) {
_danceRotate2();
} else {
// Dance complete, resume movement with speed boost
self.speed = originalSpeed * 1.5; // 50% speed boost after dance
self.isDancing = false;
// Speed boost lasts for 3 seconds
LK.setTimeout(function () {
if (self.health > 0) {
self.speed = originalSpeed; // Return to normal speed
}
}, 3000);
}
}
});
};
_danceRotate2();
// Color flash during dance
tween(graphics, {
tint: 0xFF00FF
}, {
duration: 200,
onFinish: function onFinish() {
tween(graphics, {
tint: 0x00FFFF
}, {
duration: 200,
onFinish: function onFinish() {
tween(graphics, {
tint: 0xFFFF00
}, {
duration: 200,
onFinish: function onFinish() {
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
});
}
});
}
// Special zombie all star effects
if (self.type === 'zombieAllStar') {
// Scale pulsing effect when at low health
var healthPercent = self.health / self.maxHealth;
if (healthPercent < 0.5 && !self.isPulsing) {
self.isPulsing = true;
var _pulseScale = function pulseScale() {
tween(graphics, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(graphics, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.health > 0 && healthPercent < 0.5) {
_pulseScale();
} else {
self.isPulsing = false;
}
}
});
}
});
};
_pulseScale();
}
}
// Update health bar color
var healthPercent = self.health / self.maxHealth;
if (healthPercent < 0.3) {
graphics.tint = 0xFF0000;
} else if (healthPercent < 0.6) {
graphics.tint = 0xFF6600;
}
};
// Add green glowing effect for nutrient demons
if (self.type === 'nutrient') {
// Pulsing green glow effect
if (!self.isGlowing) {
self.isGlowing = true;
var _glowPulse = function glowPulse() {
tween(graphics, {
tint: 0x00FF00,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(graphics, {
tint: 0x88FF88,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.health > 0) {
_glowPulse();
}
}
});
}
});
};
_glowPulse();
}
}
self.takeDamage = function (damage) {
self.health -= damage;
LK.getSound('hit').play();
if (self.health <= 0) {
self.shouldRemove = true;
score += self.reward;
demonsKilledInWave++;
// Drop purple nutrient stone if this is a nutrient demon
if (self.type === 'nutrient') {
var nutrient = new NutrientStone();
nutrient.x = self.x;
nutrient.y = self.y;
nutrients.push(nutrient);
game.addChild(nutrient);
}
updateUI();
}
};
return self;
});
var Golem = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'basic';
self.lastShot = 0;
self.range = 2048; // Full screen width for entire line attack
self.damage = 25;
self.shootDelay = 60; // frames
var assetName = 'basicGolem';
if (self.type === 'heavy') {
assetName = 'heavyGolem';
self.damage = 75;
self.range = 2048; // Full screen width for entire line attack
self.shootDelay = 120;
} else if (self.type === 'crystal') {
assetName = 'crystalGolem';
self.damage = 20;
self.range = 2048; // Full screen width for entire line attack
self.shootDelay = 30;
} else if (self.type === 'barrier') {
assetName = 'barrierGolem';
self.damage = 0;
self.range = 0;
self.shootDelay = 0;
} else if (self.type === 'spike') {
assetName = 'spikeGolem';
self.damage = 10;
self.range = 0;
self.shootDelay = 0;
} else if (self.type === 'stone') {
assetName = 'stoneGolem';
self.damage = 0;
self.range = 0;
self.shootDelay = 450; // Generate stone every 7.5 seconds
} else if (self.type === 'melee') {
assetName = 'meleeGolem';
self.damage = 40;
self.range = cellSize * 2; // 2 cell radius
self.shootDelay = 0; // No projectiles, direct combat
self.meleeDelay = 30; // frames between melee attacks
self.lastMelee = 0;
} else if (self.type === 'explosive') {
assetName = 'explosiveGolem';
self.damage = 80;
self.range = cellSize * 1.5; // 1.5 cell radius for detection
self.shootDelay = 0; // No projectiles, explodes on contact
self.hasExploded = false;
}
var graphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: self.type === 'spike' ? 0.0 : 0.3
});
self.update = function () {
if (self.type === 'barrier') {
return;
}
if (self.type === 'spike') {
// Check for contact damage with cooldown
if (!self.damageCooldown) {
self.damageCooldown = {};
}
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
if (self.intersects(demon)) {
var demonId = demon.id || i; // Use demon id or index as identifier
var currentTime = LK.ticks;
if (!self.damageCooldown[demonId] || currentTime - self.damageCooldown[demonId] > 60) {
demon.takeDamage(self.damage);
self.damageCooldown[demonId] = currentTime;
}
}
}
return;
}
if (self.type === 'stone') {
// Generate stones periodically
if (LK.ticks - self.lastShot > self.shootDelay) {
var stone = new Stone();
stone.x = self.x + (Math.random() - 0.5) * 100; // Spawn near golem
stone.y = self.y + (Math.random() - 0.5) * 100;
stone.value = 25; // Stone golem generates stones worth 25
stones_collectible.push(stone);
game.addChild(stone);
self.lastShot = LK.ticks;
}
return;
}
if (self.type === 'melee') {
// Melee combat - attack demons in 2 cell radius front and back
if (LK.ticks - self.lastMelee > self.meleeDelay) {
var golemRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
var golemCol = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
var attackedDemons = [];
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
var demonRow = Math.floor((demon.y - gridStartY + cellSize / 2) / cellSize);
var demonCol = Math.floor((demon.x - gridStartX + cellSize / 2) / cellSize);
// Check if demon is in same row and within 2 cells front or back
if (demonRow === golemRow) {
var colDistance = Math.abs(demonCol - golemCol);
if (colDistance <= 2 && colDistance > 0) {
var dx = demon.x - self.x;
var dy = demon.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.range) {
attackedDemons.push(demon);
}
}
}
}
if (attackedDemons.length > 0) {
// Attack all demons in range
for (var k = 0; k < attackedDemons.length; k++) {
var targetDemon = attackedDemons[k];
targetDemon.takeDamage(self.damage);
// Visual melee attack effect
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFF4444
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF
}, {
duration: 150
});
}
});
}
self.lastMelee = LK.ticks;
LK.getSound('hit').play();
}
}
return;
}
if (self.type === 'explosive') {
// Explosive golem - explode when demon gets close
if (!self.hasExploded) {
var golemRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
var golemCol = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
var demonsInRange = [];
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
var dx = demon.x - self.x;
var dy = demon.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.range) {
demonsInRange.push(demon);
}
}
if (demonsInRange.length > 0) {
// Explode in 3x3 area
self.hasExploded = true;
// Visual explosion effect
tween(self, {
scaleX: 2.0,
scaleY: 2.0,
tint: 0xFF4400,
alpha: 0.8
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 3.0,
scaleY: 3.0,
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
// Remove golem from grid after explosion
var golemGridCol = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
var golemGridRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
if (golemGridCol >= 0 && golemGridCol < gridCols && golemGridRow >= 0 && golemGridRow < gridRows) {
gridOccupied[golemGridRow][golemGridCol] = false;
}
// Remove from golems array
for (var j = golems.length - 1; j >= 0; j--) {
if (golems[j] === self) {
self.destroy();
golems.splice(j, 1);
break;
}
}
}
});
}
});
// Instantly kill all demons in 3x3 area
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
var demonRow = Math.floor((demon.y - gridStartY + cellSize / 2) / cellSize);
var demonCol = Math.floor((demon.x - gridStartX + cellSize / 2) / cellSize);
// Check if demon is within 3x3 area centered on golem
var rowDistance = Math.abs(demonRow - golemRow);
var colDistance = Math.abs(demonCol - golemCol);
if (rowDistance <= 1 && colDistance <= 1) {
// Instantly kill the demon by setting health to 0
demon.health = 0;
demon.shouldRemove = true;
stones += demon.reward;
score += demon.reward;
}
}
LK.getSound('hit').play();
}
}
return;
}
// Check if special ability has expired
if (self.hasSpecialAbility && LK.ticks > self.specialAbilityEndTime) {
self.hasSpecialAbility = false;
// Return to normal color
tween(self, {
tint: 0xFFFFFF
}, {
duration: 300
});
}
var currentShootDelay = self.shootDelay;
var currentDamage = self.damage;
// Apply special ability effects
if (self.hasSpecialAbility) {
if (self.type === 'basic' || self.type === 'heavy' || self.type === 'crystal') {
// Shooting golems: double fire rate and +50% damage
currentShootDelay = Math.floor(self.shootDelay * 0.5);
currentDamage = Math.floor(self.damage * 1.5);
} else if (self.type === 'spike') {
// Spike golems: triple damage
currentDamage = self.damage * 3;
} else if (self.type === 'stone') {
// Stone golems: generate stones 3x faster
currentShootDelay = Math.floor(self.shootDelay * 0.33);
} else if (self.type === 'melee') {
// Melee golems: double attack speed and damage
self.meleeDelay = Math.floor(30 * 0.5);
currentDamage = self.damage * 2;
} else if (self.type === 'explosive') {
// Explosive golems: larger explosion range
self.range = cellSize * 2.5;
}
}
if (LK.ticks - self.lastShot > currentShootDelay) {
var target = self.findTarget();
if (target) {
self.shoot(target);
self.lastShot = LK.ticks;
}
}
};
self.findTarget = function () {
var closestDemon = null;
var closestDistance = self.range;
var golemRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
var demonRow = Math.floor((demon.y - gridStartY + cellSize / 2) / cellSize);
// Only target demons in the same row
if (demonRow !== golemRow) {
continue;
}
var dx = demon.x - self.x;
var dy = demon.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestDemon = demon;
}
}
return closestDemon;
};
self.shoot = function (target) {
var projectile = new Projectile(self.type);
projectile.x = self.x;
projectile.y = self.y;
projectile.targetX = target.x;
projectile.targetY = target.y;
// Use enhanced damage if special ability is active
if (self.hasSpecialAbility) {
projectile.damage = Math.floor(self.damage * 1.5);
} else {
projectile.damage = self.damage;
}
projectiles.push(projectile);
game.addChild(projectile);
LK.getSound('shoot').play();
};
return self;
});
var Lawnmower = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('lawnmower', {
anchorX: 0.5,
anchorY: 0.3
});
self.isActive = false;
self.speed = 8;
self.row = 0;
self.update = function () {
if (self.isActive) {
self.x += self.speed;
// Remove lawnmower when it goes off screen
if (self.x > 2200) {
self.shouldRemove = true;
}
// Check collision with demons
for (var i = demons.length - 1; i >= 0; i--) {
var demon = demons[i];
if (self.intersects(demon)) {
// Instantly kill demon
demon.health = 0;
demon.shouldRemove = true;
score += demon.reward;
demonsKilledInWave++;
}
}
}
};
self.activate = function () {
if (!self.isActive) {
self.isActive = true;
LK.getSound('shoot').play();
}
};
return self;
});
var NutrientStone = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('stone', {
anchorX: 0.5,
anchorY: 0.3
});
// Make it purple and larger
graphics.tint = 0x8B008B;
graphics.scaleX = 1.3;
graphics.scaleY = 1.3;
self.value = 1;
self.bobOffset = Math.random() * Math.PI * 2;
self.spawnTime = LK.ticks;
self.lifespan = 600; // 10 seconds at 60 FPS
self.isCollected = false;
self.update = function () {
// Bobbing animation with purple sparkle effect
self.y += Math.sin(LK.ticks * 0.05 + self.bobOffset) * 0.5;
// Purple sparkle effect
if (LK.ticks % 30 === 0) {
tween(graphics, {
tint: 0xFF00FF
}, {
duration: 200,
onFinish: function onFinish() {
tween(graphics, {
tint: 0x8B008B
}, {
duration: 200
});
}
});
}
// Check if stone should disappear
if (LK.ticks - self.spawnTime > self.lifespan) {
self.shouldRemove = true;
}
};
return self;
});
var Projectile = Container.expand(function (golemType) {
var self = Container.call(this);
self.golemType = golemType || 'basic';
var assetName = 'projectile';
if (self.golemType === 'heavy') {
assetName = 'heavyProjectile';
} else if (self.golemType === 'crystal') {
assetName = 'crystalProjectile';
}
var graphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.3
});
self.damage = 25;
self.speed = 8;
self.targetX = 0;
self.targetY = 0;
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 < 10) {
self.shouldRemove = true;
return;
}
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
};
return self;
});
var Stone = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('stone', {
anchorX: 0.5,
anchorY: 0.3
});
self.value = 10;
self.bobOffset = Math.random() * Math.PI * 2;
self.spawnTime = LK.ticks;
self.lifespan = 300; // 5 seconds at 60 FPS
self.isMarkedForCollection = false;
self.collectionMarkTime = 0;
self.collectionDelay = 60; // 1 second at 60 FPS
self.update = function () {
// Bobbing animation
self.y += Math.sin(LK.ticks * 0.05 + self.bobOffset) * 0.3;
// Check if stone should disappear
if (LK.ticks - self.spawnTime > self.lifespan) {
self.shouldRemove = true;
}
};
return self;
});
var miniExplodeGolem = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('potatoMine', {
anchorX: 0.5,
anchorY: 0.0
});
self.isUnderground = true;
self.armingTime = 900; // 15 seconds to arm
self.spawnTime = LK.ticks;
self.isArmed = false;
self.hasExploded = false;
self.explosionDamage = 150; // High damage but less than explosiveGolem
// Start underground (invisible and smaller)
graphics.alpha = 0.3;
graphics.scaleX = 0.5;
graphics.scaleY = 0.5;
graphics.tint = 0x8B4513; // Brown color for underground
self.update = function () {
// Arm the mine after arming time
if (!self.isArmed && LK.ticks - self.spawnTime > self.armingTime) {
self.isArmed = true;
self.isUnderground = false;
// Surface animation - become visible and full size
tween(graphics, {
alpha: 1.0,
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF
}, {
duration: 500,
easing: tween.easeOut
});
}
// Check for demon collision if armed and not exploded
if (self.isArmed && !self.hasExploded) {
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
if (self.intersects(demon)) {
self.explode();
break;
}
}
}
};
self.explode = function () {
if (self.hasExploded) {
return;
}
self.hasExploded = true;
// Visual explosion effect
tween(graphics, {
scaleX: 3.0,
scaleY: 3.0,
tint: 0xFF4400,
alpha: 0.8
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(graphics, {
scaleX: 4.0,
scaleY: 4.0,
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
// Remove from grid and golems array
var mineGridCol = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
var mineGridRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
if (mineGridCol >= 0 && mineGridCol < gridCols && mineGridRow >= 0 && mineGridRow < gridRows) {
gridOccupied[mineGridRow][mineGridCol] = false;
}
// Remove from golems array
for (var j = golems.length - 1; j >= 0; j--) {
if (golems[j] === self) {
self.destroy();
golems.splice(j, 1);
break;
}
}
}
});
}
});
// Kill all demons in a 1x1 area (single cell) around the mine
var mineRow = Math.floor((self.y - gridStartY + cellSize / 2) / cellSize);
var mineCol = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
for (var i = 0; i < demons.length; i++) {
var demon = demons[i];
var demonRow = Math.floor((demon.y - gridStartY + cellSize / 2) / cellSize);
var demonCol = Math.floor((demon.x - gridStartX + cellSize / 2) / cellSize);
// Check if demon is within same cell as mine
if (demonRow === mineRow && demonCol === mineCol) {
// Damage the demon with explosion damage
demon.health -= self.explosionDamage;
if (demon.health <= 0) {
demon.shouldRemove = true;
score += demon.reward;
demonsKilledInWave++;
}
}
}
LK.getSound('hit').play();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2F5233
});
/****
* Game Code
****/
// Game variables
var stones = 100;
var score = 0;
var lives = 10;
var wave = 1;
var nextWaveTimer = 0;
var waveDelay = 300; // frames between waves
// Flag wave system variables
var totalDemonsInWave = 0;
var demonsKilledInWave = 0;
var flagWaves = [5, 10, 15, 20]; // Waves with massive demon spawns
var isFlagWave = false;
var flagWaveSpawned = false;
// Game arrays
var golems = [];
var demons = [];
var projectiles = [];
var stones_collectible = [];
var lawnmowers = [];
var nutrients = [];
// Special ability system
var specialAbilityActive = false;
var specialAbilityEndTime = 0;
var specialAbilityDuration = 300; // 5 seconds at 60 FPS
// Nutrient stone inventory system
var nutrientInventory = []; // Max 3 stones
var maxNutrientSlots = 3;
var selectedNutrientSlot = -1; // -1 means no slot selected
// Stone spawning - gets slower as waves progress
var lastStoneSpawn = 0;
var baseStoneSpawnDelay = 150;
var stoneSpawnDelay = baseStoneSpawnDelay + (wave - 1) * 20; // Slower stone spawning as waves progress
// Demon spawn cooldown
var demonSpawnCooldown = 300; // 5 seconds between demon spawns
var lastDemonSpawn = 0;
// Initial game delay - demons don't spawn for first 12 seconds
var gameStartDelay = 720; // 12 seconds at 60 FPS
var gameStartTime = 0;
var demonsCanSpawn = false;
// Grid system
var gridCols = 12;
var gridRows = 5;
var gridStartX = 224;
var gridStartY = 800;
var cellSize = 200;
// Golem costs
var golemCosts = {
basic: 100,
heavy: 250,
crystal: 75,
barrier: 80,
spike: 60,
stone: 50,
melee: 120,
explosive: 150,
miniExplodeGolem: 25,
shovel: 0
};
// Selected golem type
var selectedGolemType = 'basic';
// Double-tap detection variables
var lastClickedButton = null;
var lastClickTime = 0;
var doubleTapThreshold = 500; // 500ms window for double tap
// Golem placement cooldown system
var golemPlacementCooldown = {};
var golemCooldownDuration = 180; // 3 seconds at 60 FPS - base duration
var lastGolemPlacement = 0;
// Function to get cooldown duration based on golem cost
function getGolemCooldownDuration(golemType) {
var cost = golemCosts[golemType];
if (cost === 0) {
return 0;
} // No cooldown for free items like shovel
// More expensive golems have longer cooldowns
// Base formula: cost * 1.5 frames (at 60 FPS)
return Math.floor(cost * 1.5);
}
function updateNutrientUI() {
// Update nutrient title to show current count
nutrientTitle.setText('Nutrientes: ' + nutrientInventory.length + '/3');
// Clear existing nutrient visuals
for (var s = 0; s < maxNutrientSlots; s++) {
// Start with gray/opaque for empty slots, purple for selected
if (s < nutrientInventory.length) {
nutrientSlotBg[s].tint = selectedNutrientSlot === s ? 0xFF00FF : 0x8B008B;
nutrientSlotBg[s].alpha = 1.0;
} else {
nutrientSlotBg[s].tint = 0x666666; // Gray for empty slots
nutrientSlotBg[s].alpha = 0.6; // More opaque
}
// Remove old nutrient stones from slots
for (var c = nutrientSlotBg[s].children.length - 1; c >= 0; c--) {
nutrientSlotBg[s].children[c].destroy();
}
}
// Add nutrient stones to occupied slots
for (var i = 0; i < nutrientInventory.length; i++) {
var nutrientStone = LK.getAsset('stone', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8,
tint: 0x8B008B
});
nutrientStone.x = 0;
nutrientStone.y = 0;
nutrientSlotBg[i].addChild(nutrientStone);
}
}
function applyNutrientToGolem(golem) {
if (golem.type === 'barrier') {
// Barrier golems get permanent durability boost
if (!golem.health) {
golem.health = 160;
golem.maxHealth = 160;
}
golem.health += 80; // Permanent +80 health boost
golem.maxHealth += 80;
// Visual enhancement for barrier
tween(golem, {
tint: 0x4444FF,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 300
});
} else {
// Other golems get temporary special abilities
golem.hasSpecialAbility = true;
golem.specialAbilityEndTime = LK.ticks + specialAbilityDuration;
// Visual enhancement
tween(golem, {
tint: 0xFF8800
}, {
duration: 300
});
}
// Show effect text
var effectText = new Text2('¡POTENCIADO!', {
size: 30,
fill: 0x8B008B
});
effectText.anchor.set(0.5, 0.5);
effectText.x = golem.x;
effectText.y = golem.y - 50;
game.addChild(effectText);
// Animate effect text
tween(effectText, {
y: golem.y - 100,
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
effectText.destroy();
}
});
}
function activateSpecialAbilities() {
specialAbilityActive = true;
specialAbilityEndTime = LK.ticks + specialAbilityDuration;
// Show special ability activation message
var abilityText = new Text2('¡PODER ESPECIAL ACTIVADO!', {
size: 50,
fill: 0x8B008B
});
abilityText.anchor.set(0.5, 0.5);
abilityText.x = 2048 / 2;
abilityText.y = 300;
game.addChild(abilityText);
// Animate ability text
tween(abilityText, {
scaleX: 1.2,
scaleY: 1.2,
tint: 0xFF00FF
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(abilityText, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 1500,
onFinish: function onFinish() {
abilityText.destroy();
}
});
}
});
// Apply special effects to all golems
for (var i = 0; i < golems.length; i++) {
var golem = golems[i];
if (golem.type === 'barrier') {
// Barrier golems get permanent durability boost
if (!golem.health) {
golem.health = 160;
golem.maxHealth = 160;
}
golem.health += 80; // Permanent +80 health boost
golem.maxHealth += 80;
// Visual enhancement for barrier
tween(golem, {
tint: 0x4444FF,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 300
});
} else {
// Other golems get temporary special abilities
golem.hasSpecialAbility = true;
golem.specialAbilityEndTime = specialAbilityEndTime;
// Visual enhancement
tween(golem, {
tint: 0xFF8800
}, {
duration: 300,
onFinish: function onFinish() {
// Keep enhanced color during ability duration
}
});
}
}
}
// UI elements
var stonesText = new Text2('Stones: 100', {
size: 40,
fill: 0xFFFFFF
});
stonesText.anchor.set(0, 0);
LK.gui.topLeft.addChild(stonesText);
stonesText.x = 120;
stonesText.y = 20;
var livesText = new Text2('Lives: 10', {
size: 40,
fill: 0xFFFFFF
});
livesText.anchor.set(0, 0);
LK.gui.topLeft.addChild(livesText);
livesText.x = 120;
livesText.y = 70;
var waveText = new Text2('Wave: 1', {
size: 40,
fill: 0xFFFFFF
});
waveText.anchor.set(0, 0);
LK.gui.topLeft.addChild(waveText);
waveText.x = 120;
waveText.y = 120;
var scoreText = new Text2('Score: 0', {
size: 40,
fill: 0xFFFFFF
});
scoreText.anchor.set(1, 0);
LK.gui.topRight.addChild(scoreText);
scoreText.x = -20;
scoreText.y = 20;
// Wave progress bar
var progressBarBg = LK.getAsset('gridCell', {
scaleX: 12,
scaleY: 0.3,
alpha: 0.3,
tint: 0x444444
});
progressBarBg.x = 2048 / 2;
progressBarBg.y = 50;
game.addChild(progressBarBg);
var progressBarFill = LK.getAsset('gridCell', {
scaleX: 0,
scaleY: 0.3,
tint: 0x00FF00
});
progressBarFill.x = 2048 / 2 - 1200;
progressBarFill.y = 50;
game.addChild(progressBarFill);
var progressText = new Text2('Wave 1 Progress', {
size: 30,
fill: 0xFFFFFF
});
progressText.anchor.set(0.5, 0.5);
progressText.x = 2048 / 2;
progressText.y = 80;
game.addChild(progressText);
// Create nutrient inventory slots
var nutrientSlots = [];
var nutrientSlotBg = [];
for (var s = 0; s < maxNutrientSlots; s++) {
// Background for slot
var slotBg = LK.getAsset('gridCell', {
scaleX: 0.4,
scaleY: 0.4,
alpha: 0.5,
tint: 0x333333
});
slotBg.x = 2048 - 200 - s * 100;
slotBg.y = 200;
LK.gui.topRight.addChild(slotBg);
nutrientSlotBg.push(slotBg);
// Slot for nutrient stone (initially empty)
nutrientSlots.push(null);
}
// Nutrient inventory title
var nutrientTitle = new Text2('Nutrientes: 0/3', {
size: 50,
fill: 0x8B008B
});
nutrientTitle.anchor.set(1, 0);
nutrientTitle.x = -20;
nutrientTitle.y = 140;
LK.gui.topRight.addChild(nutrientTitle);
// Cooldown status text
var cooldownText = new Text2('', {
size: 30,
fill: 0xFF6600
});
cooldownText.anchor.set(0.5, 0.5);
cooldownText.x = 2048 / 2;
cooldownText.y = 200;
game.addChild(cooldownText);
// Golem selection buttons
var buttonY = 200;
var buttonSpacing = 150;
var basicButton = LK.getAsset('basicGolem', {
scaleX: 1.2,
scaleY: 1.2
});
basicButton.x = 50;
basicButton.y = buttonY;
LK.gui.topLeft.addChild(basicButton);
var heavyButton = LK.getAsset('heavyGolem', {
scaleX: 1.0,
scaleY: 1.0
});
heavyButton.x = 50;
heavyButton.y = buttonY + buttonSpacing;
LK.gui.topLeft.addChild(heavyButton);
var crystalButton = LK.getAsset('crystalGolem', {
scaleX: 1.1,
scaleY: 1.1
});
crystalButton.x = 50;
crystalButton.y = buttonY + buttonSpacing * 2;
LK.gui.topLeft.addChild(crystalButton);
var barrierButton = LK.getAsset('barrierGolem', {
scaleX: 0.9,
scaleY: 1.0
});
barrierButton.x = 50;
barrierButton.y = buttonY + buttonSpacing * 3;
LK.gui.topLeft.addChild(barrierButton);
var spikeButton = LK.getAsset('spikeGolem', {
scaleX: 1.2,
scaleY: 1.2
});
spikeButton.x = 50;
spikeButton.y = buttonY + buttonSpacing * 4;
LK.gui.topLeft.addChild(spikeButton);
var stoneButton = LK.getAsset('stoneGolem', {
scaleX: 1.0,
scaleY: 1.0
});
stoneButton.x = 50;
stoneButton.y = buttonY + buttonSpacing * 5;
LK.gui.topLeft.addChild(stoneButton);
var meleeButton = LK.getAsset('meleeGolem', {
scaleX: 1.0,
scaleY: 1.0
});
meleeButton.x = 50;
meleeButton.y = buttonY + buttonSpacing * 6;
LK.gui.topLeft.addChild(meleeButton);
var explosiveButton = LK.getAsset('explosiveGolem', {
scaleX: 1.0,
scaleY: 1.0
});
explosiveButton.x = 50;
explosiveButton.y = buttonY + buttonSpacing * 7;
LK.gui.topLeft.addChild(explosiveButton);
var miniExplodeGolemButton = LK.getAsset('potatoMine', {
scaleX: 1.0,
scaleY: 1.0
});
miniExplodeGolemButton.x = 50;
miniExplodeGolemButton.y = buttonY + buttonSpacing * 8;
LK.gui.topLeft.addChild(miniExplodeGolemButton);
var shovelButton = LK.getAsset('shovel', {
scaleX: 1.0,
scaleY: 1.0
});
shovelButton.x = 50;
shovelButton.y = buttonY + buttonSpacing * 9;
LK.gui.topLeft.addChild(shovelButton);
// Cost labels
var basicCostText = new Text2('100', {
size: 24,
fill: 0xFFFF00
});
basicCostText.x = 150;
basicCostText.y = buttonY + 20;
LK.gui.topLeft.addChild(basicCostText);
var heavyCostText = new Text2('250', {
size: 24,
fill: 0xFFFF00
});
heavyCostText.x = 150;
heavyCostText.y = buttonY + buttonSpacing + 20;
LK.gui.topLeft.addChild(heavyCostText);
var crystalCostText = new Text2('75', {
size: 24,
fill: 0xFFFF00
});
crystalCostText.x = 150;
crystalCostText.y = buttonY + buttonSpacing * 2 + 20;
LK.gui.topLeft.addChild(crystalCostText);
var barrierCostText = new Text2('80', {
size: 24,
fill: 0xFFFF00
});
barrierCostText.x = 150;
barrierCostText.y = buttonY + buttonSpacing * 3 + 20;
LK.gui.topLeft.addChild(barrierCostText);
var spikeCostText = new Text2('60', {
size: 24,
fill: 0xFFFF00
});
spikeCostText.x = 150;
spikeCostText.y = buttonY + buttonSpacing * 4 + 20;
LK.gui.topLeft.addChild(spikeCostText);
var stoneCostText = new Text2('50', {
size: 24,
fill: 0xFFFF00
});
stoneCostText.x = 150;
stoneCostText.y = buttonY + buttonSpacing * 5 + 20;
LK.gui.topLeft.addChild(stoneCostText);
var meleeCostText = new Text2('120', {
size: 24,
fill: 0xFFFF00
});
meleeCostText.x = 150;
meleeCostText.y = buttonY + buttonSpacing * 6 + 20;
LK.gui.topLeft.addChild(meleeCostText);
var explosiveCostText = new Text2('150', {
size: 24,
fill: 0xFFFF00
});
explosiveCostText.x = 150;
explosiveCostText.y = buttonY + buttonSpacing * 7 + 20;
LK.gui.topLeft.addChild(explosiveCostText);
var miniExplodeGolemCostText = new Text2('25', {
size: 24,
fill: 0xFFFF00
});
miniExplodeGolemCostText.x = 150;
miniExplodeGolemCostText.y = buttonY + buttonSpacing * 8 + 20;
LK.gui.topLeft.addChild(miniExplodeGolemCostText);
var shovelCostText = new Text2('Remove', {
size: 20,
fill: 0xFFFF00
});
shovelCostText.x = 150;
shovelCostText.y = buttonY + buttonSpacing * 9 + 20;
LK.gui.topLeft.addChild(shovelCostText);
// Cooldown text displays for buttons
var basicCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
basicCooldownText.anchor.set(0.5, 0.5);
basicCooldownText.x = 50;
basicCooldownText.y = buttonY - 20;
basicCooldownText.alpha = 0;
LK.gui.topLeft.addChild(basicCooldownText);
var heavyCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
heavyCooldownText.anchor.set(0.5, 0.5);
heavyCooldownText.x = 50;
heavyCooldownText.y = buttonY + buttonSpacing - 20;
heavyCooldownText.alpha = 0;
LK.gui.topLeft.addChild(heavyCooldownText);
var crystalCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
crystalCooldownText.anchor.set(0.5, 0.5);
crystalCooldownText.x = 50;
crystalCooldownText.y = buttonY + buttonSpacing * 2 - 20;
crystalCooldownText.alpha = 0;
LK.gui.topLeft.addChild(crystalCooldownText);
var barrierCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
barrierCooldownText.anchor.set(0.5, 0.5);
barrierCooldownText.x = 50;
barrierCooldownText.y = buttonY + buttonSpacing * 3 - 20;
barrierCooldownText.alpha = 0;
LK.gui.topLeft.addChild(barrierCooldownText);
var spikeCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
spikeCooldownText.anchor.set(0.5, 0.5);
spikeCooldownText.x = 50;
spikeCooldownText.y = buttonY + buttonSpacing * 4 - 20;
spikeCooldownText.alpha = 0;
LK.gui.topLeft.addChild(spikeCooldownText);
var stoneCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
stoneCooldownText.anchor.set(0.5, 0.5);
stoneCooldownText.x = 50;
stoneCooldownText.y = buttonY + buttonSpacing * 5 - 20;
stoneCooldownText.alpha = 0;
LK.gui.topLeft.addChild(stoneCooldownText);
var meleeCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
meleeCooldownText.anchor.set(0.5, 0.5);
meleeCooldownText.x = 50;
meleeCooldownText.y = buttonY + buttonSpacing * 6 - 20;
meleeCooldownText.alpha = 0;
LK.gui.topLeft.addChild(meleeCooldownText);
var explosiveCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
explosiveCooldownText.anchor.set(0.5, 0.5);
explosiveCooldownText.x = 50;
explosiveCooldownText.y = buttonY + buttonSpacing * 7 - 20;
explosiveCooldownText.alpha = 0;
LK.gui.topLeft.addChild(explosiveCooldownText);
var miniExplodeGolemCooldownText = new Text2('', {
size: 30,
fill: 0xFF0000
});
miniExplodeGolemCooldownText.anchor.set(0.5, 0.5);
miniExplodeGolemCooldownText.x = 50;
miniExplodeGolemCooldownText.y = buttonY + buttonSpacing * 8 - 20;
miniExplodeGolemCooldownText.alpha = 0;
LK.gui.topLeft.addChild(miniExplodeGolemCooldownText);
// Create grid visualization
var gridCells = [];
for (var row = 0; row < gridRows; row++) {
gridCells[row] = [];
for (var col = 0; col < gridCols; col++) {
var cell = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.3,
alpha: 0.3,
tint: row === 0 ? 0x87CEEB : 0xFFFFFF // Sky blue for first row
});
cell.x = gridStartX + col * cellSize;
cell.y = gridStartY + row * cellSize;
cell.gridX = col;
cell.gridY = row;
gridCells[row][col] = cell;
game.addChild(cell);
}
}
// Grid occupied tracking
var gridOccupied = [];
for (var row = 0; row < gridRows; row++) {
gridOccupied[row] = [];
for (var col = 0; col < gridCols; col++) {
gridOccupied[row][col] = false;
}
}
function updateUI() {
stonesText.setText('Stones: ' + stones);
livesText.setText('Lives: ' + lives);
waveText.setText('Wave: ' + wave);
scoreText.setText('Score: ' + score);
var currentTime = LK.ticks;
// Update button availability based on cost and cooldown
function getButtonAlpha(golemType, cost) {
var hasEnoughStones = stones >= cost;
var cooldownDuration = getGolemCooldownDuration(golemType);
var isOnCooldown = golemPlacementCooldown[golemType] && currentTime - golemPlacementCooldown[golemType] < cooldownDuration;
return hasEnoughStones && !isOnCooldown ? 1.0 : 0.5;
}
basicButton.alpha = getButtonAlpha('basic', golemCosts.basic);
heavyButton.alpha = getButtonAlpha('heavy', golemCosts.heavy);
crystalButton.alpha = getButtonAlpha('crystal', golemCosts.crystal);
barrierButton.alpha = getButtonAlpha('barrier', golemCosts.barrier);
spikeButton.alpha = getButtonAlpha('spike', golemCosts.spike);
stoneButton.alpha = getButtonAlpha('stone', golemCosts.stone);
meleeButton.alpha = getButtonAlpha('melee', golemCosts.melee);
explosiveButton.alpha = getButtonAlpha('explosive', golemCosts.explosive);
miniExplodeGolemButton.alpha = getButtonAlpha('miniExplodeGolem', golemCosts.miniExplodeGolem);
shovelButton.alpha = 1.0; // Shovel is always available
// Update cooldown displays
function updateCooldownDisplay(golemType, cooldownText) {
var cooldownDuration = getGolemCooldownDuration(golemType);
if (golemPlacementCooldown[golemType] && currentTime - golemPlacementCooldown[golemType] < cooldownDuration) {
var remainingTime = Math.ceil((cooldownDuration - (currentTime - golemPlacementCooldown[golemType])) / 60);
cooldownText.setText(remainingTime + 's');
cooldownText.alpha = 1;
} else {
cooldownText.alpha = 0;
}
}
updateCooldownDisplay('basic', basicCooldownText);
updateCooldownDisplay('heavy', heavyCooldownText);
updateCooldownDisplay('crystal', crystalCooldownText);
updateCooldownDisplay('barrier', barrierCooldownText);
updateCooldownDisplay('spike', spikeCooldownText);
updateCooldownDisplay('stone', stoneCooldownText);
updateCooldownDisplay('melee', meleeCooldownText);
updateCooldownDisplay('explosive', explosiveCooldownText);
updateCooldownDisplay('miniExplodeGolem', miniExplodeGolemCooldownText);
}
function spawnWave() {
// Don't spawn demons if initial delay hasn't passed
if (!demonsCanSpawn) {
return;
}
// Check if this is a flag wave
isFlagWave = flagWaves.indexOf(wave) !== -1;
flagWaveSpawned = false;
var demonsToSpawn;
if (isFlagWave) {
// Flag waves spawn 3x more demons
demonsToSpawn = Math.floor((2 + Math.floor(wave * 1.5)) * 3);
} else {
// Dramatically increase demons per wave - starts at 3, increases significantly
demonsToSpawn = Math.max(3, 2 + Math.floor(wave * 1.5)); // More aggressive scaling
}
totalDemonsInWave = demonsToSpawn;
demonsKilledInWave = 0;
var spawnDelay = 300; // 5 seconds delay (300 frames at 60 FPS)
// Decrease spawn delay between demons as waves progress
var delayBetweenSpawns = Math.max(150, 450 - wave * 15); // Faster spawning in later waves
// Flag waves spawn demons much faster
if (isFlagWave) {
delayBetweenSpawns = Math.max(60, delayBetweenSpawns / 2);
}
for (var i = 0; i < demonsToSpawn; i++) {
LK.setTimeout(function () {
// Check if enough time has passed since last demon spawn
if (LK.ticks - lastDemonSpawn >= demonSpawnCooldown) {
var demonType = 'basic';
// More aggressive probability scaling based on wave
var waveScaling = Math.min(wave / 10, 1.0); // Scale from 0 to 1 over 10 waves
// Flag waves have higher chance of special demons
var flagMultiplier = isFlagWave ? 1.5 : 1.0;
if (wave > 2 && Math.random() < (0.2 + waveScaling * 0.4) * flagMultiplier) {
demonType = 'fast';
} // Up to 60% chance
if (wave > 3 && Math.random() < (0.15 + waveScaling * 0.3) * flagMultiplier) {
demonType = 'dancing';
} // Up to 45% chance
if (wave > 4 && Math.random() < (0.1 + waveScaling * 0.4) * flagMultiplier) {
demonType = 'tank';
} // Up to 50% chance
if (wave > 5 && Math.random() < (0.05 + waveScaling * 0.3) * flagMultiplier) {
demonType = 'speedy';
} // Up to 35% chance
// Add elite demon spawning for high waves
if (wave > 7 && Math.random() < (0.05 + waveScaling * 0.15) * flagMultiplier) {
demonType = 'zombieAllStar';
} // Up to 20% chance
// Add nutrient demons starting from wave 3
if (wave > 2 && Math.random() < 0.15) {
demonType = 'nutrient';
} // 15% chance for nutrient demons
var demon = new Demon(demonType);
demon.x = 2048;
demon.y = gridStartY + Math.floor(Math.random() * gridRows) * cellSize;
demons.push(demon);
game.addChild(demon);
lastDemonSpawn = LK.ticks;
}
}, spawnDelay);
spawnDelay += delayBetweenSpawns; // Use variable delay that decreases with wave
}
}
function getGridPosition(x, y) {
var col = Math.floor((x - gridStartX + cellSize / 2) / cellSize);
var row = Math.floor((y - gridStartY + cellSize / 2) / cellSize);
if (col >= 0 && col < gridCols && row >= 0 && row < gridRows) {
return {
col: col,
row: row
};
}
return null;
}
// Button click handlers
basicButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'basic' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.basic) {
selectedGolemType = 'basic';
lastClickedButton = 'basic';
lastClickTime = currentTime;
updateButtonSelection();
}
};
heavyButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'heavy' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.heavy) {
selectedGolemType = 'heavy';
lastClickedButton = 'heavy';
lastClickTime = currentTime;
updateButtonSelection();
}
};
crystalButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'crystal' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.crystal) {
selectedGolemType = 'crystal';
lastClickedButton = 'crystal';
lastClickTime = currentTime;
updateButtonSelection();
}
};
barrierButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'barrier' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.barrier) {
selectedGolemType = 'barrier';
lastClickedButton = 'barrier';
lastClickTime = currentTime;
updateButtonSelection();
}
};
spikeButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'spike' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.spike) {
selectedGolemType = 'spike';
lastClickedButton = 'spike';
lastClickTime = currentTime;
updateButtonSelection();
}
};
stoneButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'stone' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.stone) {
selectedGolemType = 'stone';
lastClickedButton = 'stone';
lastClickTime = currentTime;
updateButtonSelection();
}
};
meleeButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'melee' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.melee) {
selectedGolemType = 'melee';
lastClickedButton = 'melee';
lastClickTime = currentTime;
updateButtonSelection();
}
};
explosiveButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'explosive' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.explosive) {
selectedGolemType = 'explosive';
lastClickedButton = 'explosive';
lastClickTime = currentTime;
updateButtonSelection();
}
};
miniExplodeGolemButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'miniExplodeGolem' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else if (stones >= golemCosts.miniExplodeGolem) {
selectedGolemType = 'miniExplodeGolem';
lastClickedButton = 'miniExplodeGolem';
lastClickTime = currentTime;
updateButtonSelection();
}
};
shovelButton.down = function () {
var currentTime = LK.ticks;
if (lastClickedButton === 'shovel' && currentTime - lastClickTime < doubleTapThreshold) {
// Double tap detected - deselect all
selectedGolemType = null;
lastClickedButton = null;
updateButtonSelection();
} else {
selectedGolemType = 'shovel';
lastClickedButton = 'shovel';
lastClickTime = currentTime;
updateButtonSelection();
}
};
function updateButtonSelection() {
// If no golem is selected, all buttons return to normal color
basicButton.tint = selectedGolemType === 'basic' ? 0x00FF00 : 0xFFFFFF;
heavyButton.tint = selectedGolemType === 'heavy' ? 0x00FF00 : 0xFFFFFF;
crystalButton.tint = selectedGolemType === 'crystal' ? 0x00FF00 : 0xFFFFFF;
barrierButton.tint = selectedGolemType === 'barrier' ? 0x00FF00 : 0xFFFFFF;
spikeButton.tint = selectedGolemType === 'spike' ? 0x00FF00 : 0xFFFFFF;
stoneButton.tint = selectedGolemType === 'stone' ? 0x00FF00 : 0xFFFFFF;
meleeButton.tint = selectedGolemType === 'melee' ? 0x00FF00 : 0xFFFFFF;
explosiveButton.tint = selectedGolemType === 'explosive' ? 0x00FF00 : 0xFFFFFF;
miniExplodeGolemButton.tint = selectedGolemType === 'miniExplodeGolem' ? 0x00FF00 : 0xFFFFFF;
shovelButton.tint = selectedGolemType === 'shovel' ? 0x00FF00 : 0xFFFFFF;
}
// Initialize button selection
updateButtonSelection();
// Initialize lawnmowers
for (var row = 0; row < gridRows; row++) {
var lawnmower = new Lawnmower();
lawnmower.x = 300;
lawnmower.y = gridStartY + row * cellSize;
lawnmower.row = row;
lawnmowers.push(lawnmower);
game.addChild(lawnmower);
}
// Game click handler
game.down = function (x, y, obj) {
// Check if clicking on a nutrient stone first
for (var n = 0; n < nutrients.length; n++) {
var nutrient = nutrients[n];
var dx = x - nutrient.x;
var dy = y - nutrient.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 50 && nutrientInventory.length < maxNutrientSlots) {
// Collect the nutrient stone
tween(nutrient, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
// Add to inventory
nutrientInventory.push(1);
updateNutrientUI();
nutrient.destroy();
}
});
nutrients.splice(n, 1);
return; // Exit early, don't process grid placement
}
}
// Check if clicking on nutrient inventory slots
var slotClicked = -1;
for (var s = 0; s < maxNutrientSlots; s++) {
var slotX = 2048 - 200 - s * 100;
var slotY = 200;
var globalSlotPos = LK.gui.topRight.toGlobal({
x: slotX,
y: slotY
});
var dx = x - globalSlotPos.x;
var dy = y - globalSlotPos.y;
if (Math.abs(dx) < 40 && Math.abs(dy) < 40 && s < nutrientInventory.length) {
slotClicked = s;
break;
}
}
if (slotClicked >= 0) {
selectedNutrientSlot = selectedNutrientSlot === slotClicked ? -1 : slotClicked;
updateNutrientUI();
return;
}
var gridPos = getGridPosition(x, y);
if (gridPos) {
if (selectedGolemType === 'shovel') {
// Remove golem if one exists at this position
if (gridOccupied[gridPos.row][gridPos.col]) {
for (var i = golems.length - 1; i >= 0; i--) {
var golem = golems[i];
var golemGridCol = Math.floor((golem.x - gridStartX + cellSize / 2) / cellSize);
var golemGridRow = Math.floor((golem.y - gridStartY + cellSize / 2) / cellSize);
if (golemGridCol === gridPos.col && golemGridRow === gridPos.row) {
golem.destroy();
golems.splice(i, 1);
gridOccupied[gridPos.row][gridPos.col] = false;
LK.getSound('remove').play();
break;
}
}
}
} else if (!gridOccupied[gridPos.row][gridPos.col]) {
// Check if we have a nutrient selected and there's a golem here
if (selectedNutrientSlot >= 0 && gridOccupied[gridPos.row][gridPos.col]) {
// Find the golem at this position
for (var g = 0; g < golems.length; g++) {
var golem = golems[g];
var golemGridCol = Math.floor((golem.x - gridStartX + cellSize / 2) / cellSize);
var golemGridRow = Math.floor((golem.y - gridStartY + cellSize / 2) / cellSize);
if (golemGridCol === gridPos.col && golemGridRow === gridPos.row) {
// Apply nutrient stone to this golem
applyNutrientToGolem(golem);
// Remove nutrient from inventory
nutrientInventory.splice(selectedNutrientSlot, 1);
selectedNutrientSlot = -1;
updateNutrientUI();
return;
}
}
}
var cost = golemCosts[selectedGolemType];
var currentTime = LK.ticks;
var cooldownKey = selectedGolemType;
var cooldownDuration = getGolemCooldownDuration(selectedGolemType);
// Check if golem type is on cooldown
if (golemPlacementCooldown[cooldownKey] && currentTime - golemPlacementCooldown[cooldownKey] < cooldownDuration) {
var remainingTime = Math.ceil((cooldownDuration - (currentTime - golemPlacementCooldown[cooldownKey])) / 60);
cooldownText.setText('Espera ' + remainingTime + 's para colocar ' + selectedGolemType);
cooldownText.alpha = 1;
tween(cooldownText, {
alpha: 0
}, {
duration: 2000
});
return;
}
if (stones >= cost) {
var golem;
if (selectedGolemType === 'miniExplodeGolem') {
golem = new miniExplodeGolem();
} else {
golem = new Golem(selectedGolemType);
}
golem.x = gridStartX + gridPos.col * cellSize;
golem.y = gridStartY + gridPos.row * cellSize;
golems.push(golem);
game.addChild(golem);
gridOccupied[gridPos.row][gridPos.col] = true;
stones -= cost;
golemPlacementCooldown[cooldownKey] = currentTime;
lastGolemPlacement = currentTime;
updateUI();
LK.getSound('place').play();
}
}
}
};
// Countdown sequence before first wave
var countdownText = new Text2('', {
size: 80,
fill: 0xFFFFFF
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 2048 / 2;
countdownText.y = 2732 / 2;
game.addChild(countdownText);
// Show "preparado..." at 4 seconds
LK.setTimeout(function () {
countdownText.setText('preparado...');
countdownText.alpha = 1;
// Fade in effect
tween(countdownText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(countdownText, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200
});
}
});
}, 4000);
// Show "listo..." at 8 seconds
LK.setTimeout(function () {
countdownText.setText('listo...');
// Scale and color effect
tween(countdownText, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFFFF00
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(countdownText, {
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}, 8000);
// Show "¡PELEA!" at 12 seconds and start wave
LK.setTimeout(function () {
countdownText.setText('¡PELEA!');
// Dramatic entrance effect
tween(countdownText, {
scaleX: 1.5,
scaleY: 1.5,
tint: 0xFF0000
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
// Enable demon spawning after PELEA text is shown
demonsCanSpawn = true;
// Start first wave immediately after PELEA
spawnWave();
// Fade out after showing PELEA
tween(countdownText, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 500,
onFinish: function onFinish() {
countdownText.destroy();
}
});
}
});
}, 12000);
game.update = function () {
// Update golems
for (var i = golems.length - 1; i >= 0; i--) {
var golem = golems[i];
if (golem.update) {
golem.update();
}
}
// Update demons
for (var i = demons.length - 1; i >= 0; i--) {
var demon = demons[i];
demon.update();
// Check if demon reached left side
if (demon.x < 350) {
// Check if there's a lawnmower in this row to activate
var demonRow = Math.floor((demon.y - gridStartY + cellSize / 2) / cellSize);
var lawnmowerActivated = false;
for (var l = 0; l < lawnmowers.length; l++) {
var lawnmower = lawnmowers[l];
if (lawnmower.row === demonRow && !lawnmower.isActive) {
lawnmower.activate();
lawnmowerActivated = true;
break;
}
}
if (!lawnmowerActivated) {
lives--;
demon.destroy();
demons.splice(i, 1);
updateUI();
if (lives <= 0) {
LK.setScore(score);
LK.showGameOver();
return;
}
}
} else if (demon.shouldRemove) {
demon.destroy();
demons.splice(i, 1);
}
}
// Update lawnmowers
for (var i = lawnmowers.length - 1; i >= 0; i--) {
var lawnmower = lawnmowers[i];
lawnmower.update();
if (lawnmower.shouldRemove) {
lawnmower.destroy();
lawnmowers.splice(i, 1);
}
}
// Update progress bar
if (totalDemonsInWave > 0) {
var progress = demonsKilledInWave / totalDemonsInWave;
var targetScale = progress * 12;
tween.stop(progressBarFill, {
scaleX: true
});
tween(progressBarFill, {
scaleX: targetScale
}, {
duration: 200
});
// Update progress text with flag wave indicator
var progressPercentage = Math.floor(progress * 100);
var flagText = isFlagWave ? " (BANDERA!)" : "";
progressText.setText('Wave ' + wave + ': ' + progressPercentage + '%' + flagText);
// Change progress bar color for flag waves
if (isFlagWave) {
progressBarFill.tint = 0xFF4444; // Red for flag waves
progressText.tint = 0xFF4444;
} else {
progressBarFill.tint = 0x00FF00; // Green for normal waves
progressText.tint = 0xFFFFFF;
}
}
// Update projectiles
for (var i = projectiles.length - 1; i >= 0; i--) {
var projectile = projectiles[i];
projectile.update();
if (projectile.shouldRemove) {
projectile.destroy();
projectiles.splice(i, 1);
continue;
}
// Check projectile vs demon collision
for (var j = demons.length - 1; j >= 0; j--) {
var demon = demons[j];
if (projectile.intersects(demon)) {
demon.takeDamage(projectile.damage);
// Heavy golem projectiles have 25% chance to push demons backwards
if (projectile.golemType === 'heavy' && Math.random() < 0.25) {
// Push demon backwards (to the right) by 100 pixels
tween(demon, {
x: demon.x + 100
}, {
duration: 300,
easing: tween.easeOut
});
}
projectile.destroy();
projectiles.splice(i, 1);
break;
}
}
}
// Update nutrients - no auto collection, manual only
for (var i = nutrients.length - 1; i >= 0; i--) {
var nutrient = nutrients[i];
nutrient.update();
if (nutrient.shouldRemove) {
nutrient.destroy();
nutrients.splice(i, 1);
continue;
}
}
// Update stones with automatic collection only
for (var i = stones_collectible.length - 1; i >= 0; i--) {
var stone = stones_collectible[i];
stone.update();
if (stone.shouldRemove) {
stone.destroy();
stones_collectible.splice(i, 1);
continue;
}
// Auto-collect stones after 2 seconds (no manual collection)
if (!stone.isMarkedForCollection && LK.ticks - stone.spawnTime > 120) {
stone.isMarkedForCollection = true;
stone.collectionMarkTime = LK.ticks;
// Visual indicator - slight glow effect
tween(stone, {
tint: 0xFFFF88
}, {
duration: 200
});
}
// Check if marked stone should be collected after delay
if (stone.isMarkedForCollection && LK.ticks - stone.collectionMarkTime >= stone.collectionDelay) {
// Animate stone collection
tween(stone, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
stones += stone.value;
stone.destroy();
updateUI();
}
});
stones_collectible.splice(i, 1);
}
}
// Update stone spawn delay based on current wave
stoneSpawnDelay = baseStoneSpawnDelay + (wave - 1) * 20;
// Spawn stones periodically
if (LK.ticks - lastStoneSpawn > stoneSpawnDelay) {
var stone = new Stone();
stone.x = 400 + Math.random() * (2048 - 800); // Spawn in playable area
stone.y = 600 + Math.random() * 400;
stones_collectible.push(stone);
game.addChild(stone);
lastStoneSpawn = LK.ticks;
}
// Wave management
if (demons.length === 0 && demonsCanSpawn) {
nextWaveTimer++;
if (nextWaveTimer >= waveDelay) {
wave++;
nextWaveTimer = 0;
// Show flag wave announcement
if (flagWaves.indexOf(wave) !== -1) {
var flagText = new Text2('¡OLEADA BANDERA!', {
size: 60,
fill: 0xFF0000
});
flagText.anchor.set(0.5, 0.5);
flagText.x = 2048 / 2;
flagText.y = 400;
game.addChild(flagText);
// Animate flag announcement
tween(flagText, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFFFF00
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(flagText, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 1000,
onFinish: function onFinish() {
flagText.destroy();
}
});
}
});
}
spawnWave();
updateUI();
// Victory condition - now goes to wave 25
if (wave > 25) {
LK.setScore(score);
LK.showYouWin();
return;
}
}
}
};
// Initialize game start time
gameStartTime = LK.ticks;
// Add decorative elements outside garden
// Rocks scattered around the edges with rock texture
for (var i = 0; i < 15; i++) {
var rock = LK.getAsset('rockTexture', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8 + Math.random() * 1.0,
scaleY: 0.8 + Math.random() * 1.0,
tint: 0x8B7355 // Natural rock brown color
});
// Position rocks outside garden area
if (Math.random() < 0.5) {
// Left side decorations
rock.x = 50 + Math.random() * 150;
rock.y = 700 + Math.random() * 800;
} else {
// Right side decorations
rock.x = 2600 + Math.random() * 300;
rock.y = 700 + Math.random() * 800;
}
game.addChild(rock);
}
// Grass patches for decoration with grass texture
for (var i = 0; i < 20; i++) {
var grass = LK.getAsset('grassTexture', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8 + Math.random() * 0.6,
scaleY: 0.6 + Math.random() * 0.4,
alpha: 0.7,
tint: 0x32CD32 // Lime green for natural grass
});
// Scatter grass around the garden
if (Math.random() < 0.3) {
// Above garden
grass.x = 400 + Math.random() * 2000;
grass.y = 600 + Math.random() * 150;
} else if (Math.random() < 0.5) {
// Below garden
grass.x = 400 + Math.random() * 2000;
grass.y = 1800 + Math.random() * 200;
} else {
// Left and right sides
grass.x = Math.random() < 0.5 ? 100 + Math.random() * 100 : 2700 + Math.random() * 200;
grass.y = 700 + Math.random() * 1000;
}
game.addChild(grass);
}
// Small decorative stones near garden edges with rock texture
for (var i = 0; i < 8; i++) {
var smallStone = LK.getAsset('rockTexture', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5,
tint: 0x696969 // Darker gray for small stones
});
// Position near garden borders
smallStone.x = gridStartX - 50 + Math.random() * (gridCols * cellSize + 100);
smallStone.y = Math.random() < 0.5 ? gridStartY - 100 : gridStartY + gridRows * cellSize + 50;
game.addChild(smallStone);
}
// Initialize UI
updateUI();
updateNutrientUI();
un demonio bola grande 2d y semi realista con muchos cuernos y que sus ojos sean negros con pupilas blancas. In-Game asset. 2d. High contrast. No shadows
haz a este personaje mas realista
haz a este personaje mucho mas realista
recrea este personaje mucho mas realista
un golem de cristal muy realista con un cañon en la mano. In-Game asset. 2d. High contrast. No shadows
una roca circular muy realista. In-Game asset. 2d. High contrast. No shadows
haz a este personaje mas realista
una pala de madera realista. In-Game asset. 2d. High contrast. No shadows
un golem de piedra con puas plano en horizontal en 3d muy realista. In-Game asset. 2d. High contrast. No shadows
un golem de piedra que parezca un minero y que se vea ultra realista. In-Game asset. 2d. High contrast. No shadows
un demonio bola ultra realista con una peluca y traje de bailarin con lentes de sol y una sonrisa con colmillos afilados. In-Game asset. 2d. High contrast. No shadows
un golem de piedra listo para pelear con puños con puas ultra realista. In-Game asset. 2d. High contrast. No shadows
un golem de piedra ultra realista con una bomba en la espalda. In-Game asset. 2d. High contrast. No shadows
un demonio bola con un casco de jugador de futbol americano ultra realista. In-Game asset. 2d. High contrast. No shadows
haz esta imagen muy, muy, muy, muy, muy realista, es una piedra con ruedas. In-Game asset. 2d. High contrast. No shadows
una piedra con ojos muy realista con una granada encima. In-Game asset. 2d. High contrast. No shadows
un arbusto ultra realista. In-Game asset. 2d. High contrast. No shadows
una roca ultra realista. In-Game asset. 2d. High contrast. No shadows