/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Bullet class
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletSprite = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = bulletSprite.width / 2;
self.vx = 0;
self.vy = 0;
self.lifetime = 60; // frames
self.update = function () {
self.x += self.vx;
self.y += self.vy;
self.lifetime--;
};
return self;
});
// Hero class
var Hero = Container.expand(function () {
var self = Container.call(this);
var heroSprite = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 0.5
});
self.heroSprite = heroSprite;
// Add gun sprite to hero's hand
var gunSprite = self.attachAsset('gun', {
anchorX: 0.2,
// grip of gun
anchorY: 0.5,
width: heroSprite.width * 0.45,
height: heroSprite.height * 0.22
});
gunSprite.x = heroSprite.width * 0.32; // offset to right hand
gunSprite.y = 0;
self.radius = heroSprite.width / 2;
self.speed = 18; // pixels per move
self.lastShotTick = 0;
self.shootCooldown = 18; // frames between shots
// Allow skin change
self.setSkin = function (skinId) {
if (!skinId || skinId === "hero") {
// Already default
return;
}
// Remove old sprite
if (self.heroSprite && self.heroSprite.parent) {
self.removeChild(self.heroSprite);
}
// Add new sprite
var newSprite = self.attachAsset(skinId, {
anchorX: 0.5,
anchorY: 0.5,
width: heroSprite.width,
height: heroSprite.height
});
self.heroSprite = newSprite;
// Move gun to top
if (gunSprite && gunSprite.parent) {
self.removeChild(gunSprite);
self.addChild(gunSprite);
}
// Update radius
self.radius = newSprite.width / 2;
};
self.update = function () {};
return self;
});
// Powerup class
var Powerup = Container.expand(function () {
var self = Container.call(this);
var powerupSprite = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = powerupSprite.width / 2;
self.type = 'rapid'; // only one type for MVP
self.update = function () {};
return self;
});
// Zombie class
var Zombie = Container.expand(function () {
var self = Container.call(this);
// Default to normal zombie asset
var zombieSprite = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
self.zombieSprite = zombieSprite;
self.radius = zombieSprite.width / 2;
self.baseSpeed = 3 + Math.random() * 2; // base speed, will be increased by wave
self.speed = self.baseSpeed;
self.target = null; // set to hero
self.maxHp = 1;
self.hp = 1;
self.wave = 1;
self.type = "normal"; // can be "normal", "fast", "tank", "crawler"
self.setWave = function (wave) {
self.wave = wave;
// Remove old sprite if present
if (self.zombieSprite && self.zombieSprite.parent) {
self.removeChild(self.zombieSprite);
}
// Decide type
var r = Math.random();
if (wave >= 7 && r < 0.18) {
// Crawler: slow, small, hard to hit
self.type = "crawler";
self.speed = 1.7 + 0.12 * wave;
self.maxHp = 2 + Math.floor(wave / 3);
self.hp = self.maxHp;
self.zombieSprite = self.attachAsset('zombie_crawler', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.zombieSprite.width / 2;
self.zombieSprite.tint = 0x8e8e8e; // grayish
} else if (wave >= 6 && r < 0.38) {
// Tank zombie: slow but high HP
self.type = "tank";
self.speed = 2 + 0.15 * wave;
self.maxHp = 3 + Math.floor(wave / 2);
self.hp = self.maxHp;
self.zombieSprite = self.attachAsset('zombie_tank', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.zombieSprite.width / 2;
self.zombieSprite.tint = 0x6e4e2e; // brownish
} else if (wave >= 3 && r < 0.63) {
// Fast zombie: high speed, low HP
self.type = "fast";
self.speed = 5 + 0.4 * wave;
self.maxHp = 1;
self.hp = 1;
self.zombieSprite = self.attachAsset('zombie_fast', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.zombieSprite.width / 2;
self.zombieSprite.tint = 0x7cf442; // bright green
} else {
// Normal zombie
self.type = "normal";
self.speed = self.baseSpeed + (wave - 1) * 0.5;
self.maxHp = 1 + Math.floor(wave / 4);
self.hp = self.maxHp;
self.zombieSprite = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.zombieSprite.width / 2;
self.zombieSprite.tint = 0x7bbf3f; // normal green
}
};
// Health bar setup
var healthBarW = zombieSprite.width * 0.7;
var healthBarH = 16;
var healthBarBg = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: healthBarW,
height: healthBarH,
color: 0x222222
});
healthBarBg.alpha = 0.7;
var healthBarFill = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: healthBarW,
height: healthBarH,
color: 0x3FA34D
});
healthBarFill.alpha = 0.92;
self.addChild(healthBarBg);
self.addChild(healthBarFill);
self.update = function () {
if (!self.target) return;
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
self.x += self.speed * dx / dist;
self.y += self.speed * dy / dist;
}
// Update health bar position and fill
var barY = -self.radius - 30;
healthBarBg.x = -healthBarW / 2;
healthBarBg.y = barY;
healthBarFill.x = -healthBarW / 2;
healthBarFill.y = barY;
var ratio = Math.max(0, Math.min(1, self.hp / self.maxHp));
healthBarFill.width = healthBarW * ratio;
if (ratio > 0.6) {
healthBarFill.tint = 0x3FA34D;
} else if (ratio > 0.3) {
healthBarFill.tint = 0xF7E733;
} else {
healthBarFill.tint = 0xD83318;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Game area
// Hero: red box
// Zombie: green ellipse
// Bullet: yellow box
// Powerup: blue ellipse
// Sound effects
// Music
// new gun asset
var GAME_W = 2048;
var GAME_H = 2732;
// State
var hero = null;
var zombies = [];
var bullets = [];
var powerups = [];
var dragNode = null;
var lastIntersecting = false;
var score = 0;
var highScore = storage.highScore || 0;
var wave = 1;
var ticksSinceStart = 0;
var zombieSpawnTick = 0;
var zombieSpawnInterval = 90; // frames
var rapidFireTicks = 0;
// --- HEALTH SYSTEM ---
var heroMaxHp = 5;
var heroHp = heroMaxHp;
// --- AMMO SYSTEM ---
var heroMaxAmmo = 12;
var heroAmmo = heroMaxAmmo;
var heroReloading = false;
var heroReloadTicks = 0;
var heroReloadTime = 60; // frames to reload
// Ammo bar UI
var ammoBarBg = new Container();
var ammoBarWidth = 420;
var ammoBarHeight = 28;
var ammoBarBgRect = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: ammoBarWidth,
height: ammoBarHeight,
color: 0x222222
});
ammoBarBgRect.alpha = 0.7;
ammoBarBg.addChild(ammoBarBgRect);
var ammoBarFill = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: ammoBarWidth,
height: ammoBarHeight,
color: 0x3B7DD8
});
ammoBarFill.alpha = 0.92;
ammoBarBg.addChild(ammoBarFill);
ammoBarBg.x = (GAME_W - ammoBarWidth) / 2;
ammoBarBg.y = 170;
LK.gui.top.addChild(ammoBarBg);
var ammoTxt = new Text2('', {
size: 60,
fill: "#fff"
});
ammoTxt.anchor.set(0.5, 0);
ammoTxt.x = GAME_W / 2;
ammoTxt.y = 210;
LK.gui.top.addChild(ammoTxt);
function updateAmmoBar() {
var ratio = Math.max(0, Math.min(1, heroAmmo / heroMaxAmmo));
ammoBarFill.width = ammoBarWidth * ratio;
if (heroReloading) {
ammoBarFill.tint = 0xF7E733;
ammoTxt.setText('RELOADING...');
} else {
ammoBarFill.tint = 0x3B7DD8;
ammoTxt.setText(heroAmmo + ' / ' + heroMaxAmmo);
}
}
updateAmmoBar();
// Health bar UI
var healthBarBg = new Container();
var healthBarWidth = 520;
var healthBarHeight = 38;
var healthBarBgRect = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: healthBarWidth,
height: healthBarHeight,
color: 0x333333
});
healthBarBgRect.alpha = 0.7;
healthBarBg.addChild(healthBarBgRect);
var healthBarFill = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: healthBarWidth,
height: healthBarHeight,
color: 0x3FA34D
});
healthBarFill.alpha = 0.92;
healthBarBg.addChild(healthBarFill);
healthBarBg.x = (GAME_W - healthBarWidth) / 2;
healthBarBg.y = 110;
LK.gui.top.addChild(healthBarBg);
function updateHealthBar() {
var ratio = Math.max(0, Math.min(1, heroHp / heroMaxHp));
healthBarFill.width = healthBarWidth * ratio;
if (ratio > 0.6) {
healthBarFill.tint = 0x3FA34D;
} else if (ratio > 0.3) {
healthBarFill.tint = 0xF7E733;
} else {
healthBarFill.tint = 0xD83318;
}
}
updateHealthBar();
// Spawn hero at game start
menuActive = false;
if (hero) hero.destroy();
hero = new Hero();
hero.x = GAME_W / 2;
hero.y = GAME_H / 2;
game.addChild(hero);
// Reset state
// Move UI text declarations above their first use
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// High score text
var highScoreTxt = new Text2('Best: ' + highScore, {
size: 60,
fill: "#fff"
});
highScoreTxt.anchor.set(0.5, 0);
highScoreTxt.y = 120;
LK.gui.top.addChild(highScoreTxt);
// Powerup text
var powerupTxt = new Text2('', {
size: 70,
fill: 0x3B7DD8
});
powerupTxt.anchor.set(0.5, 0);
powerupTxt.y = 200;
LK.gui.top.addChild(powerupTxt);
heroHp = heroMaxHp;
updateHealthBar();
heroAmmo = heroMaxAmmo;
heroReloading = false;
heroReloadTicks = 0;
updateAmmoBar();
score = 0;
scoreTxt.setText(score);
wave = 1;
ticksSinceStart = 0;
zombieSpawnTick = 0;
zombieSpawnInterval = 90;
rapidFireTicks = 0;
for (var i = zombies.length - 1; i >= 0; i--) {
zombies[i].destroy();
zombies.splice(i, 1);
}
for (var i = bullets.length - 1; i >= 0; i--) {
bullets[i].destroy();
bullets.splice(i, 1);
}
for (var i = powerups.length - 1; i >= 0; i--) {
powerups[i].destroy();
powerups.splice(i, 1);
}
powerupTxt.setText('');
// --- CLASSIC MENU ---
// Classic menu overlay and logic
var classicMenuOverlay = new Container();
var menuBg = LK.getAsset('hero', {
anchorX: 0.5,
anchorY: 0.5,
width: 900,
height: 900,
color: 0x222222
});
menuBg.alpha = 0.92;
classicMenuOverlay.addChild(menuBg);
classicMenuOverlay.x = GAME_W / 2;
classicMenuOverlay.y = GAME_H / 2;
// Title
var menuTitle = new Text2('Zombie Survival', {
size: 120,
fill: "#fff"
});
menuTitle.anchor.set(0.5, 0);
menuTitle.y = -320;
classicMenuOverlay.addChild(menuTitle);
// Play button
var playBtn = LK.getAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: 320,
height: 120
});
playBtn.y = 80;
classicMenuOverlay.addChild(playBtn);
var playBtnText = new Text2('PLAY', {
size: 90,
fill: "#fff"
});
playBtnText.anchor.set(0.5, 0.5);
playBtnText.x = playBtn.x;
playBtnText.y = playBtn.y;
classicMenuOverlay.addChild(playBtnText);
// Cheats tab button
var cheatsBtn = LK.getAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: 320,
height: 100,
color: 0x4444aa
});
cheatsBtn.y = 240;
classicMenuOverlay.addChild(cheatsBtn);
var cheatsBtnText = new Text2('CHEATS', {
size: 70,
fill: "#fff"
});
cheatsBtnText.anchor.set(0.5, 0.5);
cheatsBtnText.x = cheatsBtn.x;
cheatsBtnText.y = cheatsBtn.y;
classicMenuOverlay.addChild(cheatsBtnText);
// High score display
var menuHighScore = new Text2('Best: ' + (storage.highScore || 0), {
size: 70,
fill: "#fff"
});
menuHighScore.anchor.set(0.5, 0);
menuHighScore.y = -120;
classicMenuOverlay.addChild(menuHighScore);
var menuActive = true;
game.addChild(classicMenuOverlay);
// Cheats tab overlay
var cheatsOverlay = new Container();
var cheatsBg = LK.getAsset('hero', {
anchorX: 0.5,
anchorY: 0.5,
width: 800,
height: 900,
color: 0x222244
});
cheatsBg.alpha = 0.97;
cheatsOverlay.addChild(cheatsBg);
cheatsOverlay.x = GAME_W / 2;
cheatsOverlay.y = GAME_H / 2;
// Cheats title
var cheatsTitle = new Text2('Cheats', {
size: 100,
fill: "#fff"
});
cheatsTitle.anchor.set(0.5, 0);
cheatsTitle.y = -340;
cheatsOverlay.addChild(cheatsTitle);
// Example cheat: Add 100 score
var cheatScoreBtn = LK.getAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: 340,
height: 110,
color: 0x3B7DD8
});
cheatScoreBtn.y = -120;
cheatsOverlay.addChild(cheatScoreBtn);
var cheatScoreBtnText = new Text2('Add 100 Score', {
size: 60,
fill: "#fff"
});
cheatScoreBtnText.anchor.set(0.5, 0.5);
cheatScoreBtnText.x = cheatScoreBtn.x;
cheatScoreBtnText.y = cheatScoreBtn.y;
cheatsOverlay.addChild(cheatScoreBtnText);
// Example cheat: Heal to full
var cheatHealBtn = LK.getAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: 340,
height: 110,
color: 0x3FA34D
});
cheatHealBtn.y = 40;
cheatsOverlay.addChild(cheatHealBtn);
var cheatHealBtnText = new Text2('Full Heal', {
size: 60,
fill: "#fff"
});
cheatHealBtnText.anchor.set(0.5, 0.5);
cheatHealBtnText.x = cheatHealBtn.x;
cheatHealBtnText.y = cheatHealBtn.y;
cheatsOverlay.addChild(cheatHealBtnText);
// Close cheats tab
var closeCheatsBtn = LK.getAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 90,
color: 0xD83318
});
closeCheatsBtn.y = 300;
cheatsOverlay.addChild(closeCheatsBtn);
var closeCheatsBtnText = new Text2('CLOSE', {
size: 55,
fill: "#fff"
});
closeCheatsBtnText.anchor.set(0.5, 0.5);
closeCheatsBtnText.x = closeCheatsBtn.x;
closeCheatsBtnText.y = closeCheatsBtn.y;
cheatsOverlay.addChild(closeCheatsBtnText);
cheatsOverlay.visible = false;
game.addChild(cheatsOverlay);
// Show/hide logic
function showClassicMenu() {
menuActive = true;
classicMenuOverlay.visible = true;
cheatsOverlay.visible = false;
// Update high score display
menuHighScore.setText('Best: ' + (storage.highScore || 0));
}
function hideClassicMenu() {
menuActive = false;
classicMenuOverlay.visible = false;
cheatsOverlay.visible = false;
}
// Cheats tab button handler
cheatsBtn.interactive = true;
cheatsBtn.down = function () {
classicMenuOverlay.visible = false;
cheatsOverlay.visible = true;
};
// Close cheats tab handler
closeCheatsBtn.interactive = true;
closeCheatsBtn.down = function () {
cheatsOverlay.visible = false;
classicMenuOverlay.visible = true;
};
// Cheat: Add 100 score
cheatScoreBtn.interactive = true;
cheatScoreBtn.down = function () {
score += 100;
scoreTxt.setText(score);
if (score > highScore) {
highScore = score;
highScoreTxt.setText('Best: ' + highScore);
storage.highScore = highScore;
menuHighScore.setText('Best: ' + highScore);
}
};
// Cheat: Full heal
cheatHealBtn.interactive = true;
cheatHealBtn.down = function () {
heroHp = heroMaxHp;
updateHealthBar();
};
// Play button handler
playBtn.interactive = true;
playBtn.down = function () {
hideClassicMenu();
// Reset game state
if (hero) hero.destroy();
hero = new Hero();
hero.x = GAME_W / 2;
hero.y = GAME_H / 2;
game.addChild(hero);
heroHp = heroMaxHp;
updateHealthBar();
heroAmmo = heroMaxAmmo;
heroReloading = false;
heroReloadTicks = 0;
updateAmmoBar();
score = 0;
scoreTxt.setText(score);
wave = 1;
ticksSinceStart = 0;
zombieSpawnTick = 0;
zombieSpawnInterval = 90;
rapidFireTicks = 0;
for (var i = zombies.length - 1; i >= 0; i--) {
zombies[i].destroy();
zombies.splice(i, 1);
}
for (var i = bullets.length - 1; i >= 0; i--) {
bullets[i].destroy();
bullets.splice(i, 1);
}
for (var i = powerups.length - 1; i >= 0; i--) {
powerups[i].destroy();
powerups.splice(i, 1);
}
powerupTxt.setText('');
};
showClassicMenu();
// Helper: spawn zombie at random edge
function spawnZombie() {
var z = new Zombie();
// Pick edge: 0=top,1=bottom,2=left,3=right
var edge = Math.floor(Math.random() * 4);
var margin = 80;
if (edge === 0) {
// top
z.x = margin + Math.random() * (GAME_W - 2 * margin);
z.y = -z.radius;
} else if (edge === 1) {
// bottom
z.x = margin + Math.random() * (GAME_W - 2 * margin);
z.y = GAME_H + z.radius;
} else if (edge === 2) {
// left
z.x = -z.radius;
z.y = margin + Math.random() * (GAME_H - 2 * margin);
} else {
// right
z.x = GAME_W + z.radius;
z.y = margin + Math.random() * (GAME_H - 2 * margin);
}
// Set wave and type
if (typeof z.setWave === "function") {
z.setWave(wave);
}
z.target = hero;
zombies.push(z);
game.addChild(z);
}
// Helper: spawn powerup
function spawnPowerup() {
var p = new Powerup();
p.x = 200 + Math.random() * (GAME_W - 400);
p.y = 300 + Math.random() * (GAME_H - 600);
powerups.push(p);
game.addChild(p);
}
// Helper: shoot bullet towards (tx,ty)
function shootBullet(tx, ty) {
if (heroReloading) return;
if (heroAmmo <= 0) {
// Start reload if not already
heroReloading = true;
heroReloadTicks = 0;
updateAmmoBar();
return;
}
if (LK.ticks - hero.lastShotTick < (rapidFireTicks > 0 ? 6 : hero.shootCooldown)) return;
var b = new Bullet();
b.x = hero.x;
b.y = hero.y;
var dx = tx - hero.x;
var dy = ty - hero.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var speed = 38;
b.vx = speed * dx / dist;
b.vy = speed * dy / dist;
bullets.push(b);
game.addChild(b);
hero.lastShotTick = LK.ticks;
heroAmmo--;
if (heroAmmo <= 0) {
heroReloading = true;
heroReloadTicks = 0;
}
updateAmmoBar();
LK.getSound('shoot').play();
}
// Helper: distance between two objects
function dist2(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
// Dragging
function handleMove(x, y, obj) {
if (dragNode) {
// Clamp hero inside game area
var r = hero.radius;
dragNode.x = Math.max(r, Math.min(GAME_W - r, x));
dragNode.y = Math.max(r, Math.min(GAME_H - r, y));
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
// Only drag if menu is not active and touch is on hero
if (!menuActive && dist2({
x: x,
y: y
}, hero) <= hero.radius * 1.2) {
dragNode = hero;
}
};
game.up = function (x, y, obj) {
dragNode = null;
if (menuActive) return;
// Shoot bullet towards release point if not dragging
if (dist2({
x: x,
y: y
}, hero) > hero.radius * 1.2) {
shootBullet(x, y);
}
};
// Main update loop
game.update = function () {
if (menuActive) return;
// No menuActive check; game always runs
ticksSinceStart++;
// Increase wave every 900 ticks (~15s)
if (ticksSinceStart % 900 === 0) {
wave++;
if (zombieSpawnInterval > 30) zombieSpawnInterval -= 8;
}
// Spawn zombies
if (LK.ticks - zombieSpawnTick > zombieSpawnInterval) {
// More zombies per spawn as wave increases
var zombiesToSpawn = 1 + Math.floor(wave / 4);
for (var n = 0; n < zombiesToSpawn; n++) {
spawnZombie();
}
zombieSpawnTick = LK.ticks;
}
// Maybe spawn powerup
if (powerups.length === 0 && Math.random() < 0.002) {
spawnPowerup();
}
// Ammo reload logic
if (heroReloading) {
heroReloadTicks++;
if (heroReloadTicks >= heroReloadTime) {
heroAmmo = heroMaxAmmo;
heroReloading = false;
heroReloadTicks = 0;
updateAmmoBar();
} else {
updateAmmoBar();
}
}
// Update hero (nothing for now)
hero.update();
// Update zombies
for (var i = zombies.length - 1; i >= 0; i--) {
var z = zombies[i];
z.update();
// Check collision with hero
if (dist2(z, hero) < z.radius + hero.radius - 10) {
LK.effects.flashScreen(0xff0000, 400);
heroHp--;
updateHealthBar();
if (heroHp <= 0) {
if (score > highScore) {
storage.highScore = score;
}
heroAmmo = heroMaxAmmo;
heroReloading = false;
heroReloadTicks = 0;
updateAmmoBar();
LK.showGameOver();
showClassicMenu();
return;
}
// Remove zombie on hit to prevent instant double hit
z.destroy();
zombies.splice(i, 1);
continue;
}
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; i--) {
var b = bullets[i];
b.update();
// Remove if out of bounds or expired
if (b.x < -100 || b.x > GAME_W + 100 || b.y < -100 || b.y > GAME_H + 100 || b.lifetime <= 0) {
b.destroy();
bullets.splice(i, 1);
continue;
}
// Check collision with zombies
for (var j = zombies.length - 1; j >= 0; j--) {
var z = zombies[j];
if (dist2(b, z) < b.radius + z.radius - 10) {
// Damage zombie
if (typeof z.hp === "number") {
z.hp--;
LK.effects.flashObject(z, 0xffffff, 120);
if (z.hp <= 0) {
LK.getSound('zombie_die').play();
score++;
scoreTxt.setText(score);
if (score > highScore) {
highScore = score;
highScoreTxt.setText('Best: ' + highScore);
storage.highScore = highScore;
}
z.destroy();
zombies.splice(j, 1);
}
} else {
// fallback: destroy if no hp
LK.getSound('zombie_die').play();
score++;
scoreTxt.setText(score);
if (score > highScore) {
highScore = score;
highScoreTxt.setText('Best: ' + highScore);
storage.highScore = highScore;
}
z.destroy();
zombies.splice(j, 1);
}
b.destroy();
bullets.splice(i, 1);
break;
}
}
}
// Update powerups
for (var i = powerups.length - 1; i >= 0; i--) {
var p = powerups[i];
// Check collision with hero
if (dist2(p, hero) < p.radius + hero.radius - 10) {
LK.getSound('powerup').play();
rapidFireTicks = 360; // 6 seconds
powerupTxt.setText('Rapid Fire!');
LK.effects.flashObject(hero, 0x3b7dd8, 400);
p.destroy();
powerups.splice(i, 1);
}
}
// Powerup timer
if (rapidFireTicks > 0) {
rapidFireTicks--;
if (rapidFireTicks === 0) {
powerupTxt.setText('');
}
}
// Update powerup text alpha for effect
if (rapidFireTicks > 0) {
powerupTxt.alpha = 0.7 + 0.3 * Math.sin(LK.ticks / 6);
} else {
powerupTxt.alpha = 1;
}
};
// Start music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.7,
duration: 1200
}
}); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Bullet class
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletSprite = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = bulletSprite.width / 2;
self.vx = 0;
self.vy = 0;
self.lifetime = 60; // frames
self.update = function () {
self.x += self.vx;
self.y += self.vy;
self.lifetime--;
};
return self;
});
// Hero class
var Hero = Container.expand(function () {
var self = Container.call(this);
var heroSprite = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 0.5
});
self.heroSprite = heroSprite;
// Add gun sprite to hero's hand
var gunSprite = self.attachAsset('gun', {
anchorX: 0.2,
// grip of gun
anchorY: 0.5,
width: heroSprite.width * 0.45,
height: heroSprite.height * 0.22
});
gunSprite.x = heroSprite.width * 0.32; // offset to right hand
gunSprite.y = 0;
self.radius = heroSprite.width / 2;
self.speed = 18; // pixels per move
self.lastShotTick = 0;
self.shootCooldown = 18; // frames between shots
// Allow skin change
self.setSkin = function (skinId) {
if (!skinId || skinId === "hero") {
// Already default
return;
}
// Remove old sprite
if (self.heroSprite && self.heroSprite.parent) {
self.removeChild(self.heroSprite);
}
// Add new sprite
var newSprite = self.attachAsset(skinId, {
anchorX: 0.5,
anchorY: 0.5,
width: heroSprite.width,
height: heroSprite.height
});
self.heroSprite = newSprite;
// Move gun to top
if (gunSprite && gunSprite.parent) {
self.removeChild(gunSprite);
self.addChild(gunSprite);
}
// Update radius
self.radius = newSprite.width / 2;
};
self.update = function () {};
return self;
});
// Powerup class
var Powerup = Container.expand(function () {
var self = Container.call(this);
var powerupSprite = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = powerupSprite.width / 2;
self.type = 'rapid'; // only one type for MVP
self.update = function () {};
return self;
});
// Zombie class
var Zombie = Container.expand(function () {
var self = Container.call(this);
// Default to normal zombie asset
var zombieSprite = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
self.zombieSprite = zombieSprite;
self.radius = zombieSprite.width / 2;
self.baseSpeed = 3 + Math.random() * 2; // base speed, will be increased by wave
self.speed = self.baseSpeed;
self.target = null; // set to hero
self.maxHp = 1;
self.hp = 1;
self.wave = 1;
self.type = "normal"; // can be "normal", "fast", "tank", "crawler"
self.setWave = function (wave) {
self.wave = wave;
// Remove old sprite if present
if (self.zombieSprite && self.zombieSprite.parent) {
self.removeChild(self.zombieSprite);
}
// Decide type
var r = Math.random();
if (wave >= 7 && r < 0.18) {
// Crawler: slow, small, hard to hit
self.type = "crawler";
self.speed = 1.7 + 0.12 * wave;
self.maxHp = 2 + Math.floor(wave / 3);
self.hp = self.maxHp;
self.zombieSprite = self.attachAsset('zombie_crawler', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.zombieSprite.width / 2;
self.zombieSprite.tint = 0x8e8e8e; // grayish
} else if (wave >= 6 && r < 0.38) {
// Tank zombie: slow but high HP
self.type = "tank";
self.speed = 2 + 0.15 * wave;
self.maxHp = 3 + Math.floor(wave / 2);
self.hp = self.maxHp;
self.zombieSprite = self.attachAsset('zombie_tank', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.zombieSprite.width / 2;
self.zombieSprite.tint = 0x6e4e2e; // brownish
} else if (wave >= 3 && r < 0.63) {
// Fast zombie: high speed, low HP
self.type = "fast";
self.speed = 5 + 0.4 * wave;
self.maxHp = 1;
self.hp = 1;
self.zombieSprite = self.attachAsset('zombie_fast', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.zombieSprite.width / 2;
self.zombieSprite.tint = 0x7cf442; // bright green
} else {
// Normal zombie
self.type = "normal";
self.speed = self.baseSpeed + (wave - 1) * 0.5;
self.maxHp = 1 + Math.floor(wave / 4);
self.hp = self.maxHp;
self.zombieSprite = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.zombieSprite.width / 2;
self.zombieSprite.tint = 0x7bbf3f; // normal green
}
};
// Health bar setup
var healthBarW = zombieSprite.width * 0.7;
var healthBarH = 16;
var healthBarBg = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: healthBarW,
height: healthBarH,
color: 0x222222
});
healthBarBg.alpha = 0.7;
var healthBarFill = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: healthBarW,
height: healthBarH,
color: 0x3FA34D
});
healthBarFill.alpha = 0.92;
self.addChild(healthBarBg);
self.addChild(healthBarFill);
self.update = function () {
if (!self.target) return;
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
self.x += self.speed * dx / dist;
self.y += self.speed * dy / dist;
}
// Update health bar position and fill
var barY = -self.radius - 30;
healthBarBg.x = -healthBarW / 2;
healthBarBg.y = barY;
healthBarFill.x = -healthBarW / 2;
healthBarFill.y = barY;
var ratio = Math.max(0, Math.min(1, self.hp / self.maxHp));
healthBarFill.width = healthBarW * ratio;
if (ratio > 0.6) {
healthBarFill.tint = 0x3FA34D;
} else if (ratio > 0.3) {
healthBarFill.tint = 0xF7E733;
} else {
healthBarFill.tint = 0xD83318;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Game area
// Hero: red box
// Zombie: green ellipse
// Bullet: yellow box
// Powerup: blue ellipse
// Sound effects
// Music
// new gun asset
var GAME_W = 2048;
var GAME_H = 2732;
// State
var hero = null;
var zombies = [];
var bullets = [];
var powerups = [];
var dragNode = null;
var lastIntersecting = false;
var score = 0;
var highScore = storage.highScore || 0;
var wave = 1;
var ticksSinceStart = 0;
var zombieSpawnTick = 0;
var zombieSpawnInterval = 90; // frames
var rapidFireTicks = 0;
// --- HEALTH SYSTEM ---
var heroMaxHp = 5;
var heroHp = heroMaxHp;
// --- AMMO SYSTEM ---
var heroMaxAmmo = 12;
var heroAmmo = heroMaxAmmo;
var heroReloading = false;
var heroReloadTicks = 0;
var heroReloadTime = 60; // frames to reload
// Ammo bar UI
var ammoBarBg = new Container();
var ammoBarWidth = 420;
var ammoBarHeight = 28;
var ammoBarBgRect = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: ammoBarWidth,
height: ammoBarHeight,
color: 0x222222
});
ammoBarBgRect.alpha = 0.7;
ammoBarBg.addChild(ammoBarBgRect);
var ammoBarFill = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: ammoBarWidth,
height: ammoBarHeight,
color: 0x3B7DD8
});
ammoBarFill.alpha = 0.92;
ammoBarBg.addChild(ammoBarFill);
ammoBarBg.x = (GAME_W - ammoBarWidth) / 2;
ammoBarBg.y = 170;
LK.gui.top.addChild(ammoBarBg);
var ammoTxt = new Text2('', {
size: 60,
fill: "#fff"
});
ammoTxt.anchor.set(0.5, 0);
ammoTxt.x = GAME_W / 2;
ammoTxt.y = 210;
LK.gui.top.addChild(ammoTxt);
function updateAmmoBar() {
var ratio = Math.max(0, Math.min(1, heroAmmo / heroMaxAmmo));
ammoBarFill.width = ammoBarWidth * ratio;
if (heroReloading) {
ammoBarFill.tint = 0xF7E733;
ammoTxt.setText('RELOADING...');
} else {
ammoBarFill.tint = 0x3B7DD8;
ammoTxt.setText(heroAmmo + ' / ' + heroMaxAmmo);
}
}
updateAmmoBar();
// Health bar UI
var healthBarBg = new Container();
var healthBarWidth = 520;
var healthBarHeight = 38;
var healthBarBgRect = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: healthBarWidth,
height: healthBarHeight,
color: 0x333333
});
healthBarBgRect.alpha = 0.7;
healthBarBg.addChild(healthBarBgRect);
var healthBarFill = LK.getAsset('hero', {
anchorX: 0,
anchorY: 0,
width: healthBarWidth,
height: healthBarHeight,
color: 0x3FA34D
});
healthBarFill.alpha = 0.92;
healthBarBg.addChild(healthBarFill);
healthBarBg.x = (GAME_W - healthBarWidth) / 2;
healthBarBg.y = 110;
LK.gui.top.addChild(healthBarBg);
function updateHealthBar() {
var ratio = Math.max(0, Math.min(1, heroHp / heroMaxHp));
healthBarFill.width = healthBarWidth * ratio;
if (ratio > 0.6) {
healthBarFill.tint = 0x3FA34D;
} else if (ratio > 0.3) {
healthBarFill.tint = 0xF7E733;
} else {
healthBarFill.tint = 0xD83318;
}
}
updateHealthBar();
// Spawn hero at game start
menuActive = false;
if (hero) hero.destroy();
hero = new Hero();
hero.x = GAME_W / 2;
hero.y = GAME_H / 2;
game.addChild(hero);
// Reset state
// Move UI text declarations above their first use
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// High score text
var highScoreTxt = new Text2('Best: ' + highScore, {
size: 60,
fill: "#fff"
});
highScoreTxt.anchor.set(0.5, 0);
highScoreTxt.y = 120;
LK.gui.top.addChild(highScoreTxt);
// Powerup text
var powerupTxt = new Text2('', {
size: 70,
fill: 0x3B7DD8
});
powerupTxt.anchor.set(0.5, 0);
powerupTxt.y = 200;
LK.gui.top.addChild(powerupTxt);
heroHp = heroMaxHp;
updateHealthBar();
heroAmmo = heroMaxAmmo;
heroReloading = false;
heroReloadTicks = 0;
updateAmmoBar();
score = 0;
scoreTxt.setText(score);
wave = 1;
ticksSinceStart = 0;
zombieSpawnTick = 0;
zombieSpawnInterval = 90;
rapidFireTicks = 0;
for (var i = zombies.length - 1; i >= 0; i--) {
zombies[i].destroy();
zombies.splice(i, 1);
}
for (var i = bullets.length - 1; i >= 0; i--) {
bullets[i].destroy();
bullets.splice(i, 1);
}
for (var i = powerups.length - 1; i >= 0; i--) {
powerups[i].destroy();
powerups.splice(i, 1);
}
powerupTxt.setText('');
// --- CLASSIC MENU ---
// Classic menu overlay and logic
var classicMenuOverlay = new Container();
var menuBg = LK.getAsset('hero', {
anchorX: 0.5,
anchorY: 0.5,
width: 900,
height: 900,
color: 0x222222
});
menuBg.alpha = 0.92;
classicMenuOverlay.addChild(menuBg);
classicMenuOverlay.x = GAME_W / 2;
classicMenuOverlay.y = GAME_H / 2;
// Title
var menuTitle = new Text2('Zombie Survival', {
size: 120,
fill: "#fff"
});
menuTitle.anchor.set(0.5, 0);
menuTitle.y = -320;
classicMenuOverlay.addChild(menuTitle);
// Play button
var playBtn = LK.getAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: 320,
height: 120
});
playBtn.y = 80;
classicMenuOverlay.addChild(playBtn);
var playBtnText = new Text2('PLAY', {
size: 90,
fill: "#fff"
});
playBtnText.anchor.set(0.5, 0.5);
playBtnText.x = playBtn.x;
playBtnText.y = playBtn.y;
classicMenuOverlay.addChild(playBtnText);
// Cheats tab button
var cheatsBtn = LK.getAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: 320,
height: 100,
color: 0x4444aa
});
cheatsBtn.y = 240;
classicMenuOverlay.addChild(cheatsBtn);
var cheatsBtnText = new Text2('CHEATS', {
size: 70,
fill: "#fff"
});
cheatsBtnText.anchor.set(0.5, 0.5);
cheatsBtnText.x = cheatsBtn.x;
cheatsBtnText.y = cheatsBtn.y;
classicMenuOverlay.addChild(cheatsBtnText);
// High score display
var menuHighScore = new Text2('Best: ' + (storage.highScore || 0), {
size: 70,
fill: "#fff"
});
menuHighScore.anchor.set(0.5, 0);
menuHighScore.y = -120;
classicMenuOverlay.addChild(menuHighScore);
var menuActive = true;
game.addChild(classicMenuOverlay);
// Cheats tab overlay
var cheatsOverlay = new Container();
var cheatsBg = LK.getAsset('hero', {
anchorX: 0.5,
anchorY: 0.5,
width: 800,
height: 900,
color: 0x222244
});
cheatsBg.alpha = 0.97;
cheatsOverlay.addChild(cheatsBg);
cheatsOverlay.x = GAME_W / 2;
cheatsOverlay.y = GAME_H / 2;
// Cheats title
var cheatsTitle = new Text2('Cheats', {
size: 100,
fill: "#fff"
});
cheatsTitle.anchor.set(0.5, 0);
cheatsTitle.y = -340;
cheatsOverlay.addChild(cheatsTitle);
// Example cheat: Add 100 score
var cheatScoreBtn = LK.getAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: 340,
height: 110,
color: 0x3B7DD8
});
cheatScoreBtn.y = -120;
cheatsOverlay.addChild(cheatScoreBtn);
var cheatScoreBtnText = new Text2('Add 100 Score', {
size: 60,
fill: "#fff"
});
cheatScoreBtnText.anchor.set(0.5, 0.5);
cheatScoreBtnText.x = cheatScoreBtn.x;
cheatScoreBtnText.y = cheatScoreBtn.y;
cheatsOverlay.addChild(cheatScoreBtnText);
// Example cheat: Heal to full
var cheatHealBtn = LK.getAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: 340,
height: 110,
color: 0x3FA34D
});
cheatHealBtn.y = 40;
cheatsOverlay.addChild(cheatHealBtn);
var cheatHealBtnText = new Text2('Full Heal', {
size: 60,
fill: "#fff"
});
cheatHealBtnText.anchor.set(0.5, 0.5);
cheatHealBtnText.x = cheatHealBtn.x;
cheatHealBtnText.y = cheatHealBtn.y;
cheatsOverlay.addChild(cheatHealBtnText);
// Close cheats tab
var closeCheatsBtn = LK.getAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 90,
color: 0xD83318
});
closeCheatsBtn.y = 300;
cheatsOverlay.addChild(closeCheatsBtn);
var closeCheatsBtnText = new Text2('CLOSE', {
size: 55,
fill: "#fff"
});
closeCheatsBtnText.anchor.set(0.5, 0.5);
closeCheatsBtnText.x = closeCheatsBtn.x;
closeCheatsBtnText.y = closeCheatsBtn.y;
cheatsOverlay.addChild(closeCheatsBtnText);
cheatsOverlay.visible = false;
game.addChild(cheatsOverlay);
// Show/hide logic
function showClassicMenu() {
menuActive = true;
classicMenuOverlay.visible = true;
cheatsOverlay.visible = false;
// Update high score display
menuHighScore.setText('Best: ' + (storage.highScore || 0));
}
function hideClassicMenu() {
menuActive = false;
classicMenuOverlay.visible = false;
cheatsOverlay.visible = false;
}
// Cheats tab button handler
cheatsBtn.interactive = true;
cheatsBtn.down = function () {
classicMenuOverlay.visible = false;
cheatsOverlay.visible = true;
};
// Close cheats tab handler
closeCheatsBtn.interactive = true;
closeCheatsBtn.down = function () {
cheatsOverlay.visible = false;
classicMenuOverlay.visible = true;
};
// Cheat: Add 100 score
cheatScoreBtn.interactive = true;
cheatScoreBtn.down = function () {
score += 100;
scoreTxt.setText(score);
if (score > highScore) {
highScore = score;
highScoreTxt.setText('Best: ' + highScore);
storage.highScore = highScore;
menuHighScore.setText('Best: ' + highScore);
}
};
// Cheat: Full heal
cheatHealBtn.interactive = true;
cheatHealBtn.down = function () {
heroHp = heroMaxHp;
updateHealthBar();
};
// Play button handler
playBtn.interactive = true;
playBtn.down = function () {
hideClassicMenu();
// Reset game state
if (hero) hero.destroy();
hero = new Hero();
hero.x = GAME_W / 2;
hero.y = GAME_H / 2;
game.addChild(hero);
heroHp = heroMaxHp;
updateHealthBar();
heroAmmo = heroMaxAmmo;
heroReloading = false;
heroReloadTicks = 0;
updateAmmoBar();
score = 0;
scoreTxt.setText(score);
wave = 1;
ticksSinceStart = 0;
zombieSpawnTick = 0;
zombieSpawnInterval = 90;
rapidFireTicks = 0;
for (var i = zombies.length - 1; i >= 0; i--) {
zombies[i].destroy();
zombies.splice(i, 1);
}
for (var i = bullets.length - 1; i >= 0; i--) {
bullets[i].destroy();
bullets.splice(i, 1);
}
for (var i = powerups.length - 1; i >= 0; i--) {
powerups[i].destroy();
powerups.splice(i, 1);
}
powerupTxt.setText('');
};
showClassicMenu();
// Helper: spawn zombie at random edge
function spawnZombie() {
var z = new Zombie();
// Pick edge: 0=top,1=bottom,2=left,3=right
var edge = Math.floor(Math.random() * 4);
var margin = 80;
if (edge === 0) {
// top
z.x = margin + Math.random() * (GAME_W - 2 * margin);
z.y = -z.radius;
} else if (edge === 1) {
// bottom
z.x = margin + Math.random() * (GAME_W - 2 * margin);
z.y = GAME_H + z.radius;
} else if (edge === 2) {
// left
z.x = -z.radius;
z.y = margin + Math.random() * (GAME_H - 2 * margin);
} else {
// right
z.x = GAME_W + z.radius;
z.y = margin + Math.random() * (GAME_H - 2 * margin);
}
// Set wave and type
if (typeof z.setWave === "function") {
z.setWave(wave);
}
z.target = hero;
zombies.push(z);
game.addChild(z);
}
// Helper: spawn powerup
function spawnPowerup() {
var p = new Powerup();
p.x = 200 + Math.random() * (GAME_W - 400);
p.y = 300 + Math.random() * (GAME_H - 600);
powerups.push(p);
game.addChild(p);
}
// Helper: shoot bullet towards (tx,ty)
function shootBullet(tx, ty) {
if (heroReloading) return;
if (heroAmmo <= 0) {
// Start reload if not already
heroReloading = true;
heroReloadTicks = 0;
updateAmmoBar();
return;
}
if (LK.ticks - hero.lastShotTick < (rapidFireTicks > 0 ? 6 : hero.shootCooldown)) return;
var b = new Bullet();
b.x = hero.x;
b.y = hero.y;
var dx = tx - hero.x;
var dy = ty - hero.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var speed = 38;
b.vx = speed * dx / dist;
b.vy = speed * dy / dist;
bullets.push(b);
game.addChild(b);
hero.lastShotTick = LK.ticks;
heroAmmo--;
if (heroAmmo <= 0) {
heroReloading = true;
heroReloadTicks = 0;
}
updateAmmoBar();
LK.getSound('shoot').play();
}
// Helper: distance between two objects
function dist2(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
// Dragging
function handleMove(x, y, obj) {
if (dragNode) {
// Clamp hero inside game area
var r = hero.radius;
dragNode.x = Math.max(r, Math.min(GAME_W - r, x));
dragNode.y = Math.max(r, Math.min(GAME_H - r, y));
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
// Only drag if menu is not active and touch is on hero
if (!menuActive && dist2({
x: x,
y: y
}, hero) <= hero.radius * 1.2) {
dragNode = hero;
}
};
game.up = function (x, y, obj) {
dragNode = null;
if (menuActive) return;
// Shoot bullet towards release point if not dragging
if (dist2({
x: x,
y: y
}, hero) > hero.radius * 1.2) {
shootBullet(x, y);
}
};
// Main update loop
game.update = function () {
if (menuActive) return;
// No menuActive check; game always runs
ticksSinceStart++;
// Increase wave every 900 ticks (~15s)
if (ticksSinceStart % 900 === 0) {
wave++;
if (zombieSpawnInterval > 30) zombieSpawnInterval -= 8;
}
// Spawn zombies
if (LK.ticks - zombieSpawnTick > zombieSpawnInterval) {
// More zombies per spawn as wave increases
var zombiesToSpawn = 1 + Math.floor(wave / 4);
for (var n = 0; n < zombiesToSpawn; n++) {
spawnZombie();
}
zombieSpawnTick = LK.ticks;
}
// Maybe spawn powerup
if (powerups.length === 0 && Math.random() < 0.002) {
spawnPowerup();
}
// Ammo reload logic
if (heroReloading) {
heroReloadTicks++;
if (heroReloadTicks >= heroReloadTime) {
heroAmmo = heroMaxAmmo;
heroReloading = false;
heroReloadTicks = 0;
updateAmmoBar();
} else {
updateAmmoBar();
}
}
// Update hero (nothing for now)
hero.update();
// Update zombies
for (var i = zombies.length - 1; i >= 0; i--) {
var z = zombies[i];
z.update();
// Check collision with hero
if (dist2(z, hero) < z.radius + hero.radius - 10) {
LK.effects.flashScreen(0xff0000, 400);
heroHp--;
updateHealthBar();
if (heroHp <= 0) {
if (score > highScore) {
storage.highScore = score;
}
heroAmmo = heroMaxAmmo;
heroReloading = false;
heroReloadTicks = 0;
updateAmmoBar();
LK.showGameOver();
showClassicMenu();
return;
}
// Remove zombie on hit to prevent instant double hit
z.destroy();
zombies.splice(i, 1);
continue;
}
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; i--) {
var b = bullets[i];
b.update();
// Remove if out of bounds or expired
if (b.x < -100 || b.x > GAME_W + 100 || b.y < -100 || b.y > GAME_H + 100 || b.lifetime <= 0) {
b.destroy();
bullets.splice(i, 1);
continue;
}
// Check collision with zombies
for (var j = zombies.length - 1; j >= 0; j--) {
var z = zombies[j];
if (dist2(b, z) < b.radius + z.radius - 10) {
// Damage zombie
if (typeof z.hp === "number") {
z.hp--;
LK.effects.flashObject(z, 0xffffff, 120);
if (z.hp <= 0) {
LK.getSound('zombie_die').play();
score++;
scoreTxt.setText(score);
if (score > highScore) {
highScore = score;
highScoreTxt.setText('Best: ' + highScore);
storage.highScore = highScore;
}
z.destroy();
zombies.splice(j, 1);
}
} else {
// fallback: destroy if no hp
LK.getSound('zombie_die').play();
score++;
scoreTxt.setText(score);
if (score > highScore) {
highScore = score;
highScoreTxt.setText('Best: ' + highScore);
storage.highScore = highScore;
}
z.destroy();
zombies.splice(j, 1);
}
b.destroy();
bullets.splice(i, 1);
break;
}
}
}
// Update powerups
for (var i = powerups.length - 1; i >= 0; i--) {
var p = powerups[i];
// Check collision with hero
if (dist2(p, hero) < p.radius + hero.radius - 10) {
LK.getSound('powerup').play();
rapidFireTicks = 360; // 6 seconds
powerupTxt.setText('Rapid Fire!');
LK.effects.flashObject(hero, 0x3b7dd8, 400);
p.destroy();
powerups.splice(i, 1);
}
}
// Powerup timer
if (rapidFireTicks > 0) {
rapidFireTicks--;
if (rapidFireTicks === 0) {
powerupTxt.setText('');
}
}
// Update powerup text alpha for effect
if (rapidFireTicks > 0) {
powerupTxt.alpha = 0.7 + 0.3 * Math.sin(LK.ticks / 6);
} else {
powerupTxt.alpha = 1;
}
};
// Start music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.7,
duration: 1200
}
});
zombie. In-Game asset. 2d. High contrast. No shadows
bullet. In-Game asset. 2d. High contrast. No shadows
power up. In-Game asset. 2d. High contrast. No shadows
pistol. In-Game asset. 2d. High contrast. No shadows
crawler zombie. In-Game asset. 2d. High contrast. No shadows
runner zombie. In-Game asset. 2d. High contrast. No shadows
tank zombie. In-Game asset. 2d. High contrast. No shadows
man. In-Game asset. 2d. High contrast. No shadows