/**** * 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