/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var CanonShell = Container.expand(function () {
var self = Container.call(this);
self.speed = 3.2; // 4x normal zombie speed (0.8 * 4)
self.damage = 20;
self.hasExploded = false;
self.hasReachedColumn6 = false;
self.gridY = 0;
var graphics = self.attachAsset('canonShell', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Move left towards plants
self.x -= self.speed;
// Check if reached column 3
var cellX = Math.floor((self.x - gridStartX) / cellSize);
if (cellX <= 2 && !self.hasReachedColumn6) {
self.hasReachedColumn6 = true;
self.explode();
return;
}
// Check collision with plants
var cellX = Math.floor((self.x - gridStartX) / cellSize);
if (cellX >= 0 && cellX < gridCols && plants[self.gridY] && plants[self.gridY][cellX]) {
var plant = plants[self.gridY][cellX];
if (self.intersects(plant)) {
plant.takeDamage(self.damage);
self.explode();
return;
}
}
// Remove if goes off screen
if (self.x < -100) {
self.removeFromGame();
}
};
self.explode = function () {
if (self.hasExploded) {
return;
}
self.hasExploded = true;
// Create explosion effect
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 1.5,
scaleY: 1.5
});
game.addChild(explosion);
// Animate explosion and remove it
tween(explosion, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
// Transform shell into normal zombidito at current position
var zombidito = new Zombidito();
zombidito.x = self.x;
zombidito.y = self.y;
zombidito.gridY = self.gridY;
enemies.push(zombidito);
game.addChild(zombidito);
// Remove shell
self.removeFromGame();
};
self.removeFromGame = function () {
for (var i = 0; i < canonShells.length; i++) {
if (canonShells[i] === self) {
canonShells.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var Lawnmower = Container.expand(function (row) {
var self = Container.call(this);
self.gridY = row;
self.speed = 0;
self.isActivated = false;
self.x = gridStartX - 100;
self.y = gridStartY + row * cellSize;
var graphics = self.attachAsset('lawnmower', {
anchorX: 0.5,
anchorY: 0.5
});
self.activate = function () {
if (!self.isActivated) {
self.isActivated = true;
self.speed = 6.4;
}
};
self.update = function () {
if (self.isActivated) {
self.x += self.speed;
// Check collision with zombies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && self.intersects(enemy)) {
enemy.die();
// Count as player kill for score tracking
enemiesKilled++;
}
}
// Remove lawnmower when it reaches right edge
if (self.x > 2148) {
self.removeFromGame();
}
}
};
self.removeFromGame = function () {
for (var i = 0; i < lawnmowers.length; i++) {
if (lawnmowers[i] === self) {
lawnmowers.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var PlantBase = Container.expand(function (plantType) {
var self = Container.call(this);
self.plantType = plantType;
self.health = 100;
self.maxHealth = 100;
self.cost = 50;
self.shootTimer = 0;
self.shootDelay = 72;
self.gridX = 0;
self.gridY = 0;
self.level = 1;
var graphics = self.attachAsset(plantType, {
anchorX: 0.5,
anchorY: 0.5
});
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFF0000, 200);
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
// Clean up projectile tracking for this plant
var plantId = self.gridX + '_' + self.gridY;
if (plantProjectileCount[plantId]) {
delete plantProjectileCount[plantId];
}
plants[self.gridY][self.gridX] = null;
self.destroy();
};
return self;
});
var Wallnut = PlantBase.expand(function () {
var self = PlantBase.call(this, 'wallnut');
self.cost = 50;
self.health = 300;
self.maxHealth = 300;
self.isDamaged = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if wallnut should switch to damaged appearance
if (self.health < 150 && !self.isDamaged) {
self.isDamaged = true;
// Remove current graphics and add damaged version
self.removeChild(self.children[0]);
var damagedGraphics = self.attachAsset('wallnut_damaged', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
var Sunflower = PlantBase.expand(function () {
var self = PlantBase.call(this, 'sunflower');
self.cost = 50;
self.sunTimer = 0;
self.update = function () {
self.sunTimer++;
if (self.sunTimer >= 1080) {
// 18 seconds
var sun = new Sun(self.x, self.y - 60);
suns.push(sun);
game.addChild(sun);
self.sunTimer = 0;
LK.effects.flashObject(self, 0xFFFF00, 300);
}
};
return self;
});
var SnowPea = PlantBase.expand(function () {
var self = PlantBase.call(this, 'snowpea');
self.cost = 175;
self.health = 120;
self.maxHealth = 120;
self.update = function () {
self.shootTimer++;
if (self.shootTimer >= self.shootDelay) {
var target = self.findTarget();
if (target) {
self.shoot(target);
self.shootTimer = 0;
}
}
};
self.findTarget = function () {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && enemy.x > self.x) {
return enemy;
}
}
return null;
};
self.shoot = function (target) {
var plantId = self.gridX + '_' + self.gridY; // Unique plant identifier
var pea = createProjectile('snowshot', self.x, self.y, 5.12, 25 * self.level, plantId); // 6.4 * 0.8 = 5.12 (20% slower)
if (pea) {
LK.getSound('shoot').play();
}
};
return self;
});
var Repetidora = PlantBase.expand(function () {
var self = PlantBase.call(this, 'repetidora');
self.cost = 200;
self.isTrackingFirstPea = false;
self.firstPea = null;
self.firstPeaStartX = 0;
self.update = function () {
self.shootTimer++;
if (self.shootTimer >= self.shootDelay) {
var target = self.findTarget();
if (target && !self.isTrackingFirstPea) {
var firstPea = self.shoot(target);
if (firstPea) {
self.firstPea = firstPea;
self.firstPeaStartX = firstPea.x;
self.isTrackingFirstPea = true;
}
self.shootTimer = 0;
}
}
// Handle second pea timing based on first pea position
if (self.isTrackingFirstPea && self.firstPea) {
// Check if first pea has advanced half a cell (72 pixels)
if (self.firstPea.x >= self.firstPeaStartX + 72) {
var target = self.findTarget();
if (target) {
self.shoot(target);
}
// Stop tracking to prevent lag
self.isTrackingFirstPea = false;
self.firstPea = null;
}
// Also stop tracking if first pea no longer exists
else if (!self.firstPea.parent) {
self.isTrackingFirstPea = false;
self.firstPea = null;
}
}
};
self.findTarget = function () {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && enemy.x > self.x) {
return enemy;
}
}
return null;
};
self.shoot = function (target) {
var plantId = self.gridX + '_' + self.gridY; // Unique plant identifier
var pea = createProjectile('pea', self.x, self.y, 8.64, 20 * self.level, plantId);
if (pea) {
LK.getSound('shoot').play();
}
return pea; // Return the pea for tracking
};
return self;
});
var Rapaz = PlantBase.expand(function () {
var self = PlantBase.call(this, 'rapaz');
self.cost = 325;
self.health = 425;
self.maxHealth = 425;
self.damageTimer = 0; // Timer for dealing damage to high-HP zombies
self.cooldownTimer = 0; // Timer for cooldown after killing zombie <400 HP
self.isOnCooldown = false; // Track if rapaz is on cooldown
self.targetZombie = null; // Track zombie being consumed
var graphics = self.children[0]; // Reference to graphics for color changes
self.update = function () {
// Handle cooldown timer
if (self.isOnCooldown && self.cooldownTimer > 0) {
self.cooldownTimer--;
if (self.cooldownTimer === 0) {
// End cooldown - restore normal color
self.isOnCooldown = false;
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
return; // Don't attack while on cooldown
}
self.damageTimer++;
// Check every 60 ticks (1 second) for zombies to attack
if (self.damageTimer >= 60) {
// Skip if rapaz is already consuming a zombie
if (self.targetZombie && self.targetZombie.parent) {
return; // Don't attack while already consuming
}
var targetZombies = self.findTargetZombies();
for (var i = 0; i < targetZombies.length; i++) {
var zombie = targetZombies[i];
// Skip zombies already being consumed
if (zombie.beingConsumed) {
continue;
}
if (zombie.health < 400) {
// Mark zombie as being consumed and make it move toward rapaz
zombie.beingConsumed = true;
zombie.consumingPlant = self;
self.targetZombie = zombie;
zombie.speed = 0.8 * 4; // 4x normal zombie speed
zombie.damage = 0; // Stop attacking while being consumed
// Start continuous rotation while moving
zombie.rotationSpeed = 0.15; // Rotation speed per frame
break; // Only consume one zombie per cycle
} else {
// Deal 25 damage to zombie with ≥400 HP
zombie.takeDamage(25);
// Add flashing effect to zombie when taking damage from rapaz
tween(zombie, {
tint: 0xFF0000
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(zombie, {
tint: 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
}
});
// Check if zombie now has <400 HP and should be consumed
if (zombie.health < 400 && zombie.parent) {
// Mark zombie as being consumed
zombie.beingConsumed = true;
zombie.consumingPlant = self;
self.targetZombie = zombie;
zombie.speed = 0.8 * 4; // 4x normal zombie speed
zombie.damage = 0; // Stop attacking while being consumed
// Start continuous rotation while moving
zombie.rotationSpeed = 0.15; // Rotation speed per frame
break; // Only consume one zombie per cycle
}
}
}
self.damageTimer = 0;
}
// Check if target zombie has collided with rapaz
if (self.targetZombie && self.targetZombie.parent) {
// Check for collision between rapaz and consuming zombie
if (self.intersects(self.targetZombie)) {
// Zombie reached rapaz - consume it immediately
// First remove zombie from enemies array to prevent other systems from accessing it
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] === self.targetZombie) {
enemies.splice(i, 1);
break;
}
}
// Destroy the zombie
self.targetZombie.destroy();
enemiesKilled++;
self.targetZombie = null;
// Start 10 second cooldown and gray out the plant
self.isOnCooldown = true;
self.cooldownTimer = 600; // 10 seconds at 60fps
tween(graphics, {
tint: 0x888888 // Gray tint
}, {
duration: 200,
easing: tween.easeOut
});
}
} else if (self.targetZombie && !self.targetZombie.parent) {
// Target zombie was destroyed, clear reference
self.targetZombie = null;
}
};
self.findTargetZombies = function () {
var targets = [];
// Check current cell and seven cells in front (8 total cells)
for (var checkX = self.gridX; checkX <= self.gridX + 7 && checkX < gridCols; checkX++) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var enemyGridX = Math.floor((enemy.x - gridStartX + cellSize / 2) / cellSize);
// Check if zombie is in the target cell
if (enemy.gridY === self.gridY && enemyGridX === checkX) {
targets.push(enemy);
}
}
}
return targets;
};
// Override die to clean up consumed zombie
var originalDie = self.die;
self.die = function () {
// If consuming a zombie, release it from consumption
if (self.targetZombie && self.targetZombie.parent) {
self.targetZombie.beingConsumed = false;
self.targetZombie.consumingPlant = null;
self.targetZombie.speed = self.targetZombie.originalSpeed;
self.targetZombie.damage = 25; // Restore normal damage
self.targetZombie.rotationSpeed = 0; // Stop continuous rotation
if (self.targetZombie.children[0]) {
self.targetZombie.children[0].rotation = 0; // Reset rotation
}
}
originalDie();
};
return self;
});
var Plantorcha = PlantBase.expand(function () {
var self = PlantBase.call(this, 'plantorcha');
self.cost = 175;
self.health = 300;
self.maxHealth = 300;
// Plantorcha doesn't shoot - it only converts projectiles
self.update = function () {
// No shooting behavior - just exists to convert projectiles
};
return self;
});
var Petacereza = PlantBase.expand(function () {
var self = PlantBase.call(this, 'petacereza');
self.cost = 150;
self.health = 1; // Very low health since it explodes
self.maxHealth = 1;
self.isArmed = false; // Zombies ignore it until armed
self.armTimer = 90; // 1.5 seconds at 60fps
self.update = function () {
if (self.armTimer > 0) {
self.armTimer--;
if (self.armTimer === 0) {
// Arm the petacereza and explode
self.isArmed = true;
self.explode();
}
}
};
self.explode = function () {
// Create explosion effect
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 3,
// Larger explosion for 3x3 area
scaleY: 3
});
game.addChild(explosion);
// Animate explosion and remove it
tween(explosion, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
// Damage all zombies in 3x3 grid area around petacereza
var centerGridX = self.gridX;
var centerGridY = self.gridY;
var damagedEnemies = []; // Track enemies to damage
// First pass: identify all enemies in range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var enemyGridX = Math.floor((enemy.x - gridStartX + cellSize / 2) / cellSize);
var enemyGridY = enemy.gridY;
// Check if enemy is within 3x3 area (center ± 1 in both directions)
var deltaX = Math.abs(enemyGridX - centerGridX);
var deltaY = Math.abs(enemyGridY - centerGridY);
if (deltaX <= 1 && deltaY <= 1) {
damagedEnemies.push(enemy);
}
}
// Second pass: damage all identified enemies
for (var j = 0; j < damagedEnemies.length; j++) {
damagedEnemies[j].takeDamage(325);
}
// Remove petacereza after explosion
self.die();
};
// Override getPlantInFront for zombies to ignore unarmed petacereza
self.shouldBeIgnoredByZombies = function () {
return !self.isArmed;
};
return self;
});
var Peashooter = PlantBase.expand(function () {
var self = PlantBase.call(this, 'peashooter');
self.cost = 100;
self.update = function () {
self.shootTimer++;
if (self.shootTimer >= self.shootDelay) {
var target = self.findTarget();
if (target) {
self.shoot(target);
self.shootTimer = 0;
}
}
};
self.findTarget = function () {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && enemy.x > self.x) {
return enemy;
}
}
return null;
};
self.shoot = function (target) {
var plantId = self.gridX + '_' + self.gridY; // Unique plant identifier
var pea = createProjectile('pea', self.x, self.y, 6.4, 20 * self.level, plantId);
if (pea) {
LK.getSound('shoot').play();
}
};
return self;
});
var NuezPrimitiva = PlantBase.expand(function () {
var self = PlantBase.call(this, 'nuezPrimitiva');
self.cost = 125;
self.health = 625;
self.maxHealth = 625;
self.hasChangedToWallnut = false;
self.hasChangedToDamaged = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if should switch to normal wallnut appearance (< 300 hp)
if (self.health < 300 && !self.hasChangedToWallnut) {
self.hasChangedToWallnut = true;
// Remove current graphics and add wallnut version
self.removeChild(self.children[0]);
var wallnutGraphics = self.attachAsset('wallnut', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Check if should switch to damaged wallnut appearance (< 150 hp)
else if (self.health < 150 && !self.hasChangedToDamaged) {
self.hasChangedToDamaged = true;
// Remove current graphics and add damaged version
self.removeChild(self.children[0]);
var damagedGraphics = self.attachAsset('wallnut_damaged', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
var Fireshot = PlantBase.expand(function () {
var self = PlantBase.call(this, 'fireshot');
self.cost = 100;
self.health = 150;
self.maxHealth = 150;
self.update = function () {
self.shootTimer++;
if (self.shootTimer >= self.shootDelay) {
var target = self.findTarget();
if (target) {
self.shoot(target);
self.shootTimer = 0;
}
}
};
self.findTarget = function () {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && enemy.x > self.x) {
return enemy;
}
}
return null;
};
self.shoot = function (target) {
var plantId = self.gridX + '_' + self.gridY; // Unique plant identifier
// Calculate damage based on target type
var damage = 35 * self.level;
if (target.zombieType === 'cone_zombie' || target.zombieType === 'bucket_zombie') {
damage = 50 * self.level;
}
var pea = createProjectile('shotfire', self.x, self.y, 6.4, damage, plantId);
if (pea) {
LK.getSound('shoot').play();
}
};
return self;
});
var Carnivora = PlantBase.expand(function () {
var self = PlantBase.call(this, 'carnivora');
self.cost = 25;
self.health = 300;
self.maxHealth = 300;
self.damageTimer = 0; // Timer for dealing damage to high-HP zombies
self.cooldownTimer = 0; // Timer for cooldown after killing zombie <400 HP
self.isOnCooldown = false; // Track if carnivora is on cooldown
var graphics = self.children[0]; // Reference to graphics for color changes
self.update = function () {
// Handle cooldown timer
if (self.isOnCooldown && self.cooldownTimer > 0) {
self.cooldownTimer--;
if (self.cooldownTimer === 0) {
// End cooldown - restore normal color
self.isOnCooldown = false;
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
return; // Don't attack while on cooldown
}
self.damageTimer++;
// Check every 60 ticks (1 second) for zombies to attack
if (self.damageTimer >= 60) {
var targetZombies = self.findTargetZombies();
for (var i = 0; i < targetZombies.length; i++) {
var zombie = targetZombies[i];
if (zombie.health < 400) {
// Instantly kill zombie with <400 HP
zombie.die();
enemiesKilled++;
// Start 15 second cooldown and gray out the plant
self.isOnCooldown = true;
self.cooldownTimer = 900; // 15 seconds at 60fps
tween(graphics, {
tint: 0x888888 // Gray tint
}, {
duration: 200,
easing: tween.easeOut
});
break; // Only kill one zombie per cycle
} else {
// Deal 25 damage to zombie with ≥400 HP
zombie.takeDamage(25);
// Add flashing effect to zombie when taking damage from carnivora
tween(zombie, {
tint: 0xFF0000
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(zombie, {
tint: 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
}
});
// Check if zombie now has <400 HP and kill it
if (zombie.health < 400 && zombie.parent) {
zombie.die();
enemiesKilled++;
// Start 15 second cooldown and gray out the plant
self.isOnCooldown = true;
self.cooldownTimer = 900; // 15 seconds at 60fps
tween(graphics, {
tint: 0x888888 // Gray tint
}, {
duration: 200,
easing: tween.easeOut
});
break; // Only kill one zombie per cycle
}
}
}
self.damageTimer = 0;
}
};
self.findTargetZombies = function () {
var targets = [];
// Check current cell and cell in front
for (var checkX = self.gridX; checkX <= self.gridX + 1 && checkX < gridCols; checkX++) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var enemyGridX = Math.floor((enemy.x - gridStartX + cellSize / 2) / cellSize);
// Check if zombie is in the target cell
if (enemy.gridY === self.gridY && enemyGridX === checkX) {
targets.push(enemy);
}
}
}
return targets;
};
return self;
});
var Birasol = PlantBase.expand(function () {
var self = PlantBase.call(this, 'birasol');
self.cost = 125;
self.health = 125;
self.maxHealth = 125;
self.sunTimer = 0;
self.update = function () {
self.sunTimer++;
if (self.sunTimer >= 900) {
// 15 seconds (900 ticks at 60fps)
var sun1 = new Sun(self.x - 30, self.y - 60);
var sun2 = new Sun(self.x + 30, self.y - 60);
suns.push(sun1);
suns.push(sun2);
game.addChild(sun1);
game.addChild(sun2);
self.sunTimer = 0;
LK.effects.flashObject(self, 0xFFFF00, 300);
}
};
return self;
});
var Projectile = Container.expand(function (type, startX, startY, speed, damage) {
var self = Container.call(this);
self.speed = speed;
self.damage = damage;
self.x = startX;
self.y = startY;
self.type = type;
self.gridY = Math.floor((startY - gridStartY + cellSize / 2) / cellSize);
self.ownerId = null; // Plant that owns this projectile
var graphics = self.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
self.x += self.speed;
if (self.x > 2048) {
self.removeFromGame();
return;
}
// Check collision with plantorcha to convert pea to fire projectile
if (self.type === 'pea') {
var projectileGridX = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
var projectileGridY = self.gridY;
if (projectileGridX >= 0 && projectileGridX < gridCols && projectileGridY >= 0 && projectileGridY < gridRows) {
var plantInCell = plants[projectileGridY][projectileGridX];
if (plantInCell && plantInCell.plantType === 'plantorcha' && self.intersects(plantInCell)) {
// Convert pea to shotfire and add 10 damage
self.removeChild(self.children[0]);
var fireGraphics = self.attachAsset('shotfire', {
anchorX: 0.5,
anchorY: 0.5
});
self.type = 'shotfire';
self.damage += 10;
// Continue with normal movement
}
}
}
// Only check collisions with zombies in same row and within 100px range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && Math.abs(enemy.x - self.x) <= 100 && self.intersects(enemy)) {
// Skip damage if zombie is being consumed by rapaz unless projectile is from rapaz
if (!enemy.beingConsumed || enemy.beingConsumed && enemy.consumingPlant && self.ownerId && self.ownerId.includes(enemy.consumingPlant.gridX + '_' + enemy.consumingPlant.gridY)) {
enemy.takeDamage(self.damage);
// Apply ice effect if this is a snowshot
if (self.type === 'snowshot') {
enemy.applyIceEffect();
}
self.removeFromGame();
break;
}
}
}
};
self.removeFromGame = function () {
// Decrease projectile count for the plant that owns this projectile
if (self.ownerId && plantProjectileCount[self.ownerId]) {
plantProjectileCount[self.ownerId]--;
}
for (var i = 0; i < projectiles.length; i++) {
if (projectiles[i] === self) {
projectiles.splice(i, 1);
break;
}
}
// Return to pool instead of destroying
self.returnToPool();
};
self.returnToPool = function () {
if (projectilePool.length < maxPoolSize) {
self.removeChildren();
if (self.parent) {
self.parent.removeChild(self);
}
projectilePool.push(self);
} else {
self.destroy();
}
};
return self;
});
var SlowProjectile = Projectile.expand(function (type, startX, startY, speed, damage) {
var self = Projectile.call(this, type, startX, startY, speed, damage);
var originalUpdate = self.update;
self.update = function () {
originalUpdate();
// Only check collisions with zombies in same row and within 100px range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && Math.abs(enemy.x - self.x) <= 100 && self.intersects(enemy)) {
enemy.applySlowEffect();
break;
}
}
};
return self;
});
var Sun = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.value = 25;
self.collectTimer = 0;
self.isBeingAttracted = false;
self.attractedBy = null;
var graphics = self.attachAsset('sun', {
anchorX: 0.5,
anchorY: 0.5
});
self.down = function (x, y, obj) {
// If being attracted by zombie sol, stop the attraction
if (self.isBeingAttracted && self.attractedBy) {
tween.stop(self);
self.attractedBy.targetSun = null;
self.attractedBy.isAttractingSun = false;
self.attractedBy.speed = self.attractedBy.originalSpeed;
self.isBeingAttracted = false;
self.attractedBy = null;
}
sunPoints += self.value;
updateSunDisplay();
self.collect();
};
self.collect = function () {
// Clean up attraction if being collected
if (self.isBeingAttracted && self.attractedBy) {
tween.stop(self);
self.attractedBy.targetSun = null;
self.attractedBy.isAttractingSun = false;
self.attractedBy.speed = self.attractedBy.originalSpeed;
}
for (var i = 0; i < suns.length; i++) {
if (suns[i] === self) {
suns.splice(i, 1);
break;
}
}
self.destroy();
};
self.update = function () {
self.collectTimer++;
if (self.collectTimer > 720) {
// 12 seconds timeout
self.collect();
}
};
return self;
});
var ZombieBase = Container.expand(function (zombieType) {
var self = Container.call(this);
self.zombieType = zombieType;
self.health = 190;
self.maxHealth = 190;
self.speed = 0.8;
self.damage = 25;
self.gridY = 0;
self.attackTimer = 0;
self.slowTimer = 0;
self.iceTimer = 0;
self.isIced = false;
self.originalSpeed = self.speed;
self.beingConsumed = false; // Track if zombie is being consumed by rapaz
self.consumingPlant = null; // Reference to the plant consuming this zombie
var graphics = self.attachAsset(zombieType, {
anchorX: 0.5,
anchorY: 0.5
});
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFF0000, 200);
LK.getSound('zombie_hit').play();
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
// Check if this is Zombiestein being killed
if (self.zombieType === 'zombiestein') {
zombiesteinAlive = false;
}
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
};
self.applySlowEffect = function () {
self.slowTimer = 180;
self.speed = self.originalSpeed * 0.5;
graphics.tint = 0x81C784;
};
self.applyIceEffect = function () {
// Set ice effect timer for 3 seconds if not already active or extend it
self.iceTimer = 180; // 3 seconds at 60fps
self.speed = self.originalSpeed * 0.8; // 20% slower
// Only start the visual tween if not already iced
if (!self.isIced) {
self.isIced = true;
// Calculate 35% blue tint (mix current color with blue)
var blueColor = 0x5555FF; // Blue color
// Apply blue tint
tween(graphics, {
tint: blueColor
}, {
duration: 100,
easing: tween.easeOut
});
}
};
self.update = function () {
// Handle being consumed by rapaz - move toward the consuming plant
if (self.beingConsumed && self.consumingPlant) {
// Apply continuous rotation while moving
if (self.rotationSpeed && self.children[0]) {
self.children[0].rotation += self.rotationSpeed;
}
// Move toward the rapaz plant position
var targetX = self.consumingPlant.x;
if (self.x > targetX) {
self.x -= self.speed; // Move left toward rapaz
} else {
// Reached rapaz position, let rapaz handle consumption
}
return; // Skip normal update logic when being consumed
}
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer === 0) {
self.speed = self.originalSpeed;
graphics.tint = 0xFFFFFF;
}
}
// Handle ice effect timer
if (self.iceTimer > 0) {
self.iceTimer--;
if (self.iceTimer === 0) {
// Ice effect ends
self.isIced = false;
self.speed = self.originalSpeed;
// Return to normal color
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
}
var plantInFront = self.getPlantInFront();
if (plantInFront) {
self.attackTimer++;
if (self.attackTimer >= 72) {
plantInFront.takeDamage(self.damage);
self.attackTimer = 0;
}
} else {
self.x -= self.speed;
// Check if zombie reached left screen edge
if (self.x < 0) {
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
return;
}
// Check collision with lawnmower
for (var i = 0; i < lawnmowers.length; i++) {
var lawnmower = lawnmowers[i];
if (lawnmower.gridY === self.gridY && self.x <= lawnmower.x + 60 && !lawnmower.isActivated) {
lawnmower.activate();
}
}
}
};
self.getPlantInFront = function () {
var cellX = Math.floor((self.x - gridStartX) / cellSize);
if (cellX >= 0 && cellX < gridCols && plants[self.gridY] && plants[self.gridY][cellX]) {
var plant = plants[self.gridY][cellX];
// Check if this is an unarmed petacereza that should be ignored
if (plant.shouldBeIgnoredByZombies && plant.shouldBeIgnoredByZombies()) {
return null; // Ignore unarmed petacereza
}
return plant;
}
return null;
};
return self;
});
var Zombiestein = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombiestein');
self.health = 2250;
self.maxHealth = 2250;
self.speed = 0.414; // 10% slower than 0.46
self.originalSpeed = 0.414;
self.damage = 500; // 500 damage per second
self.isSummoning = false;
self.hasSpawnedZombidito = false;
self.originalUpdate = self.update;
self.update = function () {
// Check if health is below 1500 and hasn't spawned Zombidito yet
if (self.health < 1500 && !self.hasSpawnedZombidito && !self.isSummoning) {
self.isSummoning = true;
self.speed = 0; // Stop moving completely
// Wait 1.5 seconds then spawn Zombidito
LK.setTimeout(function () {
// Resume movement
self.speed = self.originalSpeed;
self.isSummoning = false;
self.hasSpawnedZombidito = true;
// Calculate position 3 cells ahead (in front of Zombiestein)
var spawnX = self.x - 3 * cellSize;
var spawnY = self.y;
// Create Zombidito with explosion effect
var zombidito = new Zombidito();
zombidito.x = spawnX;
zombidito.y = spawnY;
zombidito.gridY = self.gridY;
enemies.push(zombidito);
game.addChild(zombidito);
// Create explosion effect like miner zombie
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: spawnX,
y: spawnY,
scaleX: 2,
scaleY: 2
});
game.addChild(explosion);
// Animate explosion and remove it
tween(explosion, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
}, 1500);
}
// Call original update only if not summoning (stopped)
if (!self.isSummoning) {
self.originalUpdate();
}
};
return self;
});
var ZombieSarcofago = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombie_sarcofago');
self.health = 500;
self.maxHealth = 500;
self.speed = 0.6; // Slightly slower than normal zombie
self.originalSpeed = 0.6;
// Override die function to spawn normal zombie at death location
var originalDie = self.die;
self.die = function () {
// Create spawned zombie at current position with 2x speed and 225 HP
var spawnedZombie = new SpawnedZombie();
spawnedZombie.x = self.x;
spawnedZombie.y = self.y;
spawnedZombie.gridY = self.gridY;
enemies.push(spawnedZombie);
game.addChild(spawnedZombie);
// Call original die function
originalDie();
};
return self;
});
var ZombieRa = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombieRa');
self.health = 190;
self.maxHealth = 190;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.isAttractingSun = false;
self.targetSun = null;
// Override update to handle sun attraction
var originalUpdate = self.update;
self.update = function () {
// Look for uncollected suns to attract
if (!self.isAttractingSun && !self.targetSun) {
var closestSun = null;
var closestDistance = Infinity;
// Find the closest sun that isn't already being attracted
for (var i = 0; i < suns.length; i++) {
var sun = suns[i];
if (!sun.isBeingAttracted) {
var distance = Math.abs(sun.x - self.x) + Math.abs(sun.y - self.y);
if (distance < closestDistance) {
closestDistance = distance;
closestSun = sun;
}
}
}
// If found a sun, start attracting it
if (closestSun) {
self.targetSun = closestSun;
self.isAttractingSun = true;
self.speed = 0; // Stop moving while attracting
// Mark sun as being attracted so other zombie sols don't target it
closestSun.isBeingAttracted = true;
closestSun.attractedBy = self;
// Start tweening sun towards zombie
tween(closestSun, {
x: self.x,
y: self.y
}, {
duration: closestDistance / 0.6,
// Speed reduced by 300% (was 2.4, now 0.6 = 4x slower)
easing: tween.linear,
onFinish: function onFinish() {
// Sun reached zombie - consume it
if (self.targetSun && self.targetSun.parent) {
self.consumeSun();
}
}
});
}
}
// Check if sun reached zombie (collision detection)
if (self.isAttractingSun && self.targetSun && self.targetSun.parent) {
var distance = Math.abs(self.targetSun.x - self.x) + Math.abs(self.targetSun.y - self.y);
if (distance < 50) {
// Close enough to consume
self.consumeSun();
}
}
// Call original update only if not attracting sun
if (!self.isAttractingSun) {
originalUpdate();
}
};
self.consumeSun = function () {
if (self.targetSun && self.targetSun.parent) {
// Remove sun from game without giving points to player
for (var i = 0; i < suns.length; i++) {
if (suns[i] === self.targetSun) {
suns.splice(i, 1);
break;
}
}
self.targetSun.destroy();
self.targetSun = null;
self.isAttractingSun = false;
self.speed = self.originalSpeed; // Resume movement
}
};
// Override die to stop any sun attraction
var originalDie = self.die;
self.die = function () {
// If attracting a sun, stop the tween and release the sun
if (self.targetSun && self.targetSun.parent) {
tween.stop(self.targetSun);
self.targetSun.isBeingAttracted = false;
self.targetSun.attractedBy = null;
}
originalDie();
};
return self;
});
var ZombieCascanueces = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'cascanueces');
self.health = 1000;
self.maxHealth = 1000;
self.speed = 0.96; // Normal zombie speed (0.8) * 1.2
self.originalSpeed = 0.96;
self.damage = 35; // 35 damage per second
self.isParalyzed = false;
self.paralysisTimer = 0;
self.paralysisTime = 600; // 10 seconds at 60fps
self.hasReachedColumn6 = false;
self.hasReachedColumn4 = false;
var originalUpdate = self.update;
self.update = function () {
// Handle paralysis timer
if (self.isParalyzed) {
self.paralysisTimer--;
if (self.paralysisTimer <= 0) {
// Wake up from paralysis
self.isParalyzed = false;
self.speed = self.originalSpeed;
// Flash animation when waking up
tween(self, {
alpha: 0.3
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
alpha: 1.0
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Second flash
tween(self, {
alpha: 0.3
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
alpha: 1.0
}, {
duration: 200,
easing: tween.easeInOut
});
}
});
}
});
}
});
}
return; // Don't move while paralyzed
}
// Check current column position
var cellX = Math.floor((self.x - gridStartX) / cellSize);
// Check if reached column 6 and hasn't been paralyzed there yet
if (cellX <= 6 && !self.hasReachedColumn6 && !self.isParalyzed) {
self.hasReachedColumn6 = true;
self.isParalyzed = true;
self.paralysisTimer = self.paralysisTime;
self.speed = 0; // Stop moving
return; // Don't move this frame
}
// Check if reached column 4 and hasn't been paralyzed there yet (and already passed column 6)
if (cellX <= 4 && !self.hasReachedColumn4 && self.hasReachedColumn6 && !self.isParalyzed) {
self.hasReachedColumn4 = true;
self.isParalyzed = true;
self.paralysisTimer = self.paralysisTime;
self.speed = 0; // Stop moving
return; // Don't move this frame
}
// Call original update only if not paralyzed
if (!self.isParalyzed) {
originalUpdate();
}
};
return self;
});
var ZombieCanon = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'canon');
self.health = 500;
self.maxHealth = 500;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.hasReachedPosition = false;
self.shootTimer = 0;
self.shootDelay = 600; // 10 seconds at 60fps
self.firstShot = true; // Track if this is the first shot
self.damage = 0; // Canon doesn't do regular damage
var originalUpdate = self.update;
self.update = function () {
// Move until reaching ninth column
if (!self.hasReachedPosition) {
var cellX = Math.floor((self.x - gridStartX) / cellSize);
if (cellX <= 8) {
self.hasReachedPosition = true;
self.speed = 0; // Stop moving
// Position exactly at ninth column
self.x = gridStartX + 8 * cellSize;
}
}
// If reached position, handle shooting
if (self.hasReachedPosition) {
self.shootTimer++;
var currentDelay = self.firstShot ? 180 : self.shootDelay; // 3 seconds for first shot, 10 seconds for subsequent
if (self.shootTimer >= currentDelay) {
self.shootShell();
self.shootTimer = 0;
self.firstShot = false; // After first shot, use normal delay
}
} else {
// Call original update to move normally
originalUpdate();
}
};
self.shootShell = function () {
// Create zombidito shell
var shell = new CanonShell();
shell.x = self.x;
shell.y = self.y;
shell.gridY = self.gridY;
canonShells.push(shell);
game.addChild(shell);
};
return self;
});
var Zombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombie');
self.health = 190;
self.maxHealth = 190;
self.speed = 0.8;
self.originalSpeed = 0.8;
return self;
});
var Zombidito = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombidito');
self.health = 250;
self.maxHealth = 250;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.damage = 50; // 50 damage per second
// Scale down the graphics to make it smaller
var graphics = self.children[0];
if (graphics) {
graphics.scaleX = 0.7;
graphics.scaleY = 0.7;
}
return self;
});
var SunZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'sun_zombie');
self.health = 190;
self.maxHealth = 190;
self.speed = 0.8;
self.originalSpeed = 0.8;
// Override die function to drop a sun when killed
var originalDie = self.die;
self.die = function () {
// Create a sun at zombie's position
var droppedSun = new Sun(self.x, self.y);
suns.push(droppedSun);
game.addChild(droppedSun);
// Call original die function
originalDie();
};
return self;
});
var SpawnedZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombie');
self.health = 225;
self.maxHealth = 225;
self.speed = 1.6; // 2x normal zombie speed (0.8 * 2)
self.originalSpeed = 1.6;
return self;
});
var RugbyZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'rugby');
self.health = 300;
self.maxHealth = 300;
self.speed = 2.4; // 3x normal zombie speed
self.originalSpeed = 2.4;
self.hasContactedPlant = false;
self.damage = 0; // No regular damage since it kills instantly
// Override getPlantInFront to handle instant kill
self.getPlantInFront = function () {
var cellX = Math.floor((self.x - gridStartX) / cellSize);
if (cellX >= 0 && cellX < gridCols && plants[self.gridY] && plants[self.gridY][cellX]) {
return plants[self.gridY][cellX];
}
return null;
};
// Override update to handle 500 damage to plants
var originalUpdate = self.update;
self.update = function () {
var plantInFront = self.getPlantInFront();
if (plantInFront && !self.hasContactedPlant) {
// Deal 500 damage to the plant
plantInFront.takeDamage(500);
// Reduce speed to normal zombie speed
self.hasContactedPlant = true;
self.speed = 0; // Stop for 2 seconds
self.originalSpeed = 0.8;
// Flash effect to show speed change
LK.effects.flashObject(self, 0xFFFF00, 500);
// Resume normal speed after 2 seconds
LK.setTimeout(function () {
self.speed = 0.8;
}, 2000);
}
// Call original update logic
originalUpdate();
};
return self;
});
var MinerZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'miner_zombie');
self.health = 100;
self.maxHealth = 100;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.damage = 15;
// Override the initialization to spawn at 5th column and show explosion
self.initializeMiner = function (row) {
self.gridY = row;
// Position at 5th column (index 4)
self.x = gridStartX + 4 * cellSize;
self.y = gridStartY + row * cellSize;
// Create explosion effect
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 2,
scaleY: 2
});
game.addChild(explosion);
// Animate explosion and remove it
tween(explosion, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
};
return self;
});
var ConeZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'cone_zombie');
self.health = 300;
self.maxHealth = 300;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.hasChangedAsset = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if cone zombie should switch to normal zombie appearance
if (self.health < 191 && !self.hasChangedAsset) {
self.hasChangedAsset = true;
// Remove current graphics and add normal zombie version
self.removeChild(self.children[0]);
var normalGraphics = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
var BucketZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'bucket_zombie');
self.health = 375;
self.maxHealth = 375;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.hasChangedAsset = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if bucket zombie should switch to normal zombie appearance
if (self.health < 191 && !self.hasChangedAsset) {
self.hasChangedAsset = true;
// Remove current graphics and add normal zombie version
self.removeChild(self.children[0]);
var normalGraphics = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1B5E20
});
/****
* Game Code
****/
// Sounds
// UI and grid
// Undead enemies
// Projectiles
// Plant defenders
// Grid setup - bigger grid centered on screen
var gridRows = 5;
var gridCols = 9;
var cellSize = 144;
var gridStartX = (2048 - gridCols * cellSize) / 2;
var gridStartY = (2732 - gridRows * cellSize) / 2;
// Game state
var plants = [];
var enemies = [];
var projectiles = [];
var suns = [];
var canonShells = [];
var lawnmowers = [];
var sunPoints = 500;
var lives = 3;
var currentWave = 1;
var wavesCompleted = 0;
var totalWaves = 10;
var gameMode = null; // 'normal' or 'endless'
var gameModeSelected = false;
var zombieSelectionActive = false;
var selectedZombieTypes = [];
var customWaveConfigs = {}; // Store custom wave configurations
var currentCustomizingWave = 1;
var availableZombieTypes = [{
type: 'zombie',
name: 'Normal Zombie',
asset: 'zombie'
}, {
type: 'cone_zombie',
name: 'Cone Zombie',
asset: 'cone_zombie'
}, {
type: 'bucket_zombie',
name: 'Bucket Zombie',
asset: 'bucket_zombie'
}, {
type: 'sun_zombie',
name: 'Sun Zombie',
asset: 'sun_zombie'
}, {
type: 'zombie_ra',
name: 'Zombie Ra',
asset: 'zombieRa'
}, {
type: 'zombie_canon',
name: 'Zombie Canon',
asset: 'canon'
}, {
type: 'miner_zombie',
name: 'Miner Zombie',
asset: 'miner_zombie'
}, {
type: 'rugby_zombie',
name: 'Rugby Zombie',
asset: 'rugby'
}, {
type: 'zombiestein',
name: 'Zombiestein',
asset: 'zombiestein'
}, {
type: 'zombie_cascanueces',
name: 'Zombie Cascanueces',
asset: 'cascanueces'
}, {
type: 'zombie_sarcofago',
name: 'Zombie Sarcófago',
asset: 'zombie_sarcofago'
}];
var baseEnemiesInWave = 0; // Store base enemy count for endless mode scaling
var waveInProgress = false;
var enemiesInWave = 0;
var enemiesKilled = 0;
var sunTimer = 0;
var waveStartTick = 0;
var nextWaveScheduled = false;
var sunZombieSpawnedThisWave = false;
// Spawn collision tracking for waves 1-2
var lastSpawnTime = {};
var lastSpawnRow = {};
// Miner zombie tracking
var minerZombiesSpawnedThisWave = 0;
var maxMinerZombiesPerWave = 1;
// Zombiestein tracking
var zombiesteinSpawned = false;
var zombiesteinAlive = false;
// Performance optimization variables
var maxObjectsPerType = 50;
var cleanupCounter = 0;
var lagOptimizationActive = false;
// Projectile pool for object reuse
var projectilePool = [];
var maxPoolSize = 20;
// Track active projectiles per plant
var plantProjectileCount = {};
// Selected plant type
var selectedPlantType = null;
var plantTypes = ['sunflower', 'birasol', 'peashooter', 'snowpea', 'fireshot', 'repetidora', 'wallnut', 'nuezPrimitiva', 'petacereza', 'carnivora', 'rapaz'];
var plantCosts = {
'peashooter': 100,
'sunflower': 50,
'birasol': 225,
'snowpea': 175,
'fireshot': 175,
'plantorcha': 175,
'wallnut': 50,
'repetidora': 200,
'nuezPrimitiva': 125,
'petacereza': 150,
'carnivora': 200,
'rapaz': 325
};
// Wallnut cooldown system
var wallnutCooldown = 0;
var wallnutCooldownTime = 720; // 12 seconds at 60fps
// Nuez primitiva cooldown system
var nuezPrimitivaCooldown = 0;
var nuezPrimitivaCooldownTime = 180; // 3 seconds at 60fps
// Petacereza cooldown system
var petacerezaCooldown = 0;
var petacerezaCooldownTime = 900; // 15 seconds at 60fps
// Ghost plant for preview
var ghostPlant = null;
// Initialize grid cells array
var gridCells = [];
// Initialize grid
for (var row = 0; row < gridRows; row++) {
plants[row] = [];
gridCells[row] = [];
for (var col = 0; col < gridCols; col++) {
plants[row][col] = null;
var cell = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
x: gridStartX + col * cellSize,
y: gridStartY + row * cellSize
});
gridCells[row][col] = cell;
game.addChild(cell);
}
}
// Initialize lawnmowers
for (var row = 0; row < gridRows; row++) {
var lawnmower = new Lawnmower(row);
lawnmowers.push(lawnmower);
game.addChild(lawnmower);
}
// Game mode selection UI
var modeSelectionContainer = new Container();
modeSelectionContainer.x = 2048 / 2;
modeSelectionContainer.y = 2732 / 2;
var modeTitle = new Text2('Select Game Mode', {
size: 80,
fill: 0xFFFFFF
});
modeTitle.anchor.set(0.5, 0.5);
modeTitle.y = -200;
modeSelectionContainer.addChild(modeTitle);
// Normal mode button
var normalModeButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: -240,
y: -50,
scaleX: 2,
scaleY: 1.5,
color: 0x4CAF50
});
normalModeButton.interactive = true;
normalModeButton.buttonMode = true;
normalModeButton.down = function (x, y, obj) {
gameMode = 'normal';
totalWaves = 10;
selectGameMode();
};
modeSelectionContainer.addChild(normalModeButton);
var normalModeText = new Text2('Normal', {
size: 60,
fill: 0xFFFFFF
});
normalModeText.anchor.set(0.5, 0.5);
normalModeText.x = -320;
normalModeText.y = -50;
modeSelectionContainer.addChild(normalModeText);
var normalModeSubtext = new Text2('(10 waves)', {
size: 40,
fill: 0xCCCCCC
});
normalModeSubtext.anchor.set(0.5, 0.5);
normalModeSubtext.x = -240;
normalModeSubtext.y = 10;
modeSelectionContainer.addChild(normalModeSubtext);
// Personalize mode button
var personalizeModeButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: -50,
scaleX: 2,
scaleY: 1.5,
color: 0xFF9800
});
personalizeModeButton.interactive = true;
personalizeModeButton.buttonMode = true;
personalizeModeButton.down = function (x, y, obj) {
gameMode = 'personalize';
totalWaves = 20;
showZombieSelection();
};
modeSelectionContainer.addChild(personalizeModeButton);
var personalizeModeText = new Text2('Personalize', {
size: 60,
fill: 0xFFFFFF
});
personalizeModeText.anchor.set(0.5, 0.5);
personalizeModeText.x = 0;
personalizeModeText.y = -50;
modeSelectionContainer.addChild(personalizeModeText);
var personalizeModeSubtext = new Text2('(20 waves)', {
size: 40,
fill: 0xCCCCCC
});
personalizeModeSubtext.anchor.set(0.5, 0.5);
personalizeModeSubtext.x = 0;
personalizeModeSubtext.y = 10;
modeSelectionContainer.addChild(personalizeModeSubtext);
// Endless mode button
var endlessModeButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 360,
y: -50,
scaleX: 2,
scaleY: 1.5,
color: 0x9C27B0
});
endlessModeButton.interactive = true;
endlessModeButton.buttonMode = true;
endlessModeButton.down = function (x, y, obj) {
gameMode = 'endless';
totalWaves = 999; // Large number for display purposes
selectGameMode();
};
modeSelectionContainer.addChild(endlessModeButton);
var endlessModeText = new Text2('Endless', {
size: 60,
fill: 0xFFFFFF
});
endlessModeText.anchor.set(0.5, 0.5);
endlessModeText.x = 360;
endlessModeText.y = -50;
modeSelectionContainer.addChild(endlessModeText);
var endlessModeSubtext = new Text2('(infinite waves)', {
size: 40,
fill: 0xCCCCCC
});
endlessModeSubtext.anchor.set(0.5, 0.5);
endlessModeSubtext.x = 360;
endlessModeSubtext.y = 10;
modeSelectionContainer.addChild(endlessModeSubtext);
game.addChild(modeSelectionContainer);
function selectGameMode() {
gameModeSelected = true;
modeSelectionContainer.destroy();
updateWaveDisplay();
// Create skip wave button only for endless mode
if (gameMode === 'endless') {
createSkipWaveButton();
}
// Start first wave after mode selection
LK.setTimeout(function () {
startWave();
}, 3000);
}
function showZombieSelection() {
zombieSelectionActive = true;
modeSelectionContainer.destroy();
// Create zombie selection container
var zombieSelectionContainer = new Container();
zombieSelectionContainer.x = 2048 / 2;
zombieSelectionContainer.y = 2732 / 2;
game.addChild(zombieSelectionContainer);
// Title
var selectionTitle = new Text2('Select Zombies for Endless Mode', {
size: 60,
fill: 0xFFFFFF
});
selectionTitle.anchor.set(0.5, 0.5);
selectionTitle.y = -350;
zombieSelectionContainer.addChild(selectionTitle);
// Instructions
var instructions = new Text2('Click zombies to toggle selection (min 3 required)', {
size: 40,
fill: 0xCCCCCC
});
instructions.anchor.set(0.5, 0.5);
instructions.y = -300;
zombieSelectionContainer.addChild(instructions);
// Create zombie selection buttons
var zombieButtons = [];
var cols = 4;
var rows = Math.ceil(availableZombieTypes.length / cols);
for (var i = 0; i < availableZombieTypes.length; i++) {
var zombieType = availableZombieTypes[i];
var col = i % cols;
var row = Math.floor(i / cols);
// Zombie button
var zombieButton = LK.getAsset(zombieType.asset, {
anchorX: 0.5,
anchorY: 0.5,
x: (col - (cols - 1) / 2) * 200,
y: (row - (rows - 1) / 2) * 160 - 50,
scaleX: 1.5,
scaleY: 1.5
});
zombieButton.zombieType = zombieType.type;
zombieButton.isSelected = false;
zombieButton.interactive = true;
zombieButton.buttonMode = true;
zombieButton.alpha = 0.5; // Start unselected
zombieButton.down = function (x, y, obj) {
toggleZombieSelection(this);
};
zombieSelectionContainer.addChild(zombieButton);
zombieButtons.push(zombieButton);
// Zombie name
var nameText = new Text2(zombieType.name, {
size: 30,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0.5);
nameText.x = zombieButton.x;
nameText.y = zombieButton.y + 80;
zombieSelectionContainer.addChild(nameText);
}
// Start button
var startButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 250,
scaleX: 3,
scaleY: 1.5,
color: 0x4CAF50
});
startButton.interactive = true;
startButton.buttonMode = true;
startButton.alpha = 0.5; // Start disabled
startButton.down = function (x, y, obj) {
if (selectedZombieTypes.length >= 3) {
zombieSelectionContainer.destroy();
showWaveCustomization();
}
};
zombieSelectionContainer.addChild(startButton);
var startButtonText = new Text2('Customize Waves', {
size: 50,
fill: 0xFFFFFF
});
startButtonText.anchor.set(0.5, 0.5);
startButtonText.y = 250;
zombieSelectionContainer.addChild(startButtonText);
// Store references for updates
zombieSelectionContainer.zombieButtons = zombieButtons;
zombieSelectionContainer.startButton = startButton;
zombieSelectionContainer.startButtonText = startButtonText;
game.zombieSelectionContainer = zombieSelectionContainer;
}
function toggleZombieSelection(button) {
if (button.isSelected) {
// Deselect
button.isSelected = false;
button.alpha = 0.5;
button.tint = 0xFFFFFF;
// Remove from selected array
for (var i = 0; i < selectedZombieTypes.length; i++) {
if (selectedZombieTypes[i] === button.zombieType) {
selectedZombieTypes.splice(i, 1);
break;
}
}
} else {
// Select
button.isSelected = true;
button.alpha = 1.0;
button.tint = 0x00FF00;
selectedZombieTypes.push(button.zombieType);
}
updateStartButton();
}
function updateStartButton() {
var container = game.zombieSelectionContainer;
if (!container) {
return;
}
if (selectedZombieTypes.length >= 3) {
container.startButton.alpha = 1.0;
container.startButton.tint = 0x4CAF50;
container.startButtonText.setText('Customize Waves (' + selectedZombieTypes.length + ' selected)');
} else {
container.startButton.alpha = 0.5;
container.startButton.tint = 0x888888;
container.startButtonText.setText('Select at least 3 zombies (' + selectedZombieTypes.length + '/3)');
}
}
function showWaveCustomization() {
// Initialize default configurations for all 20 waves
for (var wave = 1; wave <= 20; wave++) {
if (!customWaveConfigs[wave]) {
customWaveConfigs[wave] = {};
for (var i = 0; i < selectedZombieTypes.length; i++) {
customWaveConfigs[wave][selectedZombieTypes[i]] = wave <= 2 ? 2 : Math.min(wave, 8); // Default quantities
}
}
}
currentCustomizingWave = 1;
showWaveConfigUI();
}
function showWaveConfigUI() {
// Remove existing wave config container if any
if (game.waveConfigContainer) {
game.waveConfigContainer.destroy();
}
// Create wave customization container
var waveConfigContainer = new Container();
waveConfigContainer.x = 2048 / 2;
waveConfigContainer.y = 2732 / 2;
game.addChild(waveConfigContainer);
game.waveConfigContainer = waveConfigContainer;
// Title
var configTitle = new Text2('Wave ' + currentCustomizingWave + ' Configuration', {
size: 60,
fill: 0xFFFFFF
});
configTitle.anchor.set(0.5, 0.5);
configTitle.y = -350;
waveConfigContainer.addChild(configTitle);
// Instructions
var configInstructions = new Text2('Set zombie quantities for Wave ' + currentCustomizingWave + ' (1-20)', {
size: 40,
fill: 0xCCCCCC
});
configInstructions.anchor.set(0.5, 0.5);
configInstructions.y = -300;
waveConfigContainer.addChild(configInstructions);
// Wave navigation buttons - positioned slightly higher
var prevWaveButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: -400,
y: -250,
scaleX: 1.5,
scaleY: 1.0,
color: currentCustomizingWave > 1 ? 0x2196F3 : 0x666666
});
prevWaveButton.interactive = currentCustomizingWave > 1;
prevWaveButton.buttonMode = currentCustomizingWave > 1;
if (currentCustomizingWave > 1) {
prevWaveButton.down = function () {
currentCustomizingWave--;
showWaveConfigUI();
};
}
waveConfigContainer.addChild(prevWaveButton);
var prevWaveText = new Text2('◀ Previous', {
size: 40,
fill: 0xFFFFFF
});
prevWaveText.anchor.set(0.5, 0.5);
prevWaveText.x = -400;
prevWaveText.y = -250;
waveConfigContainer.addChild(prevWaveText);
var nextWaveButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 400,
y: -250,
scaleX: 1.5,
scaleY: 1.0,
color: currentCustomizingWave < 20 ? 0x2196F3 : 0x666666
});
nextWaveButton.interactive = currentCustomizingWave < 20;
nextWaveButton.buttonMode = currentCustomizingWave < 20;
if (currentCustomizingWave < 20) {
nextWaveButton.down = function () {
currentCustomizingWave++;
showWaveConfigUI();
};
}
waveConfigContainer.addChild(nextWaveButton);
var nextWaveText = new Text2('Next ▶', {
size: 40,
fill: 0xFFFFFF
});
nextWaveText.anchor.set(0.5, 0.5);
nextWaveText.x = 400;
nextWaveText.y = -250;
waveConfigContainer.addChild(nextWaveText);
// Add "All to 0" and "All to 1" buttons on the left side
var allToZeroButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: -700,
y: -50,
scaleX: 1.5,
scaleY: 1.0,
color: 0xF44336
});
allToZeroButton.interactive = true;
allToZeroButton.buttonMode = true;
allToZeroButton.down = function () {
// Set all zombie quantities to 0 for current wave
for (var i = 0; i < selectedZombieTypes.length; i++) {
customWaveConfigs[currentCustomizingWave][selectedZombieTypes[i]] = 0;
}
showWaveConfigUI(); // Refresh UI
};
waveConfigContainer.addChild(allToZeroButton);
var allToZeroText = new Text2('All to 0', {
size: 35,
fill: 0xFFFFFF
});
allToZeroText.anchor.set(0.5, 0.5);
allToZeroText.x = -700;
allToZeroText.y = -50;
waveConfigContainer.addChild(allToZeroText);
var allToOneButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: -700,
y: 20,
scaleX: 1.5,
scaleY: 1.0,
color: 0x4CAF50
});
allToOneButton.interactive = true;
allToOneButton.buttonMode = true;
allToOneButton.down = function () {
// Set all zombie quantities to 1 for current wave
for (var i = 0; i < selectedZombieTypes.length; i++) {
customWaveConfigs[currentCustomizingWave][selectedZombieTypes[i]] = 1;
}
showWaveConfigUI(); // Refresh UI
};
waveConfigContainer.addChild(allToOneButton);
var allToOneText = new Text2('All to 1', {
size: 35,
fill: 0xFFFFFF
});
allToOneText.anchor.set(0.5, 0.5);
allToOneText.x = -700;
allToOneText.y = 20;
waveConfigContainer.addChild(allToOneText);
// Zombie quantity controls
var yOffset = -100;
var zombieControls = [];
for (var i = 0; i < selectedZombieTypes.length; i++) {
var zombieType = selectedZombieTypes[i];
var zombieAsset = null;
for (var j = 0; j < availableZombieTypes.length; j++) {
if (availableZombieTypes[j].type === zombieType) {
zombieAsset = availableZombieTypes[j].asset;
break;
}
}
if (zombieAsset) {
// Zombie icon
var zombieIcon = LK.getAsset(zombieAsset, {
anchorX: 0.5,
anchorY: 0.5,
x: -300,
y: yOffset,
scaleX: 1.2,
scaleY: 1.2
});
waveConfigContainer.addChild(zombieIcon);
// Decrease button
var decreaseButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: -100,
y: yOffset,
scaleX: 0.8,
scaleY: 0.8,
color: 0xF44336
});
decreaseButton.zombieType = zombieType;
decreaseButton.interactive = true;
decreaseButton.buttonMode = true;
decreaseButton.down = function () {
var currentQty = customWaveConfigs[currentCustomizingWave][this.zombieType] || 0;
if (currentQty > 0) {
customWaveConfigs[currentCustomizingWave][this.zombieType] = currentQty - 1;
showWaveConfigUI();
}
};
waveConfigContainer.addChild(decreaseButton);
var decreaseText = new Text2('-', {
size: 50,
fill: 0xFFFFFF
});
decreaseText.anchor.set(0.5, 0.5);
decreaseText.x = -100;
decreaseText.y = yOffset;
waveConfigContainer.addChild(decreaseText);
// Quantity display
var quantity = customWaveConfigs[currentCustomizingWave][zombieType] || 0;
var quantityText = new Text2(quantity.toString(), {
size: 45,
fill: 0xFFFFFF
});
quantityText.anchor.set(0.5, 0.5);
quantityText.x = 0;
quantityText.y = yOffset;
waveConfigContainer.addChild(quantityText);
// Increase button
var increaseButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 100,
y: yOffset,
scaleX: 0.8,
scaleY: 0.8,
color: 0x4CAF50
});
increaseButton.zombieType = zombieType;
increaseButton.interactive = true;
increaseButton.buttonMode = true;
increaseButton.down = function () {
var currentQty = customWaveConfigs[currentCustomizingWave][this.zombieType] || 0;
if (currentQty < 15) {
// Max 15 zombies per type per wave
customWaveConfigs[currentCustomizingWave][this.zombieType] = currentQty + 1;
showWaveConfigUI();
}
};
waveConfigContainer.addChild(increaseButton);
var increaseText = new Text2('+', {
size: 40,
fill: 0xFFFFFF
});
increaseText.anchor.set(0.5, 0.5);
increaseText.x = 100;
increaseText.y = yOffset;
waveConfigContainer.addChild(increaseText);
// Zombie name
var zombieName = null;
for (var k = 0; k < availableZombieTypes.length; k++) {
if (availableZombieTypes[k].type === zombieType) {
zombieName = availableZombieTypes[k].name;
break;
}
}
var nameText = new Text2(zombieName || zombieType, {
size: 30,
fill: 0xCCCCCC
});
nameText.anchor.set(0.5, 0.5);
nameText.x = 300;
nameText.y = yOffset;
waveConfigContainer.addChild(nameText);
yOffset += 70;
}
}
// Finish customization button - positioned at bottom right
var finishButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 600,
y: 350,
scaleX: 3,
scaleY: 1.5,
color: 0x4CAF50
});
finishButton.interactive = true;
finishButton.buttonMode = true;
finishButton.down = function () {
waveConfigContainer.destroy();
selectGameMode();
};
waveConfigContainer.addChild(finishButton);
var finishButtonText = new Text2('Start Game', {
size: 50,
fill: 0xFFFFFF
});
finishButtonText.anchor.set(0.5, 0.5);
finishButtonText.x = 600;
finishButtonText.y = 350;
waveConfigContainer.addChild(finishButtonText);
}
// Shovel tool for removing plants
var selectedTool = null; // 'shovel' or null
var shovelButton = LK.getAsset('shovel', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 100,
scaleX: 1.5,
scaleY: 1.5
});
shovelButton.interactive = true;
shovelButton.buttonMode = true;
shovelButton.down = function (x, y, obj) {
console.log("Shovel button clicked");
if (selectedTool === 'shovel') {
selectedTool = null;
shovelButton.alpha = 0.8;
shovelButton.scaleX = shovelButton.scaleY = 1.5;
} else {
selectedTool = 'shovel';
selectedPlantType = null;
shovelButton.alpha = 1.0;
shovelButton.scaleX = shovelButton.scaleY = 1.8;
updatePlantSelection();
}
};
shovelButton.alpha = 0.8;
game.addChild(shovelButton);
// UI Elements
var sunDisplay = new Text2('Sun: ' + sunPoints, {
size: 60,
fill: 0xFFEB3B
});
sunDisplay.anchor.set(0, 0);
sunDisplay.x = 120; // Offset from left edge to avoid menu icon
LK.gui.topLeft.addChild(sunDisplay);
var livesDisplay = new Text2('Lives: ' + lives, {
size: 60,
fill: 0xF44336
});
livesDisplay.anchor.set(0, 0);
livesDisplay.y = 80;
LK.gui.topRight.addChild(livesDisplay);
var waveDisplay = new Text2('Wave: ' + currentWave + '/' + totalWaves, {
size: 50,
fill: 0x4CAF50
});
waveDisplay.anchor.set(0.5, 0);
LK.gui.top.addChild(waveDisplay);
// Plant selection UI at bottom
var plantButtons = [];
var uiY = 2732 - 150;
for (var i = 0; i < plantTypes.length; i++) {
var plantType = plantTypes[i];
var yPos = uiY;
var xPos = 200 + i * 220;
// Special positioning for carnivora - place it above peashooter
if (plantType === 'carnivora') {
xPos = 640; // Same x position as peashooter
yPos = uiY - 266; // Above peashooter
}
// Special positioning for petacereza - place it above birasol
else if (plantType === 'petacereza') {
xPos = 420; // Same x position as birasol
yPos = uiY - 266; // Above birasol, 10% higher than previous position (242 * 1.1 = 266)
}
// Special positioning for rapaz - place it above snowpea
else if (plantType === 'rapaz') {
xPos = 860; // Same x position as snowpea
yPos = uiY - 266; // Above snowpea
}
var button = LK.getAsset(plantType, {
anchorX: 0.5,
anchorY: 0.5,
x: xPos,
y: yPos,
scaleX: 2.25,
scaleY: 2.25
});
button.plantType = plantType;
button.interactive = true; // Enable button interactivity
button.buttonMode = true; // Make it behave like a button
button.down = function (x, y, obj) {
console.log("Plant button clicked:", this.plantType, "Cost:", plantCosts[this.plantType], "Current suns:", sunPoints);
// Check wallnut cooldown
if (this.plantType === 'wallnut' && wallnutCooldown > 0) {
return; // Don't allow selection during cooldown
}
// Check nuez primitiva cooldown
if (this.plantType === 'nuezPrimitiva' && nuezPrimitivaCooldown > 0) {
return; // Don't allow selection during cooldown
}
// Check petacereza cooldown
if (this.plantType === 'petacereza' && petacerezaCooldown > 0) {
return; // Don't allow selection during cooldown
}
if (sunPoints >= plantCosts[this.plantType]) {
selectedPlantType = this.plantType;
selectedTool = null;
shovelButton.alpha = 0.8;
shovelButton.scaleX = shovelButton.scaleY = 1.5;
updatePlantSelection();
}
};
game.addChild(button);
plantButtons.push(button);
// Add cost text
var costText = new Text2(plantCosts[plantType], {
size: 40,
fill: 0xFFFFFF
});
costText.anchor.set(0.5, 0);
costText.x = button.x;
costText.y = button.y + 80;
game.addChild(costText);
}
// Add plantorcha button above sunflower
var plantorchaButton = LK.getAsset('plantorcha', {
anchorX: 0.5,
anchorY: 0.5,
x: 200,
// Same x as sunflower
y: uiY - 351,
// Above sunflower with 10% more space above previous position (319 * 1.1 = 351)
scaleX: 2.25,
scaleY: 2.25
});
plantorchaButton.plantType = 'plantorcha';
plantorchaButton.interactive = true;
plantorchaButton.buttonMode = true;
plantorchaButton.down = function (x, y, obj) {
console.log("Plant button clicked:", this.plantType, "Cost:", plantCosts[this.plantType], "Current suns:", sunPoints);
if (sunPoints >= plantCosts[this.plantType]) {
selectedPlantType = this.plantType;
selectedTool = null;
shovelButton.alpha = 0.8;
shovelButton.scaleX = shovelButton.scaleY = 1.5;
updatePlantSelection();
}
};
game.addChild(plantorchaButton);
plantButtons.push(plantorchaButton);
// Add plantorcha cost text
var plantorchaCostText = new Text2(plantCosts['plantorcha'], {
size: 40,
fill: 0xFFFFFF
});
plantorchaCostText.anchor.set(0.5, 0);
plantorchaCostText.x = plantorchaButton.x;
plantorchaCostText.y = plantorchaButton.y + 80;
game.addChild(plantorchaCostText);
function updatePlantSelection() {
for (var i = 0; i < plantButtons.length; i++) {
if (plantButtons[i].plantType === selectedPlantType) {
plantButtons[i].alpha = 1.0;
plantButtons[i].scaleX = plantButtons[i].scaleY = 2.55;
} else if (plantButtons[i].plantType === 'wallnut' && wallnutCooldown > 0) {
// Gray out wallnut during cooldown
plantButtons[i].alpha = 0.3;
plantButtons[i].scaleX = plantButtons[i].scaleY = 2.25;
plantButtons[i].tint = 0x888888;
} else if (plantButtons[i].plantType === 'nuezPrimitiva' && nuezPrimitivaCooldown > 0) {
// Gray out nuez primitiva during cooldown
plantButtons[i].alpha = 0.3;
plantButtons[i].scaleX = plantButtons[i].scaleY = 2.25;
plantButtons[i].tint = 0x888888;
} else if (plantButtons[i].plantType === 'petacereza' && petacerezaCooldown > 0) {
// Gray out petacereza during cooldown
plantButtons[i].alpha = 0.3;
plantButtons[i].scaleX = plantButtons[i].scaleY = 2.25;
plantButtons[i].tint = 0x888888;
} else {
plantButtons[i].alpha = 0.6;
plantButtons[i].scaleX = plantButtons[i].scaleY = 2.25;
plantButtons[i].tint = 0xFFFFFF;
}
}
// Remove existing ghost plant
if (ghostPlant) {
ghostPlant.destroy();
ghostPlant = null;
}
// Create new ghost plant if a type is selected
if (selectedPlantType) {
ghostPlant = LK.getAsset(selectedPlantType, {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.6,
tint: 0x88FF88
});
game.addChild(ghostPlant);
}
// Highlight valid placement squares
for (var row = 0; row < gridRows; row++) {
for (var col = 0; col < gridCols; col++) {
if (selectedPlantType && canPlacePlant(col, row, selectedPlantType)) {
// Green highlight for valid empty squares
gridCells[row][col].tint = 0x00FF00;
gridCells[row][col].alpha = 0.7;
} else if (selectedPlantType && plants[row][col] !== null) {
// Red highlight for occupied squares when plant is selected
gridCells[row][col].tint = 0xFF0000;
gridCells[row][col].alpha = 0.5;
} else if (selectedTool === 'shovel' && plants[row][col] !== null) {
// Yellow highlight for plants that can be removed with shovel
gridCells[row][col].tint = 0xFFFF00;
gridCells[row][col].alpha = 0.7;
} else {
// Normal appearance for unselected or invalid squares
gridCells[row][col].tint = 0xFFFFFF;
gridCells[row][col].alpha = 0.3;
}
}
}
}
function updateSunDisplay() {
sunDisplay.setText('Sun: ' + sunPoints);
}
function updateLivesDisplay() {
livesDisplay.setText('Lives: ' + lives);
}
function updateWaveDisplay() {
if (gameMode === 'endless') {
waveDisplay.setText('Wave: ' + currentWave + ' / ∞');
} else if (gameMode === 'personalize') {
waveDisplay.setText('Wave: ' + currentWave + '/' + totalWaves);
} else {
waveDisplay.setText('Wave: ' + currentWave + '/' + totalWaves);
}
}
function createProjectile(type, startX, startY, speed, damage, plantId) {
// Check if plant already has 2 active projectiles
if (!plantProjectileCount[plantId]) {
plantProjectileCount[plantId] = 0;
}
if (plantProjectileCount[plantId] >= 2) {
return null; // Don't create projectile if plant already has 2
}
var projectile;
if (projectilePool.length > 0) {
// Reuse from pool
projectile = projectilePool.pop();
projectile.speed = speed;
projectile.damage = damage;
projectile.x = startX;
projectile.y = startY;
projectile.type = type;
projectile.gridY = Math.floor((startY - gridStartY + cellSize / 2) / cellSize);
projectile.ownerId = plantId;
// Re-add graphics
var graphics = projectile.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
});
} else {
// Create new projectile
projectile = new Projectile(type, startX, startY, speed, damage);
projectile.ownerId = plantId;
}
plantProjectileCount[plantId]++;
projectiles.push(projectile);
game.addChild(projectile);
return projectile;
}
function performanceCleanup() {
// Clean up destroyed objects that might still be in arrays
for (var i = enemies.length - 1; i >= 0; i--) {
if (!enemies[i].parent) {
enemies.splice(i, 1);
}
}
for (var i = projectiles.length - 1; i >= 0; i--) {
if (!projectiles[i].parent) {
projectiles.splice(i, 1);
}
}
for (var i = suns.length - 1; i >= 0; i--) {
if (!suns[i].parent) {
suns.splice(i, 1);
}
}
for (var i = canonShells.length - 1; i >= 0; i--) {
if (!canonShells[i].parent) {
canonShells.splice(i, 1);
}
}
// Clean up projectile tracking for destroyed plants
for (var plantId in plantProjectileCount) {
var coords = plantId.split('_');
var x = parseInt(coords[0]);
var y = parseInt(coords[1]);
if (y >= 0 && y < gridRows && x >= 0 && x < gridCols && !plants[y][x]) {
delete plantProjectileCount[plantId];
}
}
// Limit maximum objects if we have too many
if (enemies.length > maxObjectsPerType) {
for (var i = 0; i < enemies.length - maxObjectsPerType; i++) {
if (enemies[i]) {
enemies[i].destroy();
}
}
}
if (projectiles.length > maxObjectsPerType) {
for (var i = 0; i < projectiles.length - maxObjectsPerType; i++) {
if (projectiles[i]) {
projectiles[i].removeFromGame();
}
}
}
if (suns.length > 20) {
for (var i = 0; i < suns.length - 20; i++) {
if (suns[i]) {
suns[i].destroy();
}
}
}
// Limit projectile pool size
if (projectilePool.length > maxPoolSize) {
for (var i = maxPoolSize; i < projectilePool.length; i++) {
if (projectilePool[i]) {
projectilePool[i].destroy();
}
}
projectilePool.length = maxPoolSize;
}
}
function getGridPosition(x, y) {
var gridX = Math.floor((x - gridStartX + cellSize / 2) / cellSize);
var gridY = Math.floor((y - gridStartY + cellSize / 2) / cellSize);
if (gridX >= 0 && gridX < gridCols && gridY >= 0 && gridY < gridRows) {
return {
x: gridX,
y: gridY
};
}
return null;
}
function canPlacePlant(gridX, gridY, plantType) {
// Check if grid position is valid
if (gridX < 0 || gridX >= gridCols || gridY < 0 || gridY >= gridRows) {
return false;
}
// Check wallnut cooldown
if (plantType === 'wallnut' && wallnutCooldown > 0) {
return false;
}
// Check nuez primitiva cooldown
if (plantType === 'nuezPrimitiva' && nuezPrimitivaCooldown > 0) {
return false;
}
// Check petacereza cooldown
if (plantType === 'petacereza' && petacerezaCooldown > 0) {
return false;
}
// Check if square is empty and player has enough suns
return plants[gridY][gridX] === null && sunPoints >= plantCosts[plantType];
}
function placePlant(gridX, gridY, plantType) {
// Double-check that placement is valid before proceeding
if (!canPlacePlant(gridX, gridY, plantType)) {
return false;
}
var plant;
switch (plantType) {
case 'peashooter':
plant = new Peashooter();
break;
case 'sunflower':
plant = new Sunflower();
break;
case 'birasol':
plant = new Birasol();
break;
case 'snowpea':
plant = new SnowPea();
break;
case 'fireshot':
plant = new Fireshot();
break;
case 'plantorcha':
plant = new Plantorcha();
break;
case 'wallnut':
plant = new Wallnut();
break;
case 'snowpea':
plant = new SnowPea();
break;
case 'repetidora':
plant = new Repetidora();
break;
case 'nuezPrimitiva':
plant = new NuezPrimitiva();
break;
case 'petacereza':
plant = new Petacereza();
break;
case 'carnivora':
plant = new Carnivora();
break;
case 'rapaz':
plant = new Rapaz();
break;
default:
return false;
}
// Set plant position and grid reference
plant.gridX = gridX;
plant.gridY = gridY;
plant.x = gridStartX + gridX * cellSize;
plant.y = gridStartY + gridY * cellSize;
// Place plant in grid and add to game
plants[gridY][gridX] = plant;
game.addChild(plant);
// Deduct cost and update UI
sunPoints -= plantCosts[plantType];
updateSunDisplay();
LK.getSound('plant').play();
// Start wallnut cooldown if placing wallnut
if (plantType === 'wallnut') {
wallnutCooldown = wallnutCooldownTime;
selectedPlantType = null; // Deselect wallnut after placing
updatePlantSelection();
}
// Start nuez primitiva cooldown if placing nuez primitiva
if (plantType === 'nuezPrimitiva') {
nuezPrimitivaCooldown = nuezPrimitivaCooldownTime;
selectedPlantType = null; // Deselect nuez primitiva after placing
updatePlantSelection();
}
// Start petacereza cooldown if placing petacereza
if (plantType === 'petacereza') {
petacerezaCooldown = petacerezaCooldownTime;
selectedPlantType = null; // Deselect petacereza after placing
updatePlantSelection();
}
// Flash the grid cell to show successful placement
LK.effects.flashObject(gridCells[gridY][gridX], 0x00FF00, 300);
return true;
}
function canSpawnWithoutCollision(row) {
// Only apply collision detection for waves 1-2
if (currentWave > 2) {
return true;
}
var currentTime = LK.ticks;
var oneSecond = 60; // 60 ticks = 1 second at 60fps
// Check if same row was used in last second
if (lastSpawnRow[row] && currentTime - lastSpawnRow[row] < oneSecond) {
return false;
}
// Check if any zombie was spawned in last second
for (var checkRow in lastSpawnTime) {
if (currentTime - lastSpawnTime[checkRow] < oneSecond) {
return false;
}
}
return true;
}
function spawnZombie(type, row) {
var zombie;
switch (type) {
case 'zombie':
zombie = new Zombie();
break;
case 'cone_zombie':
zombie = new ConeZombie();
break;
case 'bucket_zombie':
zombie = new BucketZombie();
break;
case 'sun_zombie':
zombie = new SunZombie();
break;
case 'zombie_ra':
zombie = new ZombieRa();
break;
case 'zombie_canon':
zombie = new ZombieCanon();
break;
case 'miner_zombie':
zombie = new MinerZombie();
// Special positioning for miner
zombie.initializeMiner(row);
// Apply health bonus for endless mode after wave 10
if (gameMode === 'endless' && currentWave > 10) {
var healthMultiplier = 1 + (currentWave - 10) * 0.05;
zombie.health = Math.floor(zombie.health * healthMultiplier);
zombie.maxHealth = Math.floor(zombie.maxHealth * healthMultiplier);
}
enemies.push(zombie);
game.addChild(zombie);
return;
// Return early since positioning is handled
case 'zombiestein':
zombie = new Zombiestein();
break;
case 'rugby_zombie':
zombie = new RugbyZombie();
break;
case 'zombie_cascanueces':
zombie = new ZombieCascanueces();
break;
case 'zombie_sarcofago':
zombie = new ZombieSarcofago();
break;
default:
return;
}
zombie.gridY = row;
zombie.x = 2100;
zombie.y = gridStartY + row * cellSize;
// Apply health bonus for endless mode after wave 10
if (gameMode === 'endless' && currentWave > 10) {
var healthMultiplier = 1 + (currentWave - 10) * 0.05;
zombie.health = Math.floor(zombie.health * healthMultiplier);
zombie.maxHealth = Math.floor(zombie.maxHealth * healthMultiplier);
}
enemies.push(zombie);
game.addChild(zombie);
}
function startWave() {
if (waveInProgress) {
return;
}
waveInProgress = true;
waveStartTick = LK.ticks; // Record when wave started
sunZombieSpawnedThisWave = false; // Reset sun zombie spawn flag for this wave
// Reset and update miner zombie tracking for this wave
minerZombiesSpawnedThisWave = 0;
if (currentWave >= 8) {
maxMinerZombiesPerWave = 2;
} else {
maxMinerZombiesPerWave = 1;
}
var bucketZombiesLeft = 0;
// Handle custom wave configurations for waves 1-20 in personalize mode
if (gameMode === 'personalize' && currentWave <= 20 && customWaveConfigs[currentWave]) {
var waveConfig = customWaveConfigs[currentWave];
var totalZombies = 0;
var spawnQueue = [];
// Build spawn queue from custom configuration
for (var zombieType in waveConfig) {
var quantity = waveConfig[zombieType];
for (var i = 0; i < quantity; i++) {
spawnQueue.push(zombieType);
totalZombies++;
}
}
enemiesInWave = totalZombies;
enemiesKilled = 0;
// Spawn zombies from custom configuration
if (spawnQueue.length > 0) {
var spawnIndex = 0;
var customSpawnTimer = LK.setInterval(function () {
if (spawnIndex < spawnQueue.length) {
var zombieType = spawnQueue[spawnIndex];
var row = Math.floor(Math.random() * gridRows);
spawnZombie(zombieType, row);
spawnIndex++;
} else {
LK.clearInterval(customSpawnTimer);
}
}, 800); // Spawn every 800ms
}
return; // Exit early for custom waves
}
// Handle endless mode waves (but not personalize mode)
if (gameMode === 'endless' && currentWave > 10) {
// For endless mode waves 11+, spawn Zombiestein every 5 waves
if (currentWave % 5 === 0) {
// Calculate number of Zombiesteins to spawn based on wave
var zombiesteinCount = Math.floor((currentWave - 10) / 5) + 1;
for (var z = 0; z < zombiesteinCount; z++) {
var zombiesteinRow = Math.floor(Math.random() * gridRows);
spawnZombie('zombiestein', zombiesteinRow);
}
zombiesteinSpawned = true;
zombiesteinAlive = true;
enemiesInWave += zombiesteinCount - 1; // Adjust enemy count for additional Zombiesteins
}
// Zombie Sarcófago spawning logic for endless mode waves 15+
if (currentWave >= 15) {
// Count bucket zombies in this wave to determine if we can spawn sarcófago
var bucketZombieCount = Math.floor(scaledEnemyCount * 0.6);
// 50% chance to spawn if at least 2 bucket zombies
if (bucketZombieCount >= 2 && Math.random() < 0.5) {
// Calculate max sarcófagos: 1 + (waves past 15) / 5
var maxSarcofagos = 1 + Math.floor((currentWave - 15) / 5);
var sarcofagosToSpawn = Math.min(maxSarcofagos, 1); // Start with 1, increase every 5 waves
for (var s = 0; s < sarcofagosToSpawn; s++) {
var sarcofagoRow = Math.floor(Math.random() * gridRows);
spawnZombie('zombie_sarcofago', sarcofagoRow);
enemiesInWave++; // Add to enemy count
}
}
}
// Calculate enemy count with 5% increase per wave after wave 10
var waveMultiplier = 1 + (currentWave - 10) * 0.05;
var baseCount = 7; // Base count similar to wave 10
var scaledEnemyCount = Math.floor(baseCount * waveMultiplier);
bucketZombiesLeft = Math.floor(scaledEnemyCount * 0.6); // 60% bucket zombies
// Calculate rugby zombies for endless mode (every 5 waves adds 1 more)
var rugbyZombiesLeft = 0;
if (currentWave >= 11) {
var rugbyBaseCount = 3; // Base count for waves 11-15
var additionalRugby = Math.floor((currentWave - 11) / 5); // +1 every 5 waves
rugbyZombiesLeft = rugbyBaseCount + additionalRugby;
}
enemiesInWave = scaledEnemyCount + rugbyZombiesLeft + (currentWave % 5 === 0 ? 1 : 0); // Add rugby and Zombiestein if applicable
enemiesKilled = 0;
// Set up spawn timer for endless waves
var normalZombiesLeft = Math.floor(scaledEnemyCount * 0.15); // 15% normal
var coneZombiesLeft = Math.floor(scaledEnemyCount * 0.25); // 25% cone
var spawnTimer = LK.setInterval(function () {
if (enemiesInWave > 0) {
var row = Math.floor(Math.random() * gridRows);
var zombieType = null;
// Miner zombie spawning (every wave in endless mode) - only if selected
var shouldSpawnMinerZombie = minerZombiesSpawnedThisWave < maxMinerZombiesPerWave && selectedZombieTypes.indexOf('miner_zombie') !== -1 && Math.random() < 0.3;
// Check if we should spawn a rugby zombie for endless mode (only if selected)
var shouldSpawnRugbyZombie = rugbyZombiesLeft > 0 && selectedZombieTypes.indexOf('rugby_zombie') !== -1 && Math.random() < 0.2;
// Check if we should spawn a zombie ra (odd waves excluding 1 and 3) in endless mode (only if selected)
var shouldSpawnZombieRa = currentWave % 2 === 1 && currentWave > 3 && selectedZombieTypes.indexOf('zombie_ra') !== -1 && Math.random() < 0.25;
// Check if we should spawn a zombie canon (wave 9+) in endless mode (only if selected)
var shouldSpawnZombieCanon = currentWave >= 9 && selectedZombieTypes.indexOf('zombie_canon') !== -1 && Math.random() < 0.3;
if (shouldSpawnMinerZombie) {
zombieType = 'miner_zombie';
minerZombiesSpawnedThisWave++;
} else if (shouldSpawnZombieCanon) {
zombieType = 'zombie_canon';
} else if (shouldSpawnRugbyZombie) {
zombieType = 'rugby_zombie';
rugbyZombiesLeft--;
} else if (shouldSpawnZombieRa) {
zombieType = 'zombie_ra';
} else {
// Pick random zombie type from selected types (excluding special zombies)
var availableTypes = [];
if (selectedZombieTypes.indexOf('zombie') !== -1) {
availableTypes.push('zombie');
}
if (selectedZombieTypes.indexOf('cone_zombie') !== -1) {
availableTypes.push('cone_zombie');
}
if (selectedZombieTypes.indexOf('bucket_zombie') !== -1) {
availableTypes.push('bucket_zombie');
}
if (selectedZombieTypes.indexOf('sun_zombie') !== -1) {
availableTypes.push('sun_zombie');
}
if (selectedZombieTypes.indexOf('zombie_canon') !== -1) {
availableTypes.push('zombie_canon');
}
if (selectedZombieTypes.indexOf('zombie_cascanueces') !== -1) {
availableTypes.push('zombie_cascanueces');
}
if (availableTypes.length > 0) {
var randomIndex = Math.floor(Math.random() * availableTypes.length);
zombieType = availableTypes[randomIndex];
}
}
// Only spawn if we have a valid zombie type
if (zombieType) {
spawnZombie(zombieType, row);
}
enemiesInWave--;
} else {
LK.clearInterval(spawnTimer);
}
}, 800); // Faster spawn rate for endless mode
return; // Exit function early for endless mode
}
if (currentWave === 1) {
// Wave 1: 3-4 normal zombies with collision detection
var normalCount = Math.floor(Math.random() * 2) + 3;
var spawnQueue = [];
for (var i = 0; i < normalCount; i++) {
var row = Math.floor(Math.random() * gridRows);
// For personalize mode, check if zombie type is selected, otherwise use normal zombie
var zombieType = 'zombie';
if (gameMode === 'personalize' && selectedZombieTypes.indexOf('zombie') === -1) {
// If normal zombie not selected, pick a random selected type
if (selectedZombieTypes.length > 0) {
var availableEndlessTypes = selectedZombieTypes.filter(function (type) {
return type !== 'miner_zombie' && type !== 'rugby_zombie' && type !== 'zombiestein' && type !== 'zombie_sol' && type !== 'zombie_canon';
});
if (availableEndlessTypes.length > 0) {
var randomIndex = Math.floor(Math.random() * availableEndlessTypes.length);
zombieType = availableEndlessTypes[randomIndex];
} else {
zombieType = selectedZombieTypes[Math.floor(Math.random() * selectedZombieTypes.length)];
}
}
}
spawnQueue.push({
type: zombieType,
row: row,
delay: 0
});
}
// Process spawn queue with collision detection
var spawnIndex = 0;
var spawnProcessor = LK.setInterval(function () {
if (spawnIndex < spawnQueue.length) {
var spawn = spawnQueue[spawnIndex];
if (canSpawnWithoutCollision(spawn.row)) {
spawnZombie(spawn.type, spawn.row);
lastSpawnTime[spawn.row] = LK.ticks;
lastSpawnRow[spawn.row] = LK.ticks;
spawnIndex++;
} else {
// Add 1 second delay
spawn.delay += 60;
}
} else {
LK.clearInterval(spawnProcessor);
}
}, 60); // Check every second
enemiesInWave = normalCount;
} else if (currentWave === 2) {
// Wave 2: 3-4 normal zombies, 75% chance for 1 sun zombie, 1 cone zombie with collision detection
var normalCount = Math.floor(Math.random() * 2) + 3;
var sunZombieCount = gameMode === 'endless' && selectedZombieTypes.indexOf('sun_zombie') === -1 ? 0 : Math.random() < 0.75 ? 1 : 0;
var coneCount = gameMode === 'endless' && selectedZombieTypes.indexOf('cone_zombie') === -1 ? 0 : 1;
var spawnQueue = [];
// Add normal zombies to queue
for (var i = 0; i < normalCount; i++) {
var row = Math.floor(Math.random() * gridRows);
// For endless mode, check if zombie type is selected
var zombieType = 'zombie';
if (gameMode === 'endless' && selectedZombieTypes.indexOf('zombie') === -1) {
// If normal zombie not selected, pick a random selected type
if (selectedZombieTypes.length > 0) {
var randomIndex = Math.floor(Math.random() * selectedZombieTypes.length);
zombieType = selectedZombieTypes[randomIndex];
}
}
spawnQueue.push({
type: zombieType,
row: row,
delay: 0
});
}
// Add sun zombie if chance succeeds and selected in endless mode
if (sunZombieCount > 0) {
var row = Math.floor(Math.random() * gridRows);
spawnQueue.push({
type: 'sun_zombie',
row: row,
delay: 0
});
sunZombieSpawnedThisWave = true;
}
// Add cone zombie if selected in endless mode
if (coneCount > 0) {
var row = Math.floor(Math.random() * gridRows);
spawnQueue.push({
type: 'cone_zombie',
row: row,
delay: 0
});
}
// Process spawn queue with collision detection
var spawnIndex = 0;
var spawnProcessor = LK.setInterval(function () {
if (spawnIndex < spawnQueue.length) {
var spawn = spawnQueue[spawnIndex];
if (canSpawnWithoutCollision(spawn.row)) {
spawnZombie(spawn.type, spawn.row);
lastSpawnTime[spawn.row] = LK.ticks;
lastSpawnRow[spawn.row] = LK.ticks;
spawnIndex++;
} else {
// Add 1 second delay
spawn.delay += 60;
}
} else {
LK.clearInterval(spawnProcessor);
}
}, 60); // Check every second
enemiesInWave = normalCount + sunZombieCount + coneCount;
} else if (currentWave === 3) {
bucketZombiesLeft = gameMode === 'personalize' && selectedZombieTypes.indexOf('bucket_zombie') === -1 ? 0 : 1; // Only 1 bucket zombie in wave 3 if selected
enemiesInWave = Math.floor(Math.random() * 2) + 3 + Math.floor(Math.random() * 2) + 1 + bucketZombiesLeft; // normal + cone + bucket
} else if (currentWave === 4) {
bucketZombiesLeft = gameMode === 'personalize' && selectedZombieTypes.indexOf('bucket_zombie') === -1 ? 0 : Math.floor(Math.random() * 2) + 2; // 2-3 bucket zombies if selected
enemiesInWave = Math.floor(Math.random() * 2) + 3 + Math.floor(Math.random() * 2) + 1 + bucketZombiesLeft;
} else if (currentWave === 5) {
bucketZombiesLeft = gameMode === 'personalize' && selectedZombieTypes.indexOf('bucket_zombie') === -1 ? 0 : Math.floor(Math.random() * 2) + 3; // 3-4 bucket zombies if selected
enemiesInWave = Math.floor(Math.random() * 2) + 3 + Math.floor(Math.random() * 2) + 1 + bucketZombiesLeft;
} else {
// From wave 6+: spawn 50% fewer zombies but increase cone/bucket probability by 25%
bucketZombiesLeft = gameMode === 'personalize' && selectedZombieTypes.indexOf('bucket_zombie') === -1 ? 0 : Math.floor((Math.floor(Math.random() * 2) + 4 + (currentWave - 6)) * 0.5); // 50% fewer bucket zombies if selected
enemiesInWave = Math.floor((5 + currentWave * 2) * 0.5); // 50% fewer total enemies
// Add rugby zombies for waves 8-10
var rugbyZombiesLeft = 0;
if (currentWave >= 8 && currentWave <= 9 && (gameMode === 'normal' || selectedZombieTypes.indexOf('rugby_zombie') !== -1)) {
rugbyZombiesLeft = 1;
} else if (currentWave === 10 && (gameMode === 'normal' || selectedZombieTypes.indexOf('rugby_zombie') !== -1)) {
rugbyZombiesLeft = 2;
}
enemiesInWave += rugbyZombiesLeft;
// Spawn Zombiestein in wave 10 (only once per game)
if (currentWave === 10 && !zombiesteinSpawned && (gameMode === 'normal' || selectedZombieTypes.indexOf('zombiestein') !== -1)) {
var zombiesteinRow = Math.floor(Math.random() * gridRows);
spawnZombie('zombiestein', zombiesteinRow);
zombiesteinSpawned = true;
zombiesteinAlive = true;
enemiesInWave++; // Add Zombiestein to enemy count
}
}
enemiesKilled = 0;
// Only set up spawn timer for waves 3 and above (waves 1-2 spawn immediately)
if (currentWave >= 3) {
var normalZombiesLeft = Math.floor(Math.random() * 2) + 3;
var coneZombiesLeft = Math.floor(Math.random() * 2) + 1;
var spawnTimer = LK.setInterval(function () {
if (enemiesInWave > 0) {
var row = Math.floor(Math.random() * gridRows);
var zombieType;
// Determine sun zombie probability based on wave
var sunZombieProbability = 0;
if (currentWave === 3) {
sunZombieProbability = 0.50;
} else if (currentWave === 4) {
sunZombieProbability = 0.25;
}
// Check if we should spawn a sun zombie (only waves 3-4, max 1 per wave)
var shouldSpawnSunZombie = currentWave >= 3 && currentWave <= 4 && !sunZombieSpawnedThisWave && Math.random() < sunZombieProbability && (gameMode === 'normal' || selectedZombieTypes.indexOf('sun_zombie') !== -1);
// Check if we should spawn a miner zombie (waves 6+, 100% chance, max per wave limit)
var shouldSpawnMinerZombie = currentWave >= 6 && minerZombiesSpawnedThisWave < maxMinerZombiesPerWave && Math.random() < 1.0 && (gameMode === 'normal' || selectedZombieTypes.indexOf('miner_zombie') !== -1);
// Check if we should spawn a rugby zombie (waves 8-10)
var shouldSpawnRugbyZombie = currentWave >= 8 && currentWave <= 10 && rugbyZombiesLeft > 0 && Math.random() < 0.3 && (gameMode === 'normal' || selectedZombieTypes.indexOf('rugby_zombie') !== -1);
// Check if we should spawn a zombie ra (odd waves excluding 1 and 3)
var shouldSpawnZombieRa = currentWave % 2 === 1 && currentWave > 3 && Math.random() < 0.25 && (gameMode === 'normal' || selectedZombieTypes.indexOf('zombie_ra') !== -1);
// Check if we should spawn a zombie canon (wave 9+, before zombiestein wave)
var shouldSpawnZombieCanon = currentWave >= 9 && currentWave < (zombiesteinSpawned ? currentWave : 10) && Math.random() < 0.3 && (gameMode === 'normal' || selectedZombieTypes.indexOf('zombie_canon') !== -1);
// Check if we should spawn a zombie cascanueces (waves 12, 17, 22, 27, 32, etc.)
var shouldSpawnZombieCascanueces = (currentWave - 12) % 5 === 0 && currentWave >= 12 && Math.random() < 0.3 && (gameMode === 'normal' || selectedZombieTypes.indexOf('zombie_cascanueces') !== -1);
if (shouldSpawnSunZombie) {
zombieType = 'sun_zombie';
sunZombieSpawnedThisWave = true; // Mark that sun zombie has been spawned this wave
} else if (shouldSpawnMinerZombie) {
zombieType = 'miner_zombie';
minerZombiesSpawnedThisWave++; // Increment miner count for this wave
} else if (shouldSpawnRugbyZombie) {
zombieType = 'rugby_zombie';
rugbyZombiesLeft--;
} else if (shouldSpawnZombieRa) {
zombieType = 'zombie_ra';
} else if (shouldSpawnZombieCanon) {
zombieType = 'zombie_canon';
} else if (shouldSpawnZombieCascanueces) {
zombieType = 'zombie_cascanueces';
} else if (currentWave >= 3) {
// From wave 3 onwards, include bucket zombies
// For personalize mode, only spawn selected zombie types
if (gameMode === 'personalize') {
// Pick random zombie type from selected types (excluding special zombies)
var availableTypes = [];
if (selectedZombieTypes.indexOf('zombie') !== -1 && normalZombiesLeft > 0) {
availableTypes.push('zombie');
}
if (selectedZombieTypes.indexOf('cone_zombie') !== -1 && coneZombiesLeft > 0) {
availableTypes.push('cone_zombie');
}
if (selectedZombieTypes.indexOf('bucket_zombie') !== -1 && bucketZombiesLeft > 0) {
availableTypes.push('bucket_zombie');
}
if (selectedZombieTypes.indexOf('sun_zombie') !== -1) {
availableTypes.push('sun_zombie');
}
if (selectedZombieTypes.indexOf('zombie_canon') !== -1) {
availableTypes.push('zombie_canon');
}
if (selectedZombieTypes.indexOf('zombie_cascanueces') !== -1) {
availableTypes.push('zombie_cascanueces');
}
if (availableTypes.length > 0) {
var randomIndex = Math.floor(Math.random() * availableTypes.length);
zombieType = availableTypes[randomIndex];
// Decrement counters
if (zombieType === 'zombie') {
normalZombiesLeft--;
} else if (zombieType === 'cone_zombie') {
coneZombiesLeft--;
} else if (zombieType === 'bucket_zombie') {
bucketZombiesLeft--;
}
} else {
zombieType = 'zombie'; // fallback
}
} else {
// Original logic for normal mode
// For wave 6+, increase cone and bucket zombie probability by 25%
if (normalZombiesLeft > 0 && coneZombiesLeft > 0 && bucketZombiesLeft > 0) {
var rand = Math.random();
if (currentWave >= 6) {
// Wave 6+: 25% normal, 37.5% cone, 37.5% bucket (25% more cone/bucket)
if (rand < 0.25) {
zombieType = 'zombie';
normalZombiesLeft--;
} else if (rand < 0.625) {
zombieType = 'cone_zombie';
coneZombiesLeft--;
} else {
zombieType = 'bucket_zombie';
bucketZombiesLeft--;
}
} else {
// Wave 3-5: original probabilities
if (rand < 0.4) {
zombieType = 'zombie';
normalZombiesLeft--;
} else if (rand < 0.7) {
zombieType = 'cone_zombie';
coneZombiesLeft--;
} else {
zombieType = 'bucket_zombie';
bucketZombiesLeft--;
}
}
} else if (normalZombiesLeft > 0 && coneZombiesLeft > 0) {
if (currentWave >= 6) {
// Wave 6+: 35% normal, 65% cone (25% more cone)
zombieType = Math.random() < 0.35 ? 'zombie' : 'cone_zombie';
} else {
// Wave 3-5: original 60/40 split
zombieType = Math.random() < 0.6 ? 'zombie' : 'cone_zombie';
}
if (zombieType === 'zombie') {
normalZombiesLeft--;
} else {
coneZombiesLeft--;
}
} else if (normalZombiesLeft > 0 && bucketZombiesLeft > 0) {
if (currentWave >= 6) {
// Wave 6+: 35% normal, 65% bucket (25% more bucket)
zombieType = Math.random() < 0.35 ? 'zombie' : 'bucket_zombie';
} else {
// Wave 3-5: original 60/40 split
zombieType = Math.random() < 0.6 ? 'zombie' : 'bucket_zombie';
}
if (zombieType === 'zombie') {
normalZombiesLeft--;
} else {
bucketZombiesLeft--;
}
} else if (coneZombiesLeft > 0 && bucketZombiesLeft > 0) {
zombieType = Math.random() < 0.5 ? 'cone_zombie' : 'bucket_zombie';
if (zombieType === 'cone_zombie') {
coneZombiesLeft--;
} else {
bucketZombiesLeft--;
}
} else if (normalZombiesLeft > 0) {
zombieType = 'zombie';
normalZombiesLeft--;
} else if (coneZombiesLeft > 0) {
zombieType = 'cone_zombie';
coneZombiesLeft--;
} else if (bucketZombiesLeft > 0) {
zombieType = 'bucket_zombie';
bucketZombiesLeft--;
} else {
zombieType = 'zombie'; // fallback
}
}
} else {
// Fallback for other waves
if (gameMode === 'personalize') {
// Pick from selected types
var availableTypes = [];
if (selectedZombieTypes.indexOf('zombie') !== -1) {
availableTypes.push('zombie');
}
if (selectedZombieTypes.indexOf('cone_zombie') !== -1) {
availableTypes.push('cone_zombie');
}
if (availableTypes.length > 0) {
var randomIndex = Math.floor(Math.random() * availableTypes.length);
zombieType = availableTypes[randomIndex];
} else {
zombieType = 'zombie'; // fallback
}
} else {
zombieType = Math.random() < 0.5 ? 'zombie' : 'cone_zombie';
}
}
spawnZombie(zombieType, row);
enemiesInWave--;
} else {
LK.clearInterval(spawnTimer);
}
}, 1200);
} // Close the if (currentWave >= 3) block
}
// Mouse move handler to update ghost plant position
game.move = function (x, y, obj) {
if (ghostPlant && selectedPlantType) {
var gridPos = getGridPosition(x, y);
if (gridPos) {
// Snap to grid position
ghostPlant.x = gridStartX + gridPos.x * cellSize;
ghostPlant.y = gridStartY + gridPos.y * cellSize;
// Change color based on validity
if (canPlacePlant(gridPos.x, gridPos.y, selectedPlantType)) {
ghostPlant.tint = 0x88FF88; // Green tint for valid placement
} else {
ghostPlant.tint = 0xFF8888; // Red tint for invalid placement
}
} else {
// Follow cursor if outside grid
ghostPlant.x = x;
ghostPlant.y = y;
ghostPlant.tint = 0xFF8888; // Red tint when outside grid
}
}
};
// Game input handling
game.down = function (x, y, obj) {
// Check if clicking on a sun
for (var i = 0; i < suns.length; i++) {
if (suns[i].intersects({
x: x,
y: y,
width: 1,
height: 1
})) {
return; // Let sun handle its own click
}
}
// Check if using shovel to remove plant
if (selectedTool === 'shovel') {
var gridPos = getGridPosition(x, y);
if (gridPos && plants[gridPos.y][gridPos.x] !== null) {
// Remove the plant
var plant = plants[gridPos.y][gridPos.x];
plant.destroy();
plants[gridPos.y][gridPos.x] = null;
LK.effects.flashObject(gridCells[gridPos.y][gridPos.x], 0xFFFF00, 300);
updatePlantSelection();
return;
} else if (gridPos) {
// Flash red if trying to remove from empty square
LK.effects.flashObject(gridCells[gridPos.y][gridPos.x], 0xFF0000, 300);
} else {
// Clicking outside grid cancels shovel selection
selectedTool = null;
shovelButton.alpha = 0.8;
shovelButton.scaleX = shovelButton.scaleY = 1.5;
updatePlantSelection();
}
}
// Check if placing a plant
else if (selectedPlantType) {
var gridPos = getGridPosition(x, y);
if (gridPos) {
if (canPlacePlant(gridPos.x, gridPos.y, selectedPlantType)) {
// Successfully place the plant
if (placePlant(gridPos.x, gridPos.y, selectedPlantType)) {
selectedPlantType = null;
updatePlantSelection();
return;
}
} else if (plants[gridPos.y][gridPos.x] !== null) {
// Flash red if trying to place on occupied square
LK.effects.flashObject(gridCells[gridPos.y][gridPos.x], 0xFF0000, 300);
} else if (sunPoints < plantCosts[selectedPlantType]) {
// Flash yellow if not enough suns
LK.effects.flashObject(gridCells[gridPos.y][gridPos.x], 0xFFFF00, 300);
}
} else {
// Clicking outside grid cancels selection
selectedPlantType = null;
updatePlantSelection();
}
}
};
// Initialize UI
updatePlantSelection();
updateSunDisplay();
updateLivesDisplay();
updateWaveDisplay();
// Skip wave button for endless mode
var skipWaveButton = null;
function createSkipWaveButton() {
if (gameMode === 'endless' && !skipWaveButton) {
skipWaveButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 1800,
y: 100,
scaleX: 2,
scaleY: 1,
color: 0x888888 // Start gray
});
skipWaveButton.interactive = true;
skipWaveButton.buttonMode = true;
skipWaveButton.alpha = 0.5; // Start disabled
skipWaveButton.down = function (x, y, obj) {
if (waveInProgress) {
// Force end current wave and start next one
waveInProgress = false;
wavesCompleted++;
currentWave++;
updateWaveDisplay();
updateSkipWaveButton();
// Start next wave immediately
LK.setTimeout(function () {
startWave();
}, 500);
}
};
game.addChild(skipWaveButton);
var skipWaveText = new Text2('Skip Wave', {
size: 40,
fill: 0xFFFFFF
});
skipWaveText.anchor.set(0.5, 0.5);
skipWaveText.x = 1800;
skipWaveText.y = 100;
game.addChild(skipWaveText);
skipWaveButton.textElement = skipWaveText;
}
}
function canSkipWave() {
// Check if all zombies for current wave have been spawned
return waveInProgress && enemiesInWave <= 0;
}
function updateSkipWaveButton() {
if (skipWaveButton && gameMode === 'endless') {
if (waveInProgress) {
// Enable button - wave is in progress
skipWaveButton.alpha = 1.0;
skipWaveButton.tint = 0x4CAF50; // Green
skipWaveButton.interactive = true;
} else {
// Disable button - no wave in progress
skipWaveButton.alpha = 0.5;
skipWaveButton.tint = 0x888888; // Gray
skipWaveButton.interactive = false;
}
}
}
// Game will start after mode selection
// Main game loop
game.update = function () {
// Enable lag optimization from wave 8 onwards
if (currentWave >= 8 && !lagOptimizationActive) {
lagOptimizationActive = true;
console.log("Lag optimization activated for wave", currentWave);
}
// Performance cleanup every 3 seconds when lag optimization is active
if (lagOptimizationActive) {
cleanupCounter++;
if (cleanupCounter >= 180) {
// Every 3 seconds at 60fps
performanceCleanup();
cleanupCounter = 0;
}
}
// Update wallnut cooldown
if (wallnutCooldown > 0) {
wallnutCooldown--;
if (wallnutCooldown === 0) {
updatePlantSelection(); // Refresh button appearance when cooldown ends
}
}
// Update nuez primitiva cooldown
if (nuezPrimitivaCooldown > 0) {
nuezPrimitivaCooldown--;
if (nuezPrimitivaCooldown === 0) {
updatePlantSelection(); // Refresh button appearance when cooldown ends
}
}
// Update petacereza cooldown
if (petacerezaCooldown > 0) {
petacerezaCooldown--;
if (petacerezaCooldown === 0) {
updatePlantSelection(); // Refresh button appearance when cooldown ends
}
}
// Falling sun system - every 10 seconds (optimized for high waves)
sunTimer++;
var sunInterval = lagOptimizationActive ? 1080 : 720; // 18 seconds when optimized, 12 seconds normally
if (sunTimer >= sunInterval) {
// Only spawn sun if we don't have too many already
if (suns.length < (lagOptimizationActive ? 8 : 15)) {
var fallingSun = new Sun(Math.random() * (gridStartX + gridCols * cellSize - gridStartX) + gridStartX, -50);
suns.push(fallingSun);
game.addChild(fallingSun);
// Animate falling sun
tween(fallingSun, {
y: Math.random() * 200 + 300
}, {
duration: 2400,
easing: tween.easeOut
});
}
sunTimer = 0;
}
// Check if current wave should end (22 seconds timer-based)
if (waveInProgress && LK.ticks - waveStartTick >= 1584) {
// 26.4 seconds at 60fps
waveInProgress = false;
wavesCompleted++;
if (gameMode === 'normal' && wavesCompleted >= totalWaves) {
// Check if Zombiestein was spawned and is still alive
if (zombiesteinSpawned && zombiesteinAlive) {
// Don't end game until Zombiestein is defeated
console.log("All waves completed but Zombiestein still alive!");
return;
}
LK.effects.flashScreen(0x4CAF50, 1000);
LK.showYouWin();
return;
} else if (gameMode === 'endless') {
// For endless mode, just continue to next wave
// Reset Zombiestein spawn flag for next potential spawn
if (currentWave % 5 === 0) {
zombiesteinSpawned = false;
zombiesteinAlive = false;
}
}
currentWave++;
updateWaveDisplay();
// Start next wave after delay
LK.setTimeout(function () {
startWave();
}, 3000);
}
// Update skip wave button state
if (gameMode === 'endless') {
updateSkipWaveButton();
}
// Auto-start next wave if no wave in progress and we haven't completed all waves (and mode is selected)
if (gameModeSelected && !waveInProgress && (gameMode === 'endless' || wavesCompleted < totalWaves) && !nextWaveScheduled) {
nextWaveScheduled = true;
LK.setTimeout(function () {
nextWaveScheduled = false;
startWave();
}, 1000);
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var CanonShell = Container.expand(function () {
var self = Container.call(this);
self.speed = 3.2; // 4x normal zombie speed (0.8 * 4)
self.damage = 20;
self.hasExploded = false;
self.hasReachedColumn6 = false;
self.gridY = 0;
var graphics = self.attachAsset('canonShell', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Move left towards plants
self.x -= self.speed;
// Check if reached column 3
var cellX = Math.floor((self.x - gridStartX) / cellSize);
if (cellX <= 2 && !self.hasReachedColumn6) {
self.hasReachedColumn6 = true;
self.explode();
return;
}
// Check collision with plants
var cellX = Math.floor((self.x - gridStartX) / cellSize);
if (cellX >= 0 && cellX < gridCols && plants[self.gridY] && plants[self.gridY][cellX]) {
var plant = plants[self.gridY][cellX];
if (self.intersects(plant)) {
plant.takeDamage(self.damage);
self.explode();
return;
}
}
// Remove if goes off screen
if (self.x < -100) {
self.removeFromGame();
}
};
self.explode = function () {
if (self.hasExploded) {
return;
}
self.hasExploded = true;
// Create explosion effect
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 1.5,
scaleY: 1.5
});
game.addChild(explosion);
// Animate explosion and remove it
tween(explosion, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
// Transform shell into normal zombidito at current position
var zombidito = new Zombidito();
zombidito.x = self.x;
zombidito.y = self.y;
zombidito.gridY = self.gridY;
enemies.push(zombidito);
game.addChild(zombidito);
// Remove shell
self.removeFromGame();
};
self.removeFromGame = function () {
for (var i = 0; i < canonShells.length; i++) {
if (canonShells[i] === self) {
canonShells.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var Lawnmower = Container.expand(function (row) {
var self = Container.call(this);
self.gridY = row;
self.speed = 0;
self.isActivated = false;
self.x = gridStartX - 100;
self.y = gridStartY + row * cellSize;
var graphics = self.attachAsset('lawnmower', {
anchorX: 0.5,
anchorY: 0.5
});
self.activate = function () {
if (!self.isActivated) {
self.isActivated = true;
self.speed = 6.4;
}
};
self.update = function () {
if (self.isActivated) {
self.x += self.speed;
// Check collision with zombies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && self.intersects(enemy)) {
enemy.die();
// Count as player kill for score tracking
enemiesKilled++;
}
}
// Remove lawnmower when it reaches right edge
if (self.x > 2148) {
self.removeFromGame();
}
}
};
self.removeFromGame = function () {
for (var i = 0; i < lawnmowers.length; i++) {
if (lawnmowers[i] === self) {
lawnmowers.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var PlantBase = Container.expand(function (plantType) {
var self = Container.call(this);
self.plantType = plantType;
self.health = 100;
self.maxHealth = 100;
self.cost = 50;
self.shootTimer = 0;
self.shootDelay = 72;
self.gridX = 0;
self.gridY = 0;
self.level = 1;
var graphics = self.attachAsset(plantType, {
anchorX: 0.5,
anchorY: 0.5
});
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFF0000, 200);
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
// Clean up projectile tracking for this plant
var plantId = self.gridX + '_' + self.gridY;
if (plantProjectileCount[plantId]) {
delete plantProjectileCount[plantId];
}
plants[self.gridY][self.gridX] = null;
self.destroy();
};
return self;
});
var Wallnut = PlantBase.expand(function () {
var self = PlantBase.call(this, 'wallnut');
self.cost = 50;
self.health = 300;
self.maxHealth = 300;
self.isDamaged = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if wallnut should switch to damaged appearance
if (self.health < 150 && !self.isDamaged) {
self.isDamaged = true;
// Remove current graphics and add damaged version
self.removeChild(self.children[0]);
var damagedGraphics = self.attachAsset('wallnut_damaged', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
var Sunflower = PlantBase.expand(function () {
var self = PlantBase.call(this, 'sunflower');
self.cost = 50;
self.sunTimer = 0;
self.update = function () {
self.sunTimer++;
if (self.sunTimer >= 1080) {
// 18 seconds
var sun = new Sun(self.x, self.y - 60);
suns.push(sun);
game.addChild(sun);
self.sunTimer = 0;
LK.effects.flashObject(self, 0xFFFF00, 300);
}
};
return self;
});
var SnowPea = PlantBase.expand(function () {
var self = PlantBase.call(this, 'snowpea');
self.cost = 175;
self.health = 120;
self.maxHealth = 120;
self.update = function () {
self.shootTimer++;
if (self.shootTimer >= self.shootDelay) {
var target = self.findTarget();
if (target) {
self.shoot(target);
self.shootTimer = 0;
}
}
};
self.findTarget = function () {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && enemy.x > self.x) {
return enemy;
}
}
return null;
};
self.shoot = function (target) {
var plantId = self.gridX + '_' + self.gridY; // Unique plant identifier
var pea = createProjectile('snowshot', self.x, self.y, 5.12, 25 * self.level, plantId); // 6.4 * 0.8 = 5.12 (20% slower)
if (pea) {
LK.getSound('shoot').play();
}
};
return self;
});
var Repetidora = PlantBase.expand(function () {
var self = PlantBase.call(this, 'repetidora');
self.cost = 200;
self.isTrackingFirstPea = false;
self.firstPea = null;
self.firstPeaStartX = 0;
self.update = function () {
self.shootTimer++;
if (self.shootTimer >= self.shootDelay) {
var target = self.findTarget();
if (target && !self.isTrackingFirstPea) {
var firstPea = self.shoot(target);
if (firstPea) {
self.firstPea = firstPea;
self.firstPeaStartX = firstPea.x;
self.isTrackingFirstPea = true;
}
self.shootTimer = 0;
}
}
// Handle second pea timing based on first pea position
if (self.isTrackingFirstPea && self.firstPea) {
// Check if first pea has advanced half a cell (72 pixels)
if (self.firstPea.x >= self.firstPeaStartX + 72) {
var target = self.findTarget();
if (target) {
self.shoot(target);
}
// Stop tracking to prevent lag
self.isTrackingFirstPea = false;
self.firstPea = null;
}
// Also stop tracking if first pea no longer exists
else if (!self.firstPea.parent) {
self.isTrackingFirstPea = false;
self.firstPea = null;
}
}
};
self.findTarget = function () {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && enemy.x > self.x) {
return enemy;
}
}
return null;
};
self.shoot = function (target) {
var plantId = self.gridX + '_' + self.gridY; // Unique plant identifier
var pea = createProjectile('pea', self.x, self.y, 8.64, 20 * self.level, plantId);
if (pea) {
LK.getSound('shoot').play();
}
return pea; // Return the pea for tracking
};
return self;
});
var Rapaz = PlantBase.expand(function () {
var self = PlantBase.call(this, 'rapaz');
self.cost = 325;
self.health = 425;
self.maxHealth = 425;
self.damageTimer = 0; // Timer for dealing damage to high-HP zombies
self.cooldownTimer = 0; // Timer for cooldown after killing zombie <400 HP
self.isOnCooldown = false; // Track if rapaz is on cooldown
self.targetZombie = null; // Track zombie being consumed
var graphics = self.children[0]; // Reference to graphics for color changes
self.update = function () {
// Handle cooldown timer
if (self.isOnCooldown && self.cooldownTimer > 0) {
self.cooldownTimer--;
if (self.cooldownTimer === 0) {
// End cooldown - restore normal color
self.isOnCooldown = false;
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
return; // Don't attack while on cooldown
}
self.damageTimer++;
// Check every 60 ticks (1 second) for zombies to attack
if (self.damageTimer >= 60) {
// Skip if rapaz is already consuming a zombie
if (self.targetZombie && self.targetZombie.parent) {
return; // Don't attack while already consuming
}
var targetZombies = self.findTargetZombies();
for (var i = 0; i < targetZombies.length; i++) {
var zombie = targetZombies[i];
// Skip zombies already being consumed
if (zombie.beingConsumed) {
continue;
}
if (zombie.health < 400) {
// Mark zombie as being consumed and make it move toward rapaz
zombie.beingConsumed = true;
zombie.consumingPlant = self;
self.targetZombie = zombie;
zombie.speed = 0.8 * 4; // 4x normal zombie speed
zombie.damage = 0; // Stop attacking while being consumed
// Start continuous rotation while moving
zombie.rotationSpeed = 0.15; // Rotation speed per frame
break; // Only consume one zombie per cycle
} else {
// Deal 25 damage to zombie with ≥400 HP
zombie.takeDamage(25);
// Add flashing effect to zombie when taking damage from rapaz
tween(zombie, {
tint: 0xFF0000
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(zombie, {
tint: 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
}
});
// Check if zombie now has <400 HP and should be consumed
if (zombie.health < 400 && zombie.parent) {
// Mark zombie as being consumed
zombie.beingConsumed = true;
zombie.consumingPlant = self;
self.targetZombie = zombie;
zombie.speed = 0.8 * 4; // 4x normal zombie speed
zombie.damage = 0; // Stop attacking while being consumed
// Start continuous rotation while moving
zombie.rotationSpeed = 0.15; // Rotation speed per frame
break; // Only consume one zombie per cycle
}
}
}
self.damageTimer = 0;
}
// Check if target zombie has collided with rapaz
if (self.targetZombie && self.targetZombie.parent) {
// Check for collision between rapaz and consuming zombie
if (self.intersects(self.targetZombie)) {
// Zombie reached rapaz - consume it immediately
// First remove zombie from enemies array to prevent other systems from accessing it
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] === self.targetZombie) {
enemies.splice(i, 1);
break;
}
}
// Destroy the zombie
self.targetZombie.destroy();
enemiesKilled++;
self.targetZombie = null;
// Start 10 second cooldown and gray out the plant
self.isOnCooldown = true;
self.cooldownTimer = 600; // 10 seconds at 60fps
tween(graphics, {
tint: 0x888888 // Gray tint
}, {
duration: 200,
easing: tween.easeOut
});
}
} else if (self.targetZombie && !self.targetZombie.parent) {
// Target zombie was destroyed, clear reference
self.targetZombie = null;
}
};
self.findTargetZombies = function () {
var targets = [];
// Check current cell and seven cells in front (8 total cells)
for (var checkX = self.gridX; checkX <= self.gridX + 7 && checkX < gridCols; checkX++) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var enemyGridX = Math.floor((enemy.x - gridStartX + cellSize / 2) / cellSize);
// Check if zombie is in the target cell
if (enemy.gridY === self.gridY && enemyGridX === checkX) {
targets.push(enemy);
}
}
}
return targets;
};
// Override die to clean up consumed zombie
var originalDie = self.die;
self.die = function () {
// If consuming a zombie, release it from consumption
if (self.targetZombie && self.targetZombie.parent) {
self.targetZombie.beingConsumed = false;
self.targetZombie.consumingPlant = null;
self.targetZombie.speed = self.targetZombie.originalSpeed;
self.targetZombie.damage = 25; // Restore normal damage
self.targetZombie.rotationSpeed = 0; // Stop continuous rotation
if (self.targetZombie.children[0]) {
self.targetZombie.children[0].rotation = 0; // Reset rotation
}
}
originalDie();
};
return self;
});
var Plantorcha = PlantBase.expand(function () {
var self = PlantBase.call(this, 'plantorcha');
self.cost = 175;
self.health = 300;
self.maxHealth = 300;
// Plantorcha doesn't shoot - it only converts projectiles
self.update = function () {
// No shooting behavior - just exists to convert projectiles
};
return self;
});
var Petacereza = PlantBase.expand(function () {
var self = PlantBase.call(this, 'petacereza');
self.cost = 150;
self.health = 1; // Very low health since it explodes
self.maxHealth = 1;
self.isArmed = false; // Zombies ignore it until armed
self.armTimer = 90; // 1.5 seconds at 60fps
self.update = function () {
if (self.armTimer > 0) {
self.armTimer--;
if (self.armTimer === 0) {
// Arm the petacereza and explode
self.isArmed = true;
self.explode();
}
}
};
self.explode = function () {
// Create explosion effect
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 3,
// Larger explosion for 3x3 area
scaleY: 3
});
game.addChild(explosion);
// Animate explosion and remove it
tween(explosion, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
// Damage all zombies in 3x3 grid area around petacereza
var centerGridX = self.gridX;
var centerGridY = self.gridY;
var damagedEnemies = []; // Track enemies to damage
// First pass: identify all enemies in range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var enemyGridX = Math.floor((enemy.x - gridStartX + cellSize / 2) / cellSize);
var enemyGridY = enemy.gridY;
// Check if enemy is within 3x3 area (center ± 1 in both directions)
var deltaX = Math.abs(enemyGridX - centerGridX);
var deltaY = Math.abs(enemyGridY - centerGridY);
if (deltaX <= 1 && deltaY <= 1) {
damagedEnemies.push(enemy);
}
}
// Second pass: damage all identified enemies
for (var j = 0; j < damagedEnemies.length; j++) {
damagedEnemies[j].takeDamage(325);
}
// Remove petacereza after explosion
self.die();
};
// Override getPlantInFront for zombies to ignore unarmed petacereza
self.shouldBeIgnoredByZombies = function () {
return !self.isArmed;
};
return self;
});
var Peashooter = PlantBase.expand(function () {
var self = PlantBase.call(this, 'peashooter');
self.cost = 100;
self.update = function () {
self.shootTimer++;
if (self.shootTimer >= self.shootDelay) {
var target = self.findTarget();
if (target) {
self.shoot(target);
self.shootTimer = 0;
}
}
};
self.findTarget = function () {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && enemy.x > self.x) {
return enemy;
}
}
return null;
};
self.shoot = function (target) {
var plantId = self.gridX + '_' + self.gridY; // Unique plant identifier
var pea = createProjectile('pea', self.x, self.y, 6.4, 20 * self.level, plantId);
if (pea) {
LK.getSound('shoot').play();
}
};
return self;
});
var NuezPrimitiva = PlantBase.expand(function () {
var self = PlantBase.call(this, 'nuezPrimitiva');
self.cost = 125;
self.health = 625;
self.maxHealth = 625;
self.hasChangedToWallnut = false;
self.hasChangedToDamaged = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if should switch to normal wallnut appearance (< 300 hp)
if (self.health < 300 && !self.hasChangedToWallnut) {
self.hasChangedToWallnut = true;
// Remove current graphics and add wallnut version
self.removeChild(self.children[0]);
var wallnutGraphics = self.attachAsset('wallnut', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Check if should switch to damaged wallnut appearance (< 150 hp)
else if (self.health < 150 && !self.hasChangedToDamaged) {
self.hasChangedToDamaged = true;
// Remove current graphics and add damaged version
self.removeChild(self.children[0]);
var damagedGraphics = self.attachAsset('wallnut_damaged', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
var Fireshot = PlantBase.expand(function () {
var self = PlantBase.call(this, 'fireshot');
self.cost = 100;
self.health = 150;
self.maxHealth = 150;
self.update = function () {
self.shootTimer++;
if (self.shootTimer >= self.shootDelay) {
var target = self.findTarget();
if (target) {
self.shoot(target);
self.shootTimer = 0;
}
}
};
self.findTarget = function () {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && enemy.x > self.x) {
return enemy;
}
}
return null;
};
self.shoot = function (target) {
var plantId = self.gridX + '_' + self.gridY; // Unique plant identifier
// Calculate damage based on target type
var damage = 35 * self.level;
if (target.zombieType === 'cone_zombie' || target.zombieType === 'bucket_zombie') {
damage = 50 * self.level;
}
var pea = createProjectile('shotfire', self.x, self.y, 6.4, damage, plantId);
if (pea) {
LK.getSound('shoot').play();
}
};
return self;
});
var Carnivora = PlantBase.expand(function () {
var self = PlantBase.call(this, 'carnivora');
self.cost = 25;
self.health = 300;
self.maxHealth = 300;
self.damageTimer = 0; // Timer for dealing damage to high-HP zombies
self.cooldownTimer = 0; // Timer for cooldown after killing zombie <400 HP
self.isOnCooldown = false; // Track if carnivora is on cooldown
var graphics = self.children[0]; // Reference to graphics for color changes
self.update = function () {
// Handle cooldown timer
if (self.isOnCooldown && self.cooldownTimer > 0) {
self.cooldownTimer--;
if (self.cooldownTimer === 0) {
// End cooldown - restore normal color
self.isOnCooldown = false;
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
return; // Don't attack while on cooldown
}
self.damageTimer++;
// Check every 60 ticks (1 second) for zombies to attack
if (self.damageTimer >= 60) {
var targetZombies = self.findTargetZombies();
for (var i = 0; i < targetZombies.length; i++) {
var zombie = targetZombies[i];
if (zombie.health < 400) {
// Instantly kill zombie with <400 HP
zombie.die();
enemiesKilled++;
// Start 15 second cooldown and gray out the plant
self.isOnCooldown = true;
self.cooldownTimer = 900; // 15 seconds at 60fps
tween(graphics, {
tint: 0x888888 // Gray tint
}, {
duration: 200,
easing: tween.easeOut
});
break; // Only kill one zombie per cycle
} else {
// Deal 25 damage to zombie with ≥400 HP
zombie.takeDamage(25);
// Add flashing effect to zombie when taking damage from carnivora
tween(zombie, {
tint: 0xFF0000
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(zombie, {
tint: 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
}
});
// Check if zombie now has <400 HP and kill it
if (zombie.health < 400 && zombie.parent) {
zombie.die();
enemiesKilled++;
// Start 15 second cooldown and gray out the plant
self.isOnCooldown = true;
self.cooldownTimer = 900; // 15 seconds at 60fps
tween(graphics, {
tint: 0x888888 // Gray tint
}, {
duration: 200,
easing: tween.easeOut
});
break; // Only kill one zombie per cycle
}
}
}
self.damageTimer = 0;
}
};
self.findTargetZombies = function () {
var targets = [];
// Check current cell and cell in front
for (var checkX = self.gridX; checkX <= self.gridX + 1 && checkX < gridCols; checkX++) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var enemyGridX = Math.floor((enemy.x - gridStartX + cellSize / 2) / cellSize);
// Check if zombie is in the target cell
if (enemy.gridY === self.gridY && enemyGridX === checkX) {
targets.push(enemy);
}
}
}
return targets;
};
return self;
});
var Birasol = PlantBase.expand(function () {
var self = PlantBase.call(this, 'birasol');
self.cost = 125;
self.health = 125;
self.maxHealth = 125;
self.sunTimer = 0;
self.update = function () {
self.sunTimer++;
if (self.sunTimer >= 900) {
// 15 seconds (900 ticks at 60fps)
var sun1 = new Sun(self.x - 30, self.y - 60);
var sun2 = new Sun(self.x + 30, self.y - 60);
suns.push(sun1);
suns.push(sun2);
game.addChild(sun1);
game.addChild(sun2);
self.sunTimer = 0;
LK.effects.flashObject(self, 0xFFFF00, 300);
}
};
return self;
});
var Projectile = Container.expand(function (type, startX, startY, speed, damage) {
var self = Container.call(this);
self.speed = speed;
self.damage = damage;
self.x = startX;
self.y = startY;
self.type = type;
self.gridY = Math.floor((startY - gridStartY + cellSize / 2) / cellSize);
self.ownerId = null; // Plant that owns this projectile
var graphics = self.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
self.x += self.speed;
if (self.x > 2048) {
self.removeFromGame();
return;
}
// Check collision with plantorcha to convert pea to fire projectile
if (self.type === 'pea') {
var projectileGridX = Math.floor((self.x - gridStartX + cellSize / 2) / cellSize);
var projectileGridY = self.gridY;
if (projectileGridX >= 0 && projectileGridX < gridCols && projectileGridY >= 0 && projectileGridY < gridRows) {
var plantInCell = plants[projectileGridY][projectileGridX];
if (plantInCell && plantInCell.plantType === 'plantorcha' && self.intersects(plantInCell)) {
// Convert pea to shotfire and add 10 damage
self.removeChild(self.children[0]);
var fireGraphics = self.attachAsset('shotfire', {
anchorX: 0.5,
anchorY: 0.5
});
self.type = 'shotfire';
self.damage += 10;
// Continue with normal movement
}
}
}
// Only check collisions with zombies in same row and within 100px range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && Math.abs(enemy.x - self.x) <= 100 && self.intersects(enemy)) {
// Skip damage if zombie is being consumed by rapaz unless projectile is from rapaz
if (!enemy.beingConsumed || enemy.beingConsumed && enemy.consumingPlant && self.ownerId && self.ownerId.includes(enemy.consumingPlant.gridX + '_' + enemy.consumingPlant.gridY)) {
enemy.takeDamage(self.damage);
// Apply ice effect if this is a snowshot
if (self.type === 'snowshot') {
enemy.applyIceEffect();
}
self.removeFromGame();
break;
}
}
}
};
self.removeFromGame = function () {
// Decrease projectile count for the plant that owns this projectile
if (self.ownerId && plantProjectileCount[self.ownerId]) {
plantProjectileCount[self.ownerId]--;
}
for (var i = 0; i < projectiles.length; i++) {
if (projectiles[i] === self) {
projectiles.splice(i, 1);
break;
}
}
// Return to pool instead of destroying
self.returnToPool();
};
self.returnToPool = function () {
if (projectilePool.length < maxPoolSize) {
self.removeChildren();
if (self.parent) {
self.parent.removeChild(self);
}
projectilePool.push(self);
} else {
self.destroy();
}
};
return self;
});
var SlowProjectile = Projectile.expand(function (type, startX, startY, speed, damage) {
var self = Projectile.call(this, type, startX, startY, speed, damage);
var originalUpdate = self.update;
self.update = function () {
originalUpdate();
// Only check collisions with zombies in same row and within 100px range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.gridY === self.gridY && Math.abs(enemy.x - self.x) <= 100 && self.intersects(enemy)) {
enemy.applySlowEffect();
break;
}
}
};
return self;
});
var Sun = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.value = 25;
self.collectTimer = 0;
self.isBeingAttracted = false;
self.attractedBy = null;
var graphics = self.attachAsset('sun', {
anchorX: 0.5,
anchorY: 0.5
});
self.down = function (x, y, obj) {
// If being attracted by zombie sol, stop the attraction
if (self.isBeingAttracted && self.attractedBy) {
tween.stop(self);
self.attractedBy.targetSun = null;
self.attractedBy.isAttractingSun = false;
self.attractedBy.speed = self.attractedBy.originalSpeed;
self.isBeingAttracted = false;
self.attractedBy = null;
}
sunPoints += self.value;
updateSunDisplay();
self.collect();
};
self.collect = function () {
// Clean up attraction if being collected
if (self.isBeingAttracted && self.attractedBy) {
tween.stop(self);
self.attractedBy.targetSun = null;
self.attractedBy.isAttractingSun = false;
self.attractedBy.speed = self.attractedBy.originalSpeed;
}
for (var i = 0; i < suns.length; i++) {
if (suns[i] === self) {
suns.splice(i, 1);
break;
}
}
self.destroy();
};
self.update = function () {
self.collectTimer++;
if (self.collectTimer > 720) {
// 12 seconds timeout
self.collect();
}
};
return self;
});
var ZombieBase = Container.expand(function (zombieType) {
var self = Container.call(this);
self.zombieType = zombieType;
self.health = 190;
self.maxHealth = 190;
self.speed = 0.8;
self.damage = 25;
self.gridY = 0;
self.attackTimer = 0;
self.slowTimer = 0;
self.iceTimer = 0;
self.isIced = false;
self.originalSpeed = self.speed;
self.beingConsumed = false; // Track if zombie is being consumed by rapaz
self.consumingPlant = null; // Reference to the plant consuming this zombie
var graphics = self.attachAsset(zombieType, {
anchorX: 0.5,
anchorY: 0.5
});
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFF0000, 200);
LK.getSound('zombie_hit').play();
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
// Check if this is Zombiestein being killed
if (self.zombieType === 'zombiestein') {
zombiesteinAlive = false;
}
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
};
self.applySlowEffect = function () {
self.slowTimer = 180;
self.speed = self.originalSpeed * 0.5;
graphics.tint = 0x81C784;
};
self.applyIceEffect = function () {
// Set ice effect timer for 3 seconds if not already active or extend it
self.iceTimer = 180; // 3 seconds at 60fps
self.speed = self.originalSpeed * 0.8; // 20% slower
// Only start the visual tween if not already iced
if (!self.isIced) {
self.isIced = true;
// Calculate 35% blue tint (mix current color with blue)
var blueColor = 0x5555FF; // Blue color
// Apply blue tint
tween(graphics, {
tint: blueColor
}, {
duration: 100,
easing: tween.easeOut
});
}
};
self.update = function () {
// Handle being consumed by rapaz - move toward the consuming plant
if (self.beingConsumed && self.consumingPlant) {
// Apply continuous rotation while moving
if (self.rotationSpeed && self.children[0]) {
self.children[0].rotation += self.rotationSpeed;
}
// Move toward the rapaz plant position
var targetX = self.consumingPlant.x;
if (self.x > targetX) {
self.x -= self.speed; // Move left toward rapaz
} else {
// Reached rapaz position, let rapaz handle consumption
}
return; // Skip normal update logic when being consumed
}
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer === 0) {
self.speed = self.originalSpeed;
graphics.tint = 0xFFFFFF;
}
}
// Handle ice effect timer
if (self.iceTimer > 0) {
self.iceTimer--;
if (self.iceTimer === 0) {
// Ice effect ends
self.isIced = false;
self.speed = self.originalSpeed;
// Return to normal color
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
}
var plantInFront = self.getPlantInFront();
if (plantInFront) {
self.attackTimer++;
if (self.attackTimer >= 72) {
plantInFront.takeDamage(self.damage);
self.attackTimer = 0;
}
} else {
self.x -= self.speed;
// Check if zombie reached left screen edge
if (self.x < 0) {
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
return;
}
// Check collision with lawnmower
for (var i = 0; i < lawnmowers.length; i++) {
var lawnmower = lawnmowers[i];
if (lawnmower.gridY === self.gridY && self.x <= lawnmower.x + 60 && !lawnmower.isActivated) {
lawnmower.activate();
}
}
}
};
self.getPlantInFront = function () {
var cellX = Math.floor((self.x - gridStartX) / cellSize);
if (cellX >= 0 && cellX < gridCols && plants[self.gridY] && plants[self.gridY][cellX]) {
var plant = plants[self.gridY][cellX];
// Check if this is an unarmed petacereza that should be ignored
if (plant.shouldBeIgnoredByZombies && plant.shouldBeIgnoredByZombies()) {
return null; // Ignore unarmed petacereza
}
return plant;
}
return null;
};
return self;
});
var Zombiestein = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombiestein');
self.health = 2250;
self.maxHealth = 2250;
self.speed = 0.414; // 10% slower than 0.46
self.originalSpeed = 0.414;
self.damage = 500; // 500 damage per second
self.isSummoning = false;
self.hasSpawnedZombidito = false;
self.originalUpdate = self.update;
self.update = function () {
// Check if health is below 1500 and hasn't spawned Zombidito yet
if (self.health < 1500 && !self.hasSpawnedZombidito && !self.isSummoning) {
self.isSummoning = true;
self.speed = 0; // Stop moving completely
// Wait 1.5 seconds then spawn Zombidito
LK.setTimeout(function () {
// Resume movement
self.speed = self.originalSpeed;
self.isSummoning = false;
self.hasSpawnedZombidito = true;
// Calculate position 3 cells ahead (in front of Zombiestein)
var spawnX = self.x - 3 * cellSize;
var spawnY = self.y;
// Create Zombidito with explosion effect
var zombidito = new Zombidito();
zombidito.x = spawnX;
zombidito.y = spawnY;
zombidito.gridY = self.gridY;
enemies.push(zombidito);
game.addChild(zombidito);
// Create explosion effect like miner zombie
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: spawnX,
y: spawnY,
scaleX: 2,
scaleY: 2
});
game.addChild(explosion);
// Animate explosion and remove it
tween(explosion, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
}, 1500);
}
// Call original update only if not summoning (stopped)
if (!self.isSummoning) {
self.originalUpdate();
}
};
return self;
});
var ZombieSarcofago = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombie_sarcofago');
self.health = 500;
self.maxHealth = 500;
self.speed = 0.6; // Slightly slower than normal zombie
self.originalSpeed = 0.6;
// Override die function to spawn normal zombie at death location
var originalDie = self.die;
self.die = function () {
// Create spawned zombie at current position with 2x speed and 225 HP
var spawnedZombie = new SpawnedZombie();
spawnedZombie.x = self.x;
spawnedZombie.y = self.y;
spawnedZombie.gridY = self.gridY;
enemies.push(spawnedZombie);
game.addChild(spawnedZombie);
// Call original die function
originalDie();
};
return self;
});
var ZombieRa = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombieRa');
self.health = 190;
self.maxHealth = 190;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.isAttractingSun = false;
self.targetSun = null;
// Override update to handle sun attraction
var originalUpdate = self.update;
self.update = function () {
// Look for uncollected suns to attract
if (!self.isAttractingSun && !self.targetSun) {
var closestSun = null;
var closestDistance = Infinity;
// Find the closest sun that isn't already being attracted
for (var i = 0; i < suns.length; i++) {
var sun = suns[i];
if (!sun.isBeingAttracted) {
var distance = Math.abs(sun.x - self.x) + Math.abs(sun.y - self.y);
if (distance < closestDistance) {
closestDistance = distance;
closestSun = sun;
}
}
}
// If found a sun, start attracting it
if (closestSun) {
self.targetSun = closestSun;
self.isAttractingSun = true;
self.speed = 0; // Stop moving while attracting
// Mark sun as being attracted so other zombie sols don't target it
closestSun.isBeingAttracted = true;
closestSun.attractedBy = self;
// Start tweening sun towards zombie
tween(closestSun, {
x: self.x,
y: self.y
}, {
duration: closestDistance / 0.6,
// Speed reduced by 300% (was 2.4, now 0.6 = 4x slower)
easing: tween.linear,
onFinish: function onFinish() {
// Sun reached zombie - consume it
if (self.targetSun && self.targetSun.parent) {
self.consumeSun();
}
}
});
}
}
// Check if sun reached zombie (collision detection)
if (self.isAttractingSun && self.targetSun && self.targetSun.parent) {
var distance = Math.abs(self.targetSun.x - self.x) + Math.abs(self.targetSun.y - self.y);
if (distance < 50) {
// Close enough to consume
self.consumeSun();
}
}
// Call original update only if not attracting sun
if (!self.isAttractingSun) {
originalUpdate();
}
};
self.consumeSun = function () {
if (self.targetSun && self.targetSun.parent) {
// Remove sun from game without giving points to player
for (var i = 0; i < suns.length; i++) {
if (suns[i] === self.targetSun) {
suns.splice(i, 1);
break;
}
}
self.targetSun.destroy();
self.targetSun = null;
self.isAttractingSun = false;
self.speed = self.originalSpeed; // Resume movement
}
};
// Override die to stop any sun attraction
var originalDie = self.die;
self.die = function () {
// If attracting a sun, stop the tween and release the sun
if (self.targetSun && self.targetSun.parent) {
tween.stop(self.targetSun);
self.targetSun.isBeingAttracted = false;
self.targetSun.attractedBy = null;
}
originalDie();
};
return self;
});
var ZombieCascanueces = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'cascanueces');
self.health = 1000;
self.maxHealth = 1000;
self.speed = 0.96; // Normal zombie speed (0.8) * 1.2
self.originalSpeed = 0.96;
self.damage = 35; // 35 damage per second
self.isParalyzed = false;
self.paralysisTimer = 0;
self.paralysisTime = 600; // 10 seconds at 60fps
self.hasReachedColumn6 = false;
self.hasReachedColumn4 = false;
var originalUpdate = self.update;
self.update = function () {
// Handle paralysis timer
if (self.isParalyzed) {
self.paralysisTimer--;
if (self.paralysisTimer <= 0) {
// Wake up from paralysis
self.isParalyzed = false;
self.speed = self.originalSpeed;
// Flash animation when waking up
tween(self, {
alpha: 0.3
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
alpha: 1.0
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Second flash
tween(self, {
alpha: 0.3
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
alpha: 1.0
}, {
duration: 200,
easing: tween.easeInOut
});
}
});
}
});
}
});
}
return; // Don't move while paralyzed
}
// Check current column position
var cellX = Math.floor((self.x - gridStartX) / cellSize);
// Check if reached column 6 and hasn't been paralyzed there yet
if (cellX <= 6 && !self.hasReachedColumn6 && !self.isParalyzed) {
self.hasReachedColumn6 = true;
self.isParalyzed = true;
self.paralysisTimer = self.paralysisTime;
self.speed = 0; // Stop moving
return; // Don't move this frame
}
// Check if reached column 4 and hasn't been paralyzed there yet (and already passed column 6)
if (cellX <= 4 && !self.hasReachedColumn4 && self.hasReachedColumn6 && !self.isParalyzed) {
self.hasReachedColumn4 = true;
self.isParalyzed = true;
self.paralysisTimer = self.paralysisTime;
self.speed = 0; // Stop moving
return; // Don't move this frame
}
// Call original update only if not paralyzed
if (!self.isParalyzed) {
originalUpdate();
}
};
return self;
});
var ZombieCanon = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'canon');
self.health = 500;
self.maxHealth = 500;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.hasReachedPosition = false;
self.shootTimer = 0;
self.shootDelay = 600; // 10 seconds at 60fps
self.firstShot = true; // Track if this is the first shot
self.damage = 0; // Canon doesn't do regular damage
var originalUpdate = self.update;
self.update = function () {
// Move until reaching ninth column
if (!self.hasReachedPosition) {
var cellX = Math.floor((self.x - gridStartX) / cellSize);
if (cellX <= 8) {
self.hasReachedPosition = true;
self.speed = 0; // Stop moving
// Position exactly at ninth column
self.x = gridStartX + 8 * cellSize;
}
}
// If reached position, handle shooting
if (self.hasReachedPosition) {
self.shootTimer++;
var currentDelay = self.firstShot ? 180 : self.shootDelay; // 3 seconds for first shot, 10 seconds for subsequent
if (self.shootTimer >= currentDelay) {
self.shootShell();
self.shootTimer = 0;
self.firstShot = false; // After first shot, use normal delay
}
} else {
// Call original update to move normally
originalUpdate();
}
};
self.shootShell = function () {
// Create zombidito shell
var shell = new CanonShell();
shell.x = self.x;
shell.y = self.y;
shell.gridY = self.gridY;
canonShells.push(shell);
game.addChild(shell);
};
return self;
});
var Zombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombie');
self.health = 190;
self.maxHealth = 190;
self.speed = 0.8;
self.originalSpeed = 0.8;
return self;
});
var Zombidito = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombidito');
self.health = 250;
self.maxHealth = 250;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.damage = 50; // 50 damage per second
// Scale down the graphics to make it smaller
var graphics = self.children[0];
if (graphics) {
graphics.scaleX = 0.7;
graphics.scaleY = 0.7;
}
return self;
});
var SunZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'sun_zombie');
self.health = 190;
self.maxHealth = 190;
self.speed = 0.8;
self.originalSpeed = 0.8;
// Override die function to drop a sun when killed
var originalDie = self.die;
self.die = function () {
// Create a sun at zombie's position
var droppedSun = new Sun(self.x, self.y);
suns.push(droppedSun);
game.addChild(droppedSun);
// Call original die function
originalDie();
};
return self;
});
var SpawnedZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombie');
self.health = 225;
self.maxHealth = 225;
self.speed = 1.6; // 2x normal zombie speed (0.8 * 2)
self.originalSpeed = 1.6;
return self;
});
var RugbyZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'rugby');
self.health = 300;
self.maxHealth = 300;
self.speed = 2.4; // 3x normal zombie speed
self.originalSpeed = 2.4;
self.hasContactedPlant = false;
self.damage = 0; // No regular damage since it kills instantly
// Override getPlantInFront to handle instant kill
self.getPlantInFront = function () {
var cellX = Math.floor((self.x - gridStartX) / cellSize);
if (cellX >= 0 && cellX < gridCols && plants[self.gridY] && plants[self.gridY][cellX]) {
return plants[self.gridY][cellX];
}
return null;
};
// Override update to handle 500 damage to plants
var originalUpdate = self.update;
self.update = function () {
var plantInFront = self.getPlantInFront();
if (plantInFront && !self.hasContactedPlant) {
// Deal 500 damage to the plant
plantInFront.takeDamage(500);
// Reduce speed to normal zombie speed
self.hasContactedPlant = true;
self.speed = 0; // Stop for 2 seconds
self.originalSpeed = 0.8;
// Flash effect to show speed change
LK.effects.flashObject(self, 0xFFFF00, 500);
// Resume normal speed after 2 seconds
LK.setTimeout(function () {
self.speed = 0.8;
}, 2000);
}
// Call original update logic
originalUpdate();
};
return self;
});
var MinerZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'miner_zombie');
self.health = 100;
self.maxHealth = 100;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.damage = 15;
// Override the initialization to spawn at 5th column and show explosion
self.initializeMiner = function (row) {
self.gridY = row;
// Position at 5th column (index 4)
self.x = gridStartX + 4 * cellSize;
self.y = gridStartY + row * cellSize;
// Create explosion effect
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 2,
scaleY: 2
});
game.addChild(explosion);
// Animate explosion and remove it
tween(explosion, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
};
return self;
});
var ConeZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'cone_zombie');
self.health = 300;
self.maxHealth = 300;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.hasChangedAsset = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if cone zombie should switch to normal zombie appearance
if (self.health < 191 && !self.hasChangedAsset) {
self.hasChangedAsset = true;
// Remove current graphics and add normal zombie version
self.removeChild(self.children[0]);
var normalGraphics = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
var BucketZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'bucket_zombie');
self.health = 375;
self.maxHealth = 375;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.hasChangedAsset = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if bucket zombie should switch to normal zombie appearance
if (self.health < 191 && !self.hasChangedAsset) {
self.hasChangedAsset = true;
// Remove current graphics and add normal zombie version
self.removeChild(self.children[0]);
var normalGraphics = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1B5E20
});
/****
* Game Code
****/
// Sounds
// UI and grid
// Undead enemies
// Projectiles
// Plant defenders
// Grid setup - bigger grid centered on screen
var gridRows = 5;
var gridCols = 9;
var cellSize = 144;
var gridStartX = (2048 - gridCols * cellSize) / 2;
var gridStartY = (2732 - gridRows * cellSize) / 2;
// Game state
var plants = [];
var enemies = [];
var projectiles = [];
var suns = [];
var canonShells = [];
var lawnmowers = [];
var sunPoints = 500;
var lives = 3;
var currentWave = 1;
var wavesCompleted = 0;
var totalWaves = 10;
var gameMode = null; // 'normal' or 'endless'
var gameModeSelected = false;
var zombieSelectionActive = false;
var selectedZombieTypes = [];
var customWaveConfigs = {}; // Store custom wave configurations
var currentCustomizingWave = 1;
var availableZombieTypes = [{
type: 'zombie',
name: 'Normal Zombie',
asset: 'zombie'
}, {
type: 'cone_zombie',
name: 'Cone Zombie',
asset: 'cone_zombie'
}, {
type: 'bucket_zombie',
name: 'Bucket Zombie',
asset: 'bucket_zombie'
}, {
type: 'sun_zombie',
name: 'Sun Zombie',
asset: 'sun_zombie'
}, {
type: 'zombie_ra',
name: 'Zombie Ra',
asset: 'zombieRa'
}, {
type: 'zombie_canon',
name: 'Zombie Canon',
asset: 'canon'
}, {
type: 'miner_zombie',
name: 'Miner Zombie',
asset: 'miner_zombie'
}, {
type: 'rugby_zombie',
name: 'Rugby Zombie',
asset: 'rugby'
}, {
type: 'zombiestein',
name: 'Zombiestein',
asset: 'zombiestein'
}, {
type: 'zombie_cascanueces',
name: 'Zombie Cascanueces',
asset: 'cascanueces'
}, {
type: 'zombie_sarcofago',
name: 'Zombie Sarcófago',
asset: 'zombie_sarcofago'
}];
var baseEnemiesInWave = 0; // Store base enemy count for endless mode scaling
var waveInProgress = false;
var enemiesInWave = 0;
var enemiesKilled = 0;
var sunTimer = 0;
var waveStartTick = 0;
var nextWaveScheduled = false;
var sunZombieSpawnedThisWave = false;
// Spawn collision tracking for waves 1-2
var lastSpawnTime = {};
var lastSpawnRow = {};
// Miner zombie tracking
var minerZombiesSpawnedThisWave = 0;
var maxMinerZombiesPerWave = 1;
// Zombiestein tracking
var zombiesteinSpawned = false;
var zombiesteinAlive = false;
// Performance optimization variables
var maxObjectsPerType = 50;
var cleanupCounter = 0;
var lagOptimizationActive = false;
// Projectile pool for object reuse
var projectilePool = [];
var maxPoolSize = 20;
// Track active projectiles per plant
var plantProjectileCount = {};
// Selected plant type
var selectedPlantType = null;
var plantTypes = ['sunflower', 'birasol', 'peashooter', 'snowpea', 'fireshot', 'repetidora', 'wallnut', 'nuezPrimitiva', 'petacereza', 'carnivora', 'rapaz'];
var plantCosts = {
'peashooter': 100,
'sunflower': 50,
'birasol': 225,
'snowpea': 175,
'fireshot': 175,
'plantorcha': 175,
'wallnut': 50,
'repetidora': 200,
'nuezPrimitiva': 125,
'petacereza': 150,
'carnivora': 200,
'rapaz': 325
};
// Wallnut cooldown system
var wallnutCooldown = 0;
var wallnutCooldownTime = 720; // 12 seconds at 60fps
// Nuez primitiva cooldown system
var nuezPrimitivaCooldown = 0;
var nuezPrimitivaCooldownTime = 180; // 3 seconds at 60fps
// Petacereza cooldown system
var petacerezaCooldown = 0;
var petacerezaCooldownTime = 900; // 15 seconds at 60fps
// Ghost plant for preview
var ghostPlant = null;
// Initialize grid cells array
var gridCells = [];
// Initialize grid
for (var row = 0; row < gridRows; row++) {
plants[row] = [];
gridCells[row] = [];
for (var col = 0; col < gridCols; col++) {
plants[row][col] = null;
var cell = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
x: gridStartX + col * cellSize,
y: gridStartY + row * cellSize
});
gridCells[row][col] = cell;
game.addChild(cell);
}
}
// Initialize lawnmowers
for (var row = 0; row < gridRows; row++) {
var lawnmower = new Lawnmower(row);
lawnmowers.push(lawnmower);
game.addChild(lawnmower);
}
// Game mode selection UI
var modeSelectionContainer = new Container();
modeSelectionContainer.x = 2048 / 2;
modeSelectionContainer.y = 2732 / 2;
var modeTitle = new Text2('Select Game Mode', {
size: 80,
fill: 0xFFFFFF
});
modeTitle.anchor.set(0.5, 0.5);
modeTitle.y = -200;
modeSelectionContainer.addChild(modeTitle);
// Normal mode button
var normalModeButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: -240,
y: -50,
scaleX: 2,
scaleY: 1.5,
color: 0x4CAF50
});
normalModeButton.interactive = true;
normalModeButton.buttonMode = true;
normalModeButton.down = function (x, y, obj) {
gameMode = 'normal';
totalWaves = 10;
selectGameMode();
};
modeSelectionContainer.addChild(normalModeButton);
var normalModeText = new Text2('Normal', {
size: 60,
fill: 0xFFFFFF
});
normalModeText.anchor.set(0.5, 0.5);
normalModeText.x = -320;
normalModeText.y = -50;
modeSelectionContainer.addChild(normalModeText);
var normalModeSubtext = new Text2('(10 waves)', {
size: 40,
fill: 0xCCCCCC
});
normalModeSubtext.anchor.set(0.5, 0.5);
normalModeSubtext.x = -240;
normalModeSubtext.y = 10;
modeSelectionContainer.addChild(normalModeSubtext);
// Personalize mode button
var personalizeModeButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: -50,
scaleX: 2,
scaleY: 1.5,
color: 0xFF9800
});
personalizeModeButton.interactive = true;
personalizeModeButton.buttonMode = true;
personalizeModeButton.down = function (x, y, obj) {
gameMode = 'personalize';
totalWaves = 20;
showZombieSelection();
};
modeSelectionContainer.addChild(personalizeModeButton);
var personalizeModeText = new Text2('Personalize', {
size: 60,
fill: 0xFFFFFF
});
personalizeModeText.anchor.set(0.5, 0.5);
personalizeModeText.x = 0;
personalizeModeText.y = -50;
modeSelectionContainer.addChild(personalizeModeText);
var personalizeModeSubtext = new Text2('(20 waves)', {
size: 40,
fill: 0xCCCCCC
});
personalizeModeSubtext.anchor.set(0.5, 0.5);
personalizeModeSubtext.x = 0;
personalizeModeSubtext.y = 10;
modeSelectionContainer.addChild(personalizeModeSubtext);
// Endless mode button
var endlessModeButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 360,
y: -50,
scaleX: 2,
scaleY: 1.5,
color: 0x9C27B0
});
endlessModeButton.interactive = true;
endlessModeButton.buttonMode = true;
endlessModeButton.down = function (x, y, obj) {
gameMode = 'endless';
totalWaves = 999; // Large number for display purposes
selectGameMode();
};
modeSelectionContainer.addChild(endlessModeButton);
var endlessModeText = new Text2('Endless', {
size: 60,
fill: 0xFFFFFF
});
endlessModeText.anchor.set(0.5, 0.5);
endlessModeText.x = 360;
endlessModeText.y = -50;
modeSelectionContainer.addChild(endlessModeText);
var endlessModeSubtext = new Text2('(infinite waves)', {
size: 40,
fill: 0xCCCCCC
});
endlessModeSubtext.anchor.set(0.5, 0.5);
endlessModeSubtext.x = 360;
endlessModeSubtext.y = 10;
modeSelectionContainer.addChild(endlessModeSubtext);
game.addChild(modeSelectionContainer);
function selectGameMode() {
gameModeSelected = true;
modeSelectionContainer.destroy();
updateWaveDisplay();
// Create skip wave button only for endless mode
if (gameMode === 'endless') {
createSkipWaveButton();
}
// Start first wave after mode selection
LK.setTimeout(function () {
startWave();
}, 3000);
}
function showZombieSelection() {
zombieSelectionActive = true;
modeSelectionContainer.destroy();
// Create zombie selection container
var zombieSelectionContainer = new Container();
zombieSelectionContainer.x = 2048 / 2;
zombieSelectionContainer.y = 2732 / 2;
game.addChild(zombieSelectionContainer);
// Title
var selectionTitle = new Text2('Select Zombies for Endless Mode', {
size: 60,
fill: 0xFFFFFF
});
selectionTitle.anchor.set(0.5, 0.5);
selectionTitle.y = -350;
zombieSelectionContainer.addChild(selectionTitle);
// Instructions
var instructions = new Text2('Click zombies to toggle selection (min 3 required)', {
size: 40,
fill: 0xCCCCCC
});
instructions.anchor.set(0.5, 0.5);
instructions.y = -300;
zombieSelectionContainer.addChild(instructions);
// Create zombie selection buttons
var zombieButtons = [];
var cols = 4;
var rows = Math.ceil(availableZombieTypes.length / cols);
for (var i = 0; i < availableZombieTypes.length; i++) {
var zombieType = availableZombieTypes[i];
var col = i % cols;
var row = Math.floor(i / cols);
// Zombie button
var zombieButton = LK.getAsset(zombieType.asset, {
anchorX: 0.5,
anchorY: 0.5,
x: (col - (cols - 1) / 2) * 200,
y: (row - (rows - 1) / 2) * 160 - 50,
scaleX: 1.5,
scaleY: 1.5
});
zombieButton.zombieType = zombieType.type;
zombieButton.isSelected = false;
zombieButton.interactive = true;
zombieButton.buttonMode = true;
zombieButton.alpha = 0.5; // Start unselected
zombieButton.down = function (x, y, obj) {
toggleZombieSelection(this);
};
zombieSelectionContainer.addChild(zombieButton);
zombieButtons.push(zombieButton);
// Zombie name
var nameText = new Text2(zombieType.name, {
size: 30,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0.5);
nameText.x = zombieButton.x;
nameText.y = zombieButton.y + 80;
zombieSelectionContainer.addChild(nameText);
}
// Start button
var startButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 250,
scaleX: 3,
scaleY: 1.5,
color: 0x4CAF50
});
startButton.interactive = true;
startButton.buttonMode = true;
startButton.alpha = 0.5; // Start disabled
startButton.down = function (x, y, obj) {
if (selectedZombieTypes.length >= 3) {
zombieSelectionContainer.destroy();
showWaveCustomization();
}
};
zombieSelectionContainer.addChild(startButton);
var startButtonText = new Text2('Customize Waves', {
size: 50,
fill: 0xFFFFFF
});
startButtonText.anchor.set(0.5, 0.5);
startButtonText.y = 250;
zombieSelectionContainer.addChild(startButtonText);
// Store references for updates
zombieSelectionContainer.zombieButtons = zombieButtons;
zombieSelectionContainer.startButton = startButton;
zombieSelectionContainer.startButtonText = startButtonText;
game.zombieSelectionContainer = zombieSelectionContainer;
}
function toggleZombieSelection(button) {
if (button.isSelected) {
// Deselect
button.isSelected = false;
button.alpha = 0.5;
button.tint = 0xFFFFFF;
// Remove from selected array
for (var i = 0; i < selectedZombieTypes.length; i++) {
if (selectedZombieTypes[i] === button.zombieType) {
selectedZombieTypes.splice(i, 1);
break;
}
}
} else {
// Select
button.isSelected = true;
button.alpha = 1.0;
button.tint = 0x00FF00;
selectedZombieTypes.push(button.zombieType);
}
updateStartButton();
}
function updateStartButton() {
var container = game.zombieSelectionContainer;
if (!container) {
return;
}
if (selectedZombieTypes.length >= 3) {
container.startButton.alpha = 1.0;
container.startButton.tint = 0x4CAF50;
container.startButtonText.setText('Customize Waves (' + selectedZombieTypes.length + ' selected)');
} else {
container.startButton.alpha = 0.5;
container.startButton.tint = 0x888888;
container.startButtonText.setText('Select at least 3 zombies (' + selectedZombieTypes.length + '/3)');
}
}
function showWaveCustomization() {
// Initialize default configurations for all 20 waves
for (var wave = 1; wave <= 20; wave++) {
if (!customWaveConfigs[wave]) {
customWaveConfigs[wave] = {};
for (var i = 0; i < selectedZombieTypes.length; i++) {
customWaveConfigs[wave][selectedZombieTypes[i]] = wave <= 2 ? 2 : Math.min(wave, 8); // Default quantities
}
}
}
currentCustomizingWave = 1;
showWaveConfigUI();
}
function showWaveConfigUI() {
// Remove existing wave config container if any
if (game.waveConfigContainer) {
game.waveConfigContainer.destroy();
}
// Create wave customization container
var waveConfigContainer = new Container();
waveConfigContainer.x = 2048 / 2;
waveConfigContainer.y = 2732 / 2;
game.addChild(waveConfigContainer);
game.waveConfigContainer = waveConfigContainer;
// Title
var configTitle = new Text2('Wave ' + currentCustomizingWave + ' Configuration', {
size: 60,
fill: 0xFFFFFF
});
configTitle.anchor.set(0.5, 0.5);
configTitle.y = -350;
waveConfigContainer.addChild(configTitle);
// Instructions
var configInstructions = new Text2('Set zombie quantities for Wave ' + currentCustomizingWave + ' (1-20)', {
size: 40,
fill: 0xCCCCCC
});
configInstructions.anchor.set(0.5, 0.5);
configInstructions.y = -300;
waveConfigContainer.addChild(configInstructions);
// Wave navigation buttons - positioned slightly higher
var prevWaveButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: -400,
y: -250,
scaleX: 1.5,
scaleY: 1.0,
color: currentCustomizingWave > 1 ? 0x2196F3 : 0x666666
});
prevWaveButton.interactive = currentCustomizingWave > 1;
prevWaveButton.buttonMode = currentCustomizingWave > 1;
if (currentCustomizingWave > 1) {
prevWaveButton.down = function () {
currentCustomizingWave--;
showWaveConfigUI();
};
}
waveConfigContainer.addChild(prevWaveButton);
var prevWaveText = new Text2('◀ Previous', {
size: 40,
fill: 0xFFFFFF
});
prevWaveText.anchor.set(0.5, 0.5);
prevWaveText.x = -400;
prevWaveText.y = -250;
waveConfigContainer.addChild(prevWaveText);
var nextWaveButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 400,
y: -250,
scaleX: 1.5,
scaleY: 1.0,
color: currentCustomizingWave < 20 ? 0x2196F3 : 0x666666
});
nextWaveButton.interactive = currentCustomizingWave < 20;
nextWaveButton.buttonMode = currentCustomizingWave < 20;
if (currentCustomizingWave < 20) {
nextWaveButton.down = function () {
currentCustomizingWave++;
showWaveConfigUI();
};
}
waveConfigContainer.addChild(nextWaveButton);
var nextWaveText = new Text2('Next ▶', {
size: 40,
fill: 0xFFFFFF
});
nextWaveText.anchor.set(0.5, 0.5);
nextWaveText.x = 400;
nextWaveText.y = -250;
waveConfigContainer.addChild(nextWaveText);
// Add "All to 0" and "All to 1" buttons on the left side
var allToZeroButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: -700,
y: -50,
scaleX: 1.5,
scaleY: 1.0,
color: 0xF44336
});
allToZeroButton.interactive = true;
allToZeroButton.buttonMode = true;
allToZeroButton.down = function () {
// Set all zombie quantities to 0 for current wave
for (var i = 0; i < selectedZombieTypes.length; i++) {
customWaveConfigs[currentCustomizingWave][selectedZombieTypes[i]] = 0;
}
showWaveConfigUI(); // Refresh UI
};
waveConfigContainer.addChild(allToZeroButton);
var allToZeroText = new Text2('All to 0', {
size: 35,
fill: 0xFFFFFF
});
allToZeroText.anchor.set(0.5, 0.5);
allToZeroText.x = -700;
allToZeroText.y = -50;
waveConfigContainer.addChild(allToZeroText);
var allToOneButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: -700,
y: 20,
scaleX: 1.5,
scaleY: 1.0,
color: 0x4CAF50
});
allToOneButton.interactive = true;
allToOneButton.buttonMode = true;
allToOneButton.down = function () {
// Set all zombie quantities to 1 for current wave
for (var i = 0; i < selectedZombieTypes.length; i++) {
customWaveConfigs[currentCustomizingWave][selectedZombieTypes[i]] = 1;
}
showWaveConfigUI(); // Refresh UI
};
waveConfigContainer.addChild(allToOneButton);
var allToOneText = new Text2('All to 1', {
size: 35,
fill: 0xFFFFFF
});
allToOneText.anchor.set(0.5, 0.5);
allToOneText.x = -700;
allToOneText.y = 20;
waveConfigContainer.addChild(allToOneText);
// Zombie quantity controls
var yOffset = -100;
var zombieControls = [];
for (var i = 0; i < selectedZombieTypes.length; i++) {
var zombieType = selectedZombieTypes[i];
var zombieAsset = null;
for (var j = 0; j < availableZombieTypes.length; j++) {
if (availableZombieTypes[j].type === zombieType) {
zombieAsset = availableZombieTypes[j].asset;
break;
}
}
if (zombieAsset) {
// Zombie icon
var zombieIcon = LK.getAsset(zombieAsset, {
anchorX: 0.5,
anchorY: 0.5,
x: -300,
y: yOffset,
scaleX: 1.2,
scaleY: 1.2
});
waveConfigContainer.addChild(zombieIcon);
// Decrease button
var decreaseButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: -100,
y: yOffset,
scaleX: 0.8,
scaleY: 0.8,
color: 0xF44336
});
decreaseButton.zombieType = zombieType;
decreaseButton.interactive = true;
decreaseButton.buttonMode = true;
decreaseButton.down = function () {
var currentQty = customWaveConfigs[currentCustomizingWave][this.zombieType] || 0;
if (currentQty > 0) {
customWaveConfigs[currentCustomizingWave][this.zombieType] = currentQty - 1;
showWaveConfigUI();
}
};
waveConfigContainer.addChild(decreaseButton);
var decreaseText = new Text2('-', {
size: 50,
fill: 0xFFFFFF
});
decreaseText.anchor.set(0.5, 0.5);
decreaseText.x = -100;
decreaseText.y = yOffset;
waveConfigContainer.addChild(decreaseText);
// Quantity display
var quantity = customWaveConfigs[currentCustomizingWave][zombieType] || 0;
var quantityText = new Text2(quantity.toString(), {
size: 45,
fill: 0xFFFFFF
});
quantityText.anchor.set(0.5, 0.5);
quantityText.x = 0;
quantityText.y = yOffset;
waveConfigContainer.addChild(quantityText);
// Increase button
var increaseButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 100,
y: yOffset,
scaleX: 0.8,
scaleY: 0.8,
color: 0x4CAF50
});
increaseButton.zombieType = zombieType;
increaseButton.interactive = true;
increaseButton.buttonMode = true;
increaseButton.down = function () {
var currentQty = customWaveConfigs[currentCustomizingWave][this.zombieType] || 0;
if (currentQty < 15) {
// Max 15 zombies per type per wave
customWaveConfigs[currentCustomizingWave][this.zombieType] = currentQty + 1;
showWaveConfigUI();
}
};
waveConfigContainer.addChild(increaseButton);
var increaseText = new Text2('+', {
size: 40,
fill: 0xFFFFFF
});
increaseText.anchor.set(0.5, 0.5);
increaseText.x = 100;
increaseText.y = yOffset;
waveConfigContainer.addChild(increaseText);
// Zombie name
var zombieName = null;
for (var k = 0; k < availableZombieTypes.length; k++) {
if (availableZombieTypes[k].type === zombieType) {
zombieName = availableZombieTypes[k].name;
break;
}
}
var nameText = new Text2(zombieName || zombieType, {
size: 30,
fill: 0xCCCCCC
});
nameText.anchor.set(0.5, 0.5);
nameText.x = 300;
nameText.y = yOffset;
waveConfigContainer.addChild(nameText);
yOffset += 70;
}
}
// Finish customization button - positioned at bottom right
var finishButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 600,
y: 350,
scaleX: 3,
scaleY: 1.5,
color: 0x4CAF50
});
finishButton.interactive = true;
finishButton.buttonMode = true;
finishButton.down = function () {
waveConfigContainer.destroy();
selectGameMode();
};
waveConfigContainer.addChild(finishButton);
var finishButtonText = new Text2('Start Game', {
size: 50,
fill: 0xFFFFFF
});
finishButtonText.anchor.set(0.5, 0.5);
finishButtonText.x = 600;
finishButtonText.y = 350;
waveConfigContainer.addChild(finishButtonText);
}
// Shovel tool for removing plants
var selectedTool = null; // 'shovel' or null
var shovelButton = LK.getAsset('shovel', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 100,
scaleX: 1.5,
scaleY: 1.5
});
shovelButton.interactive = true;
shovelButton.buttonMode = true;
shovelButton.down = function (x, y, obj) {
console.log("Shovel button clicked");
if (selectedTool === 'shovel') {
selectedTool = null;
shovelButton.alpha = 0.8;
shovelButton.scaleX = shovelButton.scaleY = 1.5;
} else {
selectedTool = 'shovel';
selectedPlantType = null;
shovelButton.alpha = 1.0;
shovelButton.scaleX = shovelButton.scaleY = 1.8;
updatePlantSelection();
}
};
shovelButton.alpha = 0.8;
game.addChild(shovelButton);
// UI Elements
var sunDisplay = new Text2('Sun: ' + sunPoints, {
size: 60,
fill: 0xFFEB3B
});
sunDisplay.anchor.set(0, 0);
sunDisplay.x = 120; // Offset from left edge to avoid menu icon
LK.gui.topLeft.addChild(sunDisplay);
var livesDisplay = new Text2('Lives: ' + lives, {
size: 60,
fill: 0xF44336
});
livesDisplay.anchor.set(0, 0);
livesDisplay.y = 80;
LK.gui.topRight.addChild(livesDisplay);
var waveDisplay = new Text2('Wave: ' + currentWave + '/' + totalWaves, {
size: 50,
fill: 0x4CAF50
});
waveDisplay.anchor.set(0.5, 0);
LK.gui.top.addChild(waveDisplay);
// Plant selection UI at bottom
var plantButtons = [];
var uiY = 2732 - 150;
for (var i = 0; i < plantTypes.length; i++) {
var plantType = plantTypes[i];
var yPos = uiY;
var xPos = 200 + i * 220;
// Special positioning for carnivora - place it above peashooter
if (plantType === 'carnivora') {
xPos = 640; // Same x position as peashooter
yPos = uiY - 266; // Above peashooter
}
// Special positioning for petacereza - place it above birasol
else if (plantType === 'petacereza') {
xPos = 420; // Same x position as birasol
yPos = uiY - 266; // Above birasol, 10% higher than previous position (242 * 1.1 = 266)
}
// Special positioning for rapaz - place it above snowpea
else if (plantType === 'rapaz') {
xPos = 860; // Same x position as snowpea
yPos = uiY - 266; // Above snowpea
}
var button = LK.getAsset(plantType, {
anchorX: 0.5,
anchorY: 0.5,
x: xPos,
y: yPos,
scaleX: 2.25,
scaleY: 2.25
});
button.plantType = plantType;
button.interactive = true; // Enable button interactivity
button.buttonMode = true; // Make it behave like a button
button.down = function (x, y, obj) {
console.log("Plant button clicked:", this.plantType, "Cost:", plantCosts[this.plantType], "Current suns:", sunPoints);
// Check wallnut cooldown
if (this.plantType === 'wallnut' && wallnutCooldown > 0) {
return; // Don't allow selection during cooldown
}
// Check nuez primitiva cooldown
if (this.plantType === 'nuezPrimitiva' && nuezPrimitivaCooldown > 0) {
return; // Don't allow selection during cooldown
}
// Check petacereza cooldown
if (this.plantType === 'petacereza' && petacerezaCooldown > 0) {
return; // Don't allow selection during cooldown
}
if (sunPoints >= plantCosts[this.plantType]) {
selectedPlantType = this.plantType;
selectedTool = null;
shovelButton.alpha = 0.8;
shovelButton.scaleX = shovelButton.scaleY = 1.5;
updatePlantSelection();
}
};
game.addChild(button);
plantButtons.push(button);
// Add cost text
var costText = new Text2(plantCosts[plantType], {
size: 40,
fill: 0xFFFFFF
});
costText.anchor.set(0.5, 0);
costText.x = button.x;
costText.y = button.y + 80;
game.addChild(costText);
}
// Add plantorcha button above sunflower
var plantorchaButton = LK.getAsset('plantorcha', {
anchorX: 0.5,
anchorY: 0.5,
x: 200,
// Same x as sunflower
y: uiY - 351,
// Above sunflower with 10% more space above previous position (319 * 1.1 = 351)
scaleX: 2.25,
scaleY: 2.25
});
plantorchaButton.plantType = 'plantorcha';
plantorchaButton.interactive = true;
plantorchaButton.buttonMode = true;
plantorchaButton.down = function (x, y, obj) {
console.log("Plant button clicked:", this.plantType, "Cost:", plantCosts[this.plantType], "Current suns:", sunPoints);
if (sunPoints >= plantCosts[this.plantType]) {
selectedPlantType = this.plantType;
selectedTool = null;
shovelButton.alpha = 0.8;
shovelButton.scaleX = shovelButton.scaleY = 1.5;
updatePlantSelection();
}
};
game.addChild(plantorchaButton);
plantButtons.push(plantorchaButton);
// Add plantorcha cost text
var plantorchaCostText = new Text2(plantCosts['plantorcha'], {
size: 40,
fill: 0xFFFFFF
});
plantorchaCostText.anchor.set(0.5, 0);
plantorchaCostText.x = plantorchaButton.x;
plantorchaCostText.y = plantorchaButton.y + 80;
game.addChild(plantorchaCostText);
function updatePlantSelection() {
for (var i = 0; i < plantButtons.length; i++) {
if (plantButtons[i].plantType === selectedPlantType) {
plantButtons[i].alpha = 1.0;
plantButtons[i].scaleX = plantButtons[i].scaleY = 2.55;
} else if (plantButtons[i].plantType === 'wallnut' && wallnutCooldown > 0) {
// Gray out wallnut during cooldown
plantButtons[i].alpha = 0.3;
plantButtons[i].scaleX = plantButtons[i].scaleY = 2.25;
plantButtons[i].tint = 0x888888;
} else if (plantButtons[i].plantType === 'nuezPrimitiva' && nuezPrimitivaCooldown > 0) {
// Gray out nuez primitiva during cooldown
plantButtons[i].alpha = 0.3;
plantButtons[i].scaleX = plantButtons[i].scaleY = 2.25;
plantButtons[i].tint = 0x888888;
} else if (plantButtons[i].plantType === 'petacereza' && petacerezaCooldown > 0) {
// Gray out petacereza during cooldown
plantButtons[i].alpha = 0.3;
plantButtons[i].scaleX = plantButtons[i].scaleY = 2.25;
plantButtons[i].tint = 0x888888;
} else {
plantButtons[i].alpha = 0.6;
plantButtons[i].scaleX = plantButtons[i].scaleY = 2.25;
plantButtons[i].tint = 0xFFFFFF;
}
}
// Remove existing ghost plant
if (ghostPlant) {
ghostPlant.destroy();
ghostPlant = null;
}
// Create new ghost plant if a type is selected
if (selectedPlantType) {
ghostPlant = LK.getAsset(selectedPlantType, {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.6,
tint: 0x88FF88
});
game.addChild(ghostPlant);
}
// Highlight valid placement squares
for (var row = 0; row < gridRows; row++) {
for (var col = 0; col < gridCols; col++) {
if (selectedPlantType && canPlacePlant(col, row, selectedPlantType)) {
// Green highlight for valid empty squares
gridCells[row][col].tint = 0x00FF00;
gridCells[row][col].alpha = 0.7;
} else if (selectedPlantType && plants[row][col] !== null) {
// Red highlight for occupied squares when plant is selected
gridCells[row][col].tint = 0xFF0000;
gridCells[row][col].alpha = 0.5;
} else if (selectedTool === 'shovel' && plants[row][col] !== null) {
// Yellow highlight for plants that can be removed with shovel
gridCells[row][col].tint = 0xFFFF00;
gridCells[row][col].alpha = 0.7;
} else {
// Normal appearance for unselected or invalid squares
gridCells[row][col].tint = 0xFFFFFF;
gridCells[row][col].alpha = 0.3;
}
}
}
}
function updateSunDisplay() {
sunDisplay.setText('Sun: ' + sunPoints);
}
function updateLivesDisplay() {
livesDisplay.setText('Lives: ' + lives);
}
function updateWaveDisplay() {
if (gameMode === 'endless') {
waveDisplay.setText('Wave: ' + currentWave + ' / ∞');
} else if (gameMode === 'personalize') {
waveDisplay.setText('Wave: ' + currentWave + '/' + totalWaves);
} else {
waveDisplay.setText('Wave: ' + currentWave + '/' + totalWaves);
}
}
function createProjectile(type, startX, startY, speed, damage, plantId) {
// Check if plant already has 2 active projectiles
if (!plantProjectileCount[plantId]) {
plantProjectileCount[plantId] = 0;
}
if (plantProjectileCount[plantId] >= 2) {
return null; // Don't create projectile if plant already has 2
}
var projectile;
if (projectilePool.length > 0) {
// Reuse from pool
projectile = projectilePool.pop();
projectile.speed = speed;
projectile.damage = damage;
projectile.x = startX;
projectile.y = startY;
projectile.type = type;
projectile.gridY = Math.floor((startY - gridStartY + cellSize / 2) / cellSize);
projectile.ownerId = plantId;
// Re-add graphics
var graphics = projectile.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
});
} else {
// Create new projectile
projectile = new Projectile(type, startX, startY, speed, damage);
projectile.ownerId = plantId;
}
plantProjectileCount[plantId]++;
projectiles.push(projectile);
game.addChild(projectile);
return projectile;
}
function performanceCleanup() {
// Clean up destroyed objects that might still be in arrays
for (var i = enemies.length - 1; i >= 0; i--) {
if (!enemies[i].parent) {
enemies.splice(i, 1);
}
}
for (var i = projectiles.length - 1; i >= 0; i--) {
if (!projectiles[i].parent) {
projectiles.splice(i, 1);
}
}
for (var i = suns.length - 1; i >= 0; i--) {
if (!suns[i].parent) {
suns.splice(i, 1);
}
}
for (var i = canonShells.length - 1; i >= 0; i--) {
if (!canonShells[i].parent) {
canonShells.splice(i, 1);
}
}
// Clean up projectile tracking for destroyed plants
for (var plantId in plantProjectileCount) {
var coords = plantId.split('_');
var x = parseInt(coords[0]);
var y = parseInt(coords[1]);
if (y >= 0 && y < gridRows && x >= 0 && x < gridCols && !plants[y][x]) {
delete plantProjectileCount[plantId];
}
}
// Limit maximum objects if we have too many
if (enemies.length > maxObjectsPerType) {
for (var i = 0; i < enemies.length - maxObjectsPerType; i++) {
if (enemies[i]) {
enemies[i].destroy();
}
}
}
if (projectiles.length > maxObjectsPerType) {
for (var i = 0; i < projectiles.length - maxObjectsPerType; i++) {
if (projectiles[i]) {
projectiles[i].removeFromGame();
}
}
}
if (suns.length > 20) {
for (var i = 0; i < suns.length - 20; i++) {
if (suns[i]) {
suns[i].destroy();
}
}
}
// Limit projectile pool size
if (projectilePool.length > maxPoolSize) {
for (var i = maxPoolSize; i < projectilePool.length; i++) {
if (projectilePool[i]) {
projectilePool[i].destroy();
}
}
projectilePool.length = maxPoolSize;
}
}
function getGridPosition(x, y) {
var gridX = Math.floor((x - gridStartX + cellSize / 2) / cellSize);
var gridY = Math.floor((y - gridStartY + cellSize / 2) / cellSize);
if (gridX >= 0 && gridX < gridCols && gridY >= 0 && gridY < gridRows) {
return {
x: gridX,
y: gridY
};
}
return null;
}
function canPlacePlant(gridX, gridY, plantType) {
// Check if grid position is valid
if (gridX < 0 || gridX >= gridCols || gridY < 0 || gridY >= gridRows) {
return false;
}
// Check wallnut cooldown
if (plantType === 'wallnut' && wallnutCooldown > 0) {
return false;
}
// Check nuez primitiva cooldown
if (plantType === 'nuezPrimitiva' && nuezPrimitivaCooldown > 0) {
return false;
}
// Check petacereza cooldown
if (plantType === 'petacereza' && petacerezaCooldown > 0) {
return false;
}
// Check if square is empty and player has enough suns
return plants[gridY][gridX] === null && sunPoints >= plantCosts[plantType];
}
function placePlant(gridX, gridY, plantType) {
// Double-check that placement is valid before proceeding
if (!canPlacePlant(gridX, gridY, plantType)) {
return false;
}
var plant;
switch (plantType) {
case 'peashooter':
plant = new Peashooter();
break;
case 'sunflower':
plant = new Sunflower();
break;
case 'birasol':
plant = new Birasol();
break;
case 'snowpea':
plant = new SnowPea();
break;
case 'fireshot':
plant = new Fireshot();
break;
case 'plantorcha':
plant = new Plantorcha();
break;
case 'wallnut':
plant = new Wallnut();
break;
case 'snowpea':
plant = new SnowPea();
break;
case 'repetidora':
plant = new Repetidora();
break;
case 'nuezPrimitiva':
plant = new NuezPrimitiva();
break;
case 'petacereza':
plant = new Petacereza();
break;
case 'carnivora':
plant = new Carnivora();
break;
case 'rapaz':
plant = new Rapaz();
break;
default:
return false;
}
// Set plant position and grid reference
plant.gridX = gridX;
plant.gridY = gridY;
plant.x = gridStartX + gridX * cellSize;
plant.y = gridStartY + gridY * cellSize;
// Place plant in grid and add to game
plants[gridY][gridX] = plant;
game.addChild(plant);
// Deduct cost and update UI
sunPoints -= plantCosts[plantType];
updateSunDisplay();
LK.getSound('plant').play();
// Start wallnut cooldown if placing wallnut
if (plantType === 'wallnut') {
wallnutCooldown = wallnutCooldownTime;
selectedPlantType = null; // Deselect wallnut after placing
updatePlantSelection();
}
// Start nuez primitiva cooldown if placing nuez primitiva
if (plantType === 'nuezPrimitiva') {
nuezPrimitivaCooldown = nuezPrimitivaCooldownTime;
selectedPlantType = null; // Deselect nuez primitiva after placing
updatePlantSelection();
}
// Start petacereza cooldown if placing petacereza
if (plantType === 'petacereza') {
petacerezaCooldown = petacerezaCooldownTime;
selectedPlantType = null; // Deselect petacereza after placing
updatePlantSelection();
}
// Flash the grid cell to show successful placement
LK.effects.flashObject(gridCells[gridY][gridX], 0x00FF00, 300);
return true;
}
function canSpawnWithoutCollision(row) {
// Only apply collision detection for waves 1-2
if (currentWave > 2) {
return true;
}
var currentTime = LK.ticks;
var oneSecond = 60; // 60 ticks = 1 second at 60fps
// Check if same row was used in last second
if (lastSpawnRow[row] && currentTime - lastSpawnRow[row] < oneSecond) {
return false;
}
// Check if any zombie was spawned in last second
for (var checkRow in lastSpawnTime) {
if (currentTime - lastSpawnTime[checkRow] < oneSecond) {
return false;
}
}
return true;
}
function spawnZombie(type, row) {
var zombie;
switch (type) {
case 'zombie':
zombie = new Zombie();
break;
case 'cone_zombie':
zombie = new ConeZombie();
break;
case 'bucket_zombie':
zombie = new BucketZombie();
break;
case 'sun_zombie':
zombie = new SunZombie();
break;
case 'zombie_ra':
zombie = new ZombieRa();
break;
case 'zombie_canon':
zombie = new ZombieCanon();
break;
case 'miner_zombie':
zombie = new MinerZombie();
// Special positioning for miner
zombie.initializeMiner(row);
// Apply health bonus for endless mode after wave 10
if (gameMode === 'endless' && currentWave > 10) {
var healthMultiplier = 1 + (currentWave - 10) * 0.05;
zombie.health = Math.floor(zombie.health * healthMultiplier);
zombie.maxHealth = Math.floor(zombie.maxHealth * healthMultiplier);
}
enemies.push(zombie);
game.addChild(zombie);
return;
// Return early since positioning is handled
case 'zombiestein':
zombie = new Zombiestein();
break;
case 'rugby_zombie':
zombie = new RugbyZombie();
break;
case 'zombie_cascanueces':
zombie = new ZombieCascanueces();
break;
case 'zombie_sarcofago':
zombie = new ZombieSarcofago();
break;
default:
return;
}
zombie.gridY = row;
zombie.x = 2100;
zombie.y = gridStartY + row * cellSize;
// Apply health bonus for endless mode after wave 10
if (gameMode === 'endless' && currentWave > 10) {
var healthMultiplier = 1 + (currentWave - 10) * 0.05;
zombie.health = Math.floor(zombie.health * healthMultiplier);
zombie.maxHealth = Math.floor(zombie.maxHealth * healthMultiplier);
}
enemies.push(zombie);
game.addChild(zombie);
}
function startWave() {
if (waveInProgress) {
return;
}
waveInProgress = true;
waveStartTick = LK.ticks; // Record when wave started
sunZombieSpawnedThisWave = false; // Reset sun zombie spawn flag for this wave
// Reset and update miner zombie tracking for this wave
minerZombiesSpawnedThisWave = 0;
if (currentWave >= 8) {
maxMinerZombiesPerWave = 2;
} else {
maxMinerZombiesPerWave = 1;
}
var bucketZombiesLeft = 0;
// Handle custom wave configurations for waves 1-20 in personalize mode
if (gameMode === 'personalize' && currentWave <= 20 && customWaveConfigs[currentWave]) {
var waveConfig = customWaveConfigs[currentWave];
var totalZombies = 0;
var spawnQueue = [];
// Build spawn queue from custom configuration
for (var zombieType in waveConfig) {
var quantity = waveConfig[zombieType];
for (var i = 0; i < quantity; i++) {
spawnQueue.push(zombieType);
totalZombies++;
}
}
enemiesInWave = totalZombies;
enemiesKilled = 0;
// Spawn zombies from custom configuration
if (spawnQueue.length > 0) {
var spawnIndex = 0;
var customSpawnTimer = LK.setInterval(function () {
if (spawnIndex < spawnQueue.length) {
var zombieType = spawnQueue[spawnIndex];
var row = Math.floor(Math.random() * gridRows);
spawnZombie(zombieType, row);
spawnIndex++;
} else {
LK.clearInterval(customSpawnTimer);
}
}, 800); // Spawn every 800ms
}
return; // Exit early for custom waves
}
// Handle endless mode waves (but not personalize mode)
if (gameMode === 'endless' && currentWave > 10) {
// For endless mode waves 11+, spawn Zombiestein every 5 waves
if (currentWave % 5 === 0) {
// Calculate number of Zombiesteins to spawn based on wave
var zombiesteinCount = Math.floor((currentWave - 10) / 5) + 1;
for (var z = 0; z < zombiesteinCount; z++) {
var zombiesteinRow = Math.floor(Math.random() * gridRows);
spawnZombie('zombiestein', zombiesteinRow);
}
zombiesteinSpawned = true;
zombiesteinAlive = true;
enemiesInWave += zombiesteinCount - 1; // Adjust enemy count for additional Zombiesteins
}
// Zombie Sarcófago spawning logic for endless mode waves 15+
if (currentWave >= 15) {
// Count bucket zombies in this wave to determine if we can spawn sarcófago
var bucketZombieCount = Math.floor(scaledEnemyCount * 0.6);
// 50% chance to spawn if at least 2 bucket zombies
if (bucketZombieCount >= 2 && Math.random() < 0.5) {
// Calculate max sarcófagos: 1 + (waves past 15) / 5
var maxSarcofagos = 1 + Math.floor((currentWave - 15) / 5);
var sarcofagosToSpawn = Math.min(maxSarcofagos, 1); // Start with 1, increase every 5 waves
for (var s = 0; s < sarcofagosToSpawn; s++) {
var sarcofagoRow = Math.floor(Math.random() * gridRows);
spawnZombie('zombie_sarcofago', sarcofagoRow);
enemiesInWave++; // Add to enemy count
}
}
}
// Calculate enemy count with 5% increase per wave after wave 10
var waveMultiplier = 1 + (currentWave - 10) * 0.05;
var baseCount = 7; // Base count similar to wave 10
var scaledEnemyCount = Math.floor(baseCount * waveMultiplier);
bucketZombiesLeft = Math.floor(scaledEnemyCount * 0.6); // 60% bucket zombies
// Calculate rugby zombies for endless mode (every 5 waves adds 1 more)
var rugbyZombiesLeft = 0;
if (currentWave >= 11) {
var rugbyBaseCount = 3; // Base count for waves 11-15
var additionalRugby = Math.floor((currentWave - 11) / 5); // +1 every 5 waves
rugbyZombiesLeft = rugbyBaseCount + additionalRugby;
}
enemiesInWave = scaledEnemyCount + rugbyZombiesLeft + (currentWave % 5 === 0 ? 1 : 0); // Add rugby and Zombiestein if applicable
enemiesKilled = 0;
// Set up spawn timer for endless waves
var normalZombiesLeft = Math.floor(scaledEnemyCount * 0.15); // 15% normal
var coneZombiesLeft = Math.floor(scaledEnemyCount * 0.25); // 25% cone
var spawnTimer = LK.setInterval(function () {
if (enemiesInWave > 0) {
var row = Math.floor(Math.random() * gridRows);
var zombieType = null;
// Miner zombie spawning (every wave in endless mode) - only if selected
var shouldSpawnMinerZombie = minerZombiesSpawnedThisWave < maxMinerZombiesPerWave && selectedZombieTypes.indexOf('miner_zombie') !== -1 && Math.random() < 0.3;
// Check if we should spawn a rugby zombie for endless mode (only if selected)
var shouldSpawnRugbyZombie = rugbyZombiesLeft > 0 && selectedZombieTypes.indexOf('rugby_zombie') !== -1 && Math.random() < 0.2;
// Check if we should spawn a zombie ra (odd waves excluding 1 and 3) in endless mode (only if selected)
var shouldSpawnZombieRa = currentWave % 2 === 1 && currentWave > 3 && selectedZombieTypes.indexOf('zombie_ra') !== -1 && Math.random() < 0.25;
// Check if we should spawn a zombie canon (wave 9+) in endless mode (only if selected)
var shouldSpawnZombieCanon = currentWave >= 9 && selectedZombieTypes.indexOf('zombie_canon') !== -1 && Math.random() < 0.3;
if (shouldSpawnMinerZombie) {
zombieType = 'miner_zombie';
minerZombiesSpawnedThisWave++;
} else if (shouldSpawnZombieCanon) {
zombieType = 'zombie_canon';
} else if (shouldSpawnRugbyZombie) {
zombieType = 'rugby_zombie';
rugbyZombiesLeft--;
} else if (shouldSpawnZombieRa) {
zombieType = 'zombie_ra';
} else {
// Pick random zombie type from selected types (excluding special zombies)
var availableTypes = [];
if (selectedZombieTypes.indexOf('zombie') !== -1) {
availableTypes.push('zombie');
}
if (selectedZombieTypes.indexOf('cone_zombie') !== -1) {
availableTypes.push('cone_zombie');
}
if (selectedZombieTypes.indexOf('bucket_zombie') !== -1) {
availableTypes.push('bucket_zombie');
}
if (selectedZombieTypes.indexOf('sun_zombie') !== -1) {
availableTypes.push('sun_zombie');
}
if (selectedZombieTypes.indexOf('zombie_canon') !== -1) {
availableTypes.push('zombie_canon');
}
if (selectedZombieTypes.indexOf('zombie_cascanueces') !== -1) {
availableTypes.push('zombie_cascanueces');
}
if (availableTypes.length > 0) {
var randomIndex = Math.floor(Math.random() * availableTypes.length);
zombieType = availableTypes[randomIndex];
}
}
// Only spawn if we have a valid zombie type
if (zombieType) {
spawnZombie(zombieType, row);
}
enemiesInWave--;
} else {
LK.clearInterval(spawnTimer);
}
}, 800); // Faster spawn rate for endless mode
return; // Exit function early for endless mode
}
if (currentWave === 1) {
// Wave 1: 3-4 normal zombies with collision detection
var normalCount = Math.floor(Math.random() * 2) + 3;
var spawnQueue = [];
for (var i = 0; i < normalCount; i++) {
var row = Math.floor(Math.random() * gridRows);
// For personalize mode, check if zombie type is selected, otherwise use normal zombie
var zombieType = 'zombie';
if (gameMode === 'personalize' && selectedZombieTypes.indexOf('zombie') === -1) {
// If normal zombie not selected, pick a random selected type
if (selectedZombieTypes.length > 0) {
var availableEndlessTypes = selectedZombieTypes.filter(function (type) {
return type !== 'miner_zombie' && type !== 'rugby_zombie' && type !== 'zombiestein' && type !== 'zombie_sol' && type !== 'zombie_canon';
});
if (availableEndlessTypes.length > 0) {
var randomIndex = Math.floor(Math.random() * availableEndlessTypes.length);
zombieType = availableEndlessTypes[randomIndex];
} else {
zombieType = selectedZombieTypes[Math.floor(Math.random() * selectedZombieTypes.length)];
}
}
}
spawnQueue.push({
type: zombieType,
row: row,
delay: 0
});
}
// Process spawn queue with collision detection
var spawnIndex = 0;
var spawnProcessor = LK.setInterval(function () {
if (spawnIndex < spawnQueue.length) {
var spawn = spawnQueue[spawnIndex];
if (canSpawnWithoutCollision(spawn.row)) {
spawnZombie(spawn.type, spawn.row);
lastSpawnTime[spawn.row] = LK.ticks;
lastSpawnRow[spawn.row] = LK.ticks;
spawnIndex++;
} else {
// Add 1 second delay
spawn.delay += 60;
}
} else {
LK.clearInterval(spawnProcessor);
}
}, 60); // Check every second
enemiesInWave = normalCount;
} else if (currentWave === 2) {
// Wave 2: 3-4 normal zombies, 75% chance for 1 sun zombie, 1 cone zombie with collision detection
var normalCount = Math.floor(Math.random() * 2) + 3;
var sunZombieCount = gameMode === 'endless' && selectedZombieTypes.indexOf('sun_zombie') === -1 ? 0 : Math.random() < 0.75 ? 1 : 0;
var coneCount = gameMode === 'endless' && selectedZombieTypes.indexOf('cone_zombie') === -1 ? 0 : 1;
var spawnQueue = [];
// Add normal zombies to queue
for (var i = 0; i < normalCount; i++) {
var row = Math.floor(Math.random() * gridRows);
// For endless mode, check if zombie type is selected
var zombieType = 'zombie';
if (gameMode === 'endless' && selectedZombieTypes.indexOf('zombie') === -1) {
// If normal zombie not selected, pick a random selected type
if (selectedZombieTypes.length > 0) {
var randomIndex = Math.floor(Math.random() * selectedZombieTypes.length);
zombieType = selectedZombieTypes[randomIndex];
}
}
spawnQueue.push({
type: zombieType,
row: row,
delay: 0
});
}
// Add sun zombie if chance succeeds and selected in endless mode
if (sunZombieCount > 0) {
var row = Math.floor(Math.random() * gridRows);
spawnQueue.push({
type: 'sun_zombie',
row: row,
delay: 0
});
sunZombieSpawnedThisWave = true;
}
// Add cone zombie if selected in endless mode
if (coneCount > 0) {
var row = Math.floor(Math.random() * gridRows);
spawnQueue.push({
type: 'cone_zombie',
row: row,
delay: 0
});
}
// Process spawn queue with collision detection
var spawnIndex = 0;
var spawnProcessor = LK.setInterval(function () {
if (spawnIndex < spawnQueue.length) {
var spawn = spawnQueue[spawnIndex];
if (canSpawnWithoutCollision(spawn.row)) {
spawnZombie(spawn.type, spawn.row);
lastSpawnTime[spawn.row] = LK.ticks;
lastSpawnRow[spawn.row] = LK.ticks;
spawnIndex++;
} else {
// Add 1 second delay
spawn.delay += 60;
}
} else {
LK.clearInterval(spawnProcessor);
}
}, 60); // Check every second
enemiesInWave = normalCount + sunZombieCount + coneCount;
} else if (currentWave === 3) {
bucketZombiesLeft = gameMode === 'personalize' && selectedZombieTypes.indexOf('bucket_zombie') === -1 ? 0 : 1; // Only 1 bucket zombie in wave 3 if selected
enemiesInWave = Math.floor(Math.random() * 2) + 3 + Math.floor(Math.random() * 2) + 1 + bucketZombiesLeft; // normal + cone + bucket
} else if (currentWave === 4) {
bucketZombiesLeft = gameMode === 'personalize' && selectedZombieTypes.indexOf('bucket_zombie') === -1 ? 0 : Math.floor(Math.random() * 2) + 2; // 2-3 bucket zombies if selected
enemiesInWave = Math.floor(Math.random() * 2) + 3 + Math.floor(Math.random() * 2) + 1 + bucketZombiesLeft;
} else if (currentWave === 5) {
bucketZombiesLeft = gameMode === 'personalize' && selectedZombieTypes.indexOf('bucket_zombie') === -1 ? 0 : Math.floor(Math.random() * 2) + 3; // 3-4 bucket zombies if selected
enemiesInWave = Math.floor(Math.random() * 2) + 3 + Math.floor(Math.random() * 2) + 1 + bucketZombiesLeft;
} else {
// From wave 6+: spawn 50% fewer zombies but increase cone/bucket probability by 25%
bucketZombiesLeft = gameMode === 'personalize' && selectedZombieTypes.indexOf('bucket_zombie') === -1 ? 0 : Math.floor((Math.floor(Math.random() * 2) + 4 + (currentWave - 6)) * 0.5); // 50% fewer bucket zombies if selected
enemiesInWave = Math.floor((5 + currentWave * 2) * 0.5); // 50% fewer total enemies
// Add rugby zombies for waves 8-10
var rugbyZombiesLeft = 0;
if (currentWave >= 8 && currentWave <= 9 && (gameMode === 'normal' || selectedZombieTypes.indexOf('rugby_zombie') !== -1)) {
rugbyZombiesLeft = 1;
} else if (currentWave === 10 && (gameMode === 'normal' || selectedZombieTypes.indexOf('rugby_zombie') !== -1)) {
rugbyZombiesLeft = 2;
}
enemiesInWave += rugbyZombiesLeft;
// Spawn Zombiestein in wave 10 (only once per game)
if (currentWave === 10 && !zombiesteinSpawned && (gameMode === 'normal' || selectedZombieTypes.indexOf('zombiestein') !== -1)) {
var zombiesteinRow = Math.floor(Math.random() * gridRows);
spawnZombie('zombiestein', zombiesteinRow);
zombiesteinSpawned = true;
zombiesteinAlive = true;
enemiesInWave++; // Add Zombiestein to enemy count
}
}
enemiesKilled = 0;
// Only set up spawn timer for waves 3 and above (waves 1-2 spawn immediately)
if (currentWave >= 3) {
var normalZombiesLeft = Math.floor(Math.random() * 2) + 3;
var coneZombiesLeft = Math.floor(Math.random() * 2) + 1;
var spawnTimer = LK.setInterval(function () {
if (enemiesInWave > 0) {
var row = Math.floor(Math.random() * gridRows);
var zombieType;
// Determine sun zombie probability based on wave
var sunZombieProbability = 0;
if (currentWave === 3) {
sunZombieProbability = 0.50;
} else if (currentWave === 4) {
sunZombieProbability = 0.25;
}
// Check if we should spawn a sun zombie (only waves 3-4, max 1 per wave)
var shouldSpawnSunZombie = currentWave >= 3 && currentWave <= 4 && !sunZombieSpawnedThisWave && Math.random() < sunZombieProbability && (gameMode === 'normal' || selectedZombieTypes.indexOf('sun_zombie') !== -1);
// Check if we should spawn a miner zombie (waves 6+, 100% chance, max per wave limit)
var shouldSpawnMinerZombie = currentWave >= 6 && minerZombiesSpawnedThisWave < maxMinerZombiesPerWave && Math.random() < 1.0 && (gameMode === 'normal' || selectedZombieTypes.indexOf('miner_zombie') !== -1);
// Check if we should spawn a rugby zombie (waves 8-10)
var shouldSpawnRugbyZombie = currentWave >= 8 && currentWave <= 10 && rugbyZombiesLeft > 0 && Math.random() < 0.3 && (gameMode === 'normal' || selectedZombieTypes.indexOf('rugby_zombie') !== -1);
// Check if we should spawn a zombie ra (odd waves excluding 1 and 3)
var shouldSpawnZombieRa = currentWave % 2 === 1 && currentWave > 3 && Math.random() < 0.25 && (gameMode === 'normal' || selectedZombieTypes.indexOf('zombie_ra') !== -1);
// Check if we should spawn a zombie canon (wave 9+, before zombiestein wave)
var shouldSpawnZombieCanon = currentWave >= 9 && currentWave < (zombiesteinSpawned ? currentWave : 10) && Math.random() < 0.3 && (gameMode === 'normal' || selectedZombieTypes.indexOf('zombie_canon') !== -1);
// Check if we should spawn a zombie cascanueces (waves 12, 17, 22, 27, 32, etc.)
var shouldSpawnZombieCascanueces = (currentWave - 12) % 5 === 0 && currentWave >= 12 && Math.random() < 0.3 && (gameMode === 'normal' || selectedZombieTypes.indexOf('zombie_cascanueces') !== -1);
if (shouldSpawnSunZombie) {
zombieType = 'sun_zombie';
sunZombieSpawnedThisWave = true; // Mark that sun zombie has been spawned this wave
} else if (shouldSpawnMinerZombie) {
zombieType = 'miner_zombie';
minerZombiesSpawnedThisWave++; // Increment miner count for this wave
} else if (shouldSpawnRugbyZombie) {
zombieType = 'rugby_zombie';
rugbyZombiesLeft--;
} else if (shouldSpawnZombieRa) {
zombieType = 'zombie_ra';
} else if (shouldSpawnZombieCanon) {
zombieType = 'zombie_canon';
} else if (shouldSpawnZombieCascanueces) {
zombieType = 'zombie_cascanueces';
} else if (currentWave >= 3) {
// From wave 3 onwards, include bucket zombies
// For personalize mode, only spawn selected zombie types
if (gameMode === 'personalize') {
// Pick random zombie type from selected types (excluding special zombies)
var availableTypes = [];
if (selectedZombieTypes.indexOf('zombie') !== -1 && normalZombiesLeft > 0) {
availableTypes.push('zombie');
}
if (selectedZombieTypes.indexOf('cone_zombie') !== -1 && coneZombiesLeft > 0) {
availableTypes.push('cone_zombie');
}
if (selectedZombieTypes.indexOf('bucket_zombie') !== -1 && bucketZombiesLeft > 0) {
availableTypes.push('bucket_zombie');
}
if (selectedZombieTypes.indexOf('sun_zombie') !== -1) {
availableTypes.push('sun_zombie');
}
if (selectedZombieTypes.indexOf('zombie_canon') !== -1) {
availableTypes.push('zombie_canon');
}
if (selectedZombieTypes.indexOf('zombie_cascanueces') !== -1) {
availableTypes.push('zombie_cascanueces');
}
if (availableTypes.length > 0) {
var randomIndex = Math.floor(Math.random() * availableTypes.length);
zombieType = availableTypes[randomIndex];
// Decrement counters
if (zombieType === 'zombie') {
normalZombiesLeft--;
} else if (zombieType === 'cone_zombie') {
coneZombiesLeft--;
} else if (zombieType === 'bucket_zombie') {
bucketZombiesLeft--;
}
} else {
zombieType = 'zombie'; // fallback
}
} else {
// Original logic for normal mode
// For wave 6+, increase cone and bucket zombie probability by 25%
if (normalZombiesLeft > 0 && coneZombiesLeft > 0 && bucketZombiesLeft > 0) {
var rand = Math.random();
if (currentWave >= 6) {
// Wave 6+: 25% normal, 37.5% cone, 37.5% bucket (25% more cone/bucket)
if (rand < 0.25) {
zombieType = 'zombie';
normalZombiesLeft--;
} else if (rand < 0.625) {
zombieType = 'cone_zombie';
coneZombiesLeft--;
} else {
zombieType = 'bucket_zombie';
bucketZombiesLeft--;
}
} else {
// Wave 3-5: original probabilities
if (rand < 0.4) {
zombieType = 'zombie';
normalZombiesLeft--;
} else if (rand < 0.7) {
zombieType = 'cone_zombie';
coneZombiesLeft--;
} else {
zombieType = 'bucket_zombie';
bucketZombiesLeft--;
}
}
} else if (normalZombiesLeft > 0 && coneZombiesLeft > 0) {
if (currentWave >= 6) {
// Wave 6+: 35% normal, 65% cone (25% more cone)
zombieType = Math.random() < 0.35 ? 'zombie' : 'cone_zombie';
} else {
// Wave 3-5: original 60/40 split
zombieType = Math.random() < 0.6 ? 'zombie' : 'cone_zombie';
}
if (zombieType === 'zombie') {
normalZombiesLeft--;
} else {
coneZombiesLeft--;
}
} else if (normalZombiesLeft > 0 && bucketZombiesLeft > 0) {
if (currentWave >= 6) {
// Wave 6+: 35% normal, 65% bucket (25% more bucket)
zombieType = Math.random() < 0.35 ? 'zombie' : 'bucket_zombie';
} else {
// Wave 3-5: original 60/40 split
zombieType = Math.random() < 0.6 ? 'zombie' : 'bucket_zombie';
}
if (zombieType === 'zombie') {
normalZombiesLeft--;
} else {
bucketZombiesLeft--;
}
} else if (coneZombiesLeft > 0 && bucketZombiesLeft > 0) {
zombieType = Math.random() < 0.5 ? 'cone_zombie' : 'bucket_zombie';
if (zombieType === 'cone_zombie') {
coneZombiesLeft--;
} else {
bucketZombiesLeft--;
}
} else if (normalZombiesLeft > 0) {
zombieType = 'zombie';
normalZombiesLeft--;
} else if (coneZombiesLeft > 0) {
zombieType = 'cone_zombie';
coneZombiesLeft--;
} else if (bucketZombiesLeft > 0) {
zombieType = 'bucket_zombie';
bucketZombiesLeft--;
} else {
zombieType = 'zombie'; // fallback
}
}
} else {
// Fallback for other waves
if (gameMode === 'personalize') {
// Pick from selected types
var availableTypes = [];
if (selectedZombieTypes.indexOf('zombie') !== -1) {
availableTypes.push('zombie');
}
if (selectedZombieTypes.indexOf('cone_zombie') !== -1) {
availableTypes.push('cone_zombie');
}
if (availableTypes.length > 0) {
var randomIndex = Math.floor(Math.random() * availableTypes.length);
zombieType = availableTypes[randomIndex];
} else {
zombieType = 'zombie'; // fallback
}
} else {
zombieType = Math.random() < 0.5 ? 'zombie' : 'cone_zombie';
}
}
spawnZombie(zombieType, row);
enemiesInWave--;
} else {
LK.clearInterval(spawnTimer);
}
}, 1200);
} // Close the if (currentWave >= 3) block
}
// Mouse move handler to update ghost plant position
game.move = function (x, y, obj) {
if (ghostPlant && selectedPlantType) {
var gridPos = getGridPosition(x, y);
if (gridPos) {
// Snap to grid position
ghostPlant.x = gridStartX + gridPos.x * cellSize;
ghostPlant.y = gridStartY + gridPos.y * cellSize;
// Change color based on validity
if (canPlacePlant(gridPos.x, gridPos.y, selectedPlantType)) {
ghostPlant.tint = 0x88FF88; // Green tint for valid placement
} else {
ghostPlant.tint = 0xFF8888; // Red tint for invalid placement
}
} else {
// Follow cursor if outside grid
ghostPlant.x = x;
ghostPlant.y = y;
ghostPlant.tint = 0xFF8888; // Red tint when outside grid
}
}
};
// Game input handling
game.down = function (x, y, obj) {
// Check if clicking on a sun
for (var i = 0; i < suns.length; i++) {
if (suns[i].intersects({
x: x,
y: y,
width: 1,
height: 1
})) {
return; // Let sun handle its own click
}
}
// Check if using shovel to remove plant
if (selectedTool === 'shovel') {
var gridPos = getGridPosition(x, y);
if (gridPos && plants[gridPos.y][gridPos.x] !== null) {
// Remove the plant
var plant = plants[gridPos.y][gridPos.x];
plant.destroy();
plants[gridPos.y][gridPos.x] = null;
LK.effects.flashObject(gridCells[gridPos.y][gridPos.x], 0xFFFF00, 300);
updatePlantSelection();
return;
} else if (gridPos) {
// Flash red if trying to remove from empty square
LK.effects.flashObject(gridCells[gridPos.y][gridPos.x], 0xFF0000, 300);
} else {
// Clicking outside grid cancels shovel selection
selectedTool = null;
shovelButton.alpha = 0.8;
shovelButton.scaleX = shovelButton.scaleY = 1.5;
updatePlantSelection();
}
}
// Check if placing a plant
else if (selectedPlantType) {
var gridPos = getGridPosition(x, y);
if (gridPos) {
if (canPlacePlant(gridPos.x, gridPos.y, selectedPlantType)) {
// Successfully place the plant
if (placePlant(gridPos.x, gridPos.y, selectedPlantType)) {
selectedPlantType = null;
updatePlantSelection();
return;
}
} else if (plants[gridPos.y][gridPos.x] !== null) {
// Flash red if trying to place on occupied square
LK.effects.flashObject(gridCells[gridPos.y][gridPos.x], 0xFF0000, 300);
} else if (sunPoints < plantCosts[selectedPlantType]) {
// Flash yellow if not enough suns
LK.effects.flashObject(gridCells[gridPos.y][gridPos.x], 0xFFFF00, 300);
}
} else {
// Clicking outside grid cancels selection
selectedPlantType = null;
updatePlantSelection();
}
}
};
// Initialize UI
updatePlantSelection();
updateSunDisplay();
updateLivesDisplay();
updateWaveDisplay();
// Skip wave button for endless mode
var skipWaveButton = null;
function createSkipWaveButton() {
if (gameMode === 'endless' && !skipWaveButton) {
skipWaveButton = LK.getAsset('gridcell', {
anchorX: 0.5,
anchorY: 0.5,
x: 1800,
y: 100,
scaleX: 2,
scaleY: 1,
color: 0x888888 // Start gray
});
skipWaveButton.interactive = true;
skipWaveButton.buttonMode = true;
skipWaveButton.alpha = 0.5; // Start disabled
skipWaveButton.down = function (x, y, obj) {
if (waveInProgress) {
// Force end current wave and start next one
waveInProgress = false;
wavesCompleted++;
currentWave++;
updateWaveDisplay();
updateSkipWaveButton();
// Start next wave immediately
LK.setTimeout(function () {
startWave();
}, 500);
}
};
game.addChild(skipWaveButton);
var skipWaveText = new Text2('Skip Wave', {
size: 40,
fill: 0xFFFFFF
});
skipWaveText.anchor.set(0.5, 0.5);
skipWaveText.x = 1800;
skipWaveText.y = 100;
game.addChild(skipWaveText);
skipWaveButton.textElement = skipWaveText;
}
}
function canSkipWave() {
// Check if all zombies for current wave have been spawned
return waveInProgress && enemiesInWave <= 0;
}
function updateSkipWaveButton() {
if (skipWaveButton && gameMode === 'endless') {
if (waveInProgress) {
// Enable button - wave is in progress
skipWaveButton.alpha = 1.0;
skipWaveButton.tint = 0x4CAF50; // Green
skipWaveButton.interactive = true;
} else {
// Disable button - no wave in progress
skipWaveButton.alpha = 0.5;
skipWaveButton.tint = 0x888888; // Gray
skipWaveButton.interactive = false;
}
}
}
// Game will start after mode selection
// Main game loop
game.update = function () {
// Enable lag optimization from wave 8 onwards
if (currentWave >= 8 && !lagOptimizationActive) {
lagOptimizationActive = true;
console.log("Lag optimization activated for wave", currentWave);
}
// Performance cleanup every 3 seconds when lag optimization is active
if (lagOptimizationActive) {
cleanupCounter++;
if (cleanupCounter >= 180) {
// Every 3 seconds at 60fps
performanceCleanup();
cleanupCounter = 0;
}
}
// Update wallnut cooldown
if (wallnutCooldown > 0) {
wallnutCooldown--;
if (wallnutCooldown === 0) {
updatePlantSelection(); // Refresh button appearance when cooldown ends
}
}
// Update nuez primitiva cooldown
if (nuezPrimitivaCooldown > 0) {
nuezPrimitivaCooldown--;
if (nuezPrimitivaCooldown === 0) {
updatePlantSelection(); // Refresh button appearance when cooldown ends
}
}
// Update petacereza cooldown
if (petacerezaCooldown > 0) {
petacerezaCooldown--;
if (petacerezaCooldown === 0) {
updatePlantSelection(); // Refresh button appearance when cooldown ends
}
}
// Falling sun system - every 10 seconds (optimized for high waves)
sunTimer++;
var sunInterval = lagOptimizationActive ? 1080 : 720; // 18 seconds when optimized, 12 seconds normally
if (sunTimer >= sunInterval) {
// Only spawn sun if we don't have too many already
if (suns.length < (lagOptimizationActive ? 8 : 15)) {
var fallingSun = new Sun(Math.random() * (gridStartX + gridCols * cellSize - gridStartX) + gridStartX, -50);
suns.push(fallingSun);
game.addChild(fallingSun);
// Animate falling sun
tween(fallingSun, {
y: Math.random() * 200 + 300
}, {
duration: 2400,
easing: tween.easeOut
});
}
sunTimer = 0;
}
// Check if current wave should end (22 seconds timer-based)
if (waveInProgress && LK.ticks - waveStartTick >= 1584) {
// 26.4 seconds at 60fps
waveInProgress = false;
wavesCompleted++;
if (gameMode === 'normal' && wavesCompleted >= totalWaves) {
// Check if Zombiestein was spawned and is still alive
if (zombiesteinSpawned && zombiesteinAlive) {
// Don't end game until Zombiestein is defeated
console.log("All waves completed but Zombiestein still alive!");
return;
}
LK.effects.flashScreen(0x4CAF50, 1000);
LK.showYouWin();
return;
} else if (gameMode === 'endless') {
// For endless mode, just continue to next wave
// Reset Zombiestein spawn flag for next potential spawn
if (currentWave % 5 === 0) {
zombiesteinSpawned = false;
zombiesteinAlive = false;
}
}
currentWave++;
updateWaveDisplay();
// Start next wave after delay
LK.setTimeout(function () {
startWave();
}, 3000);
}
// Update skip wave button state
if (gameMode === 'endless') {
updateSkipWaveButton();
}
// Auto-start next wave if no wave in progress and we haven't completed all waves (and mode is selected)
if (gameModeSelected && !waveInProgress && (gameMode === 'endless' || wavesCompleted < totalWaves) && !nextWaveScheduled) {
nextWaveScheduled = true;
LK.setTimeout(function () {
nextWaveScheduled = false;
startWave();
}, 1000);
}
};