/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Aiming line class var AimLine = Container.expand(function () { var self = Container.call(this); // Attach aim line asset, anchor at left (0,0.5) var lineGfx = self.attachAsset('aimLine', { anchorX: 0, anchorY: 0.5 }); // Set default scale lineGfx.scaleY = 0.2; // Thin line // Set color, etc. if needed return self; }); // Bullet class var Bullet = Container.expand(function () { var self = Container.call(this); // Determine bullet tier based on score at spawn var tier = Math.floor(score / 100); if (tier >= bulletColors.length) tier = bulletColors.length - 1; self.tier = tier; // Attach bullet asset, center anchor, color by tier var bulletGfx = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, color: bulletColors[tier] }); // Direction vector (set on spawn) self.dx = 0; self.dy = 0; self.speed = 40 + tier * 2; // px per frame, slight speed bonus per tier // Damage increases with tier self.damage = 1 + tier; self.update = function () { // Bullet bounce logic: number of bounces = 1 + floor(score/10) if (typeof self.bouncesLeft === "undefined") { self.bouncesLeft = 1 + Math.floor(score / 10); self.lastHitZombies = []; } // If bullet just bounced, skip homing for this frame if (self.justBounced) { self.justBounced = false; } else if (zombies && zombies.length > 0) { // Home in on closest zombie not already hit var minDist = Infinity; var closest = null; for (var i = 0; i < zombies.length; i++) { var z = zombies[i]; // Don't home to zombies already hit by this bullet if (self.lastHitZombies.indexOf(z) !== -1) continue; var zx = z.x - self.x; var zy = z.y - self.y; var d = Math.sqrt(zx * zx + zy * zy); if (d < minDist) { minDist = d; closest = z; } } if (closest && minDist > 1) { // Adjust direction toward closest zombie var dx = closest.x - self.x; var dy = closest.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { // Smoothly adjust direction (homing) var homingStrength = 0.18; self.dx = (1 - homingStrength) * self.dx + homingStrength * (dx / dist); self.dy = (1 - homingStrength) * self.dy + homingStrength * (dy / dist); // Normalize direction var norm = Math.sqrt(self.dx * self.dx + self.dy * self.dy); if (norm > 0) { self.dx /= norm; self.dy /= norm; } } } } // At score >= 210, slow down bullet and instantly bounce to next zombie if possible if (score >= 210) { // Slow down bullet self.speed = Math.max(10, 18 + self.tier * 2); // Instantly bounce to next zombie if bounces left and zombies available if (typeof self.bouncesLeft === "undefined") { self.bouncesLeft = 1 + Math.floor(score / 10); self.lastHitZombies = []; } if (self.bouncesLeft > 0 && zombies && zombies.length > 1) { // Find next closest zombie not already hit var minDist = Infinity; var nextTarget = null; for (var zz = 0; zz < zombies.length; zz++) { var z2 = zombies[zz]; if (self.lastHitZombies.indexOf(z2) !== -1) continue; var zx = z2.x - self.x; var zy = z2.y - self.y; var d = Math.sqrt(zx * zx + zy * zy); if (d < minDist) { minDist = d; nextTarget = z2; } } if (nextTarget) { // Set direction to next zombie var dx = nextTarget.x - self.x; var dy = nextTarget.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { self.dx = dx / dist; self.dy = dy / dist; self.lastHitZombies.push(nextTarget); self.bouncesLeft--; } } } } self.x += self.dx * self.speed; self.y += self.dy * self.speed; }; return self; }); // Gun/player class var Gun = Container.expand(function () { var self = Container.call(this); // Attach gun asset, center anchor var gunGfx = self.attachAsset('gun', { anchorX: 0.5, anchorY: 0.5 }); // For possible future use: gunGfx return self; }); // Zombie class var Zombie = Container.expand(function () { var self = Container.call(this); // Determine zombie tier based on score at spawn var tier = Math.floor(score / 100); if (tier >= zombieColors.length) tier = zombieColors.length - 1; self.tier = tier; // Attach zombie asset, center anchor, color by tier var zombieGfx = self.attachAsset('zombie', { anchorX: 0.5, anchorY: 0.5, color: zombieColors[tier] }); // Target position (player center) self.targetX = 0; self.targetY = 0; self.speed = 5 + tier * 0.5; // Fixed speed, no randomness // Health increases with tier self.maxHealth = 1 + tier * 2; self.health = self.maxHealth; self.update = function () { // Move toward target var dx = self.targetX - self.x; var dy = self.targetY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { self.x += dx / dist * self.speed; self.y += dy / dist * self.speed; } }; // Take damage method self.takeDamage = function (dmg) { self.health -= dmg; if (self.health <= 0) { self.destroy(); return true; // killed } // Flash on hit LK.effects.flashObject(self, 0xffffff, 120); return false; // not dead }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222244 // Changed to a visible dark blue for better contrast }); /**** * Game Code ****/ // --- Begin: Bouncing kill line logic --- // --- Begin: 4000 kill reset logic --- if (typeof game._lastResetKill === "undefined") game._lastResetKill = 0; if (score - game._lastResetKill >= 4000) { // Save the current kill count var totalKills = score; // Reset all gameplay variables except kill count // Remove all bullets/zombies for (var i = 0; i < bullets.length; i++) bullets[i].destroy(); for (var j = 0; j < zombies.length; j++) zombies[j].destroy(); bullets = []; zombies = []; // Reset upgrades, gun, aim, etc. gunLevel = 1; lastUpgradeScore = 0; isGameOver = false; game.rightClickSpinState = false; if (game.rightClickSpinTween) { tween.kill(game.rightClickSpinTween); game.rightClickSpinTween = null; } gun.rotation = 0; aimX = gun.x; aimY = gun.y - 400; updateAimLine(aimX, aimY); // Reset spawn tick game.lastZombieSpawnTick = LK.ticks; // Reset score to 1 (kill count stays the same) score = 1; scoreTxt.setText(totalKills); // Mark last reset game._lastResetKill = totalKills; // Flash to indicate reset LK.effects.flashScreen(0x00ffff, 800); // Optionally, show a message (not required) } // --- End: 4000 kill reset logic --- // Game constants // Gun (player) asset: a box, centered // Bullet asset: small ellipse // Zombie asset: greenish box // Aiming line: thin yellow box (will be scaled) var bulletColors = [0xffe066, // yellow 0x2196f3, // blue 0xff9800, // orange 0xe91e63, // pink 0x9c27b0, // purple 0xffeb3b, // yellow2 0x795548, // brown 0x607d8b, // gray-blue 0xf44336, // red 0x00bcd4 // cyan ]; // Zombie class var zombieColors = [0x4caf50, // green 0x2196f3, // blue 0xff9800, // orange 0xe91e63, // pink 0x9c27b0, // purple 0xffeb3b, // yellow 0x795548, // brown 0x607d8b, // gray-blue 0xf44336, // red 0x00bcd4 // cyan ]; var GAME_W = 2048; var GAME_H = 2732; var PLAYER_X = GAME_W / 2; var PLAYER_Y = GAME_H / 2; // Center of the screen // Game state var bullets = []; var zombies = []; var score = 0; var isGameOver = false; // Gun upgrade state var gunLevel = 1; var lastUpgradeScore = 0; var bulletBaseSpeed = 40; var aimLineBaseLen = 700; // Create gun/player var gun = new Gun(); gun.x = PLAYER_X; gun.y = PLAYER_Y; game.addChild(gun); // Create aiming line var aimLine = new AimLine(); aimLine.x = gun.x; aimLine.y = gun.y; game.addChild(aimLine); // Score text var scoreTxt = new Text2('0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Last score text (shows after game over) var lastScoreTxt = new Text2('', { size: 80, fill: 0xFFE066 }); lastScoreTxt.anchor.set(0.5, 0); lastScoreTxt.visible = false; lastScoreTxt.y = 140; LK.gui.top.addChild(lastScoreTxt); // Helper: clamp function clamp(val, min, max) { return Math.max(min, Math.min(max, val)); } // Helper: spawn zombie at random edge, aiming at player function spawnZombie() { // Determine zombie tier based on score at spawn var tier = Math.floor(score / 100); // Deterministic edge: cycle through edges in order (top, bottom, left, right) if (typeof spawnZombie.edgeIndex === "undefined") spawnZombie.edgeIndex = 0; var edge = spawnZombie.edgeIndex % 4; spawnZombie.edgeIndex++; var x, y; if (edge === 0) { // top x = 200; y = -80; } else if (edge === 1) { // bottom x = 200; y = GAME_H + 80; } else if (edge === 2) { // left x = -80; y = 300; } else { // right x = GAME_W + 80; y = 300; } var z = new Zombie(); z.x = x; z.y = y; z.targetX = gun.x; z.targetY = gun.y; zombies.push(z); game.addChild(z); } // Helper: fire bullet toward (tx,ty) function fireBullet(tx, ty) { // If score >= 20 and there are zombies, auto-aim at closest zombie var autoAim = score >= 20 && zombies.length > 0; var dx, dy, dist; if (autoAim) { // Find closest zombie to gun var minDist = Infinity; var closest = null; for (var i = 0; i < zombies.length; i++) { var z = zombies[i]; var zx = z.x - gun.x; var zy = z.y - gun.y; var d = Math.sqrt(zx * zx + zy * zy); if (d < minDist) { minDist = d; closest = z; } } if (closest) { dx = closest.x - gun.x; dy = closest.y - gun.y; dist = Math.sqrt(dx * dx + dy * dy); // If zombie is on top of gun, fallback to normal aim if (dist < 10) { dx = tx - gun.x; dy = ty - gun.y; dist = Math.sqrt(dx * dx + dy * dy); } } else { dx = tx - gun.x; dy = ty - gun.y; dist = Math.sqrt(dx * dx + dy * dy); } } else { dx = tx - gun.x; dy = ty - gun.y; dist = Math.sqrt(dx * dx + dy * dy); } // Removed 'don't fire if too close' check dx /= dist; dy /= dist; // Only fire a single bullet, but set its bounces based on score // Bullet cap: max 100 bullets at once if (bullets.length >= 100) { return; } var angle = Math.atan2(dy, dx); var b = new Bullet(); b.x = gun.x + Math.cos(angle) * 80; // Start a bit ahead of gun b.y = gun.y + Math.sin(angle) * 80; b.dx = Math.cos(angle); b.dy = Math.sin(angle); // Upgrade bullet speed, slow down at score >= 210 if (score >= 210) { b.speed = 18 + (gunLevel - 1) * 2; } else { b.speed = bulletBaseSpeed + (gunLevel - 1) * 10; } // Track lastX and lastY for despawn logic b.lastX = b.x; b.lastY = b.y; bullets.push(b); game.addChild(b); // Play bullet fire sound LK.getSound('bulletFire').play(); } // Update aiming line to point from gun to (aimX, aimY) function updateAimLine(aimX, aimY) { var dx = aimX - gun.x; var dy = aimY - gun.y; var dist = Math.sqrt(dx * dx + dy * dy); // Clamp min/max length var minLen = 120, maxLen = aimLineBaseLen + (gunLevel - 1) * 80; var len = clamp(dist, minLen, maxLen); // Set scaleX of line aimLine.scale.x = len / 20; // asset width is 20 // Set rotation aimLine.rotation = Math.atan2(dy, dx); // Make the gun point at the aim direction gun.rotation = Math.atan2(dy, dx); } // Initial aim position: straight up var aimX = gun.x; var aimY = gun.y - 400; updateAimLine(aimX, aimY); // Touch/mouse move: update aim game.move = function (x, y, obj) { // Clamp aim point to inside game area aimX = clamp(x, 100, GAME_W - 100); aimY = clamp(y, 100, GAME_H - 100); updateAimLine(aimX, aimY); }; // Hold-to-fire state var isFiring = false; // Touch/mouse down: start firing game.down = function (x, y, obj) { if (isGameOver) return; // Only fire on left click/tap (event.button==0 or undefined for touch) if (!obj || !obj.event || obj.event.button === undefined || obj.event.button === 0) { isFiring = true; // Fire immediately on down fireBullet(x, y); } }; // Touch/mouse up: stop firing game.up = function (x, y, obj) { isFiring = false; }; // Right click: spin gun and toggle spin state game.rightClickSpinState = false; game.rightClickSpinTween = null; game.rightClick = function (x, y, obj) { if (isGameOver) return; // Toggle spin state game.rightClickSpinState = !game.rightClickSpinState; // If already spinning, stop previous tween if (game.rightClickSpinTween) { tween.kill(game.rightClickSpinTween); game.rightClickSpinTween = null; } if (game.rightClickSpinState) { // Spin gun 2 full turns (4*PI) over 0.7s var startRot = gun.rotation; var endRot = startRot + Math.PI * 4; game.rightClickSpinTween = tween.to(gun, { rotation: endRot }, 700, { easing: "easeInOutCubic" }); } else { // Spin back to original (0) over 0.7s var curRot = gun.rotation; // Normalize to [-PI, PI] for shortest path var normRot = (curRot % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2); if (normRot > Math.PI) normRot -= Math.PI * 2; game.rightClickSpinTween = tween.to(gun, { rotation: 0 }, 700, { easing: "easeInOutCubic" }); } }; // Listen for right click (button==2) game.on('down', function (x, y, obj) { if (isGameOver) return; if (obj && obj.event && obj.event.button === 2) { game.rightClick(x, y, obj); } }); // Prevent context menu on right click if (typeof window !== "undefined" && window.addEventListener) { window.addEventListener("contextmenu", function (e) { e.preventDefault(); }); } // No drag, so up is not needed // Game update loop game.update = function () { if (isGameOver) { // Show last score text if available if (storage.lastScore !== undefined) { lastScoreTxt.setText("Last: " + storage.lastScore); lastScoreTxt.visible = true; } return; } lastScoreTxt.visible = false; // --- Begin: Bouncing kill line logic --- // Remove all previous aim lines if (!game.bounceLines) game.bounceLines = []; for (var li = 0; li < game.bounceLines.length; li++) { game.bounceLines[li].destroy(); } game.bounceLines = []; // For each bullet, create a visible line that bounces from zombie to zombie, killing all zombies in the path for (var i = bullets.length - 1; i >= 0; i--) { var b = bullets[i]; // Make bullet invisible if (b.children && b.children.length > 0) { for (var ci = 0; ci < b.children.length; ci++) { b.children[ci].alpha = 0; } } // Track previous position for despawn logic if (typeof b.lastX === "undefined") b.lastX = b.x; if (typeof b.lastY === "undefined") b.lastY = b.y; // Build the bounce path: start at bullet, bounce to closest zombie not yet hit, repeat up to bouncesLeft, then to screen edge var pathPoints = [{ x: b.x, y: b.y }]; var hitZombies = []; var curX = b.x, curY = b.y; var bounces = typeof b.bouncesLeft !== "undefined" ? b.bouncesLeft : 1 + Math.floor(score / 10); var zombiesLeft = zombies.slice(); // Remove zombies already hit by this bullet if (b.lastHitZombies && b.lastHitZombies.length) { for (var lhz = 0; lhz < b.lastHitZombies.length; lhz++) { var idx = zombiesLeft.indexOf(b.lastHitZombies[lhz]); if (idx !== -1) zombiesLeft.splice(idx, 1); } } // Build bounce path for (var bounce = 0; bounce < bounces; bounce++) { // Find closest zombie not yet hit var minDist = Infinity, closest = null, closestIdx = -1; for (var zi = 0; zi < zombiesLeft.length; zi++) { var z = zombiesLeft[zi]; var dx = z.x - curX; var dy = z.y - curY; var d = Math.sqrt(dx * dx + dy * dy); if (d < minDist) { minDist = d; closest = z; closestIdx = zi; } } if (closest) { pathPoints.push({ x: closest.x, y: closest.y }); hitZombies.push(closest); curX = closest.x; curY = closest.y; zombiesLeft.splice(closestIdx, 1); } else { break; } } // After last bounce, extend line to screen edge in the same direction var lastPt = pathPoints[pathPoints.length - 1]; var prevPt = pathPoints.length > 1 ? pathPoints[pathPoints.length - 2] : { x: b.x, y: b.y }; var dx = lastPt.x - prevPt.x; var dy = lastPt.y - prevPt.y; var norm = Math.sqrt(dx * dx + dy * dy); if (norm === 0) { dx = 1; dy = 0; norm = 1; } dx /= norm; dy /= norm; // Find intersection with screen edge var tMax = 99999; var tx = dx > 0 ? (GAME_W - lastPt.x) / dx : dx < 0 ? (0 - lastPt.x) / dx : tMax; var ty = dy > 0 ? (GAME_H - lastPt.y) / dy : dy < 0 ? (0 - lastPt.y) / dy : tMax; var t = Math.min(Math.abs(tx), Math.abs(ty)); var edgeX = lastPt.x + dx * t; var edgeY = lastPt.y + dy * t; // Clamp to screen edgeX = clamp(edgeX, 0, GAME_W); edgeY = clamp(edgeY, 0, GAME_H); pathPoints.push({ x: edgeX, y: edgeY }); // Draw the visible bouncing line segments for (var pi = 0; pi < pathPoints.length - 1; pi++) { var p0 = pathPoints[pi]; var p1 = pathPoints[pi + 1]; var segLen = Math.sqrt((p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y)); if (segLen < 1) continue; var line = new AimLine(); line.x = p0.x; line.y = p0.y; line.scale.x = segLen / 20; line.scale.y = 0.18; line.rotation = Math.atan2(p1.y - p0.y, p1.x - p0.x); line.alpha = 1; game.addChild(line); game.bounceLines.push(line); } // Kill all zombies that intersect any segment of the line (except already killed this frame) var killedZombies = []; for (var pi = 0; pi < pathPoints.length - 1; pi++) { var p0 = pathPoints[pi]; var p1 = pathPoints[pi + 1]; var segDx = p1.x - p0.x; var segDy = p1.y - p0.y; var segLen = Math.sqrt(segDx * segDx + segDy * segDy); if (segLen < 1) continue; for (var j = zombies.length - 1; j >= 0; j--) { var z = zombies[j]; // Don't double-kill if (killedZombies.indexOf(z) !== -1) continue; // Distance from zombie center to segment var zx = z.x, zy = z.y; var t = ((zx - p0.x) * segDx + (zy - p0.y) * segDy) / (segLen * segLen); t = Math.max(0, Math.min(1, t)); var projX = p0.x + t * segDx; var projY = p0.y + t * segDy; var distToLine = Math.sqrt((zx - projX) * (zx - projX) + (zy - projY) * (zy - projY)); // Use zombie's width as hit radius var hitRadius = 60; if (distToLine <= hitRadius) { // Kill zombie var killed = false; if (typeof z.takeDamage === "function") { killed = z.takeDamage(9999); } else { z.destroy(); killed = true; } if (killed) { zombies.splice(j, 1); killedZombies.push(z); // Play zombie death sound LK.getSound('zombieDeath').play(); score += 1; scoreTxt.setText((typeof game._lastResetKill !== "undefined" ? game._lastResetKill : 0) + score); if (score > 0 && score % 5 === 0 && score !== lastUpgradeScore) { gunLevel += 1; lastUpgradeScore = score; LK.effects.flashObject(gun, 0x3399ff, 500); } LK.effects.flashObject(gun, 0x00ff00, 200); } } } } // Remove bullet after line is drawn and zombies are killed b.destroy(); bullets.splice(i, 1); } // --- End: Bouncing kill line logic --- // Update all zombies (no artificial cap that breaks at high scores) for (var k = zombies.length - 1; k >= 0; k--) { var z = zombies[k]; // Make it easier at score >= 200: reduce zombie speed if (score >= 200) { z.speed = Math.max(2, z.speed * 0.7); } // Only update if zombie is on screen (with margin) if (z.x > -200 && z.x < GAME_W + 200 && z.y > -200 && z.y < GAME_H + 200) { z.update(); } // Check if zombie reached player (gun) if (z.intersects(gun)) { // Game over isGameOver = true; // Save score to storage as lastScore and bestScore var totalKills = (typeof game._lastResetKill !== "undefined" ? game._lastResetKill : 0) + score; storage.lastScore = totalKills; if (!storage.bestScore || totalKills > storage.bestScore) { storage.bestScore = totalKills; } LK.effects.flashScreen(0xff0000, 1000); // Show game over and then leaderboard LK.showGameOver(function () { LK.showLeaderboard({ score: totalKills }); }); return; } } // Make it easier at score >= 200: increase bullet bounces if (score >= 200) { for (var i = 0; i < bullets.length; i++) { var b = bullets[i]; if (typeof b.bouncesLeft !== "undefined") { // Give at least 2 extra bounces at 200+ b.bouncesLeft = Math.max(b.bouncesLeft, 3 + Math.floor(score / 100)); } } } // Spawn zombies at intervals, but reduce max zombies as score increases (never limit bullets) var baseMax = 120; var tier = Math.floor(score / 100); var MAX_ZOMBIES = baseMax - tier * 15; if (MAX_ZOMBIES < 10) MAX_ZOMBIES = 10; if (score >= 1200) MAX_ZOMBIES = 10; // Cap zombies to always be less than bullet cap (60 at score >= 209) if (score >= 209) { MAX_ZOMBIES = Math.min(MAX_ZOMBIES, 40); } // Spawn zombies at a normal interval if under cap if (typeof game.lastZombieSpawnTick === "undefined") game.lastZombieSpawnTick = 0; var ZOMBIE_SPAWN_INTERVAL = 30; // spawn every 30 frames (~0.5s at 60fps) if (zombies.length < MAX_ZOMBIES && LK.ticks - game.lastZombieSpawnTick >= ZOMBIE_SPAWN_INTERVAL) { spawnZombie(); game.lastZombieSpawnTick = LK.ticks; } // Fire a bullet every frame if holding down and not game over if (isFiring && !isGameOver) { fireBullet(aimX, aimY); } // No cap on number of bullets in play }; // --- Play sick music track on game start --- LK.playMusic('musicId', { loop: true, fade: { start: 0, end: 1, duration: 1200 } }); // --- Add zoom out button to pause menu --- if (!game.zoomOutBtn) { var zoomOutBtn = new Text2("Zoom Out", { size: 90, fill: 0xFFE066 }); zoomOutBtn.anchor.set(0.5, 0.5); zoomOutBtn.interactive = true; zoomOutBtn.buttonMode = true; zoomOutBtn.x = 0; zoomOutBtn.y = 200; zoomOutBtn.down = function () { // Zoom out by scaling the game container down to 0.7x, min 0.4x, max 1x if (!game._zoomLevel) game._zoomLevel = 1; game._zoomLevel = Math.max(0.4, game._zoomLevel - 0.15); game.scale.x = game._zoomLevel; game.scale.y = game._zoomLevel; }; // Add to pause menu overlay (centered) if (LK.gui.pause && LK.gui.pause.center) { LK.gui.pause.center.addChild(zoomOutBtn); } game.zoomOutBtn = zoomOutBtn; } // Reset state on game restart game.on('reset', function () { // Remove all bullets/zombies for (var i = 0; i < bullets.length; i++) bullets[i].destroy(); for (var j = 0; j < zombies.length; j++) zombies[j].destroy(); bullets = []; zombies = []; score = 0; isGameOver = false; scoreTxt.setText((typeof game._lastResetKill !== "undefined" ? game._lastResetKill : 0) + score); lastScoreTxt.visible = false; // Reset gun upgrades gunLevel = 1; lastUpgradeScore = 0; // Reset gun spin state game.rightClickSpinState = false; if (game.rightClickSpinTween) { tween.kill(game.rightClickSpinTween); game.rightClickSpinTween = null; } gun.rotation = 0; // Reset aim aimX = gun.x; aimY = gun.y - 400; updateAimLine(aimX, aimY); // Add leaderboard button to GUI (top right) if (!game.leaderboardBtn) { var leaderboardBtn = new Text2("🏆", { size: 100, fill: 0xFFE066 }); leaderboardBtn.anchor.set(1, 0); leaderboardBtn.interactive = true; leaderboardBtn.buttonMode = true; leaderboardBtn.x = -40; leaderboardBtn.y = 20; leaderboardBtn.down = function () { LK.showLeaderboard({ score: (typeof game._lastResetKill !== "undefined" ? game._lastResetKill : 0) + score }); }; LK.gui.topRight.addChild(leaderboardBtn); game.leaderboardBtn = leaderboardBtn; } });
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Aiming line class
var AimLine = Container.expand(function () {
var self = Container.call(this);
// Attach aim line asset, anchor at left (0,0.5)
var lineGfx = self.attachAsset('aimLine', {
anchorX: 0,
anchorY: 0.5
});
// Set default scale
lineGfx.scaleY = 0.2; // Thin line
// Set color, etc. if needed
return self;
});
// Bullet class
var Bullet = Container.expand(function () {
var self = Container.call(this);
// Determine bullet tier based on score at spawn
var tier = Math.floor(score / 100);
if (tier >= bulletColors.length) tier = bulletColors.length - 1;
self.tier = tier;
// Attach bullet asset, center anchor, color by tier
var bulletGfx = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
color: bulletColors[tier]
});
// Direction vector (set on spawn)
self.dx = 0;
self.dy = 0;
self.speed = 40 + tier * 2; // px per frame, slight speed bonus per tier
// Damage increases with tier
self.damage = 1 + tier;
self.update = function () {
// Bullet bounce logic: number of bounces = 1 + floor(score/10)
if (typeof self.bouncesLeft === "undefined") {
self.bouncesLeft = 1 + Math.floor(score / 10);
self.lastHitZombies = [];
}
// If bullet just bounced, skip homing for this frame
if (self.justBounced) {
self.justBounced = false;
} else if (zombies && zombies.length > 0) {
// Home in on closest zombie not already hit
var minDist = Infinity;
var closest = null;
for (var i = 0; i < zombies.length; i++) {
var z = zombies[i];
// Don't home to zombies already hit by this bullet
if (self.lastHitZombies.indexOf(z) !== -1) continue;
var zx = z.x - self.x;
var zy = z.y - self.y;
var d = Math.sqrt(zx * zx + zy * zy);
if (d < minDist) {
minDist = d;
closest = z;
}
}
if (closest && minDist > 1) {
// Adjust direction toward closest zombie
var dx = closest.x - self.x;
var dy = closest.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
// Smoothly adjust direction (homing)
var homingStrength = 0.18;
self.dx = (1 - homingStrength) * self.dx + homingStrength * (dx / dist);
self.dy = (1 - homingStrength) * self.dy + homingStrength * (dy / dist);
// Normalize direction
var norm = Math.sqrt(self.dx * self.dx + self.dy * self.dy);
if (norm > 0) {
self.dx /= norm;
self.dy /= norm;
}
}
}
}
// At score >= 210, slow down bullet and instantly bounce to next zombie if possible
if (score >= 210) {
// Slow down bullet
self.speed = Math.max(10, 18 + self.tier * 2);
// Instantly bounce to next zombie if bounces left and zombies available
if (typeof self.bouncesLeft === "undefined") {
self.bouncesLeft = 1 + Math.floor(score / 10);
self.lastHitZombies = [];
}
if (self.bouncesLeft > 0 && zombies && zombies.length > 1) {
// Find next closest zombie not already hit
var minDist = Infinity;
var nextTarget = null;
for (var zz = 0; zz < zombies.length; zz++) {
var z2 = zombies[zz];
if (self.lastHitZombies.indexOf(z2) !== -1) continue;
var zx = z2.x - self.x;
var zy = z2.y - self.y;
var d = Math.sqrt(zx * zx + zy * zy);
if (d < minDist) {
minDist = d;
nextTarget = z2;
}
}
if (nextTarget) {
// Set direction to next zombie
var dx = nextTarget.x - self.x;
var dy = nextTarget.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
self.dx = dx / dist;
self.dy = dy / dist;
self.lastHitZombies.push(nextTarget);
self.bouncesLeft--;
}
}
}
}
self.x += self.dx * self.speed;
self.y += self.dy * self.speed;
};
return self;
});
// Gun/player class
var Gun = Container.expand(function () {
var self = Container.call(this);
// Attach gun asset, center anchor
var gunGfx = self.attachAsset('gun', {
anchorX: 0.5,
anchorY: 0.5
});
// For possible future use: gunGfx
return self;
});
// Zombie class
var Zombie = Container.expand(function () {
var self = Container.call(this);
// Determine zombie tier based on score at spawn
var tier = Math.floor(score / 100);
if (tier >= zombieColors.length) tier = zombieColors.length - 1;
self.tier = tier;
// Attach zombie asset, center anchor, color by tier
var zombieGfx = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5,
color: zombieColors[tier]
});
// Target position (player center)
self.targetX = 0;
self.targetY = 0;
self.speed = 5 + tier * 0.5; // Fixed speed, no randomness
// Health increases with tier
self.maxHealth = 1 + tier * 2;
self.health = self.maxHealth;
self.update = function () {
// Move toward target
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
};
// Take damage method
self.takeDamage = function (dmg) {
self.health -= dmg;
if (self.health <= 0) {
self.destroy();
return true; // killed
}
// Flash on hit
LK.effects.flashObject(self, 0xffffff, 120);
return false; // not dead
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222244 // Changed to a visible dark blue for better contrast
});
/****
* Game Code
****/
// --- Begin: Bouncing kill line logic ---
// --- Begin: 4000 kill reset logic ---
if (typeof game._lastResetKill === "undefined") game._lastResetKill = 0;
if (score - game._lastResetKill >= 4000) {
// Save the current kill count
var totalKills = score;
// Reset all gameplay variables except kill count
// Remove all bullets/zombies
for (var i = 0; i < bullets.length; i++) bullets[i].destroy();
for (var j = 0; j < zombies.length; j++) zombies[j].destroy();
bullets = [];
zombies = [];
// Reset upgrades, gun, aim, etc.
gunLevel = 1;
lastUpgradeScore = 0;
isGameOver = false;
game.rightClickSpinState = false;
if (game.rightClickSpinTween) {
tween.kill(game.rightClickSpinTween);
game.rightClickSpinTween = null;
}
gun.rotation = 0;
aimX = gun.x;
aimY = gun.y - 400;
updateAimLine(aimX, aimY);
// Reset spawn tick
game.lastZombieSpawnTick = LK.ticks;
// Reset score to 1 (kill count stays the same)
score = 1;
scoreTxt.setText(totalKills);
// Mark last reset
game._lastResetKill = totalKills;
// Flash to indicate reset
LK.effects.flashScreen(0x00ffff, 800);
// Optionally, show a message (not required)
}
// --- End: 4000 kill reset logic ---
// Game constants
// Gun (player) asset: a box, centered
// Bullet asset: small ellipse
// Zombie asset: greenish box
// Aiming line: thin yellow box (will be scaled)
var bulletColors = [0xffe066,
// yellow
0x2196f3,
// blue
0xff9800,
// orange
0xe91e63,
// pink
0x9c27b0,
// purple
0xffeb3b,
// yellow2
0x795548,
// brown
0x607d8b,
// gray-blue
0xf44336,
// red
0x00bcd4 // cyan
];
// Zombie class
var zombieColors = [0x4caf50,
// green
0x2196f3,
// blue
0xff9800,
// orange
0xe91e63,
// pink
0x9c27b0,
// purple
0xffeb3b,
// yellow
0x795548,
// brown
0x607d8b,
// gray-blue
0xf44336,
// red
0x00bcd4 // cyan
];
var GAME_W = 2048;
var GAME_H = 2732;
var PLAYER_X = GAME_W / 2;
var PLAYER_Y = GAME_H / 2; // Center of the screen
// Game state
var bullets = [];
var zombies = [];
var score = 0;
var isGameOver = false;
// Gun upgrade state
var gunLevel = 1;
var lastUpgradeScore = 0;
var bulletBaseSpeed = 40;
var aimLineBaseLen = 700;
// Create gun/player
var gun = new Gun();
gun.x = PLAYER_X;
gun.y = PLAYER_Y;
game.addChild(gun);
// Create aiming line
var aimLine = new AimLine();
aimLine.x = gun.x;
aimLine.y = gun.y;
game.addChild(aimLine);
// Score text
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Last score text (shows after game over)
var lastScoreTxt = new Text2('', {
size: 80,
fill: 0xFFE066
});
lastScoreTxt.anchor.set(0.5, 0);
lastScoreTxt.visible = false;
lastScoreTxt.y = 140;
LK.gui.top.addChild(lastScoreTxt);
// Helper: clamp
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Helper: spawn zombie at random edge, aiming at player
function spawnZombie() {
// Determine zombie tier based on score at spawn
var tier = Math.floor(score / 100);
// Deterministic edge: cycle through edges in order (top, bottom, left, right)
if (typeof spawnZombie.edgeIndex === "undefined") spawnZombie.edgeIndex = 0;
var edge = spawnZombie.edgeIndex % 4;
spawnZombie.edgeIndex++;
var x, y;
if (edge === 0) {
// top
x = 200;
y = -80;
} else if (edge === 1) {
// bottom
x = 200;
y = GAME_H + 80;
} else if (edge === 2) {
// left
x = -80;
y = 300;
} else {
// right
x = GAME_W + 80;
y = 300;
}
var z = new Zombie();
z.x = x;
z.y = y;
z.targetX = gun.x;
z.targetY = gun.y;
zombies.push(z);
game.addChild(z);
}
// Helper: fire bullet toward (tx,ty)
function fireBullet(tx, ty) {
// If score >= 20 and there are zombies, auto-aim at closest zombie
var autoAim = score >= 20 && zombies.length > 0;
var dx, dy, dist;
if (autoAim) {
// Find closest zombie to gun
var minDist = Infinity;
var closest = null;
for (var i = 0; i < zombies.length; i++) {
var z = zombies[i];
var zx = z.x - gun.x;
var zy = z.y - gun.y;
var d = Math.sqrt(zx * zx + zy * zy);
if (d < minDist) {
minDist = d;
closest = z;
}
}
if (closest) {
dx = closest.x - gun.x;
dy = closest.y - gun.y;
dist = Math.sqrt(dx * dx + dy * dy);
// If zombie is on top of gun, fallback to normal aim
if (dist < 10) {
dx = tx - gun.x;
dy = ty - gun.y;
dist = Math.sqrt(dx * dx + dy * dy);
}
} else {
dx = tx - gun.x;
dy = ty - gun.y;
dist = Math.sqrt(dx * dx + dy * dy);
}
} else {
dx = tx - gun.x;
dy = ty - gun.y;
dist = Math.sqrt(dx * dx + dy * dy);
}
// Removed 'don't fire if too close' check
dx /= dist;
dy /= dist;
// Only fire a single bullet, but set its bounces based on score
// Bullet cap: max 100 bullets at once
if (bullets.length >= 100) {
return;
}
var angle = Math.atan2(dy, dx);
var b = new Bullet();
b.x = gun.x + Math.cos(angle) * 80; // Start a bit ahead of gun
b.y = gun.y + Math.sin(angle) * 80;
b.dx = Math.cos(angle);
b.dy = Math.sin(angle);
// Upgrade bullet speed, slow down at score >= 210
if (score >= 210) {
b.speed = 18 + (gunLevel - 1) * 2;
} else {
b.speed = bulletBaseSpeed + (gunLevel - 1) * 10;
}
// Track lastX and lastY for despawn logic
b.lastX = b.x;
b.lastY = b.y;
bullets.push(b);
game.addChild(b);
// Play bullet fire sound
LK.getSound('bulletFire').play();
}
// Update aiming line to point from gun to (aimX, aimY)
function updateAimLine(aimX, aimY) {
var dx = aimX - gun.x;
var dy = aimY - gun.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// Clamp min/max length
var minLen = 120,
maxLen = aimLineBaseLen + (gunLevel - 1) * 80;
var len = clamp(dist, minLen, maxLen);
// Set scaleX of line
aimLine.scale.x = len / 20; // asset width is 20
// Set rotation
aimLine.rotation = Math.atan2(dy, dx);
// Make the gun point at the aim direction
gun.rotation = Math.atan2(dy, dx);
}
// Initial aim position: straight up
var aimX = gun.x;
var aimY = gun.y - 400;
updateAimLine(aimX, aimY);
// Touch/mouse move: update aim
game.move = function (x, y, obj) {
// Clamp aim point to inside game area
aimX = clamp(x, 100, GAME_W - 100);
aimY = clamp(y, 100, GAME_H - 100);
updateAimLine(aimX, aimY);
};
// Hold-to-fire state
var isFiring = false;
// Touch/mouse down: start firing
game.down = function (x, y, obj) {
if (isGameOver) return;
// Only fire on left click/tap (event.button==0 or undefined for touch)
if (!obj || !obj.event || obj.event.button === undefined || obj.event.button === 0) {
isFiring = true;
// Fire immediately on down
fireBullet(x, y);
}
};
// Touch/mouse up: stop firing
game.up = function (x, y, obj) {
isFiring = false;
};
// Right click: spin gun and toggle spin state
game.rightClickSpinState = false;
game.rightClickSpinTween = null;
game.rightClick = function (x, y, obj) {
if (isGameOver) return;
// Toggle spin state
game.rightClickSpinState = !game.rightClickSpinState;
// If already spinning, stop previous tween
if (game.rightClickSpinTween) {
tween.kill(game.rightClickSpinTween);
game.rightClickSpinTween = null;
}
if (game.rightClickSpinState) {
// Spin gun 2 full turns (4*PI) over 0.7s
var startRot = gun.rotation;
var endRot = startRot + Math.PI * 4;
game.rightClickSpinTween = tween.to(gun, {
rotation: endRot
}, 700, {
easing: "easeInOutCubic"
});
} else {
// Spin back to original (0) over 0.7s
var curRot = gun.rotation;
// Normalize to [-PI, PI] for shortest path
var normRot = (curRot % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2);
if (normRot > Math.PI) normRot -= Math.PI * 2;
game.rightClickSpinTween = tween.to(gun, {
rotation: 0
}, 700, {
easing: "easeInOutCubic"
});
}
};
// Listen for right click (button==2)
game.on('down', function (x, y, obj) {
if (isGameOver) return;
if (obj && obj.event && obj.event.button === 2) {
game.rightClick(x, y, obj);
}
});
// Prevent context menu on right click
if (typeof window !== "undefined" && window.addEventListener) {
window.addEventListener("contextmenu", function (e) {
e.preventDefault();
});
}
// No drag, so up is not needed
// Game update loop
game.update = function () {
if (isGameOver) {
// Show last score text if available
if (storage.lastScore !== undefined) {
lastScoreTxt.setText("Last: " + storage.lastScore);
lastScoreTxt.visible = true;
}
return;
}
lastScoreTxt.visible = false;
// --- Begin: Bouncing kill line logic ---
// Remove all previous aim lines
if (!game.bounceLines) game.bounceLines = [];
for (var li = 0; li < game.bounceLines.length; li++) {
game.bounceLines[li].destroy();
}
game.bounceLines = [];
// For each bullet, create a visible line that bounces from zombie to zombie, killing all zombies in the path
for (var i = bullets.length - 1; i >= 0; i--) {
var b = bullets[i];
// Make bullet invisible
if (b.children && b.children.length > 0) {
for (var ci = 0; ci < b.children.length; ci++) {
b.children[ci].alpha = 0;
}
}
// Track previous position for despawn logic
if (typeof b.lastX === "undefined") b.lastX = b.x;
if (typeof b.lastY === "undefined") b.lastY = b.y;
// Build the bounce path: start at bullet, bounce to closest zombie not yet hit, repeat up to bouncesLeft, then to screen edge
var pathPoints = [{
x: b.x,
y: b.y
}];
var hitZombies = [];
var curX = b.x,
curY = b.y;
var bounces = typeof b.bouncesLeft !== "undefined" ? b.bouncesLeft : 1 + Math.floor(score / 10);
var zombiesLeft = zombies.slice();
// Remove zombies already hit by this bullet
if (b.lastHitZombies && b.lastHitZombies.length) {
for (var lhz = 0; lhz < b.lastHitZombies.length; lhz++) {
var idx = zombiesLeft.indexOf(b.lastHitZombies[lhz]);
if (idx !== -1) zombiesLeft.splice(idx, 1);
}
}
// Build bounce path
for (var bounce = 0; bounce < bounces; bounce++) {
// Find closest zombie not yet hit
var minDist = Infinity,
closest = null,
closestIdx = -1;
for (var zi = 0; zi < zombiesLeft.length; zi++) {
var z = zombiesLeft[zi];
var dx = z.x - curX;
var dy = z.y - curY;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) {
minDist = d;
closest = z;
closestIdx = zi;
}
}
if (closest) {
pathPoints.push({
x: closest.x,
y: closest.y
});
hitZombies.push(closest);
curX = closest.x;
curY = closest.y;
zombiesLeft.splice(closestIdx, 1);
} else {
break;
}
}
// After last bounce, extend line to screen edge in the same direction
var lastPt = pathPoints[pathPoints.length - 1];
var prevPt = pathPoints.length > 1 ? pathPoints[pathPoints.length - 2] : {
x: b.x,
y: b.y
};
var dx = lastPt.x - prevPt.x;
var dy = lastPt.y - prevPt.y;
var norm = Math.sqrt(dx * dx + dy * dy);
if (norm === 0) {
dx = 1;
dy = 0;
norm = 1;
}
dx /= norm;
dy /= norm;
// Find intersection with screen edge
var tMax = 99999;
var tx = dx > 0 ? (GAME_W - lastPt.x) / dx : dx < 0 ? (0 - lastPt.x) / dx : tMax;
var ty = dy > 0 ? (GAME_H - lastPt.y) / dy : dy < 0 ? (0 - lastPt.y) / dy : tMax;
var t = Math.min(Math.abs(tx), Math.abs(ty));
var edgeX = lastPt.x + dx * t;
var edgeY = lastPt.y + dy * t;
// Clamp to screen
edgeX = clamp(edgeX, 0, GAME_W);
edgeY = clamp(edgeY, 0, GAME_H);
pathPoints.push({
x: edgeX,
y: edgeY
});
// Draw the visible bouncing line segments
for (var pi = 0; pi < pathPoints.length - 1; pi++) {
var p0 = pathPoints[pi];
var p1 = pathPoints[pi + 1];
var segLen = Math.sqrt((p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y));
if (segLen < 1) continue;
var line = new AimLine();
line.x = p0.x;
line.y = p0.y;
line.scale.x = segLen / 20;
line.scale.y = 0.18;
line.rotation = Math.atan2(p1.y - p0.y, p1.x - p0.x);
line.alpha = 1;
game.addChild(line);
game.bounceLines.push(line);
}
// Kill all zombies that intersect any segment of the line (except already killed this frame)
var killedZombies = [];
for (var pi = 0; pi < pathPoints.length - 1; pi++) {
var p0 = pathPoints[pi];
var p1 = pathPoints[pi + 1];
var segDx = p1.x - p0.x;
var segDy = p1.y - p0.y;
var segLen = Math.sqrt(segDx * segDx + segDy * segDy);
if (segLen < 1) continue;
for (var j = zombies.length - 1; j >= 0; j--) {
var z = zombies[j];
// Don't double-kill
if (killedZombies.indexOf(z) !== -1) continue;
// Distance from zombie center to segment
var zx = z.x,
zy = z.y;
var t = ((zx - p0.x) * segDx + (zy - p0.y) * segDy) / (segLen * segLen);
t = Math.max(0, Math.min(1, t));
var projX = p0.x + t * segDx;
var projY = p0.y + t * segDy;
var distToLine = Math.sqrt((zx - projX) * (zx - projX) + (zy - projY) * (zy - projY));
// Use zombie's width as hit radius
var hitRadius = 60;
if (distToLine <= hitRadius) {
// Kill zombie
var killed = false;
if (typeof z.takeDamage === "function") {
killed = z.takeDamage(9999);
} else {
z.destroy();
killed = true;
}
if (killed) {
zombies.splice(j, 1);
killedZombies.push(z);
// Play zombie death sound
LK.getSound('zombieDeath').play();
score += 1;
scoreTxt.setText((typeof game._lastResetKill !== "undefined" ? game._lastResetKill : 0) + score);
if (score > 0 && score % 5 === 0 && score !== lastUpgradeScore) {
gunLevel += 1;
lastUpgradeScore = score;
LK.effects.flashObject(gun, 0x3399ff, 500);
}
LK.effects.flashObject(gun, 0x00ff00, 200);
}
}
}
}
// Remove bullet after line is drawn and zombies are killed
b.destroy();
bullets.splice(i, 1);
}
// --- End: Bouncing kill line logic ---
// Update all zombies (no artificial cap that breaks at high scores)
for (var k = zombies.length - 1; k >= 0; k--) {
var z = zombies[k];
// Make it easier at score >= 200: reduce zombie speed
if (score >= 200) {
z.speed = Math.max(2, z.speed * 0.7);
}
// Only update if zombie is on screen (with margin)
if (z.x > -200 && z.x < GAME_W + 200 && z.y > -200 && z.y < GAME_H + 200) {
z.update();
}
// Check if zombie reached player (gun)
if (z.intersects(gun)) {
// Game over
isGameOver = true;
// Save score to storage as lastScore and bestScore
var totalKills = (typeof game._lastResetKill !== "undefined" ? game._lastResetKill : 0) + score;
storage.lastScore = totalKills;
if (!storage.bestScore || totalKills > storage.bestScore) {
storage.bestScore = totalKills;
}
LK.effects.flashScreen(0xff0000, 1000);
// Show game over and then leaderboard
LK.showGameOver(function () {
LK.showLeaderboard({
score: totalKills
});
});
return;
}
}
// Make it easier at score >= 200: increase bullet bounces
if (score >= 200) {
for (var i = 0; i < bullets.length; i++) {
var b = bullets[i];
if (typeof b.bouncesLeft !== "undefined") {
// Give at least 2 extra bounces at 200+
b.bouncesLeft = Math.max(b.bouncesLeft, 3 + Math.floor(score / 100));
}
}
}
// Spawn zombies at intervals, but reduce max zombies as score increases (never limit bullets)
var baseMax = 120;
var tier = Math.floor(score / 100);
var MAX_ZOMBIES = baseMax - tier * 15;
if (MAX_ZOMBIES < 10) MAX_ZOMBIES = 10;
if (score >= 1200) MAX_ZOMBIES = 10;
// Cap zombies to always be less than bullet cap (60 at score >= 209)
if (score >= 209) {
MAX_ZOMBIES = Math.min(MAX_ZOMBIES, 40);
}
// Spawn zombies at a normal interval if under cap
if (typeof game.lastZombieSpawnTick === "undefined") game.lastZombieSpawnTick = 0;
var ZOMBIE_SPAWN_INTERVAL = 30; // spawn every 30 frames (~0.5s at 60fps)
if (zombies.length < MAX_ZOMBIES && LK.ticks - game.lastZombieSpawnTick >= ZOMBIE_SPAWN_INTERVAL) {
spawnZombie();
game.lastZombieSpawnTick = LK.ticks;
}
// Fire a bullet every frame if holding down and not game over
if (isFiring && !isGameOver) {
fireBullet(aimX, aimY);
}
// No cap on number of bullets in play
};
// --- Play sick music track on game start ---
LK.playMusic('musicId', {
loop: true,
fade: {
start: 0,
end: 1,
duration: 1200
}
});
// --- Add zoom out button to pause menu ---
if (!game.zoomOutBtn) {
var zoomOutBtn = new Text2("Zoom Out", {
size: 90,
fill: 0xFFE066
});
zoomOutBtn.anchor.set(0.5, 0.5);
zoomOutBtn.interactive = true;
zoomOutBtn.buttonMode = true;
zoomOutBtn.x = 0;
zoomOutBtn.y = 200;
zoomOutBtn.down = function () {
// Zoom out by scaling the game container down to 0.7x, min 0.4x, max 1x
if (!game._zoomLevel) game._zoomLevel = 1;
game._zoomLevel = Math.max(0.4, game._zoomLevel - 0.15);
game.scale.x = game._zoomLevel;
game.scale.y = game._zoomLevel;
};
// Add to pause menu overlay (centered)
if (LK.gui.pause && LK.gui.pause.center) {
LK.gui.pause.center.addChild(zoomOutBtn);
}
game.zoomOutBtn = zoomOutBtn;
}
// Reset state on game restart
game.on('reset', function () {
// Remove all bullets/zombies
for (var i = 0; i < bullets.length; i++) bullets[i].destroy();
for (var j = 0; j < zombies.length; j++) zombies[j].destroy();
bullets = [];
zombies = [];
score = 0;
isGameOver = false;
scoreTxt.setText((typeof game._lastResetKill !== "undefined" ? game._lastResetKill : 0) + score);
lastScoreTxt.visible = false;
// Reset gun upgrades
gunLevel = 1;
lastUpgradeScore = 0;
// Reset gun spin state
game.rightClickSpinState = false;
if (game.rightClickSpinTween) {
tween.kill(game.rightClickSpinTween);
game.rightClickSpinTween = null;
}
gun.rotation = 0;
// Reset aim
aimX = gun.x;
aimY = gun.y - 400;
updateAimLine(aimX, aimY);
// Add leaderboard button to GUI (top right)
if (!game.leaderboardBtn) {
var leaderboardBtn = new Text2("🏆", {
size: 100,
fill: 0xFFE066
});
leaderboardBtn.anchor.set(1, 0);
leaderboardBtn.interactive = true;
leaderboardBtn.buttonMode = true;
leaderboardBtn.x = -40;
leaderboardBtn.y = 20;
leaderboardBtn.down = function () {
LK.showLeaderboard({
score: (typeof game._lastResetKill !== "undefined" ? game._lastResetKill : 0) + score
});
};
LK.gui.topRight.addChild(leaderboardBtn);
game.leaderboardBtn = leaderboardBtn;
}
});