/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Bullet class
var Bullet = Container.expand(function () {
var self = Container.call(this);
self.target = null;
self.speed = 18;
self.damage = 1;
self.type = 1;
self.active = true;
self.shooter = null;
self.setType = function (type) {
self.type = type;
var assetId = 'bullet' + type;
self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
self.update = function () {
if (!self.active || !self.target || self.target.dead) {
self.destroy();
return;
}
// Move toward target
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 30) {
// Hit
self.target.takeDamage(self.damage);
self.destroy();
return;
}
var step = self.speed / (dist || 1);
self.x += dx * step;
self.y += dy * step;
};
return self;
});
// Soldier class
var Soldier = Container.expand(function () {
var self = Container.call(this);
self.type = 1;
self.fireRate = 60; // ticks between shots
self.damage = 1;
self.range = 350;
self.cooldown = 0;
self.level = 1;
self.wallSpot = null;
self.setType = function (type) {
self.type = type;
// Remove all previous children (visuals)
while (self.children && self.children.length > 0) {
self.removeChild(self.children[0]);
}
var assetId = 'soldier' + type;
self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Set stats
if (type === 1) {
self.fireRate = 60;
self.damage = 1;
self.range = 350;
self.dualHit = false;
self.money = 3;
}
if (type === 2) {
self.fireRate = 40;
self.damage = 1;
self.range = 300;
self.dualHit = false;
self.money = 10;
}
if (type === 3) {
self.fireRate = 90;
self.damage = 3;
self.range = 320;
self.dualHit = false;
self.money = 20;
}
if (type === 4) {
self.fireRate = 60;
self.damage = 8;
self.range = 400;
self.dualHit = false; // 4th upgrade: single hit
self.money = 50;
}
};
self.update = function () {
if (self.cooldown > 0) {
self.cooldown--;
return;
}
// Find nearest zombie in range
var nearest = null;
var minDist = 99999;
for (var i = 0; i < zombies.length; ++i) {
var z = zombies[i];
if (z.dead) continue;
var dx = z.x - self.x;
var dy = z.y - self.y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < self.range && d < minDist) {
minDist = d;
nearest = z;
}
}
if (nearest) {
// Shoot
if (self.type === 4 && !self.dualHit) {
// 4th upgrade: single hit, 8 dmg
var bullet = new Bullet();
bullet.setType(self.type);
bullet.x = self.x;
bullet.y = self.y;
bullet.target = nearest;
bullet.damage = self.damage;
bullet.shooter = self;
bullets.push(bullet);
game.addChild(bullet);
self.cooldown = self.fireRate;
} else if (self.type === 4 && self.dualHit) {
// (legacy, should not occur, but keep for safety)
for (var i = 0; i < 2; ++i) {
var bullet = new Bullet();
bullet.setType(self.type);
bullet.x = self.x + (i === 0 ? -30 : 30);
bullet.y = self.y;
bullet.target = nearest;
bullet.damage = self.damage;
bullet.shooter = self;
bullets.push(bullet);
game.addChild(bullet);
}
self.cooldown = self.fireRate;
} else {
var bullet = new Bullet();
bullet.setType(self.type);
bullet.x = self.x;
bullet.y = self.y;
bullet.target = nearest;
bullet.damage = self.damage;
bullet.shooter = self;
bullets.push(bullet);
game.addChild(bullet);
self.cooldown = self.fireRate;
}
}
};
return self;
});
// Zombie class
var Zombie = Container.expand(function () {
var self = Container.call(this);
self.isRed = false; // Will be set after construction if needed
var sprite = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var hpBarBg = LK.getAsset('pathnode', {
anchorX: 0.5,
anchorY: 0.5,
width: 60 * 1.5,
height: 14 * 1.5,
y: -60,
alpha: 0.25,
color: 0x000000
});
self.setRed = function () {
if (!self.isRed) {
self.isRed = true;
// Remove old sprite
if (sprite && sprite.parent === self) self.removeChild(sprite);
// Attach red zombie asset
var redSprite = self.attachAsset('zombieRed', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
// Add yellow zombie support
self.isYellow = false;
self.setYellow = function () {
if (!self.isYellow) {
self.isYellow = true;
// Remove old sprite
if (sprite && sprite.parent === self) self.removeChild(sprite);
// Attach yellow zombie asset
var yellowSprite = self.attachAsset('zombieYellow', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
self.addChild(hpBarBg);
// Health bar foreground
var hpBar = LK.getAsset('pathnode', {
anchorX: 0.5,
anchorY: 0.5,
width: 56 * 1.5,
height: 10 * 1.5,
y: -60,
color: 0x44ff44,
// green
alpha: 0.85
});
self.addChild(hpBar);
self.speed = 1.2;
self.maxHp = 5;
self.hp = 5;
self.reward = 1;
self.pathIndex = 0;
self.progress = 0; // 0-1 between path[pathIndex] and path[pathIndex+1]
self.targetX = 0;
self.targetY = 0;
self.dead = false;
self.setStats = function (hp, speed, reward) {
self.maxHp = hp;
self.hp = hp;
self.speed = speed;
self.reward = reward;
// Update health bar on spawn
if (hpBar) {
hpBar.width = 56 * 1.5;
}
};
self.setPathIndex = function (idx) {
self.pathIndex = idx;
self.progress = 0;
};
self.update = function () {
if (self.dead) return;
// Move along path
if (self.pathIndex >= path.length - 1) return;
var from = path[self.pathIndex];
var to = path[self.pathIndex + 1];
var dx = to.x - from.x;
var dy = to.y - from.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) dist = 1;
var step = self.speed / dist;
self.progress += step;
if (self.progress >= 1) {
self.pathIndex++;
self.progress = 0;
if (self.pathIndex >= path.length - 1) {
// At base
self.x = to.x;
self.y = to.y;
self.reachedBase = true;
return;
}
}
var from = path[self.pathIndex];
var to = path[self.pathIndex + 1];
self.x = from.x + (to.x - from.x) * self.progress;
self.y = from.y + (to.y - from.y) * self.progress;
// Update health bar position and width
if (hpBar && hpBarBg) {
hpBarBg.x = 0;
hpBarBg.y = -60;
hpBar.x = 0;
hpBar.y = -60;
var ratio = Math.max(0, Math.min(1, self.hp / self.maxHp));
hpBar.width = 56 * 1.5 * ratio;
hpBar.color = 0x44ff44; // always green
}
};
self.takeDamage = function (dmg) {
self.hp -= dmg;
if (self.hp <= 0 && !self.dead) {
self.dead = true;
// Animate fade out
tween(self, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Intro screen overlay
var introOverlay = new Container();
introOverlay.visible = true;
// Fullscreen black background
var introBg = LK.getAsset('pathnode', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
width: 2048,
height: 2732,
color: 0x000000,
alpha: 1
});
introOverlay.addChild(introBg);
// (Instructions removed for fully black intro screen)
// "Welcome" text centered at top
var welcomeTxt = new Text2('Welcome', {
size: 220,
fill: 0xffffff
});
welcomeTxt.anchor.set(0.5, 0);
welcomeTxt.x = 2048 / 2;
welcomeTxt.y = 400;
introOverlay.addChild(welcomeTxt);
// Start button centered at bottom
var startBtn = new Text2('Start', {
size: 140,
fill: 0xffffff
});
startBtn.anchor.set(0.5, 0.5);
startBtn.x = 2048 / 2;
startBtn.y = 2732 - 400;
introOverlay.addChild(startBtn);
// Start button handler
startBtn.down = function (x, y, obj) {
introOverlay.visible = false;
};
// Add overlay to game
game.addChild(introOverlay);
// Path and wall spot colors
// Path definition (nodes spaced further apart horizontally for a wider and longer path)
var path = [{
x: 500,
y: 600
},
// Start
{
x: 500,
y: 1200
}, {
x: 500,
y: 1800
},
// Extend downward
{
x: 500,
y: 2300
},
// Curve right
{
x: 900,
y: 2300
}, {
x: 1300,
y: 2300
},
// Curve up
{
x: 1300,
y: 1800
}, {
x: 1300,
y: 1200
},
// Curve right again
{
x: 1750,
y: 1200
}, {
x: 1750,
y: 1800
},
// Curve up to base
{
x: 1750,
y: 2300
}, {
x: 1750,
y: 2732 - 200 // near bottom edge, but not out of bounds
}];
// Wall spot positions (10 unit placement positions, adjacent to the new, longer path)
var wallSpots = [{
x: path[1].x + 120,
y: path[1].y
}, {
x: path[2].x + 120,
y: path[2].y
}, {
x: path[3].x + 120,
y: path[3].y
}, {
x: path[4].x,
y: path[4].y - 120
}, {
x: path[5].x,
y: path[5].y - 120
}, {
x: path[6].x - 120,
y: path[6].y
}, {
x: path[7].x - 120,
y: path[7].y
}, {
x: path[8].x - 120,
y: path[8].y
}, {
x: path[9].x,
y: path[9].y + 120
}, {
x: path[10].x,
y: path[10].y + 120
}];
// Wall spot state
var wallSpotStates = [];
for (var i = 0; i < wallSpots.length; ++i) wallSpotStates[i] = null;
// Game state
var zombies = [];
var bullets = [];
var soldiers = [];
var coins = 10;
var baseHp = 10;
var wave = 1;
var spawnTimer = 0;
var spawnIdx = 0;
var zombiesToSpawn = 0;
var waveInProgress = false;
var baseNode = null;
var selectedSoldierType = 1;
var placingSoldier = false;
var placingSoldierIdx = -1;
var gameOver = false;
// GUI
var coinTxt = new Text2('Coins: 10', {
size: 80,
fill: 0xFFE680
});
coinTxt.anchor.set(0, 0);
LK.gui.top.addChild(coinTxt);
var baseHpTxt = new Text2('Base: 10', {
size: 80,
fill: 0xFFAAAA
});
baseHpTxt.anchor.set(1, 0);
LK.gui.topRight.addChild(baseHpTxt);
var waveTxt = new Text2('Wave: 1', {
size: 80,
fill: 0xAAFFAA
});
waveTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(waveTxt);
var soldierBtns = [];
var soldierCosts = [3, 10, 20, 50];
var soldierNames = ['Basic', 'Rapid', 'Heavy', 'Red Arm'];
var soldierBtnY = 0;
for (var i = 0; i < 4; ++i) {
var btn = new Text2(soldierNames[i] + "\n" + soldierCosts[i] + "c", {
size: 60,
fill: "#fff"
});
btn.anchor.set(0.5, 0);
btn.x = 300 + i * 300;
btn.y = 0;
LK.gui.bottom.addChild(btn);
soldierBtns.push(btn);
}
// Only 4 buttons shown, 4 upgrades available
// Draw path (as faint nodes)
for (var i = 0; i < path.length; ++i) {
var node = LK.getAsset('pathnode', {
anchorX: 0.5,
anchorY: 0.5,
x: path[i].x,
y: path[i].y,
alpha: 0.18,
color: 0x8B5A2B
});
game.addChild(node);
// Connect lines (not possible, so just nodes)
}
// Draw wall spots
var wallSpotNodes = [];
for (var i = 0; i < wallSpots.length; ++i) {
var ws = LK.getAsset('wallspot', {
anchorX: 0.5,
anchorY: 0.5,
x: wallSpots[i].x,
y: wallSpots[i].y,
alpha: 0.5,
color: 0x0000ff // blue
});
ws.idx = i;
wallSpotNodes.push(ws);
game.addChild(ws);
}
// Draw base
baseNode = LK.getAsset('base', {
anchorX: 0.5,
anchorY: 0.5,
x: path[path.length - 1].x,
y: path[path.length - 1].y
});
game.addChild(baseNode);
// Handle soldier button selection
for (var i = 0; i < soldierBtns.length; ++i) {
(function (idx) {
soldierBtns[idx].down = function (x, y, obj) {
selectedSoldierType = idx + 1;
for (var j = 0; j < soldierBtns.length; ++j) {
soldierBtns[j].setStyle({
fill: j === idx ? "#ffff00" : "#fff"
});
}
};
})(i);
}
soldierBtns[0].setStyle({
fill: 0xFFFF00
});
// Place soldier on wall spot
for (var i = 0; i < wallSpotNodes.length; ++i) {
(function (idx) {
wallSpotNodes[idx].down = function (x, y, obj) {
if (gameOver) return;
if (wallSpotStates[idx] === null) {
// Place soldier if empty
var cost = soldierCosts[selectedSoldierType - 1];
if (coins < cost) {
// Flash red
tween(wallSpotNodes[idx], {
tint: 0xff0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(wallSpotNodes[idx], {
tint: 0x888888
}, {
duration: 200
});
}
});
return;
}
coins -= cost;
coinTxt.setText('Coins: ' + coins);
// Place soldier
var s = new Soldier();
s.setType(selectedSoldierType);
s.x = wallSpots[idx].x;
s.y = wallSpots[idx].y;
s.wallSpot = idx;
wallSpotStates[idx] = s;
soldiers.push(s);
game.addChild(s);
// Add upgrade text overlay
var upgradeTxt = new Text2('Upgrade', {
size: 40,
fill: 0x00FF00
});
upgradeTxt.anchor.set(0.5, 1);
upgradeTxt.x = s.x;
upgradeTxt.y = s.y - 60;
upgradeTxt.visible = false;
s.upgradeTxt = upgradeTxt;
game.addChild(upgradeTxt);
// Add tap handler for upgrade
s.down = function (x, y, obj) {
if (gameOver) return;
// Show upgrade text if not max level
if (s.level < 4) {
s.upgradeTxt.visible = true;
// Hide after 1s
LK.setTimeout(function () {
if (s.upgradeTxt) s.upgradeTxt.visible = false;
}, 1000);
}
// If already at max, do nothing
};
// Add tap handler for upgrade text
upgradeTxt.down = function (x, y, obj) {
if (gameOver) return;
if (s.level >= 4) return;
// Upgrade cost: Level 2 = 10, Level 3 = 20, Level 4 = 50
var upgradeCost = 0;
if (s.level === 1) upgradeCost = 10;else if (s.level === 2) upgradeCost = 20;else if (s.level === 3) upgradeCost = 50;
if (coins < upgradeCost) {
// Flash red
tween(upgradeTxt, {
fill: 0xff0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(upgradeTxt, {
fill: 0x00ff00
}, {
duration: 200
});
}
});
return;
}
coins -= upgradeCost;
coinTxt.setText('Coins: ' + coins);
s.level += 1;
// Change type and visuals on upgrade
if (s.level === 2) {
s.setType(2);
}
if (s.level === 3) {
s.setType(3);
}
if (s.level === 4) {
s.setType(4);
}
// Show upgraded text
upgradeTxt.setText('Upgraded!');
upgradeTxt.fill = "#ffff00";
LK.setTimeout(function () {
if (upgradeTxt) {
upgradeTxt.setText('Upgrade');
upgradeTxt.fill = "#00ff00";
upgradeTxt.visible = false;
}
}, 1000);
};
} else {
// If already a soldier, tap to show upgrade
var s = wallSpotStates[idx];
if (s && s.upgradeTxt && s.level < 5) {
s.upgradeTxt.visible = true;
LK.setTimeout(function () {
if (s.upgradeTxt) s.upgradeTxt.visible = false;
}, 1000);
}
}
};
})(i);
}
// Start first wave
// Red zombie spawn plan: wave 10-20, 1 red per wave; wave 11, 2 reds; otherwise, 1 red per wave 1-10
var redZombiePlan = {}; // wave number -> array of 0/1 (1=red)
function setupRedZombiePlan() {
// For waves 1-9: 1 red per wave
for (var w = 1; w <= 9; ++w) {
var arr = [];
var totalZ = 5 + w * 2;
for (var i = 0; i < totalZ; ++i) arr.push(0);
arr[0] = 1; // Always first zombie is red (could randomize if desired)
// Shuffle so red is not always first
for (var i = arr.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
redZombiePlan[w] = arr;
}
// Wave 10: 1 red
var arr10 = [];
var totalZ10 = 5 + 10 * 2;
for (var i = 0; i < totalZ10; ++i) arr10.push(0);
arr10[0] = 1;
for (var i = arr10.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr10[i];
arr10[i] = arr10[j];
arr10[j] = tmp;
}
redZombiePlan[10] = arr10;
// Wave 11: 2 reds
var arr11 = [];
var totalZ11 = 5 + 11 * 2;
for (var i = 0; i < totalZ11; ++i) arr11.push(0);
arr11[0] = 1;
arr11[1] = 1;
for (var i = arr11.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr11[i];
arr11[i] = arr11[j];
arr11[j] = tmp;
}
redZombiePlan[11] = arr11;
// Waves 12-20: 1 red per wave
for (var w = 12; w <= 20; ++w) {
var arr = [];
var totalZ = 5 + w * 2;
for (var i = 0; i < totalZ; ++i) arr.push(0);
arr[0] = 1;
for (var i = arr.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
redZombiePlan[w] = arr;
}
}
setupRedZombiePlan();
function startWave() {
waveInProgress = true;
spawnIdx = 0;
zombiesToSpawn = 5 + wave * 2;
spawnTimer = 0;
waveTxt.setText('Wave: ' + wave);
}
startWave();
// Spawn zombie
function spawnZombie() {
var z = new Zombie();
// Stats scale with wave
var hp = 5; // Green zombie health is always 5
var speed = 1.2 + 0.08 * wave;
var reward = 1 + Math.floor(wave / 2);
// Red zombie logic
var makeRed = false;
var makeYellow = false;
if (wave >= 15 && wave <= 20) {
// Only red zombies between wave 15 and 20
makeRed = true;
} else if (wave >= 1 && wave <= 20 && redZombiePlan[wave]) {
if (spawnIdx < redZombiePlan[wave].length && redZombiePlan[wave][spawnIdx] === 1) {
makeRed = true;
}
} else if (wave > 20 && wave < 31) {
// Between wave 21 and 30, spawn a yellow zombie at a random index per wave
// Only one yellow zombie per wave, at a random spawnIdx
if (typeof spawnZombie.yellowIdx === "undefined" || spawnZombie.yellowWave !== wave) {
// Pick a random index for yellow zombie for this wave
spawnZombie.yellowIdx = 20 + Math.floor(Math.random() * Math.max(1, 5 + wave * 2 - 20));
spawnZombie.yellowWave = wave;
}
if (spawnIdx === spawnZombie.yellowIdx) {
makeYellow = true;
}
makeRed = false;
} else if (wave > 30) {
// After wave 30, only green zombies
makeRed = false;
makeYellow = false;
}
if (makeRed) {
z.setRed();
hp = 10; // Red zombie health is always 10
reward = reward * 2;
}
if (makeYellow) {
z.setYellow();
hp = 20; // Yellow zombie health is 20
reward = reward * 3;
}
z.setStats(hp, speed, reward);
z.setPathIndex(0);
z.x = path[0].x;
z.y = path[0].y;
zombies.push(z);
game.addChild(z);
}
// Main game update
game.update = function () {
if (gameOver) return;
// Spawn zombies for wave
if (waveInProgress && spawnIdx < zombiesToSpawn) {
if (spawnTimer <= 0) {
spawnZombie();
spawnIdx++;
spawnTimer = 40 - Math.min(wave * 2, 30); // Faster spawns later
} else {
spawnTimer--;
}
}
// Update zombies
for (var i = zombies.length - 1; i >= 0; --i) {
var z = zombies[i];
z.update();
if (z.dead) {
// Reward coins
if (z.isRed) {
coins += 2;
} else {
coins += 1;
}
coinTxt.setText('Coins: ' + coins);
zombies.splice(i, 1);
continue;
}
if (z.pathIndex >= path.length - 1 && !z.dead) {
// Reached base
baseHp--;
baseHpTxt.setText('Base: ' + baseHp);
LK.effects.flashObject(baseNode, 0xff0000, 400);
z.dead = true;
z.destroy();
zombies.splice(i, 1);
if (baseHp <= 0) {
gameOver = true;
LK.effects.flashScreen(0xff0000, 1200);
LK.showGameOver();
return;
}
}
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; --i) {
var b = bullets[i];
b.update();
if (!b.parent) {
bullets.splice(i, 1);
}
}
// Update soldiers
for (var i = 0; i < soldiers.length; ++i) {
soldiers[i].update();
}
// Check if wave is over
if (waveInProgress && spawnIdx >= zombiesToSpawn && zombies.length === 0) {
waveInProgress = false;
// Next wave after short delay
LK.setTimeout(function () {
wave++;
startWave();
}, 1200);
}
};
// Allow dragging soldiers to swap positions (optional, MVP skips this)
// Prevent placing elements in top left 100x100 (already handled by GUI placement)
// No background, music, or sound per requirements
// No pause, leaderboard, or game over handling (LK does this) /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Bullet class
var Bullet = Container.expand(function () {
var self = Container.call(this);
self.target = null;
self.speed = 18;
self.damage = 1;
self.type = 1;
self.active = true;
self.shooter = null;
self.setType = function (type) {
self.type = type;
var assetId = 'bullet' + type;
self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
self.update = function () {
if (!self.active || !self.target || self.target.dead) {
self.destroy();
return;
}
// Move toward target
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 30) {
// Hit
self.target.takeDamage(self.damage);
self.destroy();
return;
}
var step = self.speed / (dist || 1);
self.x += dx * step;
self.y += dy * step;
};
return self;
});
// Soldier class
var Soldier = Container.expand(function () {
var self = Container.call(this);
self.type = 1;
self.fireRate = 60; // ticks between shots
self.damage = 1;
self.range = 350;
self.cooldown = 0;
self.level = 1;
self.wallSpot = null;
self.setType = function (type) {
self.type = type;
// Remove all previous children (visuals)
while (self.children && self.children.length > 0) {
self.removeChild(self.children[0]);
}
var assetId = 'soldier' + type;
self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Set stats
if (type === 1) {
self.fireRate = 60;
self.damage = 1;
self.range = 350;
self.dualHit = false;
self.money = 3;
}
if (type === 2) {
self.fireRate = 40;
self.damage = 1;
self.range = 300;
self.dualHit = false;
self.money = 10;
}
if (type === 3) {
self.fireRate = 90;
self.damage = 3;
self.range = 320;
self.dualHit = false;
self.money = 20;
}
if (type === 4) {
self.fireRate = 60;
self.damage = 8;
self.range = 400;
self.dualHit = false; // 4th upgrade: single hit
self.money = 50;
}
};
self.update = function () {
if (self.cooldown > 0) {
self.cooldown--;
return;
}
// Find nearest zombie in range
var nearest = null;
var minDist = 99999;
for (var i = 0; i < zombies.length; ++i) {
var z = zombies[i];
if (z.dead) continue;
var dx = z.x - self.x;
var dy = z.y - self.y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < self.range && d < minDist) {
minDist = d;
nearest = z;
}
}
if (nearest) {
// Shoot
if (self.type === 4 && !self.dualHit) {
// 4th upgrade: single hit, 8 dmg
var bullet = new Bullet();
bullet.setType(self.type);
bullet.x = self.x;
bullet.y = self.y;
bullet.target = nearest;
bullet.damage = self.damage;
bullet.shooter = self;
bullets.push(bullet);
game.addChild(bullet);
self.cooldown = self.fireRate;
} else if (self.type === 4 && self.dualHit) {
// (legacy, should not occur, but keep for safety)
for (var i = 0; i < 2; ++i) {
var bullet = new Bullet();
bullet.setType(self.type);
bullet.x = self.x + (i === 0 ? -30 : 30);
bullet.y = self.y;
bullet.target = nearest;
bullet.damage = self.damage;
bullet.shooter = self;
bullets.push(bullet);
game.addChild(bullet);
}
self.cooldown = self.fireRate;
} else {
var bullet = new Bullet();
bullet.setType(self.type);
bullet.x = self.x;
bullet.y = self.y;
bullet.target = nearest;
bullet.damage = self.damage;
bullet.shooter = self;
bullets.push(bullet);
game.addChild(bullet);
self.cooldown = self.fireRate;
}
}
};
return self;
});
// Zombie class
var Zombie = Container.expand(function () {
var self = Container.call(this);
self.isRed = false; // Will be set after construction if needed
var sprite = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
// Health bar background
var hpBarBg = LK.getAsset('pathnode', {
anchorX: 0.5,
anchorY: 0.5,
width: 60 * 1.5,
height: 14 * 1.5,
y: -60,
alpha: 0.25,
color: 0x000000
});
self.setRed = function () {
if (!self.isRed) {
self.isRed = true;
// Remove old sprite
if (sprite && sprite.parent === self) self.removeChild(sprite);
// Attach red zombie asset
var redSprite = self.attachAsset('zombieRed', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
// Add yellow zombie support
self.isYellow = false;
self.setYellow = function () {
if (!self.isYellow) {
self.isYellow = true;
// Remove old sprite
if (sprite && sprite.parent === self) self.removeChild(sprite);
// Attach yellow zombie asset
var yellowSprite = self.attachAsset('zombieYellow', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
self.addChild(hpBarBg);
// Health bar foreground
var hpBar = LK.getAsset('pathnode', {
anchorX: 0.5,
anchorY: 0.5,
width: 56 * 1.5,
height: 10 * 1.5,
y: -60,
color: 0x44ff44,
// green
alpha: 0.85
});
self.addChild(hpBar);
self.speed = 1.2;
self.maxHp = 5;
self.hp = 5;
self.reward = 1;
self.pathIndex = 0;
self.progress = 0; // 0-1 between path[pathIndex] and path[pathIndex+1]
self.targetX = 0;
self.targetY = 0;
self.dead = false;
self.setStats = function (hp, speed, reward) {
self.maxHp = hp;
self.hp = hp;
self.speed = speed;
self.reward = reward;
// Update health bar on spawn
if (hpBar) {
hpBar.width = 56 * 1.5;
}
};
self.setPathIndex = function (idx) {
self.pathIndex = idx;
self.progress = 0;
};
self.update = function () {
if (self.dead) return;
// Move along path
if (self.pathIndex >= path.length - 1) return;
var from = path[self.pathIndex];
var to = path[self.pathIndex + 1];
var dx = to.x - from.x;
var dy = to.y - from.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) dist = 1;
var step = self.speed / dist;
self.progress += step;
if (self.progress >= 1) {
self.pathIndex++;
self.progress = 0;
if (self.pathIndex >= path.length - 1) {
// At base
self.x = to.x;
self.y = to.y;
self.reachedBase = true;
return;
}
}
var from = path[self.pathIndex];
var to = path[self.pathIndex + 1];
self.x = from.x + (to.x - from.x) * self.progress;
self.y = from.y + (to.y - from.y) * self.progress;
// Update health bar position and width
if (hpBar && hpBarBg) {
hpBarBg.x = 0;
hpBarBg.y = -60;
hpBar.x = 0;
hpBar.y = -60;
var ratio = Math.max(0, Math.min(1, self.hp / self.maxHp));
hpBar.width = 56 * 1.5 * ratio;
hpBar.color = 0x44ff44; // always green
}
};
self.takeDamage = function (dmg) {
self.hp -= dmg;
if (self.hp <= 0 && !self.dead) {
self.dead = true;
// Animate fade out
tween(self, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Intro screen overlay
var introOverlay = new Container();
introOverlay.visible = true;
// Fullscreen black background
var introBg = LK.getAsset('pathnode', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
width: 2048,
height: 2732,
color: 0x000000,
alpha: 1
});
introOverlay.addChild(introBg);
// (Instructions removed for fully black intro screen)
// "Welcome" text centered at top
var welcomeTxt = new Text2('Welcome', {
size: 220,
fill: 0xffffff
});
welcomeTxt.anchor.set(0.5, 0);
welcomeTxt.x = 2048 / 2;
welcomeTxt.y = 400;
introOverlay.addChild(welcomeTxt);
// Start button centered at bottom
var startBtn = new Text2('Start', {
size: 140,
fill: 0xffffff
});
startBtn.anchor.set(0.5, 0.5);
startBtn.x = 2048 / 2;
startBtn.y = 2732 - 400;
introOverlay.addChild(startBtn);
// Start button handler
startBtn.down = function (x, y, obj) {
introOverlay.visible = false;
};
// Add overlay to game
game.addChild(introOverlay);
// Path and wall spot colors
// Path definition (nodes spaced further apart horizontally for a wider and longer path)
var path = [{
x: 500,
y: 600
},
// Start
{
x: 500,
y: 1200
}, {
x: 500,
y: 1800
},
// Extend downward
{
x: 500,
y: 2300
},
// Curve right
{
x: 900,
y: 2300
}, {
x: 1300,
y: 2300
},
// Curve up
{
x: 1300,
y: 1800
}, {
x: 1300,
y: 1200
},
// Curve right again
{
x: 1750,
y: 1200
}, {
x: 1750,
y: 1800
},
// Curve up to base
{
x: 1750,
y: 2300
}, {
x: 1750,
y: 2732 - 200 // near bottom edge, but not out of bounds
}];
// Wall spot positions (10 unit placement positions, adjacent to the new, longer path)
var wallSpots = [{
x: path[1].x + 120,
y: path[1].y
}, {
x: path[2].x + 120,
y: path[2].y
}, {
x: path[3].x + 120,
y: path[3].y
}, {
x: path[4].x,
y: path[4].y - 120
}, {
x: path[5].x,
y: path[5].y - 120
}, {
x: path[6].x - 120,
y: path[6].y
}, {
x: path[7].x - 120,
y: path[7].y
}, {
x: path[8].x - 120,
y: path[8].y
}, {
x: path[9].x,
y: path[9].y + 120
}, {
x: path[10].x,
y: path[10].y + 120
}];
// Wall spot state
var wallSpotStates = [];
for (var i = 0; i < wallSpots.length; ++i) wallSpotStates[i] = null;
// Game state
var zombies = [];
var bullets = [];
var soldiers = [];
var coins = 10;
var baseHp = 10;
var wave = 1;
var spawnTimer = 0;
var spawnIdx = 0;
var zombiesToSpawn = 0;
var waveInProgress = false;
var baseNode = null;
var selectedSoldierType = 1;
var placingSoldier = false;
var placingSoldierIdx = -1;
var gameOver = false;
// GUI
var coinTxt = new Text2('Coins: 10', {
size: 80,
fill: 0xFFE680
});
coinTxt.anchor.set(0, 0);
LK.gui.top.addChild(coinTxt);
var baseHpTxt = new Text2('Base: 10', {
size: 80,
fill: 0xFFAAAA
});
baseHpTxt.anchor.set(1, 0);
LK.gui.topRight.addChild(baseHpTxt);
var waveTxt = new Text2('Wave: 1', {
size: 80,
fill: 0xAAFFAA
});
waveTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(waveTxt);
var soldierBtns = [];
var soldierCosts = [3, 10, 20, 50];
var soldierNames = ['Basic', 'Rapid', 'Heavy', 'Red Arm'];
var soldierBtnY = 0;
for (var i = 0; i < 4; ++i) {
var btn = new Text2(soldierNames[i] + "\n" + soldierCosts[i] + "c", {
size: 60,
fill: "#fff"
});
btn.anchor.set(0.5, 0);
btn.x = 300 + i * 300;
btn.y = 0;
LK.gui.bottom.addChild(btn);
soldierBtns.push(btn);
}
// Only 4 buttons shown, 4 upgrades available
// Draw path (as faint nodes)
for (var i = 0; i < path.length; ++i) {
var node = LK.getAsset('pathnode', {
anchorX: 0.5,
anchorY: 0.5,
x: path[i].x,
y: path[i].y,
alpha: 0.18,
color: 0x8B5A2B
});
game.addChild(node);
// Connect lines (not possible, so just nodes)
}
// Draw wall spots
var wallSpotNodes = [];
for (var i = 0; i < wallSpots.length; ++i) {
var ws = LK.getAsset('wallspot', {
anchorX: 0.5,
anchorY: 0.5,
x: wallSpots[i].x,
y: wallSpots[i].y,
alpha: 0.5,
color: 0x0000ff // blue
});
ws.idx = i;
wallSpotNodes.push(ws);
game.addChild(ws);
}
// Draw base
baseNode = LK.getAsset('base', {
anchorX: 0.5,
anchorY: 0.5,
x: path[path.length - 1].x,
y: path[path.length - 1].y
});
game.addChild(baseNode);
// Handle soldier button selection
for (var i = 0; i < soldierBtns.length; ++i) {
(function (idx) {
soldierBtns[idx].down = function (x, y, obj) {
selectedSoldierType = idx + 1;
for (var j = 0; j < soldierBtns.length; ++j) {
soldierBtns[j].setStyle({
fill: j === idx ? "#ffff00" : "#fff"
});
}
};
})(i);
}
soldierBtns[0].setStyle({
fill: 0xFFFF00
});
// Place soldier on wall spot
for (var i = 0; i < wallSpotNodes.length; ++i) {
(function (idx) {
wallSpotNodes[idx].down = function (x, y, obj) {
if (gameOver) return;
if (wallSpotStates[idx] === null) {
// Place soldier if empty
var cost = soldierCosts[selectedSoldierType - 1];
if (coins < cost) {
// Flash red
tween(wallSpotNodes[idx], {
tint: 0xff0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(wallSpotNodes[idx], {
tint: 0x888888
}, {
duration: 200
});
}
});
return;
}
coins -= cost;
coinTxt.setText('Coins: ' + coins);
// Place soldier
var s = new Soldier();
s.setType(selectedSoldierType);
s.x = wallSpots[idx].x;
s.y = wallSpots[idx].y;
s.wallSpot = idx;
wallSpotStates[idx] = s;
soldiers.push(s);
game.addChild(s);
// Add upgrade text overlay
var upgradeTxt = new Text2('Upgrade', {
size: 40,
fill: 0x00FF00
});
upgradeTxt.anchor.set(0.5, 1);
upgradeTxt.x = s.x;
upgradeTxt.y = s.y - 60;
upgradeTxt.visible = false;
s.upgradeTxt = upgradeTxt;
game.addChild(upgradeTxt);
// Add tap handler for upgrade
s.down = function (x, y, obj) {
if (gameOver) return;
// Show upgrade text if not max level
if (s.level < 4) {
s.upgradeTxt.visible = true;
// Hide after 1s
LK.setTimeout(function () {
if (s.upgradeTxt) s.upgradeTxt.visible = false;
}, 1000);
}
// If already at max, do nothing
};
// Add tap handler for upgrade text
upgradeTxt.down = function (x, y, obj) {
if (gameOver) return;
if (s.level >= 4) return;
// Upgrade cost: Level 2 = 10, Level 3 = 20, Level 4 = 50
var upgradeCost = 0;
if (s.level === 1) upgradeCost = 10;else if (s.level === 2) upgradeCost = 20;else if (s.level === 3) upgradeCost = 50;
if (coins < upgradeCost) {
// Flash red
tween(upgradeTxt, {
fill: 0xff0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(upgradeTxt, {
fill: 0x00ff00
}, {
duration: 200
});
}
});
return;
}
coins -= upgradeCost;
coinTxt.setText('Coins: ' + coins);
s.level += 1;
// Change type and visuals on upgrade
if (s.level === 2) {
s.setType(2);
}
if (s.level === 3) {
s.setType(3);
}
if (s.level === 4) {
s.setType(4);
}
// Show upgraded text
upgradeTxt.setText('Upgraded!');
upgradeTxt.fill = "#ffff00";
LK.setTimeout(function () {
if (upgradeTxt) {
upgradeTxt.setText('Upgrade');
upgradeTxt.fill = "#00ff00";
upgradeTxt.visible = false;
}
}, 1000);
};
} else {
// If already a soldier, tap to show upgrade
var s = wallSpotStates[idx];
if (s && s.upgradeTxt && s.level < 5) {
s.upgradeTxt.visible = true;
LK.setTimeout(function () {
if (s.upgradeTxt) s.upgradeTxt.visible = false;
}, 1000);
}
}
};
})(i);
}
// Start first wave
// Red zombie spawn plan: wave 10-20, 1 red per wave; wave 11, 2 reds; otherwise, 1 red per wave 1-10
var redZombiePlan = {}; // wave number -> array of 0/1 (1=red)
function setupRedZombiePlan() {
// For waves 1-9: 1 red per wave
for (var w = 1; w <= 9; ++w) {
var arr = [];
var totalZ = 5 + w * 2;
for (var i = 0; i < totalZ; ++i) arr.push(0);
arr[0] = 1; // Always first zombie is red (could randomize if desired)
// Shuffle so red is not always first
for (var i = arr.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
redZombiePlan[w] = arr;
}
// Wave 10: 1 red
var arr10 = [];
var totalZ10 = 5 + 10 * 2;
for (var i = 0; i < totalZ10; ++i) arr10.push(0);
arr10[0] = 1;
for (var i = arr10.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr10[i];
arr10[i] = arr10[j];
arr10[j] = tmp;
}
redZombiePlan[10] = arr10;
// Wave 11: 2 reds
var arr11 = [];
var totalZ11 = 5 + 11 * 2;
for (var i = 0; i < totalZ11; ++i) arr11.push(0);
arr11[0] = 1;
arr11[1] = 1;
for (var i = arr11.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr11[i];
arr11[i] = arr11[j];
arr11[j] = tmp;
}
redZombiePlan[11] = arr11;
// Waves 12-20: 1 red per wave
for (var w = 12; w <= 20; ++w) {
var arr = [];
var totalZ = 5 + w * 2;
for (var i = 0; i < totalZ; ++i) arr.push(0);
arr[0] = 1;
for (var i = arr.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
redZombiePlan[w] = arr;
}
}
setupRedZombiePlan();
function startWave() {
waveInProgress = true;
spawnIdx = 0;
zombiesToSpawn = 5 + wave * 2;
spawnTimer = 0;
waveTxt.setText('Wave: ' + wave);
}
startWave();
// Spawn zombie
function spawnZombie() {
var z = new Zombie();
// Stats scale with wave
var hp = 5; // Green zombie health is always 5
var speed = 1.2 + 0.08 * wave;
var reward = 1 + Math.floor(wave / 2);
// Red zombie logic
var makeRed = false;
var makeYellow = false;
if (wave >= 15 && wave <= 20) {
// Only red zombies between wave 15 and 20
makeRed = true;
} else if (wave >= 1 && wave <= 20 && redZombiePlan[wave]) {
if (spawnIdx < redZombiePlan[wave].length && redZombiePlan[wave][spawnIdx] === 1) {
makeRed = true;
}
} else if (wave > 20 && wave < 31) {
// Between wave 21 and 30, spawn a yellow zombie at a random index per wave
// Only one yellow zombie per wave, at a random spawnIdx
if (typeof spawnZombie.yellowIdx === "undefined" || spawnZombie.yellowWave !== wave) {
// Pick a random index for yellow zombie for this wave
spawnZombie.yellowIdx = 20 + Math.floor(Math.random() * Math.max(1, 5 + wave * 2 - 20));
spawnZombie.yellowWave = wave;
}
if (spawnIdx === spawnZombie.yellowIdx) {
makeYellow = true;
}
makeRed = false;
} else if (wave > 30) {
// After wave 30, only green zombies
makeRed = false;
makeYellow = false;
}
if (makeRed) {
z.setRed();
hp = 10; // Red zombie health is always 10
reward = reward * 2;
}
if (makeYellow) {
z.setYellow();
hp = 20; // Yellow zombie health is 20
reward = reward * 3;
}
z.setStats(hp, speed, reward);
z.setPathIndex(0);
z.x = path[0].x;
z.y = path[0].y;
zombies.push(z);
game.addChild(z);
}
// Main game update
game.update = function () {
if (gameOver) return;
// Spawn zombies for wave
if (waveInProgress && spawnIdx < zombiesToSpawn) {
if (spawnTimer <= 0) {
spawnZombie();
spawnIdx++;
spawnTimer = 40 - Math.min(wave * 2, 30); // Faster spawns later
} else {
spawnTimer--;
}
}
// Update zombies
for (var i = zombies.length - 1; i >= 0; --i) {
var z = zombies[i];
z.update();
if (z.dead) {
// Reward coins
if (z.isRed) {
coins += 2;
} else {
coins += 1;
}
coinTxt.setText('Coins: ' + coins);
zombies.splice(i, 1);
continue;
}
if (z.pathIndex >= path.length - 1 && !z.dead) {
// Reached base
baseHp--;
baseHpTxt.setText('Base: ' + baseHp);
LK.effects.flashObject(baseNode, 0xff0000, 400);
z.dead = true;
z.destroy();
zombies.splice(i, 1);
if (baseHp <= 0) {
gameOver = true;
LK.effects.flashScreen(0xff0000, 1200);
LK.showGameOver();
return;
}
}
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; --i) {
var b = bullets[i];
b.update();
if (!b.parent) {
bullets.splice(i, 1);
}
}
// Update soldiers
for (var i = 0; i < soldiers.length; ++i) {
soldiers[i].update();
}
// Check if wave is over
if (waveInProgress && spawnIdx >= zombiesToSpawn && zombies.length === 0) {
waveInProgress = false;
// Next wave after short delay
LK.setTimeout(function () {
wave++;
startWave();
}, 1200);
}
};
// Allow dragging soldiers to swap positions (optional, MVP skips this)
// Prevent placing elements in top left 100x100 (already handled by GUI placement)
// No background, music, or sound per requirements
// No pause, leaderboard, or game over handling (LK does this)
Soilder. In-Game asset. 2d. High contrast. No shadows
one-gun soldier. In-Game asset. 2d. High contrast. No shadows
soldier with 2 kate ak47 in his hand. In-Game asset. 2d. High contrast. No shadows
soldier with rifle. In-Game asset. 2d. High contrast. No shadows
Zombie. In-Game asset. 2d. High contrast. No shadows
Zombie. In-Game asset. 2d. High contrast. No shadows