/**** * 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 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 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)) { 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; 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 () { 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 ****/ // Grid setup - bigger grid centered on screen // Plant defenders // Projectiles // Undead enemies // UI and grid // Sounds 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']; var plantCosts = { 'peashooter': 100, 'sunflower': 50, 'birasol': 225, 'snowpea': 175, 'fireshot': 175, 'plantorcha': 175, 'wallnut': 50, 'repetidora': 200, 'nuezPrimitiva': 125, 'petacereza': 150 }; // 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 petacereza - place it above birasol if (plantType === 'petacereza') { xPos = 420; // Same x position as birasol yPos = uiY - 266; // Above birasol, 10% higher than previous position (242 * 1.1 = 266) } 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; 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 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 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)) {
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;
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 () {
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
****/
// Grid setup - bigger grid centered on screen
// Plant defenders
// Projectiles
// Undead enemies
// UI and grid
// Sounds
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'];
var plantCosts = {
'peashooter': 100,
'sunflower': 50,
'birasol': 225,
'snowpea': 175,
'fireshot': 175,
'plantorcha': 175,
'wallnut': 50,
'repetidora': 200,
'nuezPrimitiva': 125,
'petacereza': 150
};
// 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 petacereza - place it above birasol
if (plantType === 'petacereza') {
xPos = 420; // Same x position as birasol
yPos = uiY - 266; // Above birasol, 10% higher than previous position (242 * 1.1 = 266)
}
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;
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);
}
};