/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
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();
}
}
// 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.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 snowball = createProjectile('snowball', self.x, self.y, 6.4, 20 * self.level, plantId);
if (snowball) {
LK.getSound('shoot').play();
}
};
return self;
});
var Repetidora = PlantBase.expand(function () {
var self = PlantBase.call(this, 'peashooter');
self.cost = 200;
self.isShootingSecondPea = false;
self.secondPeaTimer = 0;
self.update = function () {
self.shootTimer++;
if (self.shootTimer >= self.shootDelay) {
var target = self.findTarget();
if (target && !self.isShootingSecondPea) {
self.shoot(target);
self.shootTimer = 0;
self.isShootingSecondPea = true;
self.secondPeaTimer = 18; // 0.3 seconds at 60fps
}
}
// Handle second pea timing
if (self.isShootingSecondPea) {
self.secondPeaTimer--;
if (self.secondPeaTimer <= 0) {
var target = self.findTarget();
if (target) {
self.shoot(target);
}
self.isShootingSecondPea = false;
}
}
};
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 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 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;
}
// 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);
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;
var graphics = self.attachAsset('sun', {
anchorX: 0.5,
anchorY: 0.5
});
self.down = function (x, y, obj) {
sunPoints += self.value;
updateSunDisplay();
self.collect();
};
self.collect = function () {
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.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.update = function () {
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer === 0) {
self.speed = self.originalSpeed;
graphics.tint = 0xFFFFFF;
}
}
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]) {
return plants[self.gridY][cellX];
}
return null;
};
return self;
});
var Zombiestein = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombiestein');
self.health = 3000;
self.maxHealth = 3000;
self.speed = 0.92; // 15% faster than original 0.8 speed
self.originalSpeed = 0.92;
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 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, 'zombie');
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 MinerZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'miner_zombie');
self.health = 190;
self.maxHealth = 190;
self.speed = 0.8;
self.originalSpeed = 0.8;
// Override the initialization to spawn at 5th column and show explosion
self.initializeMiner = function (row) {
self.gridY = row;
// Position at 5th column (index 4)
self.x = gridStartX + 4 * cellSize;
self.y = gridStartY + row * cellSize;
// Create explosion effect
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 2,
scaleY: 2
});
game.addChild(explosion);
// Animate explosion and remove it
tween(explosion, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
};
return self;
});
var ConeZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'cone_zombie');
self.health = 300;
self.maxHealth = 300;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.hasChangedAsset = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if cone zombie should switch to normal zombie appearance
if (self.health < 191 && !self.hasChangedAsset) {
self.hasChangedAsset = true;
// Remove current graphics and add normal zombie version
self.removeChild(self.children[0]);
var normalGraphics = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
var BucketZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'bucket_zombie');
self.health = 375;
self.maxHealth = 375;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.hasChangedAsset = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if bucket zombie should switch to normal zombie appearance
if (self.health < 191 && !self.hasChangedAsset) {
self.hasChangedAsset = true;
// Remove current graphics and add normal zombie version
self.removeChild(self.children[0]);
var normalGraphics = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1B5E20
});
/****
* Game Code
****/
// Sounds
// UI and grid
// Undead enemies
// Projectiles
// Plant defenders
// Grid setup - bigger grid centered on screen
var gridRows = 5;
var gridCols = 9;
var cellSize = 144;
var gridStartX = (2048 - gridCols * cellSize) / 2;
var gridStartY = (2732 - gridRows * cellSize) / 2;
// Game state
var plants = [];
var enemies = [];
var projectiles = [];
var suns = [];
var lawnmowers = [];
var sunPoints = 500;
var lives = 3;
var currentWave = 1;
var wavesCompleted = 0;
var totalWaves = 10;
var waveInProgress = false;
var enemiesInWave = 0;
var enemiesKilled = 0;
var sunTimer = 0;
var waveStartTick = 0;
var nextWaveScheduled = false;
var sunZombieSpawnedThisWave = false;
// 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', 'peashooter', 'wallnut', 'repetidora'];
var plantCosts = {
'peashooter': 100,
'sunflower': 50,
'wallnut': 50,
'repetidora': 200
};
// Wallnut cooldown system
var wallnutCooldown = 0;
var wallnutCooldownTime = 720; // 12 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);
}
// 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 button = LK.getAsset(plantTypes[i], {
anchorX: 0.5,
anchorY: 0.5,
x: 200 + i * 220,
y: uiY,
scaleX: 2.25,
scaleY: 2.25
});
button.plantType = plantTypes[i];
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
}
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[plantTypes[i]], {
size: 40,
fill: 0xFFFFFF
});
costText.anchor.set(0.5, 0);
costText.x = button.x;
costText.y = button.y + 80;
game.addChild(costText);
}
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 {
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() {
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);
}
}
// 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 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 'wallnut':
plant = new Wallnut();
break;
case 'snowpea':
plant = new SnowPea();
break;
case 'repetidora':
plant = new Repetidora();
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();
}
// Flash the grid cell to show successful placement
LK.effects.flashObject(gridCells[gridY][gridX], 0x00FF00, 300);
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 'miner_zombie':
zombie = new MinerZombie();
// Special positioning for miner
zombie.initializeMiner(row);
enemies.push(zombie);
game.addChild(zombie);
return;
// Return early since positioning is handled
case 'zombiestein':
zombie = new Zombiestein();
break;
default:
return;
}
zombie.gridY = row;
zombie.x = 2100;
zombie.y = gridStartY + row * cellSize;
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;
if (currentWave === 1) {
// Wave 1: 3-4 normal zombies
var normalCount = Math.floor(Math.random() * 2) + 3;
for (var i = 0; i < normalCount; i++) {
var row = Math.floor(Math.random() * gridRows);
spawnZombie('zombie', row);
}
enemiesInWave = normalCount;
} else if (currentWave === 2) {
// Wave 2: 3-4 normal zombies, 75% chance for 1 sun zombie, 1 cone zombie
var normalCount = Math.floor(Math.random() * 2) + 3;
var sunZombieCount = Math.random() < 0.75 ? 1 : 0;
var coneCount = 1;
// Spawn normal zombies
for (var i = 0; i < normalCount; i++) {
var row = Math.floor(Math.random() * gridRows);
spawnZombie('zombie', row);
}
// Spawn sun zombie if chance succeeds
if (sunZombieCount > 0) {
var row = Math.floor(Math.random() * gridRows);
spawnZombie('sun_zombie', row);
sunZombieSpawnedThisWave = true;
}
// Spawn cone zombie
var row = Math.floor(Math.random() * gridRows);
spawnZombie('cone_zombie', row);
enemiesInWave = normalCount + sunZombieCount + coneCount;
} else if (currentWave === 3) {
bucketZombiesLeft = 1; // Only 1 bucket zombie in wave 3
enemiesInWave = Math.floor(Math.random() * 2) + 3 + Math.floor(Math.random() * 2) + 1 + bucketZombiesLeft; // normal + cone + bucket
} else if (currentWave === 4) {
bucketZombiesLeft = Math.floor(Math.random() * 2) + 2; // 2-3 bucket zombies
enemiesInWave = Math.floor(Math.random() * 2) + 3 + Math.floor(Math.random() * 2) + 1 + bucketZombiesLeft;
} else if (currentWave === 5) {
bucketZombiesLeft = Math.floor(Math.random() * 2) + 3; // 3-4 bucket zombies
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 = Math.floor((Math.floor(Math.random() * 2) + 4 + (currentWave - 6)) * 0.5); // 50% fewer bucket zombies
enemiesInWave = Math.floor((5 + currentWave * 2) * 0.5); // 50% fewer total enemies
// Spawn Zombiestein in wave 10 (only once per game)
if (currentWave === 10 && !zombiesteinSpawned) {
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;
// 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;
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 (currentWave >= 3) {
// From wave 3 onwards, include bucket zombies
// 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
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();
// Start first wave after a delay
LK.setTimeout(function () {
startWave();
}, 10000);
// 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
}
}
// 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 (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;
}
currentWave++;
updateWaveDisplay();
// Start next wave after delay
LK.setTimeout(function () {
startWave();
}, 3000);
}
// Auto-start next wave if no wave in progress and we haven't completed all waves
if (!waveInProgress && wavesCompleted < totalWaves && !nextWaveScheduled) {
nextWaveScheduled = true;
LK.setTimeout(function () {
nextWaveScheduled = false;
startWave();
}, 1000);
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
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();
}
}
// 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.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 snowball = createProjectile('snowball', self.x, self.y, 6.4, 20 * self.level, plantId);
if (snowball) {
LK.getSound('shoot').play();
}
};
return self;
});
var Repetidora = PlantBase.expand(function () {
var self = PlantBase.call(this, 'peashooter');
self.cost = 200;
self.isShootingSecondPea = false;
self.secondPeaTimer = 0;
self.update = function () {
self.shootTimer++;
if (self.shootTimer >= self.shootDelay) {
var target = self.findTarget();
if (target && !self.isShootingSecondPea) {
self.shoot(target);
self.shootTimer = 0;
self.isShootingSecondPea = true;
self.secondPeaTimer = 18; // 0.3 seconds at 60fps
}
}
// Handle second pea timing
if (self.isShootingSecondPea) {
self.secondPeaTimer--;
if (self.secondPeaTimer <= 0) {
var target = self.findTarget();
if (target) {
self.shoot(target);
}
self.isShootingSecondPea = false;
}
}
};
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 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 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;
}
// 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);
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;
var graphics = self.attachAsset('sun', {
anchorX: 0.5,
anchorY: 0.5
});
self.down = function (x, y, obj) {
sunPoints += self.value;
updateSunDisplay();
self.collect();
};
self.collect = function () {
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.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.update = function () {
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer === 0) {
self.speed = self.originalSpeed;
graphics.tint = 0xFFFFFF;
}
}
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]) {
return plants[self.gridY][cellX];
}
return null;
};
return self;
});
var Zombiestein = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'zombiestein');
self.health = 3000;
self.maxHealth = 3000;
self.speed = 0.92; // 15% faster than original 0.8 speed
self.originalSpeed = 0.92;
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 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, 'zombie');
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 MinerZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'miner_zombie');
self.health = 190;
self.maxHealth = 190;
self.speed = 0.8;
self.originalSpeed = 0.8;
// Override the initialization to spawn at 5th column and show explosion
self.initializeMiner = function (row) {
self.gridY = row;
// Position at 5th column (index 4)
self.x = gridStartX + 4 * cellSize;
self.y = gridStartY + row * cellSize;
// Create explosion effect
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 2,
scaleY: 2
});
game.addChild(explosion);
// Animate explosion and remove it
tween(explosion, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
};
return self;
});
var ConeZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'cone_zombie');
self.health = 300;
self.maxHealth = 300;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.hasChangedAsset = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if cone zombie should switch to normal zombie appearance
if (self.health < 191 && !self.hasChangedAsset) {
self.hasChangedAsset = true;
// Remove current graphics and add normal zombie version
self.removeChild(self.children[0]);
var normalGraphics = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
var BucketZombie = ZombieBase.expand(function () {
var self = ZombieBase.call(this, 'bucket_zombie');
self.health = 375;
self.maxHealth = 375;
self.speed = 0.8;
self.originalSpeed = 0.8;
self.hasChangedAsset = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (damage) {
originalTakeDamage(damage);
// Check if bucket zombie should switch to normal zombie appearance
if (self.health < 191 && !self.hasChangedAsset) {
self.hasChangedAsset = true;
// Remove current graphics and add normal zombie version
self.removeChild(self.children[0]);
var normalGraphics = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1B5E20
});
/****
* Game Code
****/
// Sounds
// UI and grid
// Undead enemies
// Projectiles
// Plant defenders
// Grid setup - bigger grid centered on screen
var gridRows = 5;
var gridCols = 9;
var cellSize = 144;
var gridStartX = (2048 - gridCols * cellSize) / 2;
var gridStartY = (2732 - gridRows * cellSize) / 2;
// Game state
var plants = [];
var enemies = [];
var projectiles = [];
var suns = [];
var lawnmowers = [];
var sunPoints = 500;
var lives = 3;
var currentWave = 1;
var wavesCompleted = 0;
var totalWaves = 10;
var waveInProgress = false;
var enemiesInWave = 0;
var enemiesKilled = 0;
var sunTimer = 0;
var waveStartTick = 0;
var nextWaveScheduled = false;
var sunZombieSpawnedThisWave = false;
// 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', 'peashooter', 'wallnut', 'repetidora'];
var plantCosts = {
'peashooter': 100,
'sunflower': 50,
'wallnut': 50,
'repetidora': 200
};
// Wallnut cooldown system
var wallnutCooldown = 0;
var wallnutCooldownTime = 720; // 12 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);
}
// 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 button = LK.getAsset(plantTypes[i], {
anchorX: 0.5,
anchorY: 0.5,
x: 200 + i * 220,
y: uiY,
scaleX: 2.25,
scaleY: 2.25
});
button.plantType = plantTypes[i];
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
}
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[plantTypes[i]], {
size: 40,
fill: 0xFFFFFF
});
costText.anchor.set(0.5, 0);
costText.x = button.x;
costText.y = button.y + 80;
game.addChild(costText);
}
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 {
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() {
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);
}
}
// 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 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 'wallnut':
plant = new Wallnut();
break;
case 'snowpea':
plant = new SnowPea();
break;
case 'repetidora':
plant = new Repetidora();
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();
}
// Flash the grid cell to show successful placement
LK.effects.flashObject(gridCells[gridY][gridX], 0x00FF00, 300);
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 'miner_zombie':
zombie = new MinerZombie();
// Special positioning for miner
zombie.initializeMiner(row);
enemies.push(zombie);
game.addChild(zombie);
return;
// Return early since positioning is handled
case 'zombiestein':
zombie = new Zombiestein();
break;
default:
return;
}
zombie.gridY = row;
zombie.x = 2100;
zombie.y = gridStartY + row * cellSize;
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;
if (currentWave === 1) {
// Wave 1: 3-4 normal zombies
var normalCount = Math.floor(Math.random() * 2) + 3;
for (var i = 0; i < normalCount; i++) {
var row = Math.floor(Math.random() * gridRows);
spawnZombie('zombie', row);
}
enemiesInWave = normalCount;
} else if (currentWave === 2) {
// Wave 2: 3-4 normal zombies, 75% chance for 1 sun zombie, 1 cone zombie
var normalCount = Math.floor(Math.random() * 2) + 3;
var sunZombieCount = Math.random() < 0.75 ? 1 : 0;
var coneCount = 1;
// Spawn normal zombies
for (var i = 0; i < normalCount; i++) {
var row = Math.floor(Math.random() * gridRows);
spawnZombie('zombie', row);
}
// Spawn sun zombie if chance succeeds
if (sunZombieCount > 0) {
var row = Math.floor(Math.random() * gridRows);
spawnZombie('sun_zombie', row);
sunZombieSpawnedThisWave = true;
}
// Spawn cone zombie
var row = Math.floor(Math.random() * gridRows);
spawnZombie('cone_zombie', row);
enemiesInWave = normalCount + sunZombieCount + coneCount;
} else if (currentWave === 3) {
bucketZombiesLeft = 1; // Only 1 bucket zombie in wave 3
enemiesInWave = Math.floor(Math.random() * 2) + 3 + Math.floor(Math.random() * 2) + 1 + bucketZombiesLeft; // normal + cone + bucket
} else if (currentWave === 4) {
bucketZombiesLeft = Math.floor(Math.random() * 2) + 2; // 2-3 bucket zombies
enemiesInWave = Math.floor(Math.random() * 2) + 3 + Math.floor(Math.random() * 2) + 1 + bucketZombiesLeft;
} else if (currentWave === 5) {
bucketZombiesLeft = Math.floor(Math.random() * 2) + 3; // 3-4 bucket zombies
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 = Math.floor((Math.floor(Math.random() * 2) + 4 + (currentWave - 6)) * 0.5); // 50% fewer bucket zombies
enemiesInWave = Math.floor((5 + currentWave * 2) * 0.5); // 50% fewer total enemies
// Spawn Zombiestein in wave 10 (only once per game)
if (currentWave === 10 && !zombiesteinSpawned) {
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;
// 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;
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 (currentWave >= 3) {
// From wave 3 onwards, include bucket zombies
// 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
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();
// Start first wave after a delay
LK.setTimeout(function () {
startWave();
}, 10000);
// 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
}
}
// 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 (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;
}
currentWave++;
updateWaveDisplay();
// Start next wave after delay
LK.setTimeout(function () {
startWave();
}, 3000);
}
// Auto-start next wave if no wave in progress and we haven't completed all waves
if (!waveInProgress && wavesCompleted < totalWaves && !nextWaveScheduled) {
nextWaveScheduled = true;
LK.setTimeout(function () {
nextWaveScheduled = false;
startWave();
}, 1000);
}
};