/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 20;
self.damage = 10;
self.vx = 0;
self.vy = 0;
self.piercing = false;
self.maxDistance = 800;
self.travelDistance = 0;
self.update = function () {
var moveX = self.vx;
var moveY = self.vy;
var distance = Math.sqrt(moveX * moveX + moveY * moveY);
self.travelDistance += distance;
self.x += moveX;
self.y += moveY;
};
return self;
});
var HealthPack = Container.expand(function () {
var self = Container.call(this);
var healthGraphics = self.attachAsset('healthPack', {
anchorX: 0.5,
anchorY: 0.5
});
healthGraphics.tint = 0xFF0000; // Bright red color
// Add + text on the healthpack with white color
var plusText = new Text2('+', {
size: 48,
fill: 0xFFFFFF,
font: "'Arial Black'"
});
plusText.anchor.set(0.5, 0.5);
self.addChild(plusText);
self.healAmount = 25; // Will be calculated as percentage in Player.heal method
self.spawnTime = LK.ticks;
self.update = function () {
// Pulsing effect using tween every 1 second
if (LK.ticks % 60 === 0) {
// Pulse every 1 second from 100% down to 70%
tween(self, {
scaleX: 0.7,
scaleY: 0.7
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
// Keep + text readable during scaling
plusText.scaleX = 1 / Math.max(self.scaleX, 0.7);
plusText.scaleY = 1 / Math.max(self.scaleY, 0.7);
};
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 100;
self.maxHealth = 100;
self.fireRate = 10;
self.fireCooldown = 0;
self.moveSpeed = 5;
self.takeDamage = function (amount) {
self.health -= amount;
LK.getSound('playerHurt').play();
LK.effects.flashObject(self, 0xFF0000, 500);
if (self.health <= 0) {
self.health = 0;
return true;
}
return false;
};
self.heal = function (amount) {
// Calculate 25% of max health for health pack healing
var healAmount = Math.floor(self.maxHealth * 0.25);
self.health = Math.min(self.health + healAmount, self.maxHealth);
};
self.update = function () {
if (self.fireCooldown > 0) self.fireCooldown--;
// Ensure player is always fully visible
self.alpha = 1;
};
return self;
});
var PowerShotPowerUp = Container.expand(function () {
var self = Container.call(this);
var powerUpGraphics = self.attachAsset('powerShot', {
anchorX: 0.5,
anchorY: 0.5
});
// Add P text on the powerup with white color
var pText = new Text2('P', {
size: 48,
fill: 0xFFFFFF,
font: "'Arial Black'"
});
pText.anchor.set(0.5, 0.5);
self.addChild(pText);
self.ammoAmount = 10;
self.spawnTime = LK.ticks;
self.update = function () {
// Large pulsing effect using tween every 1 second
if (LK.ticks % 60 === 0) {
// Pulse every 1 second from 100% down to 10%
tween(self, {
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
// Keep P text readable during scaling
pText.scaleX = 1 / Math.max(self.scaleX, 0.1);
pText.scaleY = 1 / Math.max(self.scaleY, 0.1);
};
return self;
});
var SniperBullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('sniperBullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 10; // Set sniper bullet speed to 10
self.damage = 100; // 10x damage (10 * 10)
self.vx = 0;
self.vy = 0;
self.piercing = true; // Sniper bullets pierce through zombies
self.maxDistance = 1600; // 2x range (800 * 2)
self.travelDistance = 0;
self.update = function () {
var moveX = self.vx;
var moveY = self.vy;
var distance = Math.sqrt(moveX * moveX + moveY * moveY);
self.travelDistance += distance;
self.x += moveX;
self.y += moveY;
// Add spinning animation - 25% faster
self.rotation += 0.375;
// Add pulsing animation from 25% to 100% size
var pulseScale = 0.25 + 0.75 * (Math.sin(LK.ticks * 0.2) * 0.5 + 0.5);
self.scaleX = pulseScale;
self.scaleY = pulseScale;
};
return self;
});
var SniperPowerUp = Container.expand(function () {
var self = Container.call(this);
var powerUpGraphics = self.attachAsset('powerShot', {
anchorX: 0.5,
anchorY: 0.5
});
powerUpGraphics.tint = 0xFF0000; // Red tint for sniper
// Add S text on the powerup with white color
var sText = new Text2('S', {
size: 48,
fill: 0xFFFFFF,
font: "'Arial Black'"
});
sText.anchor.set(0.5, 0.5);
self.addChild(sText);
self.spawnTime = LK.ticks;
self.update = function () {
// Large pulsing effect using tween every 1 second
if (LK.ticks % 60 === 0) {
// Pulse every 1 second from 100% down to 10%
tween(self, {
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
// Keep S text readable during scaling
sText.scaleX = 1 / Math.max(self.scaleX, 0.1);
sText.scaleY = 1 / Math.max(self.scaleY, 0.1);
};
return self;
});
var TripleShot = Container.expand(function () {
var self = Container.call(this);
// Create center bullet
var centerBullet = self.attachAsset('tripleShotBullet', {
anchorX: 0.5,
anchorY: 0.5
});
centerBullet.x = 0;
centerBullet.y = 0;
centerBullet.rotation = 0;
// Create left bullet tilted 10 degrees away
var leftBullet = self.attachAsset('tripleShotBullet', {
anchorX: 0.5,
anchorY: 0.5
});
leftBullet.x = -20;
leftBullet.y = -8;
leftBullet.rotation = -10 * Math.PI / 180; // -10 degrees in radians
// Create right bullet tilted 10 degrees away
var rightBullet = self.attachAsset('tripleShotBullet', {
anchorX: 0.5,
anchorY: 0.5
});
rightBullet.x = 20;
rightBullet.y = -8;
rightBullet.rotation = 10 * Math.PI / 180; // 10 degrees in radians
self.spawnTime = LK.ticks;
self.update = function () {
// Spinning animation like Zoin - rotate the entire powerup
self.rotation += 0.075;
// Add pulsing effect with tween every 2 seconds
if (LK.ticks % 120 === 0) {
// Pulse every 2 seconds
tween(self, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
// Add floating effect
self.y += Math.sin(LK.ticks * 0.1) * 0.3;
};
return self;
});
var Zoin = Container.expand(function () {
var self = Container.call(this);
var zoinGraphics = self.attachAsset('zoin', {
anchorX: 0.5,
anchorY: 0.5
});
// Add Z text on the coin with neon green color
var zText = new Text2('Z', {
size: 24,
fill: 0x00ff00,
font: "'Arial Black'"
});
zText.anchor.set(0.5, 0.5);
self.addChild(zText);
self.spawnTime = LK.ticks;
self.spinPhase = 0; // Track spinning phase for coin flip effect
self.update = function () {
// Spinning animation - slower spin to show coin flip effect at 50% speed
self.spinPhase += 0.075;
// Scale X based on sine wave to create flipping effect
var scaleX = Math.abs(Math.sin(self.spinPhase));
self.scaleX = scaleX;
zText.scaleX = 1 / Math.max(scaleX, 0.1); // Keep Z readable and prevent division by 0
// Z is always visible on both sides - no longer hide it
zText.visible = true;
// Pulsing effect using tween
if (LK.ticks % 120 === 0) {
// Pulse every 2 seconds
tween(self, {
scaleY: 1.3
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
// Add floating effect
self.y += Math.sin(LK.ticks * 0.1) * 0.3;
};
return self;
});
var Zombie = Container.expand(function () {
var self = Container.call(this);
// Randomly choose zombie visual from available assets
var zombieAssets = ['zombie', 'zombie2', 'zombie3', 'zombie4'];
var randomAsset = zombieAssets[Math.floor(Math.random() * zombieAssets.length)];
var zombieGraphics = self.attachAsset(randomAsset, {
anchorX: 0.5,
anchorY: 0.5
});
self.baseSpeed = 1.5;
var variation = self.baseSpeed * 0.25; // 25% variation
self.speed = Math.round(self.baseSpeed + (Math.random() - 0.5) * 2 * variation); // Random between baseSpeed ± 25%
self.health = 20;
self.maxHealth = 20; // Store base health for wave scaling
self.damage = 1;
self.baseDamage = 1; // Store base damage for wave scaling
self.attackCooldown = 0;
self.isDead = false;
self.takeDamage = function (amount) {
if (self.isDead) return false;
self.health -= amount;
if (self.health <= 0) {
self.isDead = true;
LK.getSound('zombieHit').play();
return true;
}
zombieGraphics.tint = 0xFF0000;
tween(zombieGraphics, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
zombieGraphics.tint = 0xFFFFFF;
}
});
return false;
};
self.update = function () {
if (self.isDead) return;
if (self.attackCooldown > 0) self.attackCooldown--;
};
return self;
});
var ZombieHulk = Zombie.expand(function () {
var self = Zombie.call(this);
// Enhanced stats: 100% more life, 25% more speed, 50% larger
self.baseSpeed = 1.5 * 1.25; // 25% more speed
var variation = self.baseSpeed * 0.25; // 25% variation
self.speed = Math.round(self.baseSpeed + (Math.random() - 0.5) * 2 * variation); // Random between baseSpeed ± 25%
self.health = 40; // 100% more life (20 * 2)
self.maxHealth = 40; // Store base health for wave scaling
// Make 50% larger by scaling the existing zombie graphics after brief delay to ensure visual is ready
LK.setTimeout(function () {
tween(self, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.easeOut
});
}, 50);
// 50% chance to have a random powerup in inventory
self.hasPowerup = Math.random() < 0.5;
if (self.hasPowerup) {
var powerUpTypes = ['powerShotPowerUp', 'healthPack', 'tripleShot'];
self.powerupType = powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)];
}
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
var player;
var zombies = [];
var bullets = [];
var pickups = [];
var wave = 1;
var zombiesPerWave = 5;
var zombiesSpawned = 0;
var zombiesKilled = 0;
var spawnTimer = 0;
var scoreMultiplier = 1;
var killStreak = 0;
var powerAmmo = 0;
var lastPowerUpSpawn = 0;
var killsSinceLastPowerUp = 0;
var powerUpList = ['powerShotPowerUp', 'healthPack', 'tripleShot', 'sniperPowerUp'];
var lastPowerUpType = null;
var usedPowerUps = []; // Track which powerups have been used in current cycle
var availablePowerUps = powerUpList.slice(); // Copy of powerups available for selection
var hasTripleShot = false;
var tripleShotLevel = 0;
var maxTripleShotLevel = 5;
var hasSniperPowerUp = false;
var sniperLevel = 0;
var sniperCooldown = 0;
var sniperFireRate = 300; // 5 seconds at 60fps
var noZombiesTimer = 0;
var noZombiesThreshold = 600; // 10 seconds at 60fps
// Load persistent data from storage (removed reset statements)
// Load persistent data from storage without clearing
var zoins = storage.zoins || 0;
// Upgrade costs and levels - loaded from storage
var maxHealthUpgrades = storage.maxHealthUpgrades || 0;
var fireRateUpgrades = storage.fireRateUpgrades || 0;
var damageUpgrades = storage.damageUpgrades || 0;
// Save damage upgrades to storage whenever modified
storage.damageUpgrades = damageUpgrades;
// Load leaderboard data from storage
// Leaderboard data - reconstruct from flattened storage
var storedLeaderboard = storage.leaderboard || {};
var leaderboard = [];
if (storedLeaderboard.length) {
for (var i = 0; i < storedLeaderboard.length; i++) {
if (storedLeaderboard['name_' + i] !== undefined) {
leaderboard.push({
name: String(storedLeaderboard['name_' + i]),
score: Number(storedLeaderboard['score_' + i]),
wave: Number(storedLeaderboard['wave_' + i])
});
}
}
} else {
leaderboard = [];
}
// Arrays for generating player names
var adjectives = ['Brave', 'Swift', 'Mighty', 'Fierce', 'Bold', 'Quick', 'Strong', 'Wild', 'Sharp', 'Dark', 'Bright', 'Cold', 'Hot', 'Fast', 'Slow', 'Big', 'Small', 'Red', 'Blue', 'Green'];
var commonNames = ['Alex', 'Sam', 'Max', 'Jake', 'Mike', 'Tom', 'Ben', 'Dan', 'Joe', 'Ryan', 'Kyle', 'Luke', 'Dave', 'Jack', 'Matt', 'Nick', 'Chris', 'Josh', 'Mark', 'Paul', 'Emma', 'Sarah', 'Lisa', 'Amy', 'Kate', 'Anna', 'Maya', 'Zoe', 'Mia', 'Ella', 'Eva', 'Ava', 'Lily', 'Ruby', 'Nina', 'Rose', 'Lucy', 'Jade', 'Hope', 'Iris'];
// Load or generate player name
var currentPlayerName = storage.playerName || '';
if (!currentPlayerName) {
var randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
var randomName = commonNames[Math.floor(Math.random() * commonNames.length)];
currentPlayerName = randomAdjective + '-' + randomName;
storage.playerName = currentPlayerName;
}
var moveControl;
var moveStick;
var isDraggingMove = false;
// UI Elements
var waveText = new Text2('Wave: 1', {
size: 80,
fill: 0xFFFFFF
});
waveText.anchor.set(0, 0);
waveText.x = 150;
LK.gui.topLeft.addChild(waveText);
var scoreText = new Text2('Score: 0', {
size: 60,
fill: 0xFFFFFF
});
scoreText.anchor.set(1, 0);
scoreText.x = -150;
LK.gui.topRight.addChild(scoreText);
// Load hi-score from storage or default to 0
var hiScore = storage.hiScore || 0;
var hiScoreText = new Text2('Hi-Score: ' + hiScore, {
size: 60,
fill: 0xFFD700
});
hiScoreText.anchor.set(1, 0);
hiScoreText.x = -150;
hiScoreText.y = 70; // Position below score display
LK.gui.topRight.addChild(hiScoreText);
// Health bar black outline
var healthBarOutline = LK.getAsset('ammoBox', {
width: 1849,
height: 81,
anchorX: 0.5,
anchorY: 0
});
healthBarOutline.tint = 0x000000;
healthBarOutline.x = 1024;
healthBarOutline.y = 97;
game.addChildAt(healthBarOutline, 0);
// Health bar background using lifebar asset
var healthBarBg = LK.getAsset('lifebar', {
width: 2048,
height: 225,
anchorX: 0.5,
anchorY: 0.5
});
healthBarBg.x = 1024;
healthBarBg.y = 166.25;
game.addChildAt(healthBarBg, 1);
// Health bar fill
var healthBarFill = LK.getAsset('ammoBox', {
width: 1382.25,
// Reduced by 25% (1843 * 0.75)
height: 58.59375,
// 25% taller (46.875 * 1.25)
anchorX: 0.5,
anchorY: 0
});
healthBarFill.tint = 0x000000;
healthBarFill.x = 1024;
healthBarFill.y = 113.75; // Moved up 5px (118.75 - 5)
game.addChildAt(healthBarFill, 2);
// Health text display
var healthText = new Text2('100/100', {
size: 56,
fill: 0xFFFFFF
});
healthText.anchor.set(0.5, 0.5);
healthText.x = 1024;
healthText.y = 137.5; // Center of health bar (100 + 75/2)
game.addChildAt(healthText, 3);
var powerAmmoText = new Text2('Power Ammo: 0', {
size: 45,
fill: 0xFFD700
});
powerAmmoText.anchor.set(1, 1);
powerAmmoText.x = -20;
powerAmmoText.y = -80;
LK.gui.bottomRight.addChild(powerAmmoText);
var zoinText = new Text2('Zoins: ' + zoins, {
size: 45,
fill: 0xFFD700
});
zoinText.anchor.set(1, 1);
zoinText.x = -20;
zoinText.y = -140;
LK.gui.bottomRight.addChild(zoinText);
// Leaderboard display as scrolling banner
var leaderboardText = new Text2('LEADERBOARD\n', {
size: 40,
fill: 0xFFFFFF
});
leaderboardText.anchor.set(0, 0);
leaderboardText.x = 2048; // Start off-screen to the right
leaderboardText.y = 200;
game.addChild(leaderboardText);
// Leaderboard scrolling variables
var leaderboardScrolling = false;
// Update leaderboard display
function updateLeaderboardDisplay() {
var displayText = 'LEADERBOARD ';
var sortedLeaderboard = leaderboard.slice().sort(function (a, b) {
return b.score - a.score;
});
// Show top 20 entries
for (var i = 0; i < Math.min(20, sortedLeaderboard.length); i++) {
var entry = sortedLeaderboard[i];
displayText += i + 1 + '. ' + entry.name + ' - ' + entry.score + ' (Wave ' + entry.wave + ') • ';
}
// Find current player's best entry and position
var playerBestEntry = null;
var playerPosition = -1;
for (var i = 0; i < sortedLeaderboard.length; i++) {
var entry = sortedLeaderboard[i];
if (entry.name === currentPlayerName && (!playerBestEntry || entry.score > playerBestEntry.score)) {
playerBestEntry = entry;
playerPosition = i + 1;
}
}
// If player has an entry and it's not in top 20, show their position
if (playerBestEntry && playerPosition > 20) {
displayText += '... ' + playerPosition + '. ' + playerBestEntry.name + ' - ' + playerBestEntry.score + ' (Wave ' + playerBestEntry.wave + ') • ';
}
leaderboardText.setText(displayText);
startLeaderboardScroll();
}
// Start leaderboard scrolling animation
function startLeaderboardScroll() {
if (leaderboardScrolling) return;
leaderboardScrolling = true;
// Stop any existing tween
tween.stop(leaderboardText, {
x: true
});
// Reset position to start from right
leaderboardText.x = 2048;
// Calculate scroll distance based on text width
var scrollDistance = leaderboardText.width + 2048;
// Start scrolling animation
tween(leaderboardText, {
x: -scrollDistance
}, {
duration: scrollDistance * 10,
// 50% faster scrolling (15 -> 10)
easing: tween.linear,
onFinish: function onFinish() {
leaderboardScrolling = false;
// Restart the scroll immediately for continuous display
startLeaderboardScroll();
}
});
}
updateLeaderboardDisplay();
// Function to submit score to leaderboard
function submitScoreToLeaderboard() {
// Player name is now persistently generated at game start
// Create simple literal object for leaderboard entry
var newEntry = {
name: currentPlayerName,
score: LK.getScore(),
wave: wave
};
// Create cleaned leaderboard first
var cleanLeaderboard = [];
for (var i = 0; i < leaderboard.length; i++) {
var entry = leaderboard[i];
cleanLeaderboard.push({
name: String(entry.name),
score: Number(entry.score),
wave: Number(entry.wave)
});
}
// Add new entry as literal object
cleanLeaderboard.push({
name: String(newEntry.name),
score: Number(newEntry.score),
wave: Number(newEntry.wave)
});
// Sort and limit to top 50
cleanLeaderboard.sort(function (a, b) {
return b.score - a.score;
});
if (cleanLeaderboard.length > 50) {
cleanLeaderboard = cleanLeaderboard.slice(0, 50);
}
// Update local leaderboard and save to storage
leaderboard = cleanLeaderboard;
// Flatten leaderboard for storage (storage only accepts literals and 1-level objects)
var flatLeaderboard = {};
for (var i = 0; i < cleanLeaderboard.length; i++) {
var entry = cleanLeaderboard[i];
flatLeaderboard['name_' + i] = String(entry.name);
flatLeaderboard['score_' + i] = Number(entry.score);
flatLeaderboard['wave_' + i] = Number(entry.wave);
}
flatLeaderboard.length = cleanLeaderboard.length;
storage.leaderboard = flatLeaderboard;
// Update display
updateLeaderboardDisplay();
}
// Set random background on game start
var backgroundAssets = ['background1', 'background2', 'background3', 'background4', 'background5', 'background6'];
var randomBackgroundId = backgroundAssets[Math.floor(Math.random() * backgroundAssets.length)];
var gameBackground = LK.getAsset(randomBackgroundId, {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0
});
gameBackground.x = 0;
gameBackground.y = 0;
game.addChildAt(gameBackground, 0); // Add at bottom layer
// Ensure all health bar elements are above background
game.removeChild(healthBarOutline);
game.addChild(healthBarOutline);
game.removeChild(healthBarBg);
game.addChild(healthBarBg);
game.removeChild(healthBarFill);
game.addChild(healthBarFill);
game.removeChild(healthText);
game.addChild(healthText);
// Initialize player
player = game.addChild(new Player());
player.maxHealth = Math.round((maxHealthUpgrades + 1) * 2);
player.health = Math.round(player.maxHealth);
player.fireRate = Math.round(3 * Math.pow(1.1, fireRateUpgrades));
player.x = 1024;
player.y = 1366;
// Initialize controls
moveControl = LK.getAsset('moveControl', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
moveControl.x = 300;
moveControl.y = 2432;
game.addChild(moveControl);
moveStick = LK.getAsset('moveControl', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.3,
alpha: 0
});
moveStick.x = moveControl.x;
moveStick.y = moveControl.y;
game.addChild(moveStick);
// Aim control removed - using automatic targeting
// Helper functions
function countHealthPacksOnScreen() {
var healthPackCount = 0;
for (var i = 0; i < pickups.length; i++) {
if (pickups[i] instanceof HealthPack) {
healthPackCount++;
}
}
return healthPackCount;
}
function findNonOverlappingPosition(startX, startY) {
var attempts = 0;
var maxAttempts = 10;
var minDistance = 80; // Minimum distance between pickups
var finalX = startX;
var finalY = startY;
while (attempts < maxAttempts) {
var overlapping = false;
// Check distance from all existing pickups
for (var i = 0; i < pickups.length; i++) {
var existingPickup = pickups[i];
var dx = finalX - existingPickup.x;
var dy = finalY - existingPickup.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
overlapping = true;
break;
}
}
if (!overlapping) {
break; // Found a good position
}
// Try a new position in a circle around the original point
var angle = Math.random() * Math.PI * 2;
var radius = 40 + attempts * 20; // Increase radius with each attempt
finalX = startX + Math.cos(angle) * radius;
finalY = startY + Math.sin(angle) * radius;
// Keep within screen bounds
finalX = Math.max(60, Math.min(1988, finalX));
finalY = Math.max(60, Math.min(2672, finalY));
attempts++;
}
return {
x: finalX,
y: finalY
};
}
function calculateCumulativeCost(upgradeLevel) {
var totalCost = 0;
for (var i = 0; i <= upgradeLevel; i++) {
totalCost += Math.round((10 + i * 2) / 2);
}
return totalCost;
}
function resetGame() {
// Set new random background on game reset
var backgroundAssets = ['background1', 'background2', 'background3', 'background4', 'background5', 'background6'];
var randomBackgroundId = backgroundAssets[Math.floor(Math.random() * backgroundAssets.length)];
var newGameBackground = LK.getAsset(randomBackgroundId, {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0
});
newGameBackground.x = 0;
newGameBackground.y = 0;
// Remove old background if it exists
if (game.children[0] && game.children[0].width === 2048 && game.children[0].height === 2732) {
game.children[0].destroy();
game.removeChildAt(0);
}
game.addChildAt(newGameBackground, 0); // Add at bottom layer
// Ensure all health bar elements are above background
game.removeChild(healthBarOutline);
game.addChild(healthBarOutline);
game.removeChild(healthBarBg);
game.addChild(healthBarBg);
game.removeChild(healthBarFill);
game.addChild(healthBarFill);
game.removeChild(healthText);
game.addChild(healthText);
// Reset wave and zombies
wave = 1;
zombiesPerWave = 5;
zombiesSpawned = 0;
zombiesKilled = 0;
spawnTimer = 0;
hulksSpawned = 0; // Reset hulk spawn counter
// Reset score and multipliers
LK.setScore(0);
scoreMultiplier = 1;
killStreak = 0;
killsSinceLastPowerUp = 0;
lastPowerUpSpawn = 0;
lastPowerUpType = null;
// Reset powerup cycle tracking
usedPowerUps = [];
availablePowerUps = powerUpList.slice();
// Reset powerups
powerAmmo = 0;
hasTripleShot = false;
tripleShotLevel = 0;
hasSniperPowerUp = false;
sniperLevel = 0;
sniperCooldown = 0;
// Reset player state with upgrades
player.maxHealth = Math.round((maxHealthUpgrades + 1) * 2);
player.health = Math.round(player.maxHealth);
player.fireRate = Math.round(3 * Math.pow(1.1, fireRateUpgrades));
player.fireCooldown = 0;
player.alpha = 1;
// Clear all game objects
for (var i = zombies.length - 1; i >= 0; i--) {
zombies[i].destroy();
}
zombies = [];
for (var i = bullets.length - 1; i >= 0; i--) {
bullets[i].destroy();
}
bullets = [];
for (var i = pickups.length - 1; i >= 0; i--) {
pickups[i].destroy();
}
pickups = [];
// Update UI
waveText.setText('Wave: ' + wave);
scoreText.setText('Score: ' + LK.getScore());
hiScoreText.setText('Hi-Score: ' + hiScore);
// Update health bar
var healthPercent = player.health / player.maxHealth;
var depletedPercent = 1 - healthPercent; // Invert to show depleted health
healthBarFill.width = 1382.25 * depletedPercent;
healthBarFill.height = 58.59375;
healthText.setText(player.health + '/' + player.maxHealth);
powerAmmoText.setText('Power Ammo: ' + powerAmmo);
powerAmmoText.visible = false;
// Reset player position
player.x = 1024;
player.y = 1366;
// Reset no zombies timer
noZombiesTimer = 0;
// Update powerup display
updatePowerupDisplay();
}
// Track hulks spawned for current wave
var hulksSpawned = 0;
function spawnZombie() {
var zombie;
// Spawn regular zombies first, then hulks at the end
var regularZombiesCount = zombiesPerWave - wave; // Total zombies minus hulks
if (zombiesSpawned < regularZombiesCount) {
zombie = new Zombie();
} else {
zombie = new ZombieHulk();
hulksSpawned++;
}
var side = Math.floor(Math.random() * 4);
switch (side) {
case 0:
// Top
zombie.x = Math.random() * 2048;
zombie.y = -50;
break;
case 1:
// Right
zombie.x = 2098;
zombie.y = Math.random() * 2732;
break;
case 2:
// Bottom
zombie.x = Math.random() * 2048;
zombie.y = 2782;
break;
case 3:
// Left
zombie.x = -50;
zombie.y = Math.random() * 2732;
break;
}
zombie.speed = Math.round(zombie.baseSpeed * (1 + wave * 0.1) + (Math.random() - 0.5) * 2 * (zombie.baseSpeed * 0.25));
zombie.health = Math.round(zombie.health + wave * 5);
zombie.damage = Math.round(zombie.damage + Math.floor(wave * 2)); // Increase damage by 2 per wave
zombie.scaleX = 0;
zombie.scaleY = 0;
zombie.alpha = 0;
zombies.push(zombie);
game.addChild(zombie);
zombiesSpawned++;
// Animate zombie spawn
tween(zombie, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 300,
easing: tween.easeOut
});
}
function spawnPickup(x, y, zombie) {
var shouldSpawn = false;
var pickup;
var powerUpType;
// Helper function to find a non-overlapping position
function findNonOverlappingPosition(startX, startY) {
var attempts = 0;
var maxAttempts = 10;
var minDistance = 80; // Minimum distance between pickups
var finalX = startX;
var finalY = startY;
while (attempts < maxAttempts) {
var overlapping = false;
// Check distance from all existing pickups
for (var i = 0; i < pickups.length; i++) {
var existingPickup = pickups[i];
var dx = finalX - existingPickup.x;
var dy = finalY - existingPickup.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
overlapping = true;
break;
}
}
if (!overlapping) {
break; // Found a good position
}
// Try a new position in a circle around the original point
var angle = Math.random() * Math.PI * 2;
var radius = 40 + attempts * 20; // Increase radius with each attempt
finalX = startX + Math.cos(angle) * radius;
finalY = startY + Math.sin(angle) * radius;
// Keep within screen bounds
finalX = Math.max(60, Math.min(1988, finalX));
finalY = Math.max(60, Math.min(2672, finalY));
attempts++;
}
return {
x: finalX,
y: finalY
};
}
// Check if zombie hulk has powerup in inventory
if (zombie instanceof ZombieHulk && zombie.hasPowerup) {
shouldSpawn = true;
// If all powerups have been used, reset the cycle
if (availablePowerUps.length === 0) {
availablePowerUps = powerUpList.slice(); // Reset available powerups
usedPowerUps = []; // Clear used powerups
}
// Select random powerup from available list for hulk
var randomIndex = Math.floor(Math.random() * availablePowerUps.length);
powerUpType = availablePowerUps[randomIndex];
// Move selected powerup from available to used
usedPowerUps.push(powerUpType);
availablePowerUps.splice(randomIndex, 1);
if (powerUpType === 'powerShotPowerUp') {
pickup = new PowerShotPowerUp();
} else if (powerUpType === 'tripleShot') {
pickup = new TripleShot();
} else if (powerUpType === 'sniperPowerUp') {
pickup = new SniperPowerUp();
} else {
// Check if healthpack would be spawned and limit is reached
if (countHealthPacksOnScreen() >= 2) {
// Replace with 2 zoin drops with non-overlapping positions
var pos1 = findNonOverlappingPosition(x - 30, y);
var pos2 = findNonOverlappingPosition(x + 30, y);
var zoin1 = new Zoin();
zoin1.x = pos1.x;
zoin1.y = pos1.y;
zoin1.scaleX = 0;
zoin1.scaleY = 0;
pickups.push(zoin1);
game.addChild(zoin1);
var zoin2 = new Zoin();
zoin2.x = pos2.x;
zoin2.y = pos2.y;
zoin2.scaleX = 0;
zoin2.scaleY = 0;
pickups.push(zoin2);
game.addChild(zoin2);
// Animate both zoin spawns
tween(zoin1, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
tween(zoin2, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
// Don't create healthpack
pickup = null;
} else {
pickup = new HealthPack();
}
}
killsSinceLastPowerUp = 0;
if (pickup instanceof PowerShotPowerUp) {
lastPowerUpSpawn = LK.ticks;
}
}
// Force powerup spawn if 50+ kills without powerup (but not for normal zombies)
else if (killsSinceLastPowerUp >= 50) {
shouldSpawn = true;
// If all powerups have been used, reset the cycle
if (availablePowerUps.length === 0) {
availablePowerUps = powerUpList.slice(); // Reset available powerups
usedPowerUps = []; // Clear used powerups
}
// Select random powerup from available list
var randomIndex = Math.floor(Math.random() * availablePowerUps.length);
powerUpType = availablePowerUps[randomIndex];
// Move selected powerup from available to used
usedPowerUps.push(powerUpType);
availablePowerUps.splice(randomIndex, 1);
if (powerUpType === 'powerShotPowerUp') {
pickup = new PowerShotPowerUp();
} else if (powerUpType === 'tripleShot') {
pickup = new TripleShot();
} else if (powerUpType === 'sniperPowerUp') {
pickup = new SniperPowerUp();
} else {
// Check if healthpack would be spawned and limit is reached
if (countHealthPacksOnScreen() >= 2) {
// Replace with 2 zoin drops with non-overlapping positions
var pos1 = findNonOverlappingPosition(x - 30, y);
var pos2 = findNonOverlappingPosition(x + 30, y);
var zoin1 = new Zoin();
zoin1.x = pos1.x;
zoin1.y = pos1.y;
zoin1.scaleX = 0;
zoin1.scaleY = 0;
pickups.push(zoin1);
game.addChild(zoin1);
var zoin2 = new Zoin();
zoin2.x = pos2.x;
zoin2.y = pos2.y;
zoin2.scaleX = 0;
zoin2.scaleY = 0;
pickups.push(zoin2);
game.addChild(zoin2);
// Animate both zoin spawns
tween(zoin1, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
tween(zoin2, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
// Don't create healthpack
pickup = null;
} else {
pickup = new HealthPack();
}
}
killsSinceLastPowerUp = 0;
if (pickup instanceof PowerShotPowerUp) {
lastPowerUpSpawn = LK.ticks;
}
}
// No powerup spawn for normal zombies
if (shouldSpawn && pickup) {
// Find non-overlapping position for the pickup
var position = findNonOverlappingPosition(x, y);
pickup.x = position.x;
pickup.y = position.y;
pickup.scaleX = 0;
pickup.scaleY = 0;
pickups.push(pickup);
game.addChild(pickup);
// Animate pickup spawn with bounce effect
tween(pickup, {
scaleX: 1,
scaleY: 1
}, {
duration: 400,
easing: tween.elasticOut
});
}
}
function fireBullet() {
// Handle sniper firing independently
if (hasSniperPowerUp && sniperCooldown <= 0) {
// Find farthest zombie that is beyond normal bullet range (500px) and on screen
var sniperTarget = null;
var sniperDistance = 0; // Track farthest distance instead of closest
var normalBulletRange = 500; // Normal bullet maximum range
for (var i = 0; i < zombies.length; i++) {
var zombie = zombies[i];
if (!zombie || zombie.isDead) continue;
var dx = zombie.x - player.x;
var dy = zombie.y - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Only target zombies beyond normal bullet range and on screen
if (distance > normalBulletRange && distance > sniperDistance && zombie.x >= -50 && zombie.x <= 2098 && zombie.y >= -50 && zombie.y <= 2782) {
sniperDistance = distance;
sniperTarget = zombie;
}
}
if (sniperTarget) {
var dx = sniperTarget.x - player.x;
var dy = sniperTarget.y - player.y;
var angle = Math.atan2(dy, dx);
var sniperBullet = new SniperBullet();
sniperBullet.x = player.x;
sniperBullet.y = player.y;
sniperBullet.damage = Math.round(100 + damageUpgrades * 50); // 10x base damage + upgrades
sniperBullet.vx = Math.cos(angle) * sniperBullet.speed;
sniperBullet.vy = Math.sin(angle) * sniperBullet.speed;
sniperBullet.rotation = angle;
// Visual effect for sniper bullet
tween(sniperBullet, {
scaleX: 2,
scaleY: 2
}, {
duration: 100
});
bullets.push(sniperBullet);
game.addChild(sniperBullet);
// Calculate sniper fire rate: base rate decreases by 10% per sniper level (faster firing)
var sniperFireRateWithUpgrade = Math.round(sniperFireRate * Math.pow(0.9, sniperLevel - 1));
sniperCooldown = sniperFireRateWithUpgrade; // Reset sniper cooldown with improved rate
LK.getSound('shoot').play();
}
}
// Convert shots per second to ticks between shots (60 ticks = 1 second)
var shotsPerSecond = powerAmmo > 0 ? player.fireRate * 0.25 : player.fireRate; // Power ammo shoots at 25% of normal rate
var ticksBetweenShots = Math.max(1, Math.floor(60 / shotsPerSecond)); // Ensure at least 1 tick between shots
if (player.fireCooldown > 0) return;
// Find closest zombie in range
var closestZombie = null;
var closestDistance = 500; // Maximum firing range
for (var i = 0; i < zombies.length; i++) {
var zombie = zombies[i];
if (!zombie || zombie.isDead) continue;
var dx = zombie.x - player.x;
var dy = zombie.y - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestZombie = zombie;
}
}
// Only fire if there's a zombie in range
if (!closestZombie) return;
var dx = closestZombie.x - player.x;
var dy = closestZombie.y - player.y;
var angle = Math.atan2(dy, dx);
// Player visual does not rotate - only bullets aim at target
// Create bullets based on triple shot level
var bulletCount = hasTripleShot ? 1 + tripleShotLevel * 2 : 1;
var angleOffsets = [0]; // Always include center bullet
if (hasTripleShot) {
// Add bullets in pairs at increasing angles
for (var level = 1; level <= tripleShotLevel; level++) {
var angleOffset = level * 10 * Math.PI / 180;
angleOffsets.push(-angleOffset); // Left bullet
angleOffsets.push(angleOffset); // Right bullet
}
}
// Consume power ammo once per shot, not per bullet
var usingPowerAmmo = powerAmmo > 0;
if (usingPowerAmmo) {
powerAmmo--; // Consume one power ammo per shot
}
for (var b = 0; b < bulletCount; b++) {
var bullet = new Bullet();
bullet.x = player.x;
bullet.y = player.y;
bullet.damage = Math.round(10 + damageUpgrades * 5);
var bulletAngle = angle + angleOffsets[b];
bullet.vx = Math.cos(bulletAngle) * bullet.speed;
bullet.vy = Math.sin(bulletAngle) * bullet.speed;
bullet.rotation = bulletAngle;
if (usingPowerAmmo) {
bullet.piercing = true;
bullet.maxDistance = 1600; // 2x distance
bullet.damage = Math.round((10 + damageUpgrades * 5) * 5); // 5x normal damage
// Visual effect for power bullet
tween(bullet, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 100
});
bullet.tint = 0xFFD700;
} else {
bullet.maxDistance = 800;
}
bullets.push(bullet);
game.addChild(bullet);
}
player.fireCooldown = ticksBetweenShots;
LK.getSound('shoot').play();
}
// Event handlers
game.down = function (x, y, obj) {
// Prevent control stick activation in bottom 1/4 of viewport (below y = 2049)
if (y > 2049) return;
isDraggingMove = true;
// Move moveControl to click position
moveControl.x = x;
moveControl.y = y;
// Move moveStick to the same position (center of control)
moveStick.x = x;
moveStick.y = y;
// Show control stick with smooth animation
tween(moveControl, {
alpha: 0.3
}, {
duration: 200,
easing: tween.easeOut
});
tween(moveStick, {
alpha: 0.6
}, {
duration: 200,
easing: tween.easeOut
});
};
game.move = function (x, y, obj) {
if (isDraggingMove) {
moveStick.x = Math.max(moveControl.x - 150, Math.min(moveControl.x + 150, x));
moveStick.y = Math.max(moveControl.y - 150, Math.min(moveControl.y + 150, y));
}
};
game.up = function (x, y, obj) {
if (isDraggingMove) {
isDraggingMove = false;
moveStick.x = moveControl.x;
moveStick.y = moveControl.y;
// Hide control stick with smooth animation
tween(moveControl, {
alpha: 0
}, {
duration: 150,
easing: tween.easeIn
});
tween(moveStick, {
alpha: 0
}, {
duration: 150,
easing: tween.easeIn
});
}
};
// Main game loop
game.update = function () {
// Player movement
if (isDraggingMove) {
var moveX = (moveStick.x - moveControl.x) / 150;
var moveY = (moveStick.y - moveControl.y) / 150;
player.x += moveX * player.moveSpeed;
player.y += moveY * player.moveSpeed;
player.x = Math.max(40, Math.min(2008, player.x));
player.y = Math.max(40, Math.min(2692, player.y));
}
// Update sniper cooldown
if (sniperCooldown > 0) sniperCooldown--;
// Auto-fire (fireBullet now handles range checking internally)
fireBullet();
// Spawn zombies
if (zombiesSpawned < zombiesPerWave) {
spawnTimer++;
if (spawnTimer > 60) {
spawnZombie();
spawnTimer = 0;
}
} else if (zombies.length === 0) {
// Next wave with transition
wave++;
zombiesPerWave = Math.min(5 + wave * 2, 30); // Cap at 30 zombies per wave to prevent performance issues
zombiesSpawned = 0;
zombiesKilled = 0;
spawnTimer = 0; // Reset spawn timer for new wave
noZombiesTimer = 0; // Reset no zombies timer
hulksSpawned = 0; // Reset hulk spawn counter for new wave
waveText.setText('Wave: ' + wave);
transitionToNextWave();
} else {
// Check if there are zombies in view area (within screen bounds plus margin)
var zombiesInView = false;
for (var z = 0; z < zombies.length; z++) {
var zombie = zombies[z];
if (!zombie || zombie.isDead) continue;
// Check if zombie is within view area (screen bounds + 200px margin)
if (zombie.x >= -200 && zombie.x <= 2248 && zombie.y >= -200 && zombie.y <= 2932) {
zombiesInView = true;
break;
}
}
// If no zombies in view, increment timer
if (!zombiesInView && zombiesSpawned >= zombiesPerWave) {
noZombiesTimer++;
// If 10 seconds have passed without zombies in view, advance to next wave
if (noZombiesTimer >= noZombiesThreshold) {
// Remove all remaining zombies
for (var i = zombies.length - 1; i >= 0; i--) {
zombies[i].destroy();
zombies.splice(i, 1);
}
// Next wave with transition
wave++;
zombiesPerWave = Math.min(5 + wave * 2, 30); // Cap at 30 zombies per wave to prevent performance issues
zombiesSpawned = 0;
zombiesKilled = 0;
spawnTimer = 0; // Reset spawn timer for new wave
noZombiesTimer = 0; // Reset no zombies timer
hulksSpawned = 0; // Reset hulk spawn counter for new wave
waveText.setText('Wave: ' + wave);
transitionToNextWave();
}
} else {
// Reset timer if zombies are in view or we haven't spawned all zombies yet
noZombiesTimer = 0;
}
}
// Update zombies
for (var i = zombies.length - 1; i >= 0; i--) {
var zombie = zombies[i];
// Skip if zombie is undefined or dead
if (!zombie || zombie.isDead) continue;
// Move towards player
var dx = player.x - zombie.x;
var dy = player.y - zombie.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
zombie.x += dx / dist * zombie.speed;
zombie.y += dy / dist * zombie.speed;
// Removed zombie rotation to keep zombies upright
}
// Check collision with player
if (zombie.intersects(player) && zombie.attackCooldown === 0) {
var playerDied = player.takeDamage(zombie.damage);
zombie.attackCooldown = 60;
killStreak = 0;
scoreMultiplier = 1;
// Check if player died after taking damage
if (playerDied || player.health <= 0) {
// Submit score to leaderboard before game over
submitScoreToLeaderboard();
LK.showGameOver();
return; // Exit game loop immediately when player dies
}
}
}
// Check if player died during zombie processing - exit immediately
if (player.health <= 0) {
return;
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
// Check if bullet is off screen or traveled max distance
if (bullet.x < -50 || bullet.x > 2098 || bullet.y < -50 || bullet.y > 2782 || bullet.travelDistance > bullet.maxDistance) {
bullet.destroy();
bullets.splice(i, 1);
continue;
}
// Check collision with zombies
var bulletDestroyed = false;
for (var j = zombies.length - 1; j >= 0; j--) {
var zombie = zombies[j];
if (!zombie || zombie.isDead) continue;
if (bullet.intersects(zombie)) {
if (zombie.takeDamage(bullet.damage)) {
// Zombie died - add death animation
spawnPickup(zombie.x, zombie.y, zombie);
// 10% chance to drop 1 zoin when zombie dies
if (Math.random() < 0.1) {
// Find non-overlapping position for zoin drop
var zoinPosition = findNonOverlappingPosition(zombie.x, zombie.y);
var zoin = new Zoin();
zoin.x = zoinPosition.x;
zoin.y = zoinPosition.y;
zoin.scaleX = 0;
zoin.scaleY = 0;
pickups.push(zoin);
game.addChild(zoin);
// Animate zoin spawn
tween(zoin, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
}
var zombieToRemove = zombie; // Capture zombie reference for closure
var zombieIndex = j; // Capture index for closure
tween(zombie, {
scaleX: 0,
scaleY: 0,
alpha: 0,
rotation: zombie.rotation + Math.PI
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
zombieToRemove.destroy();
// Remove from zombies array after animation
var currentIndex = zombies.indexOf(zombieToRemove);
if (currentIndex !== -1) {
zombies.splice(currentIndex, 1);
}
}
});
zombiesKilled++;
killStreak++;
killsSinceLastPowerUp++;
if (killStreak % 5 === 0) {
scoreMultiplier = Math.min(scoreMultiplier + 1, 5);
}
// Give 2x score for ZombieHulk, regular score for normal zombies
var scoreToAdd = zombie instanceof ZombieHulk ? 20 * scoreMultiplier : 10 * scoreMultiplier;
LK.setScore(LK.getScore() + scoreToAdd);
scoreText.setText('Score: ' + LK.getScore());
}
if (!bullet.piercing) {
bullet.destroy();
bullets.splice(i, 1);
bulletDestroyed = true;
break;
}
}
}
if (bulletDestroyed) continue;
}
// Update pickups
for (var i = pickups.length - 1; i >= 0; i--) {
var pickup = pickups[i];
// Check if pickup has expired (10 seconds) - auto pickup non-healthpack items
var age = LK.ticks - pickup.spawnTime;
if (age >= pickup.lifespan) {
// Auto pickup all items except health packs when expiring - healthpacks are perpetual
if (pickup instanceof HealthPack) {
// HealthPack no longer expires - skip expiration logic completely
continue;
} else if (pickup instanceof PowerShotPowerUp) {
powerAmmo += pickup.ammoAmount;
LK.effects.flashObject(player, 0xFFD700, 500);
} else if (pickup instanceof TripleShot) {
if (!hasTripleShot) {
hasTripleShot = true;
tripleShotLevel = 1;
} else if (tripleShotLevel < maxTripleShotLevel) {
tripleShotLevel++;
}
LK.effects.flashObject(player, 0x00FF00, 500);
} else if (pickup instanceof SniperPowerUp) {
hasSniperPowerUp = true;
sniperLevel++; // Increase sniper level each time powerup is collected
sniperCooldown = 0; // Ready to fire immediately
LK.effects.flashObject(player, 0xFF0000, 500);
} else if (pickup instanceof Zoin) {
zoins++;
storage.zoins = zoins; // Persist zoin count
zoinText.setText('Zoins: ' + zoins);
LK.effects.flashObject(player, 0xFFD700, 300);
}
LK.getSound('pickup').play();
// Animate pickup collection
tween(pickup, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
pickup.destroy();
}
});
pickups.splice(i, 1);
continue;
}
// Manual pickup when touching
if (pickup.intersects(player)) {
if (pickup instanceof HealthPack) {
player.heal(pickup.healAmount);
} else if (pickup instanceof PowerShotPowerUp) {
powerAmmo += pickup.ammoAmount;
LK.effects.flashObject(player, 0xFFD700, 500);
} else if (pickup instanceof TripleShot) {
if (!hasTripleShot) {
hasTripleShot = true;
tripleShotLevel = 1;
} else if (tripleShotLevel < maxTripleShotLevel) {
tripleShotLevel++;
}
LK.effects.flashObject(player, 0x00FF00, 500);
} else if (pickup instanceof SniperPowerUp) {
hasSniperPowerUp = true;
sniperLevel++; // Increase sniper level each time powerup is collected
sniperCooldown = 0; // Ready to fire immediately
LK.effects.flashObject(player, 0xFF0000, 500);
} else if (pickup instanceof Zoin) {
zoins++;
storage.zoins = zoins; // Persist zoin count
zoinText.setText('Zoins: ' + zoins);
LK.effects.flashObject(player, 0xFFD700, 300);
}
LK.getSound('pickup').play();
// Animate pickup collection
tween(pickup, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
pickup.destroy();
}
});
pickups.splice(i, 1);
}
}
// Update UI
// Update hi-score if current score is higher
var currentScore = LK.getScore();
if (currentScore > hiScore) {
hiScore = currentScore;
storage.hiScore = hiScore;
hiScoreText.setText('Hi-Score: ' + hiScore);
}
// Update health bar
var healthPercent = player.health / player.maxHealth;
var depletedPercent = 1 - healthPercent; // Invert to show depleted health
healthBarFill.width = 1382.25 * depletedPercent;
healthBarFill.height = 58.59375;
healthText.setText(player.health + '/' + player.maxHealth);
powerAmmoText.setText('Power Ammo: ' + powerAmmo);
// Hide/show power ammo counter based on availability
powerAmmoText.visible = powerAmmo > 0;
// Update powerup display
updatePowerupDisplay();
// Update button state based on zoin availability
var currentPrice = calculateCumulativeCost(maxHealthUpgrades);
if (zoins < currentPrice) {
bottomLeftButton.alpha = 0.5; // Dim button when unaffordable
lifeLabel.alpha = 0.5;
priceLabel.alpha = 0.5;
} else {
bottomLeftButton.alpha = 1.0; // Full opacity when affordable
lifeLabel.alpha = 1.0;
priceLabel.alpha = 1.0;
}
// Update fire rate button state based on zoin availability
var fireRateCurrentPrice = calculateCumulativeCost(fireRateUpgrades);
if (zoins < fireRateCurrentPrice) {
secondButton.alpha = 0.5; // Dim button when unaffordable
fireRateLabel.alpha = 0.5;
fireRatePriceLabel.alpha = 0.5;
} else {
secondButton.alpha = 1.0; // Full opacity when affordable
fireRateLabel.alpha = 1.0;
fireRatePriceLabel.alpha = 1.0;
}
// Despawn distant pickups
for (var i = pickups.length - 1; i >= 0; i--) {
var pickup = pickups[i];
var dx = player.x - pickup.x;
var dy = player.y - pickup.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 2000) {
pickup.destroy();
pickups.splice(i, 1);
}
}
};
// Create bottom left button
var bottomLeftButton = LK.getAsset('ammoBox', {
width: 250,
height: 100,
anchorX: 0,
anchorY: 1
});
bottomLeftButton.tint = 0xFF0000; // Red background
bottomLeftButton.x = 20;
bottomLeftButton.y = -20;
LK.gui.bottomLeft.addChild(bottomLeftButton);
// Create black outline for button
var buttonOutline = LK.getAsset('ammoBox', {
width: 256,
height: 106,
anchorX: 0,
anchorY: 1
});
buttonOutline.tint = 0x000000; // Black outline
buttonOutline.x = 17;
buttonOutline.y = -17;
LK.gui.bottomLeft.addChildAt(buttonOutline, 0); // Add behind the button
// Create LIFE label for button
var lifeLabel = new Text2('LIFE', {
size: 36,
fill: 0xFFFFFF,
font: "'Arial Black'"
});
lifeLabel.anchor.set(0.5, 0.5);
lifeLabel.x = 145; // Center of button (250/2 + 20 offset for button position)
lifeLabel.y = -70; // Center of button (100/2 + 20 offset for button position)
LK.gui.bottomLeft.addChild(lifeLabel);
// Create price label for button with dynamic pricing
var maxLifePrice = calculateCumulativeCost(maxHealthUpgrades);
var priceLabel = new Text2(maxLifePrice + ' Zoin' + (maxLifePrice === 1 ? '' : 's'), {
size: 24,
fill: 0xFFD700
});
priceLabel.anchor.set(0.5, 1);
priceLabel.x = 145; // Center of button horizontally
priceLabel.y = -25; // Bottom of button
LK.gui.bottomLeft.addChild(priceLabel);
// Make button interactive
bottomLeftButton.buttonMode = true;
// Add button click handlers
bottomLeftButton.down = function (x, y, obj) {
var currentPrice = calculateCumulativeCost(maxHealthUpgrades);
if (zoins < currentPrice) {
return; // Don't allow clicking if insufficient zoins
}
// Change to white when pressed
tween(bottomLeftButton, {
tint: 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
};
bottomLeftButton.up = function (x, y, obj) {
var currentPrice = calculateCumulativeCost(maxHealthUpgrades);
if (zoins < currentPrice) {
return; // Don't allow clicking if insufficient zoins
}
// Deduct cost from zoins
zoins = zoins - currentPrice;
storage.zoins = zoins; // Persist updated zoin count
zoinText.setText('Zoins: ' + zoins);
// Increment upgrade level
maxHealthUpgrades++;
storage.maxHealthUpgrades = maxHealthUpgrades; // Persist upgrade level
// Update player max health
player.maxHealth = Math.round((maxHealthUpgrades + 1) * 2);
player.health = player.maxHealth; // Full heal on upgrade
// Update health bar to reflect new maximum
var healthPercent = player.health / player.maxHealth;
var depletedPercent = 1 - healthPercent; // Invert to show depleted health
healthBarFill.width = 1382.25 * depletedPercent;
healthBarFill.height = 58.59375;
healthText.setText(player.health + '/' + player.maxHealth);
// Update price label with new cost
var newPrice = calculateCumulativeCost(maxHealthUpgrades);
priceLabel.setText(newPrice + ' Zoin' + (newPrice === 1 ? '' : 's'));
// Update powerup display
updatePowerupDisplay();
// Change back to red when released
tween(bottomLeftButton, {
tint: 0xFF0000
}, {
duration: 100,
easing: tween.easeOut
});
};
// Create second button next to the max life button
var secondButton = LK.getAsset('ammoBox', {
width: 250,
height: 100,
anchorX: 0,
anchorY: 1
});
secondButton.tint = 0xFFFF00; // Yellow background
secondButton.x = 290; // Position next to max life button (20 + 250 + 20 space = 290)
secondButton.y = -20;
LK.gui.bottomLeft.addChild(secondButton);
// Create black outline for second button
var secondButtonOutline = LK.getAsset('ammoBox', {
width: 256,
height: 106,
anchorX: 0,
anchorY: 1
});
secondButtonOutline.tint = 0x000000; // Black outline
secondButtonOutline.x = 287; // Position outline behind second button
secondButtonOutline.y = -17;
LK.gui.bottomLeft.addChildAt(secondButtonOutline, 0); // Add behind the button
// Create FIRE RATE label for second button
var fireRateLabel = new Text2('FIRE RATE', {
size: 36,
fill: 0xFFFFFF,
font: "'Arial Black'"
});
fireRateLabel.anchor.set(0.5, 0.5);
fireRateLabel.x = 415; // Center of second button (250/2 + 290 offset for button position)
fireRateLabel.y = -70; // Center of button (100/2 + 20 offset for button position)
LK.gui.bottomLeft.addChild(fireRateLabel);
// Create price label for second button with dynamic pricing
var fireRatePrice = calculateCumulativeCost(fireRateUpgrades);
var fireRatePriceLabel = new Text2(fireRatePrice + ' Zoin' + (fireRatePrice === 1 ? '' : 's'), {
size: 24,
fill: 0xFFD700
});
fireRatePriceLabel.anchor.set(0.5, 1);
fireRatePriceLabel.x = 415; // Center of second button horizontally
fireRatePriceLabel.y = -25; // Bottom of button
LK.gui.bottomLeft.addChild(fireRatePriceLabel);
// Make second button interactive
secondButton.buttonMode = true;
// Add second button click handlers
secondButton.down = function (x, y, obj) {
var currentPrice = calculateCumulativeCost(fireRateUpgrades);
if (zoins < currentPrice) {
return; // Don't allow clicking if insufficient zoins
}
// Change to white when pressed
tween(secondButton, {
tint: 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
};
secondButton.up = function (x, y, obj) {
var currentPrice = calculateCumulativeCost(fireRateUpgrades);
if (zoins < currentPrice) {
return; // Don't allow clicking if insufficient zoins
}
// Deduct cost from zoins
zoins = zoins - currentPrice;
storage.zoins = zoins; // Persist updated zoin count
zoinText.setText('Zoins: ' + zoins);
// Increment upgrade level
fireRateUpgrades++;
storage.fireRateUpgrades = fireRateUpgrades; // Persist upgrade level
// Update player fire rate
player.fireRate = Math.round(3 * Math.pow(1.1, fireRateUpgrades));
// Update price label with new cost
var newPrice = calculateCumulativeCost(fireRateUpgrades);
fireRatePriceLabel.setText(newPrice + ' Zoin' + (newPrice === 1 ? '' : 's'));
// Update powerup display
updatePowerupDisplay();
// Change back to yellow when released
tween(secondButton, {
tint: 0xFFFF00
}, {
duration: 100,
easing: tween.easeOut
});
};
// Create player name display at bottom center
var playerNameText = new Text2(currentPlayerName, {
size: 45,
fill: 0xFFFFFF
});
playerNameText.anchor.set(0.5, 0.5);
playerNameText.x = 0;
playerNameText.y = -70; // Center vertically with buttons (same as button labels)
playerNameText.buttonMode = true;
// Add click handler to randomize name
playerNameText.down = function (x, y, obj) {
// Generate new random name
var randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
var randomName = commonNames[Math.floor(Math.random() * commonNames.length)];
var newPlayerName = randomAdjective + '-' + randomName;
// Update current player name and save to storage
currentPlayerName = newPlayerName;
storage.playerName = currentPlayerName;
// Update display text
playerNameText.setText(currentPlayerName);
// Visual feedback - flash effect
tween(playerNameText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(playerNameText, {
scaleX: 1,
scaleY: 1
}, {
duration: 150,
easing: tween.easeOut
});
}
});
};
LK.gui.bottom.addChild(playerNameText);
// Create powerup display container
var powerupDisplay = new Container();
powerupDisplay.x = 1024; // Center horizontally
powerupDisplay.y = 280; // Below leaderboard area
game.addChild(powerupDisplay);
// Powerup display background removed
// Triple Shot powerup icon and level
var tripleShotIcon = new TripleShot();
tripleShotIcon.x = -540;
tripleShotIcon.y = 0;
tripleShotIcon.visible = false;
powerupDisplay.addChild(tripleShotIcon);
var tripleShotLevelText = new Text2('Lv.1', {
size: 72,
fill: 0xFFFFFF
});
tripleShotLevelText.anchor.set(0.5, 0.5);
tripleShotLevelText.x = -540;
tripleShotLevelText.y = 75;
tripleShotLevelText.visible = false;
powerupDisplay.addChild(tripleShotLevelText);
// Power Ammo display
var powerAmmoIcon = new PowerShotPowerUp();
powerAmmoIcon.x = -180;
powerAmmoIcon.y = 0;
powerAmmoIcon.visible = false;
powerupDisplay.addChild(powerAmmoIcon);
var powerAmmoCountText = new Text2('x0', {
size: 72,
fill: 0xFFD700
});
powerAmmoCountText.anchor.set(0.5, 0.5);
powerAmmoCountText.x = -180;
powerAmmoCountText.y = 75;
powerAmmoCountText.visible = false;
powerupDisplay.addChild(powerAmmoCountText);
// Sniper powerup display
var sniperIcon = new SniperPowerUp();
sniperIcon.x = 180;
sniperIcon.y = 0;
sniperIcon.visible = false;
powerupDisplay.addChild(sniperIcon);
var sniperCooldownText = new Text2('READY', {
size: 72,
fill: 0xFF0000
});
sniperCooldownText.anchor.set(0.5, 0.5);
sniperCooldownText.x = 180;
sniperCooldownText.y = 75;
sniperCooldownText.visible = false;
powerupDisplay.addChild(sniperCooldownText);
// Wave transition function with fade effects
function transitionToNextWave() {
// Change background immediately without fade overlay
var backgroundAssets = ['background1', 'background2', 'background3', 'background4', 'background5', 'background6'];
var randomBackgroundId = backgroundAssets[Math.floor(Math.random() * backgroundAssets.length)];
var newGameBackground = LK.getAsset(randomBackgroundId, {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0
});
newGameBackground.x = 0;
newGameBackground.y = 0;
// Remove old background if it exists
if (game.children[0] && game.children[0].width === 2048 && game.children[0].height === 2732) {
game.children[0].destroy();
game.removeChildAt(0);
}
game.addChildAt(newGameBackground, 0);
// Ensure all health bar elements are above background
game.removeChild(healthBarOutline);
game.addChild(healthBarOutline);
game.removeChild(healthBarBg);
game.addChild(healthBarBg);
game.removeChild(healthBarFill);
game.addChild(healthBarFill);
game.removeChild(healthText);
game.addChild(healthText);
// Ensure leaderboard stays visible during transition
game.removeChild(leaderboardText);
game.addChild(leaderboardText);
// Ensure player visibility
player.alpha = 1;
player.visible = true;
// Move player to topmost layer after wave transition
game.removeChild(player);
game.addChild(player);
}
// Update function for powerup display
function updatePowerupDisplay() {
// Update Triple Shot display
if (hasTripleShot) {
tripleShotIcon.visible = true;
tripleShotLevelText.visible = true;
tripleShotLevelText.setText('Lv.' + tripleShotLevel);
} else {
tripleShotIcon.visible = false;
tripleShotLevelText.visible = false;
}
// Update Power Ammo display
if (powerAmmo > 0) {
powerAmmoIcon.visible = true;
powerAmmoCountText.visible = true;
powerAmmoCountText.setText('x' + powerAmmo);
} else {
powerAmmoIcon.visible = false;
powerAmmoCountText.visible = false;
}
// Update Sniper display
if (hasSniperPowerUp) {
sniperIcon.visible = true;
sniperCooldownText.visible = true;
var cooldownSeconds = Math.max(0, Math.ceil(sniperCooldown / 60));
sniperCooldownText.setText(cooldownSeconds > 0 ? cooldownSeconds + 's' : 'READY');
} else {
sniperIcon.visible = false;
sniperCooldownText.visible = false;
}
}
// Start music
LK.playMusic('battleMusic');
// Ensure player is always on top layer
game.removeChild(player);
game.addChild(player);
// Ensure powerup display is always on top layer
game.removeChild(powerupDisplay);
game.addChild(powerupDisplay);
;
; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 20;
self.damage = 10;
self.vx = 0;
self.vy = 0;
self.piercing = false;
self.maxDistance = 800;
self.travelDistance = 0;
self.update = function () {
var moveX = self.vx;
var moveY = self.vy;
var distance = Math.sqrt(moveX * moveX + moveY * moveY);
self.travelDistance += distance;
self.x += moveX;
self.y += moveY;
};
return self;
});
var HealthPack = Container.expand(function () {
var self = Container.call(this);
var healthGraphics = self.attachAsset('healthPack', {
anchorX: 0.5,
anchorY: 0.5
});
healthGraphics.tint = 0xFF0000; // Bright red color
// Add + text on the healthpack with white color
var plusText = new Text2('+', {
size: 48,
fill: 0xFFFFFF,
font: "'Arial Black'"
});
plusText.anchor.set(0.5, 0.5);
self.addChild(plusText);
self.healAmount = 25; // Will be calculated as percentage in Player.heal method
self.spawnTime = LK.ticks;
self.update = function () {
// Pulsing effect using tween every 1 second
if (LK.ticks % 60 === 0) {
// Pulse every 1 second from 100% down to 70%
tween(self, {
scaleX: 0.7,
scaleY: 0.7
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
// Keep + text readable during scaling
plusText.scaleX = 1 / Math.max(self.scaleX, 0.7);
plusText.scaleY = 1 / Math.max(self.scaleY, 0.7);
};
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 100;
self.maxHealth = 100;
self.fireRate = 10;
self.fireCooldown = 0;
self.moveSpeed = 5;
self.takeDamage = function (amount) {
self.health -= amount;
LK.getSound('playerHurt').play();
LK.effects.flashObject(self, 0xFF0000, 500);
if (self.health <= 0) {
self.health = 0;
return true;
}
return false;
};
self.heal = function (amount) {
// Calculate 25% of max health for health pack healing
var healAmount = Math.floor(self.maxHealth * 0.25);
self.health = Math.min(self.health + healAmount, self.maxHealth);
};
self.update = function () {
if (self.fireCooldown > 0) self.fireCooldown--;
// Ensure player is always fully visible
self.alpha = 1;
};
return self;
});
var PowerShotPowerUp = Container.expand(function () {
var self = Container.call(this);
var powerUpGraphics = self.attachAsset('powerShot', {
anchorX: 0.5,
anchorY: 0.5
});
// Add P text on the powerup with white color
var pText = new Text2('P', {
size: 48,
fill: 0xFFFFFF,
font: "'Arial Black'"
});
pText.anchor.set(0.5, 0.5);
self.addChild(pText);
self.ammoAmount = 10;
self.spawnTime = LK.ticks;
self.update = function () {
// Large pulsing effect using tween every 1 second
if (LK.ticks % 60 === 0) {
// Pulse every 1 second from 100% down to 10%
tween(self, {
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
// Keep P text readable during scaling
pText.scaleX = 1 / Math.max(self.scaleX, 0.1);
pText.scaleY = 1 / Math.max(self.scaleY, 0.1);
};
return self;
});
var SniperBullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('sniperBullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 10; // Set sniper bullet speed to 10
self.damage = 100; // 10x damage (10 * 10)
self.vx = 0;
self.vy = 0;
self.piercing = true; // Sniper bullets pierce through zombies
self.maxDistance = 1600; // 2x range (800 * 2)
self.travelDistance = 0;
self.update = function () {
var moveX = self.vx;
var moveY = self.vy;
var distance = Math.sqrt(moveX * moveX + moveY * moveY);
self.travelDistance += distance;
self.x += moveX;
self.y += moveY;
// Add spinning animation - 25% faster
self.rotation += 0.375;
// Add pulsing animation from 25% to 100% size
var pulseScale = 0.25 + 0.75 * (Math.sin(LK.ticks * 0.2) * 0.5 + 0.5);
self.scaleX = pulseScale;
self.scaleY = pulseScale;
};
return self;
});
var SniperPowerUp = Container.expand(function () {
var self = Container.call(this);
var powerUpGraphics = self.attachAsset('powerShot', {
anchorX: 0.5,
anchorY: 0.5
});
powerUpGraphics.tint = 0xFF0000; // Red tint for sniper
// Add S text on the powerup with white color
var sText = new Text2('S', {
size: 48,
fill: 0xFFFFFF,
font: "'Arial Black'"
});
sText.anchor.set(0.5, 0.5);
self.addChild(sText);
self.spawnTime = LK.ticks;
self.update = function () {
// Large pulsing effect using tween every 1 second
if (LK.ticks % 60 === 0) {
// Pulse every 1 second from 100% down to 10%
tween(self, {
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
// Keep S text readable during scaling
sText.scaleX = 1 / Math.max(self.scaleX, 0.1);
sText.scaleY = 1 / Math.max(self.scaleY, 0.1);
};
return self;
});
var TripleShot = Container.expand(function () {
var self = Container.call(this);
// Create center bullet
var centerBullet = self.attachAsset('tripleShotBullet', {
anchorX: 0.5,
anchorY: 0.5
});
centerBullet.x = 0;
centerBullet.y = 0;
centerBullet.rotation = 0;
// Create left bullet tilted 10 degrees away
var leftBullet = self.attachAsset('tripleShotBullet', {
anchorX: 0.5,
anchorY: 0.5
});
leftBullet.x = -20;
leftBullet.y = -8;
leftBullet.rotation = -10 * Math.PI / 180; // -10 degrees in radians
// Create right bullet tilted 10 degrees away
var rightBullet = self.attachAsset('tripleShotBullet', {
anchorX: 0.5,
anchorY: 0.5
});
rightBullet.x = 20;
rightBullet.y = -8;
rightBullet.rotation = 10 * Math.PI / 180; // 10 degrees in radians
self.spawnTime = LK.ticks;
self.update = function () {
// Spinning animation like Zoin - rotate the entire powerup
self.rotation += 0.075;
// Add pulsing effect with tween every 2 seconds
if (LK.ticks % 120 === 0) {
// Pulse every 2 seconds
tween(self, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
// Add floating effect
self.y += Math.sin(LK.ticks * 0.1) * 0.3;
};
return self;
});
var Zoin = Container.expand(function () {
var self = Container.call(this);
var zoinGraphics = self.attachAsset('zoin', {
anchorX: 0.5,
anchorY: 0.5
});
// Add Z text on the coin with neon green color
var zText = new Text2('Z', {
size: 24,
fill: 0x00ff00,
font: "'Arial Black'"
});
zText.anchor.set(0.5, 0.5);
self.addChild(zText);
self.spawnTime = LK.ticks;
self.spinPhase = 0; // Track spinning phase for coin flip effect
self.update = function () {
// Spinning animation - slower spin to show coin flip effect at 50% speed
self.spinPhase += 0.075;
// Scale X based on sine wave to create flipping effect
var scaleX = Math.abs(Math.sin(self.spinPhase));
self.scaleX = scaleX;
zText.scaleX = 1 / Math.max(scaleX, 0.1); // Keep Z readable and prevent division by 0
// Z is always visible on both sides - no longer hide it
zText.visible = true;
// Pulsing effect using tween
if (LK.ticks % 120 === 0) {
// Pulse every 2 seconds
tween(self, {
scaleY: 1.3
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
// Add floating effect
self.y += Math.sin(LK.ticks * 0.1) * 0.3;
};
return self;
});
var Zombie = Container.expand(function () {
var self = Container.call(this);
// Randomly choose zombie visual from available assets
var zombieAssets = ['zombie', 'zombie2', 'zombie3', 'zombie4'];
var randomAsset = zombieAssets[Math.floor(Math.random() * zombieAssets.length)];
var zombieGraphics = self.attachAsset(randomAsset, {
anchorX: 0.5,
anchorY: 0.5
});
self.baseSpeed = 1.5;
var variation = self.baseSpeed * 0.25; // 25% variation
self.speed = Math.round(self.baseSpeed + (Math.random() - 0.5) * 2 * variation); // Random between baseSpeed ± 25%
self.health = 20;
self.maxHealth = 20; // Store base health for wave scaling
self.damage = 1;
self.baseDamage = 1; // Store base damage for wave scaling
self.attackCooldown = 0;
self.isDead = false;
self.takeDamage = function (amount) {
if (self.isDead) return false;
self.health -= amount;
if (self.health <= 0) {
self.isDead = true;
LK.getSound('zombieHit').play();
return true;
}
zombieGraphics.tint = 0xFF0000;
tween(zombieGraphics, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
zombieGraphics.tint = 0xFFFFFF;
}
});
return false;
};
self.update = function () {
if (self.isDead) return;
if (self.attackCooldown > 0) self.attackCooldown--;
};
return self;
});
var ZombieHulk = Zombie.expand(function () {
var self = Zombie.call(this);
// Enhanced stats: 100% more life, 25% more speed, 50% larger
self.baseSpeed = 1.5 * 1.25; // 25% more speed
var variation = self.baseSpeed * 0.25; // 25% variation
self.speed = Math.round(self.baseSpeed + (Math.random() - 0.5) * 2 * variation); // Random between baseSpeed ± 25%
self.health = 40; // 100% more life (20 * 2)
self.maxHealth = 40; // Store base health for wave scaling
// Make 50% larger by scaling the existing zombie graphics after brief delay to ensure visual is ready
LK.setTimeout(function () {
tween(self, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.easeOut
});
}, 50);
// 50% chance to have a random powerup in inventory
self.hasPowerup = Math.random() < 0.5;
if (self.hasPowerup) {
var powerUpTypes = ['powerShotPowerUp', 'healthPack', 'tripleShot'];
self.powerupType = powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)];
}
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
var player;
var zombies = [];
var bullets = [];
var pickups = [];
var wave = 1;
var zombiesPerWave = 5;
var zombiesSpawned = 0;
var zombiesKilled = 0;
var spawnTimer = 0;
var scoreMultiplier = 1;
var killStreak = 0;
var powerAmmo = 0;
var lastPowerUpSpawn = 0;
var killsSinceLastPowerUp = 0;
var powerUpList = ['powerShotPowerUp', 'healthPack', 'tripleShot', 'sniperPowerUp'];
var lastPowerUpType = null;
var usedPowerUps = []; // Track which powerups have been used in current cycle
var availablePowerUps = powerUpList.slice(); // Copy of powerups available for selection
var hasTripleShot = false;
var tripleShotLevel = 0;
var maxTripleShotLevel = 5;
var hasSniperPowerUp = false;
var sniperLevel = 0;
var sniperCooldown = 0;
var sniperFireRate = 300; // 5 seconds at 60fps
var noZombiesTimer = 0;
var noZombiesThreshold = 600; // 10 seconds at 60fps
// Load persistent data from storage (removed reset statements)
// Load persistent data from storage without clearing
var zoins = storage.zoins || 0;
// Upgrade costs and levels - loaded from storage
var maxHealthUpgrades = storage.maxHealthUpgrades || 0;
var fireRateUpgrades = storage.fireRateUpgrades || 0;
var damageUpgrades = storage.damageUpgrades || 0;
// Save damage upgrades to storage whenever modified
storage.damageUpgrades = damageUpgrades;
// Load leaderboard data from storage
// Leaderboard data - reconstruct from flattened storage
var storedLeaderboard = storage.leaderboard || {};
var leaderboard = [];
if (storedLeaderboard.length) {
for (var i = 0; i < storedLeaderboard.length; i++) {
if (storedLeaderboard['name_' + i] !== undefined) {
leaderboard.push({
name: String(storedLeaderboard['name_' + i]),
score: Number(storedLeaderboard['score_' + i]),
wave: Number(storedLeaderboard['wave_' + i])
});
}
}
} else {
leaderboard = [];
}
// Arrays for generating player names
var adjectives = ['Brave', 'Swift', 'Mighty', 'Fierce', 'Bold', 'Quick', 'Strong', 'Wild', 'Sharp', 'Dark', 'Bright', 'Cold', 'Hot', 'Fast', 'Slow', 'Big', 'Small', 'Red', 'Blue', 'Green'];
var commonNames = ['Alex', 'Sam', 'Max', 'Jake', 'Mike', 'Tom', 'Ben', 'Dan', 'Joe', 'Ryan', 'Kyle', 'Luke', 'Dave', 'Jack', 'Matt', 'Nick', 'Chris', 'Josh', 'Mark', 'Paul', 'Emma', 'Sarah', 'Lisa', 'Amy', 'Kate', 'Anna', 'Maya', 'Zoe', 'Mia', 'Ella', 'Eva', 'Ava', 'Lily', 'Ruby', 'Nina', 'Rose', 'Lucy', 'Jade', 'Hope', 'Iris'];
// Load or generate player name
var currentPlayerName = storage.playerName || '';
if (!currentPlayerName) {
var randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
var randomName = commonNames[Math.floor(Math.random() * commonNames.length)];
currentPlayerName = randomAdjective + '-' + randomName;
storage.playerName = currentPlayerName;
}
var moveControl;
var moveStick;
var isDraggingMove = false;
// UI Elements
var waveText = new Text2('Wave: 1', {
size: 80,
fill: 0xFFFFFF
});
waveText.anchor.set(0, 0);
waveText.x = 150;
LK.gui.topLeft.addChild(waveText);
var scoreText = new Text2('Score: 0', {
size: 60,
fill: 0xFFFFFF
});
scoreText.anchor.set(1, 0);
scoreText.x = -150;
LK.gui.topRight.addChild(scoreText);
// Load hi-score from storage or default to 0
var hiScore = storage.hiScore || 0;
var hiScoreText = new Text2('Hi-Score: ' + hiScore, {
size: 60,
fill: 0xFFD700
});
hiScoreText.anchor.set(1, 0);
hiScoreText.x = -150;
hiScoreText.y = 70; // Position below score display
LK.gui.topRight.addChild(hiScoreText);
// Health bar black outline
var healthBarOutline = LK.getAsset('ammoBox', {
width: 1849,
height: 81,
anchorX: 0.5,
anchorY: 0
});
healthBarOutline.tint = 0x000000;
healthBarOutline.x = 1024;
healthBarOutline.y = 97;
game.addChildAt(healthBarOutline, 0);
// Health bar background using lifebar asset
var healthBarBg = LK.getAsset('lifebar', {
width: 2048,
height: 225,
anchorX: 0.5,
anchorY: 0.5
});
healthBarBg.x = 1024;
healthBarBg.y = 166.25;
game.addChildAt(healthBarBg, 1);
// Health bar fill
var healthBarFill = LK.getAsset('ammoBox', {
width: 1382.25,
// Reduced by 25% (1843 * 0.75)
height: 58.59375,
// 25% taller (46.875 * 1.25)
anchorX: 0.5,
anchorY: 0
});
healthBarFill.tint = 0x000000;
healthBarFill.x = 1024;
healthBarFill.y = 113.75; // Moved up 5px (118.75 - 5)
game.addChildAt(healthBarFill, 2);
// Health text display
var healthText = new Text2('100/100', {
size: 56,
fill: 0xFFFFFF
});
healthText.anchor.set(0.5, 0.5);
healthText.x = 1024;
healthText.y = 137.5; // Center of health bar (100 + 75/2)
game.addChildAt(healthText, 3);
var powerAmmoText = new Text2('Power Ammo: 0', {
size: 45,
fill: 0xFFD700
});
powerAmmoText.anchor.set(1, 1);
powerAmmoText.x = -20;
powerAmmoText.y = -80;
LK.gui.bottomRight.addChild(powerAmmoText);
var zoinText = new Text2('Zoins: ' + zoins, {
size: 45,
fill: 0xFFD700
});
zoinText.anchor.set(1, 1);
zoinText.x = -20;
zoinText.y = -140;
LK.gui.bottomRight.addChild(zoinText);
// Leaderboard display as scrolling banner
var leaderboardText = new Text2('LEADERBOARD\n', {
size: 40,
fill: 0xFFFFFF
});
leaderboardText.anchor.set(0, 0);
leaderboardText.x = 2048; // Start off-screen to the right
leaderboardText.y = 200;
game.addChild(leaderboardText);
// Leaderboard scrolling variables
var leaderboardScrolling = false;
// Update leaderboard display
function updateLeaderboardDisplay() {
var displayText = 'LEADERBOARD ';
var sortedLeaderboard = leaderboard.slice().sort(function (a, b) {
return b.score - a.score;
});
// Show top 20 entries
for (var i = 0; i < Math.min(20, sortedLeaderboard.length); i++) {
var entry = sortedLeaderboard[i];
displayText += i + 1 + '. ' + entry.name + ' - ' + entry.score + ' (Wave ' + entry.wave + ') • ';
}
// Find current player's best entry and position
var playerBestEntry = null;
var playerPosition = -1;
for (var i = 0; i < sortedLeaderboard.length; i++) {
var entry = sortedLeaderboard[i];
if (entry.name === currentPlayerName && (!playerBestEntry || entry.score > playerBestEntry.score)) {
playerBestEntry = entry;
playerPosition = i + 1;
}
}
// If player has an entry and it's not in top 20, show their position
if (playerBestEntry && playerPosition > 20) {
displayText += '... ' + playerPosition + '. ' + playerBestEntry.name + ' - ' + playerBestEntry.score + ' (Wave ' + playerBestEntry.wave + ') • ';
}
leaderboardText.setText(displayText);
startLeaderboardScroll();
}
// Start leaderboard scrolling animation
function startLeaderboardScroll() {
if (leaderboardScrolling) return;
leaderboardScrolling = true;
// Stop any existing tween
tween.stop(leaderboardText, {
x: true
});
// Reset position to start from right
leaderboardText.x = 2048;
// Calculate scroll distance based on text width
var scrollDistance = leaderboardText.width + 2048;
// Start scrolling animation
tween(leaderboardText, {
x: -scrollDistance
}, {
duration: scrollDistance * 10,
// 50% faster scrolling (15 -> 10)
easing: tween.linear,
onFinish: function onFinish() {
leaderboardScrolling = false;
// Restart the scroll immediately for continuous display
startLeaderboardScroll();
}
});
}
updateLeaderboardDisplay();
// Function to submit score to leaderboard
function submitScoreToLeaderboard() {
// Player name is now persistently generated at game start
// Create simple literal object for leaderboard entry
var newEntry = {
name: currentPlayerName,
score: LK.getScore(),
wave: wave
};
// Create cleaned leaderboard first
var cleanLeaderboard = [];
for (var i = 0; i < leaderboard.length; i++) {
var entry = leaderboard[i];
cleanLeaderboard.push({
name: String(entry.name),
score: Number(entry.score),
wave: Number(entry.wave)
});
}
// Add new entry as literal object
cleanLeaderboard.push({
name: String(newEntry.name),
score: Number(newEntry.score),
wave: Number(newEntry.wave)
});
// Sort and limit to top 50
cleanLeaderboard.sort(function (a, b) {
return b.score - a.score;
});
if (cleanLeaderboard.length > 50) {
cleanLeaderboard = cleanLeaderboard.slice(0, 50);
}
// Update local leaderboard and save to storage
leaderboard = cleanLeaderboard;
// Flatten leaderboard for storage (storage only accepts literals and 1-level objects)
var flatLeaderboard = {};
for (var i = 0; i < cleanLeaderboard.length; i++) {
var entry = cleanLeaderboard[i];
flatLeaderboard['name_' + i] = String(entry.name);
flatLeaderboard['score_' + i] = Number(entry.score);
flatLeaderboard['wave_' + i] = Number(entry.wave);
}
flatLeaderboard.length = cleanLeaderboard.length;
storage.leaderboard = flatLeaderboard;
// Update display
updateLeaderboardDisplay();
}
// Set random background on game start
var backgroundAssets = ['background1', 'background2', 'background3', 'background4', 'background5', 'background6'];
var randomBackgroundId = backgroundAssets[Math.floor(Math.random() * backgroundAssets.length)];
var gameBackground = LK.getAsset(randomBackgroundId, {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0
});
gameBackground.x = 0;
gameBackground.y = 0;
game.addChildAt(gameBackground, 0); // Add at bottom layer
// Ensure all health bar elements are above background
game.removeChild(healthBarOutline);
game.addChild(healthBarOutline);
game.removeChild(healthBarBg);
game.addChild(healthBarBg);
game.removeChild(healthBarFill);
game.addChild(healthBarFill);
game.removeChild(healthText);
game.addChild(healthText);
// Initialize player
player = game.addChild(new Player());
player.maxHealth = Math.round((maxHealthUpgrades + 1) * 2);
player.health = Math.round(player.maxHealth);
player.fireRate = Math.round(3 * Math.pow(1.1, fireRateUpgrades));
player.x = 1024;
player.y = 1366;
// Initialize controls
moveControl = LK.getAsset('moveControl', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
moveControl.x = 300;
moveControl.y = 2432;
game.addChild(moveControl);
moveStick = LK.getAsset('moveControl', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.3,
alpha: 0
});
moveStick.x = moveControl.x;
moveStick.y = moveControl.y;
game.addChild(moveStick);
// Aim control removed - using automatic targeting
// Helper functions
function countHealthPacksOnScreen() {
var healthPackCount = 0;
for (var i = 0; i < pickups.length; i++) {
if (pickups[i] instanceof HealthPack) {
healthPackCount++;
}
}
return healthPackCount;
}
function findNonOverlappingPosition(startX, startY) {
var attempts = 0;
var maxAttempts = 10;
var minDistance = 80; // Minimum distance between pickups
var finalX = startX;
var finalY = startY;
while (attempts < maxAttempts) {
var overlapping = false;
// Check distance from all existing pickups
for (var i = 0; i < pickups.length; i++) {
var existingPickup = pickups[i];
var dx = finalX - existingPickup.x;
var dy = finalY - existingPickup.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
overlapping = true;
break;
}
}
if (!overlapping) {
break; // Found a good position
}
// Try a new position in a circle around the original point
var angle = Math.random() * Math.PI * 2;
var radius = 40 + attempts * 20; // Increase radius with each attempt
finalX = startX + Math.cos(angle) * radius;
finalY = startY + Math.sin(angle) * radius;
// Keep within screen bounds
finalX = Math.max(60, Math.min(1988, finalX));
finalY = Math.max(60, Math.min(2672, finalY));
attempts++;
}
return {
x: finalX,
y: finalY
};
}
function calculateCumulativeCost(upgradeLevel) {
var totalCost = 0;
for (var i = 0; i <= upgradeLevel; i++) {
totalCost += Math.round((10 + i * 2) / 2);
}
return totalCost;
}
function resetGame() {
// Set new random background on game reset
var backgroundAssets = ['background1', 'background2', 'background3', 'background4', 'background5', 'background6'];
var randomBackgroundId = backgroundAssets[Math.floor(Math.random() * backgroundAssets.length)];
var newGameBackground = LK.getAsset(randomBackgroundId, {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0
});
newGameBackground.x = 0;
newGameBackground.y = 0;
// Remove old background if it exists
if (game.children[0] && game.children[0].width === 2048 && game.children[0].height === 2732) {
game.children[0].destroy();
game.removeChildAt(0);
}
game.addChildAt(newGameBackground, 0); // Add at bottom layer
// Ensure all health bar elements are above background
game.removeChild(healthBarOutline);
game.addChild(healthBarOutline);
game.removeChild(healthBarBg);
game.addChild(healthBarBg);
game.removeChild(healthBarFill);
game.addChild(healthBarFill);
game.removeChild(healthText);
game.addChild(healthText);
// Reset wave and zombies
wave = 1;
zombiesPerWave = 5;
zombiesSpawned = 0;
zombiesKilled = 0;
spawnTimer = 0;
hulksSpawned = 0; // Reset hulk spawn counter
// Reset score and multipliers
LK.setScore(0);
scoreMultiplier = 1;
killStreak = 0;
killsSinceLastPowerUp = 0;
lastPowerUpSpawn = 0;
lastPowerUpType = null;
// Reset powerup cycle tracking
usedPowerUps = [];
availablePowerUps = powerUpList.slice();
// Reset powerups
powerAmmo = 0;
hasTripleShot = false;
tripleShotLevel = 0;
hasSniperPowerUp = false;
sniperLevel = 0;
sniperCooldown = 0;
// Reset player state with upgrades
player.maxHealth = Math.round((maxHealthUpgrades + 1) * 2);
player.health = Math.round(player.maxHealth);
player.fireRate = Math.round(3 * Math.pow(1.1, fireRateUpgrades));
player.fireCooldown = 0;
player.alpha = 1;
// Clear all game objects
for (var i = zombies.length - 1; i >= 0; i--) {
zombies[i].destroy();
}
zombies = [];
for (var i = bullets.length - 1; i >= 0; i--) {
bullets[i].destroy();
}
bullets = [];
for (var i = pickups.length - 1; i >= 0; i--) {
pickups[i].destroy();
}
pickups = [];
// Update UI
waveText.setText('Wave: ' + wave);
scoreText.setText('Score: ' + LK.getScore());
hiScoreText.setText('Hi-Score: ' + hiScore);
// Update health bar
var healthPercent = player.health / player.maxHealth;
var depletedPercent = 1 - healthPercent; // Invert to show depleted health
healthBarFill.width = 1382.25 * depletedPercent;
healthBarFill.height = 58.59375;
healthText.setText(player.health + '/' + player.maxHealth);
powerAmmoText.setText('Power Ammo: ' + powerAmmo);
powerAmmoText.visible = false;
// Reset player position
player.x = 1024;
player.y = 1366;
// Reset no zombies timer
noZombiesTimer = 0;
// Update powerup display
updatePowerupDisplay();
}
// Track hulks spawned for current wave
var hulksSpawned = 0;
function spawnZombie() {
var zombie;
// Spawn regular zombies first, then hulks at the end
var regularZombiesCount = zombiesPerWave - wave; // Total zombies minus hulks
if (zombiesSpawned < regularZombiesCount) {
zombie = new Zombie();
} else {
zombie = new ZombieHulk();
hulksSpawned++;
}
var side = Math.floor(Math.random() * 4);
switch (side) {
case 0:
// Top
zombie.x = Math.random() * 2048;
zombie.y = -50;
break;
case 1:
// Right
zombie.x = 2098;
zombie.y = Math.random() * 2732;
break;
case 2:
// Bottom
zombie.x = Math.random() * 2048;
zombie.y = 2782;
break;
case 3:
// Left
zombie.x = -50;
zombie.y = Math.random() * 2732;
break;
}
zombie.speed = Math.round(zombie.baseSpeed * (1 + wave * 0.1) + (Math.random() - 0.5) * 2 * (zombie.baseSpeed * 0.25));
zombie.health = Math.round(zombie.health + wave * 5);
zombie.damage = Math.round(zombie.damage + Math.floor(wave * 2)); // Increase damage by 2 per wave
zombie.scaleX = 0;
zombie.scaleY = 0;
zombie.alpha = 0;
zombies.push(zombie);
game.addChild(zombie);
zombiesSpawned++;
// Animate zombie spawn
tween(zombie, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 300,
easing: tween.easeOut
});
}
function spawnPickup(x, y, zombie) {
var shouldSpawn = false;
var pickup;
var powerUpType;
// Helper function to find a non-overlapping position
function findNonOverlappingPosition(startX, startY) {
var attempts = 0;
var maxAttempts = 10;
var minDistance = 80; // Minimum distance between pickups
var finalX = startX;
var finalY = startY;
while (attempts < maxAttempts) {
var overlapping = false;
// Check distance from all existing pickups
for (var i = 0; i < pickups.length; i++) {
var existingPickup = pickups[i];
var dx = finalX - existingPickup.x;
var dy = finalY - existingPickup.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
overlapping = true;
break;
}
}
if (!overlapping) {
break; // Found a good position
}
// Try a new position in a circle around the original point
var angle = Math.random() * Math.PI * 2;
var radius = 40 + attempts * 20; // Increase radius with each attempt
finalX = startX + Math.cos(angle) * radius;
finalY = startY + Math.sin(angle) * radius;
// Keep within screen bounds
finalX = Math.max(60, Math.min(1988, finalX));
finalY = Math.max(60, Math.min(2672, finalY));
attempts++;
}
return {
x: finalX,
y: finalY
};
}
// Check if zombie hulk has powerup in inventory
if (zombie instanceof ZombieHulk && zombie.hasPowerup) {
shouldSpawn = true;
// If all powerups have been used, reset the cycle
if (availablePowerUps.length === 0) {
availablePowerUps = powerUpList.slice(); // Reset available powerups
usedPowerUps = []; // Clear used powerups
}
// Select random powerup from available list for hulk
var randomIndex = Math.floor(Math.random() * availablePowerUps.length);
powerUpType = availablePowerUps[randomIndex];
// Move selected powerup from available to used
usedPowerUps.push(powerUpType);
availablePowerUps.splice(randomIndex, 1);
if (powerUpType === 'powerShotPowerUp') {
pickup = new PowerShotPowerUp();
} else if (powerUpType === 'tripleShot') {
pickup = new TripleShot();
} else if (powerUpType === 'sniperPowerUp') {
pickup = new SniperPowerUp();
} else {
// Check if healthpack would be spawned and limit is reached
if (countHealthPacksOnScreen() >= 2) {
// Replace with 2 zoin drops with non-overlapping positions
var pos1 = findNonOverlappingPosition(x - 30, y);
var pos2 = findNonOverlappingPosition(x + 30, y);
var zoin1 = new Zoin();
zoin1.x = pos1.x;
zoin1.y = pos1.y;
zoin1.scaleX = 0;
zoin1.scaleY = 0;
pickups.push(zoin1);
game.addChild(zoin1);
var zoin2 = new Zoin();
zoin2.x = pos2.x;
zoin2.y = pos2.y;
zoin2.scaleX = 0;
zoin2.scaleY = 0;
pickups.push(zoin2);
game.addChild(zoin2);
// Animate both zoin spawns
tween(zoin1, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
tween(zoin2, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
// Don't create healthpack
pickup = null;
} else {
pickup = new HealthPack();
}
}
killsSinceLastPowerUp = 0;
if (pickup instanceof PowerShotPowerUp) {
lastPowerUpSpawn = LK.ticks;
}
}
// Force powerup spawn if 50+ kills without powerup (but not for normal zombies)
else if (killsSinceLastPowerUp >= 50) {
shouldSpawn = true;
// If all powerups have been used, reset the cycle
if (availablePowerUps.length === 0) {
availablePowerUps = powerUpList.slice(); // Reset available powerups
usedPowerUps = []; // Clear used powerups
}
// Select random powerup from available list
var randomIndex = Math.floor(Math.random() * availablePowerUps.length);
powerUpType = availablePowerUps[randomIndex];
// Move selected powerup from available to used
usedPowerUps.push(powerUpType);
availablePowerUps.splice(randomIndex, 1);
if (powerUpType === 'powerShotPowerUp') {
pickup = new PowerShotPowerUp();
} else if (powerUpType === 'tripleShot') {
pickup = new TripleShot();
} else if (powerUpType === 'sniperPowerUp') {
pickup = new SniperPowerUp();
} else {
// Check if healthpack would be spawned and limit is reached
if (countHealthPacksOnScreen() >= 2) {
// Replace with 2 zoin drops with non-overlapping positions
var pos1 = findNonOverlappingPosition(x - 30, y);
var pos2 = findNonOverlappingPosition(x + 30, y);
var zoin1 = new Zoin();
zoin1.x = pos1.x;
zoin1.y = pos1.y;
zoin1.scaleX = 0;
zoin1.scaleY = 0;
pickups.push(zoin1);
game.addChild(zoin1);
var zoin2 = new Zoin();
zoin2.x = pos2.x;
zoin2.y = pos2.y;
zoin2.scaleX = 0;
zoin2.scaleY = 0;
pickups.push(zoin2);
game.addChild(zoin2);
// Animate both zoin spawns
tween(zoin1, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
tween(zoin2, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
// Don't create healthpack
pickup = null;
} else {
pickup = new HealthPack();
}
}
killsSinceLastPowerUp = 0;
if (pickup instanceof PowerShotPowerUp) {
lastPowerUpSpawn = LK.ticks;
}
}
// No powerup spawn for normal zombies
if (shouldSpawn && pickup) {
// Find non-overlapping position for the pickup
var position = findNonOverlappingPosition(x, y);
pickup.x = position.x;
pickup.y = position.y;
pickup.scaleX = 0;
pickup.scaleY = 0;
pickups.push(pickup);
game.addChild(pickup);
// Animate pickup spawn with bounce effect
tween(pickup, {
scaleX: 1,
scaleY: 1
}, {
duration: 400,
easing: tween.elasticOut
});
}
}
function fireBullet() {
// Handle sniper firing independently
if (hasSniperPowerUp && sniperCooldown <= 0) {
// Find farthest zombie that is beyond normal bullet range (500px) and on screen
var sniperTarget = null;
var sniperDistance = 0; // Track farthest distance instead of closest
var normalBulletRange = 500; // Normal bullet maximum range
for (var i = 0; i < zombies.length; i++) {
var zombie = zombies[i];
if (!zombie || zombie.isDead) continue;
var dx = zombie.x - player.x;
var dy = zombie.y - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Only target zombies beyond normal bullet range and on screen
if (distance > normalBulletRange && distance > sniperDistance && zombie.x >= -50 && zombie.x <= 2098 && zombie.y >= -50 && zombie.y <= 2782) {
sniperDistance = distance;
sniperTarget = zombie;
}
}
if (sniperTarget) {
var dx = sniperTarget.x - player.x;
var dy = sniperTarget.y - player.y;
var angle = Math.atan2(dy, dx);
var sniperBullet = new SniperBullet();
sniperBullet.x = player.x;
sniperBullet.y = player.y;
sniperBullet.damage = Math.round(100 + damageUpgrades * 50); // 10x base damage + upgrades
sniperBullet.vx = Math.cos(angle) * sniperBullet.speed;
sniperBullet.vy = Math.sin(angle) * sniperBullet.speed;
sniperBullet.rotation = angle;
// Visual effect for sniper bullet
tween(sniperBullet, {
scaleX: 2,
scaleY: 2
}, {
duration: 100
});
bullets.push(sniperBullet);
game.addChild(sniperBullet);
// Calculate sniper fire rate: base rate decreases by 10% per sniper level (faster firing)
var sniperFireRateWithUpgrade = Math.round(sniperFireRate * Math.pow(0.9, sniperLevel - 1));
sniperCooldown = sniperFireRateWithUpgrade; // Reset sniper cooldown with improved rate
LK.getSound('shoot').play();
}
}
// Convert shots per second to ticks between shots (60 ticks = 1 second)
var shotsPerSecond = powerAmmo > 0 ? player.fireRate * 0.25 : player.fireRate; // Power ammo shoots at 25% of normal rate
var ticksBetweenShots = Math.max(1, Math.floor(60 / shotsPerSecond)); // Ensure at least 1 tick between shots
if (player.fireCooldown > 0) return;
// Find closest zombie in range
var closestZombie = null;
var closestDistance = 500; // Maximum firing range
for (var i = 0; i < zombies.length; i++) {
var zombie = zombies[i];
if (!zombie || zombie.isDead) continue;
var dx = zombie.x - player.x;
var dy = zombie.y - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestZombie = zombie;
}
}
// Only fire if there's a zombie in range
if (!closestZombie) return;
var dx = closestZombie.x - player.x;
var dy = closestZombie.y - player.y;
var angle = Math.atan2(dy, dx);
// Player visual does not rotate - only bullets aim at target
// Create bullets based on triple shot level
var bulletCount = hasTripleShot ? 1 + tripleShotLevel * 2 : 1;
var angleOffsets = [0]; // Always include center bullet
if (hasTripleShot) {
// Add bullets in pairs at increasing angles
for (var level = 1; level <= tripleShotLevel; level++) {
var angleOffset = level * 10 * Math.PI / 180;
angleOffsets.push(-angleOffset); // Left bullet
angleOffsets.push(angleOffset); // Right bullet
}
}
// Consume power ammo once per shot, not per bullet
var usingPowerAmmo = powerAmmo > 0;
if (usingPowerAmmo) {
powerAmmo--; // Consume one power ammo per shot
}
for (var b = 0; b < bulletCount; b++) {
var bullet = new Bullet();
bullet.x = player.x;
bullet.y = player.y;
bullet.damage = Math.round(10 + damageUpgrades * 5);
var bulletAngle = angle + angleOffsets[b];
bullet.vx = Math.cos(bulletAngle) * bullet.speed;
bullet.vy = Math.sin(bulletAngle) * bullet.speed;
bullet.rotation = bulletAngle;
if (usingPowerAmmo) {
bullet.piercing = true;
bullet.maxDistance = 1600; // 2x distance
bullet.damage = Math.round((10 + damageUpgrades * 5) * 5); // 5x normal damage
// Visual effect for power bullet
tween(bullet, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 100
});
bullet.tint = 0xFFD700;
} else {
bullet.maxDistance = 800;
}
bullets.push(bullet);
game.addChild(bullet);
}
player.fireCooldown = ticksBetweenShots;
LK.getSound('shoot').play();
}
// Event handlers
game.down = function (x, y, obj) {
// Prevent control stick activation in bottom 1/4 of viewport (below y = 2049)
if (y > 2049) return;
isDraggingMove = true;
// Move moveControl to click position
moveControl.x = x;
moveControl.y = y;
// Move moveStick to the same position (center of control)
moveStick.x = x;
moveStick.y = y;
// Show control stick with smooth animation
tween(moveControl, {
alpha: 0.3
}, {
duration: 200,
easing: tween.easeOut
});
tween(moveStick, {
alpha: 0.6
}, {
duration: 200,
easing: tween.easeOut
});
};
game.move = function (x, y, obj) {
if (isDraggingMove) {
moveStick.x = Math.max(moveControl.x - 150, Math.min(moveControl.x + 150, x));
moveStick.y = Math.max(moveControl.y - 150, Math.min(moveControl.y + 150, y));
}
};
game.up = function (x, y, obj) {
if (isDraggingMove) {
isDraggingMove = false;
moveStick.x = moveControl.x;
moveStick.y = moveControl.y;
// Hide control stick with smooth animation
tween(moveControl, {
alpha: 0
}, {
duration: 150,
easing: tween.easeIn
});
tween(moveStick, {
alpha: 0
}, {
duration: 150,
easing: tween.easeIn
});
}
};
// Main game loop
game.update = function () {
// Player movement
if (isDraggingMove) {
var moveX = (moveStick.x - moveControl.x) / 150;
var moveY = (moveStick.y - moveControl.y) / 150;
player.x += moveX * player.moveSpeed;
player.y += moveY * player.moveSpeed;
player.x = Math.max(40, Math.min(2008, player.x));
player.y = Math.max(40, Math.min(2692, player.y));
}
// Update sniper cooldown
if (sniperCooldown > 0) sniperCooldown--;
// Auto-fire (fireBullet now handles range checking internally)
fireBullet();
// Spawn zombies
if (zombiesSpawned < zombiesPerWave) {
spawnTimer++;
if (spawnTimer > 60) {
spawnZombie();
spawnTimer = 0;
}
} else if (zombies.length === 0) {
// Next wave with transition
wave++;
zombiesPerWave = Math.min(5 + wave * 2, 30); // Cap at 30 zombies per wave to prevent performance issues
zombiesSpawned = 0;
zombiesKilled = 0;
spawnTimer = 0; // Reset spawn timer for new wave
noZombiesTimer = 0; // Reset no zombies timer
hulksSpawned = 0; // Reset hulk spawn counter for new wave
waveText.setText('Wave: ' + wave);
transitionToNextWave();
} else {
// Check if there are zombies in view area (within screen bounds plus margin)
var zombiesInView = false;
for (var z = 0; z < zombies.length; z++) {
var zombie = zombies[z];
if (!zombie || zombie.isDead) continue;
// Check if zombie is within view area (screen bounds + 200px margin)
if (zombie.x >= -200 && zombie.x <= 2248 && zombie.y >= -200 && zombie.y <= 2932) {
zombiesInView = true;
break;
}
}
// If no zombies in view, increment timer
if (!zombiesInView && zombiesSpawned >= zombiesPerWave) {
noZombiesTimer++;
// If 10 seconds have passed without zombies in view, advance to next wave
if (noZombiesTimer >= noZombiesThreshold) {
// Remove all remaining zombies
for (var i = zombies.length - 1; i >= 0; i--) {
zombies[i].destroy();
zombies.splice(i, 1);
}
// Next wave with transition
wave++;
zombiesPerWave = Math.min(5 + wave * 2, 30); // Cap at 30 zombies per wave to prevent performance issues
zombiesSpawned = 0;
zombiesKilled = 0;
spawnTimer = 0; // Reset spawn timer for new wave
noZombiesTimer = 0; // Reset no zombies timer
hulksSpawned = 0; // Reset hulk spawn counter for new wave
waveText.setText('Wave: ' + wave);
transitionToNextWave();
}
} else {
// Reset timer if zombies are in view or we haven't spawned all zombies yet
noZombiesTimer = 0;
}
}
// Update zombies
for (var i = zombies.length - 1; i >= 0; i--) {
var zombie = zombies[i];
// Skip if zombie is undefined or dead
if (!zombie || zombie.isDead) continue;
// Move towards player
var dx = player.x - zombie.x;
var dy = player.y - zombie.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
zombie.x += dx / dist * zombie.speed;
zombie.y += dy / dist * zombie.speed;
// Removed zombie rotation to keep zombies upright
}
// Check collision with player
if (zombie.intersects(player) && zombie.attackCooldown === 0) {
var playerDied = player.takeDamage(zombie.damage);
zombie.attackCooldown = 60;
killStreak = 0;
scoreMultiplier = 1;
// Check if player died after taking damage
if (playerDied || player.health <= 0) {
// Submit score to leaderboard before game over
submitScoreToLeaderboard();
LK.showGameOver();
return; // Exit game loop immediately when player dies
}
}
}
// Check if player died during zombie processing - exit immediately
if (player.health <= 0) {
return;
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
// Check if bullet is off screen or traveled max distance
if (bullet.x < -50 || bullet.x > 2098 || bullet.y < -50 || bullet.y > 2782 || bullet.travelDistance > bullet.maxDistance) {
bullet.destroy();
bullets.splice(i, 1);
continue;
}
// Check collision with zombies
var bulletDestroyed = false;
for (var j = zombies.length - 1; j >= 0; j--) {
var zombie = zombies[j];
if (!zombie || zombie.isDead) continue;
if (bullet.intersects(zombie)) {
if (zombie.takeDamage(bullet.damage)) {
// Zombie died - add death animation
spawnPickup(zombie.x, zombie.y, zombie);
// 10% chance to drop 1 zoin when zombie dies
if (Math.random() < 0.1) {
// Find non-overlapping position for zoin drop
var zoinPosition = findNonOverlappingPosition(zombie.x, zombie.y);
var zoin = new Zoin();
zoin.x = zoinPosition.x;
zoin.y = zoinPosition.y;
zoin.scaleX = 0;
zoin.scaleY = 0;
pickups.push(zoin);
game.addChild(zoin);
// Animate zoin spawn
tween(zoin, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
}
var zombieToRemove = zombie; // Capture zombie reference for closure
var zombieIndex = j; // Capture index for closure
tween(zombie, {
scaleX: 0,
scaleY: 0,
alpha: 0,
rotation: zombie.rotation + Math.PI
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
zombieToRemove.destroy();
// Remove from zombies array after animation
var currentIndex = zombies.indexOf(zombieToRemove);
if (currentIndex !== -1) {
zombies.splice(currentIndex, 1);
}
}
});
zombiesKilled++;
killStreak++;
killsSinceLastPowerUp++;
if (killStreak % 5 === 0) {
scoreMultiplier = Math.min(scoreMultiplier + 1, 5);
}
// Give 2x score for ZombieHulk, regular score for normal zombies
var scoreToAdd = zombie instanceof ZombieHulk ? 20 * scoreMultiplier : 10 * scoreMultiplier;
LK.setScore(LK.getScore() + scoreToAdd);
scoreText.setText('Score: ' + LK.getScore());
}
if (!bullet.piercing) {
bullet.destroy();
bullets.splice(i, 1);
bulletDestroyed = true;
break;
}
}
}
if (bulletDestroyed) continue;
}
// Update pickups
for (var i = pickups.length - 1; i >= 0; i--) {
var pickup = pickups[i];
// Check if pickup has expired (10 seconds) - auto pickup non-healthpack items
var age = LK.ticks - pickup.spawnTime;
if (age >= pickup.lifespan) {
// Auto pickup all items except health packs when expiring - healthpacks are perpetual
if (pickup instanceof HealthPack) {
// HealthPack no longer expires - skip expiration logic completely
continue;
} else if (pickup instanceof PowerShotPowerUp) {
powerAmmo += pickup.ammoAmount;
LK.effects.flashObject(player, 0xFFD700, 500);
} else if (pickup instanceof TripleShot) {
if (!hasTripleShot) {
hasTripleShot = true;
tripleShotLevel = 1;
} else if (tripleShotLevel < maxTripleShotLevel) {
tripleShotLevel++;
}
LK.effects.flashObject(player, 0x00FF00, 500);
} else if (pickup instanceof SniperPowerUp) {
hasSniperPowerUp = true;
sniperLevel++; // Increase sniper level each time powerup is collected
sniperCooldown = 0; // Ready to fire immediately
LK.effects.flashObject(player, 0xFF0000, 500);
} else if (pickup instanceof Zoin) {
zoins++;
storage.zoins = zoins; // Persist zoin count
zoinText.setText('Zoins: ' + zoins);
LK.effects.flashObject(player, 0xFFD700, 300);
}
LK.getSound('pickup').play();
// Animate pickup collection
tween(pickup, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
pickup.destroy();
}
});
pickups.splice(i, 1);
continue;
}
// Manual pickup when touching
if (pickup.intersects(player)) {
if (pickup instanceof HealthPack) {
player.heal(pickup.healAmount);
} else if (pickup instanceof PowerShotPowerUp) {
powerAmmo += pickup.ammoAmount;
LK.effects.flashObject(player, 0xFFD700, 500);
} else if (pickup instanceof TripleShot) {
if (!hasTripleShot) {
hasTripleShot = true;
tripleShotLevel = 1;
} else if (tripleShotLevel < maxTripleShotLevel) {
tripleShotLevel++;
}
LK.effects.flashObject(player, 0x00FF00, 500);
} else if (pickup instanceof SniperPowerUp) {
hasSniperPowerUp = true;
sniperLevel++; // Increase sniper level each time powerup is collected
sniperCooldown = 0; // Ready to fire immediately
LK.effects.flashObject(player, 0xFF0000, 500);
} else if (pickup instanceof Zoin) {
zoins++;
storage.zoins = zoins; // Persist zoin count
zoinText.setText('Zoins: ' + zoins);
LK.effects.flashObject(player, 0xFFD700, 300);
}
LK.getSound('pickup').play();
// Animate pickup collection
tween(pickup, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
pickup.destroy();
}
});
pickups.splice(i, 1);
}
}
// Update UI
// Update hi-score if current score is higher
var currentScore = LK.getScore();
if (currentScore > hiScore) {
hiScore = currentScore;
storage.hiScore = hiScore;
hiScoreText.setText('Hi-Score: ' + hiScore);
}
// Update health bar
var healthPercent = player.health / player.maxHealth;
var depletedPercent = 1 - healthPercent; // Invert to show depleted health
healthBarFill.width = 1382.25 * depletedPercent;
healthBarFill.height = 58.59375;
healthText.setText(player.health + '/' + player.maxHealth);
powerAmmoText.setText('Power Ammo: ' + powerAmmo);
// Hide/show power ammo counter based on availability
powerAmmoText.visible = powerAmmo > 0;
// Update powerup display
updatePowerupDisplay();
// Update button state based on zoin availability
var currentPrice = calculateCumulativeCost(maxHealthUpgrades);
if (zoins < currentPrice) {
bottomLeftButton.alpha = 0.5; // Dim button when unaffordable
lifeLabel.alpha = 0.5;
priceLabel.alpha = 0.5;
} else {
bottomLeftButton.alpha = 1.0; // Full opacity when affordable
lifeLabel.alpha = 1.0;
priceLabel.alpha = 1.0;
}
// Update fire rate button state based on zoin availability
var fireRateCurrentPrice = calculateCumulativeCost(fireRateUpgrades);
if (zoins < fireRateCurrentPrice) {
secondButton.alpha = 0.5; // Dim button when unaffordable
fireRateLabel.alpha = 0.5;
fireRatePriceLabel.alpha = 0.5;
} else {
secondButton.alpha = 1.0; // Full opacity when affordable
fireRateLabel.alpha = 1.0;
fireRatePriceLabel.alpha = 1.0;
}
// Despawn distant pickups
for (var i = pickups.length - 1; i >= 0; i--) {
var pickup = pickups[i];
var dx = player.x - pickup.x;
var dy = player.y - pickup.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 2000) {
pickup.destroy();
pickups.splice(i, 1);
}
}
};
// Create bottom left button
var bottomLeftButton = LK.getAsset('ammoBox', {
width: 250,
height: 100,
anchorX: 0,
anchorY: 1
});
bottomLeftButton.tint = 0xFF0000; // Red background
bottomLeftButton.x = 20;
bottomLeftButton.y = -20;
LK.gui.bottomLeft.addChild(bottomLeftButton);
// Create black outline for button
var buttonOutline = LK.getAsset('ammoBox', {
width: 256,
height: 106,
anchorX: 0,
anchorY: 1
});
buttonOutline.tint = 0x000000; // Black outline
buttonOutline.x = 17;
buttonOutline.y = -17;
LK.gui.bottomLeft.addChildAt(buttonOutline, 0); // Add behind the button
// Create LIFE label for button
var lifeLabel = new Text2('LIFE', {
size: 36,
fill: 0xFFFFFF,
font: "'Arial Black'"
});
lifeLabel.anchor.set(0.5, 0.5);
lifeLabel.x = 145; // Center of button (250/2 + 20 offset for button position)
lifeLabel.y = -70; // Center of button (100/2 + 20 offset for button position)
LK.gui.bottomLeft.addChild(lifeLabel);
// Create price label for button with dynamic pricing
var maxLifePrice = calculateCumulativeCost(maxHealthUpgrades);
var priceLabel = new Text2(maxLifePrice + ' Zoin' + (maxLifePrice === 1 ? '' : 's'), {
size: 24,
fill: 0xFFD700
});
priceLabel.anchor.set(0.5, 1);
priceLabel.x = 145; // Center of button horizontally
priceLabel.y = -25; // Bottom of button
LK.gui.bottomLeft.addChild(priceLabel);
// Make button interactive
bottomLeftButton.buttonMode = true;
// Add button click handlers
bottomLeftButton.down = function (x, y, obj) {
var currentPrice = calculateCumulativeCost(maxHealthUpgrades);
if (zoins < currentPrice) {
return; // Don't allow clicking if insufficient zoins
}
// Change to white when pressed
tween(bottomLeftButton, {
tint: 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
};
bottomLeftButton.up = function (x, y, obj) {
var currentPrice = calculateCumulativeCost(maxHealthUpgrades);
if (zoins < currentPrice) {
return; // Don't allow clicking if insufficient zoins
}
// Deduct cost from zoins
zoins = zoins - currentPrice;
storage.zoins = zoins; // Persist updated zoin count
zoinText.setText('Zoins: ' + zoins);
// Increment upgrade level
maxHealthUpgrades++;
storage.maxHealthUpgrades = maxHealthUpgrades; // Persist upgrade level
// Update player max health
player.maxHealth = Math.round((maxHealthUpgrades + 1) * 2);
player.health = player.maxHealth; // Full heal on upgrade
// Update health bar to reflect new maximum
var healthPercent = player.health / player.maxHealth;
var depletedPercent = 1 - healthPercent; // Invert to show depleted health
healthBarFill.width = 1382.25 * depletedPercent;
healthBarFill.height = 58.59375;
healthText.setText(player.health + '/' + player.maxHealth);
// Update price label with new cost
var newPrice = calculateCumulativeCost(maxHealthUpgrades);
priceLabel.setText(newPrice + ' Zoin' + (newPrice === 1 ? '' : 's'));
// Update powerup display
updatePowerupDisplay();
// Change back to red when released
tween(bottomLeftButton, {
tint: 0xFF0000
}, {
duration: 100,
easing: tween.easeOut
});
};
// Create second button next to the max life button
var secondButton = LK.getAsset('ammoBox', {
width: 250,
height: 100,
anchorX: 0,
anchorY: 1
});
secondButton.tint = 0xFFFF00; // Yellow background
secondButton.x = 290; // Position next to max life button (20 + 250 + 20 space = 290)
secondButton.y = -20;
LK.gui.bottomLeft.addChild(secondButton);
// Create black outline for second button
var secondButtonOutline = LK.getAsset('ammoBox', {
width: 256,
height: 106,
anchorX: 0,
anchorY: 1
});
secondButtonOutline.tint = 0x000000; // Black outline
secondButtonOutline.x = 287; // Position outline behind second button
secondButtonOutline.y = -17;
LK.gui.bottomLeft.addChildAt(secondButtonOutline, 0); // Add behind the button
// Create FIRE RATE label for second button
var fireRateLabel = new Text2('FIRE RATE', {
size: 36,
fill: 0xFFFFFF,
font: "'Arial Black'"
});
fireRateLabel.anchor.set(0.5, 0.5);
fireRateLabel.x = 415; // Center of second button (250/2 + 290 offset for button position)
fireRateLabel.y = -70; // Center of button (100/2 + 20 offset for button position)
LK.gui.bottomLeft.addChild(fireRateLabel);
// Create price label for second button with dynamic pricing
var fireRatePrice = calculateCumulativeCost(fireRateUpgrades);
var fireRatePriceLabel = new Text2(fireRatePrice + ' Zoin' + (fireRatePrice === 1 ? '' : 's'), {
size: 24,
fill: 0xFFD700
});
fireRatePriceLabel.anchor.set(0.5, 1);
fireRatePriceLabel.x = 415; // Center of second button horizontally
fireRatePriceLabel.y = -25; // Bottom of button
LK.gui.bottomLeft.addChild(fireRatePriceLabel);
// Make second button interactive
secondButton.buttonMode = true;
// Add second button click handlers
secondButton.down = function (x, y, obj) {
var currentPrice = calculateCumulativeCost(fireRateUpgrades);
if (zoins < currentPrice) {
return; // Don't allow clicking if insufficient zoins
}
// Change to white when pressed
tween(secondButton, {
tint: 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
};
secondButton.up = function (x, y, obj) {
var currentPrice = calculateCumulativeCost(fireRateUpgrades);
if (zoins < currentPrice) {
return; // Don't allow clicking if insufficient zoins
}
// Deduct cost from zoins
zoins = zoins - currentPrice;
storage.zoins = zoins; // Persist updated zoin count
zoinText.setText('Zoins: ' + zoins);
// Increment upgrade level
fireRateUpgrades++;
storage.fireRateUpgrades = fireRateUpgrades; // Persist upgrade level
// Update player fire rate
player.fireRate = Math.round(3 * Math.pow(1.1, fireRateUpgrades));
// Update price label with new cost
var newPrice = calculateCumulativeCost(fireRateUpgrades);
fireRatePriceLabel.setText(newPrice + ' Zoin' + (newPrice === 1 ? '' : 's'));
// Update powerup display
updatePowerupDisplay();
// Change back to yellow when released
tween(secondButton, {
tint: 0xFFFF00
}, {
duration: 100,
easing: tween.easeOut
});
};
// Create player name display at bottom center
var playerNameText = new Text2(currentPlayerName, {
size: 45,
fill: 0xFFFFFF
});
playerNameText.anchor.set(0.5, 0.5);
playerNameText.x = 0;
playerNameText.y = -70; // Center vertically with buttons (same as button labels)
playerNameText.buttonMode = true;
// Add click handler to randomize name
playerNameText.down = function (x, y, obj) {
// Generate new random name
var randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
var randomName = commonNames[Math.floor(Math.random() * commonNames.length)];
var newPlayerName = randomAdjective + '-' + randomName;
// Update current player name and save to storage
currentPlayerName = newPlayerName;
storage.playerName = currentPlayerName;
// Update display text
playerNameText.setText(currentPlayerName);
// Visual feedback - flash effect
tween(playerNameText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(playerNameText, {
scaleX: 1,
scaleY: 1
}, {
duration: 150,
easing: tween.easeOut
});
}
});
};
LK.gui.bottom.addChild(playerNameText);
// Create powerup display container
var powerupDisplay = new Container();
powerupDisplay.x = 1024; // Center horizontally
powerupDisplay.y = 280; // Below leaderboard area
game.addChild(powerupDisplay);
// Powerup display background removed
// Triple Shot powerup icon and level
var tripleShotIcon = new TripleShot();
tripleShotIcon.x = -540;
tripleShotIcon.y = 0;
tripleShotIcon.visible = false;
powerupDisplay.addChild(tripleShotIcon);
var tripleShotLevelText = new Text2('Lv.1', {
size: 72,
fill: 0xFFFFFF
});
tripleShotLevelText.anchor.set(0.5, 0.5);
tripleShotLevelText.x = -540;
tripleShotLevelText.y = 75;
tripleShotLevelText.visible = false;
powerupDisplay.addChild(tripleShotLevelText);
// Power Ammo display
var powerAmmoIcon = new PowerShotPowerUp();
powerAmmoIcon.x = -180;
powerAmmoIcon.y = 0;
powerAmmoIcon.visible = false;
powerupDisplay.addChild(powerAmmoIcon);
var powerAmmoCountText = new Text2('x0', {
size: 72,
fill: 0xFFD700
});
powerAmmoCountText.anchor.set(0.5, 0.5);
powerAmmoCountText.x = -180;
powerAmmoCountText.y = 75;
powerAmmoCountText.visible = false;
powerupDisplay.addChild(powerAmmoCountText);
// Sniper powerup display
var sniperIcon = new SniperPowerUp();
sniperIcon.x = 180;
sniperIcon.y = 0;
sniperIcon.visible = false;
powerupDisplay.addChild(sniperIcon);
var sniperCooldownText = new Text2('READY', {
size: 72,
fill: 0xFF0000
});
sniperCooldownText.anchor.set(0.5, 0.5);
sniperCooldownText.x = 180;
sniperCooldownText.y = 75;
sniperCooldownText.visible = false;
powerupDisplay.addChild(sniperCooldownText);
// Wave transition function with fade effects
function transitionToNextWave() {
// Change background immediately without fade overlay
var backgroundAssets = ['background1', 'background2', 'background3', 'background4', 'background5', 'background6'];
var randomBackgroundId = backgroundAssets[Math.floor(Math.random() * backgroundAssets.length)];
var newGameBackground = LK.getAsset(randomBackgroundId, {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0
});
newGameBackground.x = 0;
newGameBackground.y = 0;
// Remove old background if it exists
if (game.children[0] && game.children[0].width === 2048 && game.children[0].height === 2732) {
game.children[0].destroy();
game.removeChildAt(0);
}
game.addChildAt(newGameBackground, 0);
// Ensure all health bar elements are above background
game.removeChild(healthBarOutline);
game.addChild(healthBarOutline);
game.removeChild(healthBarBg);
game.addChild(healthBarBg);
game.removeChild(healthBarFill);
game.addChild(healthBarFill);
game.removeChild(healthText);
game.addChild(healthText);
// Ensure leaderboard stays visible during transition
game.removeChild(leaderboardText);
game.addChild(leaderboardText);
// Ensure player visibility
player.alpha = 1;
player.visible = true;
// Move player to topmost layer after wave transition
game.removeChild(player);
game.addChild(player);
}
// Update function for powerup display
function updatePowerupDisplay() {
// Update Triple Shot display
if (hasTripleShot) {
tripleShotIcon.visible = true;
tripleShotLevelText.visible = true;
tripleShotLevelText.setText('Lv.' + tripleShotLevel);
} else {
tripleShotIcon.visible = false;
tripleShotLevelText.visible = false;
}
// Update Power Ammo display
if (powerAmmo > 0) {
powerAmmoIcon.visible = true;
powerAmmoCountText.visible = true;
powerAmmoCountText.setText('x' + powerAmmo);
} else {
powerAmmoIcon.visible = false;
powerAmmoCountText.visible = false;
}
// Update Sniper display
if (hasSniperPowerUp) {
sniperIcon.visible = true;
sniperCooldownText.visible = true;
var cooldownSeconds = Math.max(0, Math.ceil(sniperCooldown / 60));
sniperCooldownText.setText(cooldownSeconds > 0 ? cooldownSeconds + 's' : 'READY');
} else {
sniperIcon.visible = false;
sniperCooldownText.visible = false;
}
}
// Start music
LK.playMusic('battleMusic');
// Ensure player is always on top layer
game.removeChild(player);
game.addChild(player);
// Ensure powerup display is always on top layer
game.removeChild(powerupDisplay);
game.addChild(powerupDisplay);
;
;