/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // LaserLine: a segment between two points, visually a rotated rectangle (was NeonLine) var LaserLine = Container.expand(function () { var self = Container.call(this); // Set up endpoints self.x1 = 0; self.y1 = 0; self.x2 = 0; self.y2 = 0; // The visual line asset var lineAsset = self.attachAsset('laserLine', { anchorX: 0.5, anchorY: 0.5 }); // Set endpoints and update position/rotation/length self.setEndpoints = function (x1, y1, x2, y2) { self.x1 = x1; self.y1 = y1; self.x2 = x2; self.y2 = y2; // Center of the line self.x = (x1 + x2) / 2; self.y = (y1 + y2) / 2; // Angle var dx = x2 - x1; var dy = y2 - y1; var len = Math.sqrt(dx * dx + dy * dy); self.rotation = Math.atan2(dy, dx); // Set length lineAsset.width = len; // Simulate glow by alpha pulse lineAsset.alpha = 0.85 + 0.15 * Math.sin(LK.ticks / 8); }; // For update pulse self.update = function () { // Animate glow var lineAsset = self.children[0]; lineAsset.alpha = 0.85 + 0.15 * Math.sin(LK.ticks / 8 + self.x * 0.01 + self.y * 0.01); }; return self; }); // Police: the starting node for laser lines (was PowerSource) var Police = Container.expand(function () { var self = Container.call(this); var policeAsset = self.attachAsset('police', { anchorX: 0.5, anchorY: 0.5 }); // Pulse effect self.update = function () { policeAsset.alpha = 0.95 + 0.05 * Math.sin(LK.ticks / 8); }; return self; }); // Thief: a thief to catch (was Device) var Thief = Container.expand(function () { var self = Container.call(this); var thiefAsset = self.attachAsset('thief', { anchorX: 0.5, anchorY: 0.5 }); // Caught state self.caught = false; self.energy = 1; // 1 = not caught, 0 = caught // For pulsing effect when caught self.update = function () { if (self.caught) { thiefAsset.alpha = 0.95 + 0.05 * Math.sin(LK.ticks / 6 + self.x * 0.01); } else { thiefAsset.alpha = 1.0; } }; // Flash when caught self.flash = function () { tween(thiefAsset, { tint: 0xffffff }, { duration: 120, onFinish: function onFinish() { tween(thiefAsset, { tint: 0x222222 }, { duration: 200 }); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ name: "Shoot The Thiefs", backgroundColor: 0x0a0a1a }); /**** * Game Code ****/ // --- Prison background setup --- var prisonBg = LK.getAsset('prisonBg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: GAME_WIDTH, height: GAME_HEIGHT }); game.addChild(prisonBg); // --- UI Setup --- // Police: blue // Laser/rope: police blue // Thief: dark/gray // Thief-Police theme: 'device' is now 'thief', 'powerSource' is 'police', neonLine is a rope/laser // Tutorial button // Police node (was power source) // Thief node (was device) // Laser line segment (was neon line) // --- Game constants --- var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; var LEVELS = [ // Each level: {thiefCount, positions: [ {x, y}, ... ] } { thiefCount: 3, positions: [{ x: 400, y: 600 }, { x: GAME_WIDTH - 400, y: 600 }, { x: GAME_WIDTH / 2, y: 300 }] }, { thiefCount: 4, positions: [{ x: 400, y: 600 }, { x: GAME_WIDTH - 400, y: 600 }, { x: 400, y: 1400 }, { x: GAME_WIDTH - 400, y: 1400 }] }, { thiefCount: 5, positions: [{ x: 400, y: 600 }, { x: GAME_WIDTH - 400, y: 600 }, { x: 400, y: 1400 }, { x: GAME_WIDTH - 400, y: 1400 }, { x: GAME_WIDTH / 2, y: 300 }] }, { thiefCount: 6, positions: [{ x: 400, y: 600 }, { x: GAME_WIDTH - 400, y: 600 }, { x: 400, y: 1400 }, { x: GAME_WIDTH - 400, y: 1400 }, { x: GAME_WIDTH / 2, y: 300 }, { x: GAME_WIDTH / 2, y: 1000 }] }, { thiefCount: 7, positions: [{ x: 400, y: 600 }, { x: GAME_WIDTH - 400, y: 600 }, { x: 400, y: 1400 }, { x: GAME_WIDTH - 400, y: 1400 }, { x: GAME_WIDTH / 2, y: 300 }, { x: GAME_WIDTH / 2, y: 1000 }, { x: GAME_WIDTH / 2, y: 1800 }] }]; var currentLevel = 0; var POWER_DRAIN_PER_PIXEL = 0.00012; // Power cost per pixel of line var POWER_REGEN_PER_TICK = 0.0002; // Power regen per tick (if desired) var THIEF_ENERGY_LOSS_PER_TICK = 0.0012; // How fast thieves escape var THIEF_CATCH_RADIUS = 100; // px, how close a line endpoint must be to catch var POWER_MIN = 0; var POWER_MAX = 1; // --- Game state --- var power = 1; // Shared power meter (0-1) var thieves = []; var lines = []; var police = null; var drawing = false; var drawStart = null; // {x, y} var drawLine = null; // LaserLine being drawn var drawFromPolice = false; var caughtThieves = 0; var tutorialOpen = false; // --- Timer state --- var TIMER_START = 13; // seconds var timer = TIMER_START; var timerInterval = null; var timerText = null; var timerEnded = false; // --- UI elements --- var powerBarBg = null; var powerBarFg = null; var tutorialBtn = null; var tutorialText = null; var winText = null; var levelText = null; // Level counter UI // --- Helper functions --- // Clamp value between min and max function clamp(val, min, max) { return Math.max(min, Math.min(max, val)); } // Distance between two points function dist(x1, y1, x2, y2) { var dx = x2 - x1; var dy = y2 - y1; return Math.sqrt(dx * dx + dy * dy); } // Check if a point is inside a device (for connecting) function pointInDevice(x, y, device) { var dx = x - device.x; var dy = y - device.y; var r = device.children[0].width / 2; return dx * dx + dy * dy <= r * r; } // --- UI Setup --- // Power bar (top center, not in top left 100x100) powerBarBg = new Container(); var bgBar = LK.getAsset('neonLine', { width: 600, height: 36, anchorX: 0, anchorY: 0.5 }); bgBar.tint = 0x222244; powerBarBg.addChild(bgBar); powerBarFg = LK.getAsset('neonLine', { width: 600, height: 36, anchorX: 0, anchorY: 0.5 }); powerBarFg.tint = 0x00fff7; powerBarBg.addChild(powerBarFg); powerBarBg.x = (LK.gui.width - 600) / 2; powerBarBg.y = 110; LK.gui.top.addChild(powerBarBg); // Level counter (top right, above tutorial button) levelText = new Text2("Level " + (currentLevel + 1), { size: 70, fill: 0x000000, align: "right" }); levelText.anchor.set(1, 0); // anchor to right top levelText.x = LK.gui.width - 40; // 40px from right edge, avoids top right crowding levelText.y = 10; // just below the top edge, but not in top left 100x100 LK.gui.top.addChild(levelText); // Timer UI (bottom left) timerText = new Text2("01:00", { size: 80, fill: 0x112266, align: "center" }); timerText.anchor.set(0, 1); // left aligned, bottom edge timerText.x = 60; timerText.y = LK.gui.height - 60; if (timerText && !timerText.parent) { LK.gui.addChild(timerText); } // Tutorial button (top right, not in top left) tutorialBtn = LK.getAsset('tutorialBtn', { anchorX: 0.5, anchorY: 0.5 }); tutorialBtn.x = LK.gui.width - 120; tutorialBtn.y = 110; LK.gui.top.addChild(tutorialBtn); var tutorialBtnText = new Text2('?', { size: 60, fill: 0xFFFFFF }); tutorialBtnText.anchor.set(0.5, 0.5); tutorialBtnText.x = tutorialBtn.x; tutorialBtnText.y = tutorialBtn.y; LK.gui.top.addChild(tutorialBtnText); // Tutorial popup (hidden by default) tutorialText = new Text2("How to Play:\n\n" + "1. Draw blue police laser lines from the police (bottom) to catch all the thieves (gray circles).\n" + "2. Every laser drains the police's energy meter at the top.\n" + "3. Catch all thieves before the police run out of energy!\n" + "4. Lasers must touch a thief to catch them.\n\n" + "Tip: Plan your path to use as little energy as possible.\n\n" + "Tap the '?' button anytime for help.", { size: 70, fill: 0x1976d2, align: "center", wordWrap: true, wordWrapWidth: 1200 }); tutorialText.anchor.set(0.5, 0.5); tutorialText.x = LK.gui.width / 2; tutorialText.y = LK.gui.height / 2; tutorialText.visible = false; LK.gui.center.addChild(tutorialText); // Win text (hidden by default) // (Removed lower right corner text as requested) // --- Game Setup --- // Place police (bottom center) police = new Police(); police.x = GAME_WIDTH / 2; police.y = GAME_HEIGHT - 300; game.addChild(police); // Start timer countdown function updateTimerText() { var min = Math.floor(timer / 60); var sec = Math.floor(timer % 60); var str = (min < 10 ? "0" : "") + min + ":" + (sec < 10 ? "0" : "") + sec; if (timerText) timerText.setText(str); } updateTimerText(); if (timerInterval) { LK.clearInterval(timerInterval); } timerEnded = false; timer = TIMER_START; timerInterval = LK.setInterval(function () { if (tutorialOpen || winText && winText.visible) return; if (timerEnded) return; timer--; if (timer < 0) timer = 0; updateTimerText(); if (timer === 0 && !timerEnded) { timerEnded = true; LK.effects.flashScreen(0xff0033, 1200); if (timerText && timerText.parent) { timerText.parent.removeChild(timerText); timerText = null; } LK.setTimeout(function () { LK.showGameOver(); }, 1200); } }, 1000); // Place thieves for the current level, randomizing positions if needed function shufflePositions(arr) { // Fisher-Yates shuffle for (var i = arr.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } return arr; } function setupLevel(levelIdx) { // Remove old thieves for (var i = 0; i < thieves.length; i++) { if (thieves[i].parent) thieves[i].parent.removeChild(thieves[i]); } // Remove all laserLine assets from previous level for (var i = 0; i < lines.length; i++) { if (lines[i] && lines[i].parent) lines[i].parent.removeChild(lines[i]); } thieves = []; lines = []; caughtThieves = 0; power = 1; var level = LEVELS[levelIdx]; var positions = []; // If there are more thieves than positions, generate random positions if (level.positions.length >= level.thiefCount) { // Shuffle and pick first N positions = shufflePositions(level.positions.slice()).slice(0, level.thiefCount); } else { // Use all positions, and add random ones for the rest positions = level.positions.slice(); while (positions.length < level.thiefCount) { positions.push({ x: 200 + Math.random() * (GAME_WIDTH - 400), y: 300 + Math.random() * (GAME_HEIGHT - 1000) }); } } for (var i = 0; i < level.thiefCount; i++) { var t = new Thief(); t.x = positions[i].x; t.y = positions[i].y; thieves.push(t); game.addChild(t); } } setupLevel(currentLevel); if (levelText) { levelText.setText("Level " + (currentLevel + 1)); } // --- Drawing logic --- // Helper: find thief at (x, y) function findThiefAt(x, y) { for (var i = 0; i < thieves.length; i++) { if (pointInDevice(x, y, thieves[i])) return thieves[i]; } return null; } // Helper: check if a line endpoint is close enough to a thief to catch function thiefInRadius(x, y) { for (var i = 0; i < thieves.length; i++) { if (!thieves[i].caught && dist(x, y, thieves[i].x, thieves[i].y) < THIEF_CATCH_RADIUS) { return thieves[i]; } } return null; } // Helper: check if a point is close to the police function pointNearPolice(x, y) { var dx = x - police.x; var dy = y - police.y; var r = police.children[0].width / 2 + 30; return dx * dx + dy * dy <= r * r; } // --- Input handlers --- // Only allow drawing if not in tutorial or win function canDraw() { // Block all game input if tutorial is open or win text is visible return !tutorialOpen && (!winText || !winText.visible) && power > 0; } // Start drawing: must start from police or a caught thief game.down = function (x, y, obj) { if (tutorialOpen) return; if (!canDraw()) return; // Convert to game coordinates var gx = x, gy = y; // Start from police? if (pointNearPolice(gx, gy)) { drawFromPolice = true; drawStart = { x: police.x, y: police.y }; } else { // Start from caught thief? for (var i = 0; i < thieves.length; i++) { if (thieves[i].caught && pointInDevice(gx, gy, thieves[i])) { drawFromPolice = false; drawStart = { x: thieves[i].x, y: thieves[i].y }; break; } } } if (drawStart) { drawing = true; drawLine = new LaserLine(); drawLine.setEndpoints(drawStart.x, drawStart.y, gx, gy); game.addChild(drawLine); } }; // Drawing in progress game.move = function (x, y, obj) { if (tutorialOpen) return; if (!drawing || !drawLine) return; if (!canDraw()) return; var gx = x, gy = y; drawLine.setEndpoints(drawStart.x, drawStart.y, gx, gy); // Show line in real time, but don't finalize until up }; // Finish drawing: if endpoint is on an uncaught thief, connect and catch game.up = function (x, y, obj) { if (tutorialOpen) return; if (!drawing || !drawLine) { drawing = false; drawStart = null; return; } if (!canDraw()) { // Remove preview line if (drawLine) { drawLine.destroy(); drawLine = null; } drawing = false; drawStart = null; return; } var gx = x, gy = y; drawLine.setEndpoints(drawStart.x, drawStart.y, gx, gy); // Check if endpoint is close to an uncaught thief var targetThief = thiefInRadius(gx, gy); // Only allow connecting to uncaught thief if (targetThief) { // Calculate line length var len = dist(drawStart.x, drawStart.y, targetThief.x, targetThief.y); var cost = len * POWER_DRAIN_PER_PIXEL; if (power >= cost) { // Snap endpoint to thief center drawLine.setEndpoints(drawStart.x, drawStart.y, targetThief.x, targetThief.y); lines.push(drawLine); // Drain power power = clamp(power - cost, POWER_MIN, POWER_MAX); // Catch thief targetThief.caught = true; targetThief.energy = 1; targetThief.flash(); caughtThieves++; // Win condition if (caughtThieves === thieves.length) { if (timerInterval) { LK.clearInterval(timerInterval); timerInterval = null; } if (winText) winText.visible = true; if (timerText && timerText.parent) { timerText.parent.removeChild(timerText); timerText = null; } LK.effects.flashScreen(0x1976d2, 1200); LK.setTimeout(function () { // Advance to next level if available, else show win if (currentLevel < LEVELS.length - 1) { // Remove timerText when moving to next level if (timerText && timerText.parent) { timerText.parent.removeChild(timerText); timerText = null; } if (timerInterval) { LK.clearInterval(timerInterval); timerInterval = null; } currentLevel++; setupLevel(currentLevel); // Update level counter if (levelText) { levelText.setText("Level " + (currentLevel + 1)); } // Reset timer and UI timer = TIMER_START; timerEnded = false; // Add timerText when next level starts if (!timerText) { timerText = new Text2("01:00", { size: 80, fill: 0x112266, align: "center" }); timerText.anchor.set(0, 1); timerText.x = 60; timerText.y = LK.gui.height - 60; LK.gui.addChild(timerText); } updateTimerText(); if (timerInterval) LK.clearInterval(timerInterval); timerInterval = LK.setInterval(function () { if (tutorialOpen || winText && winText.visible) return; if (timerEnded) return; timer--; if (timer < 0) timer = 0; updateTimerText(); if (timer === 0 && !timerEnded) { timerEnded = true; LK.effects.flashScreen(0xff0033, 1200); if (timerText && timerText.parent) { timerText.parent.removeChild(timerText); timerText = null; } LK.setTimeout(function () { LK.showGameOver(); }, 1200); } }, 1000); } else { LK.showYouWin(); } }, 1800); } drawLine = null; drawing = false; drawStart = null; return; } else { // Not enough power, flash bar LK.effects.flashObject(powerBarFg, 0xff0000, 400); } } // Not a valid connection, remove preview line if (drawLine) { drawLine.destroy(); drawLine = null; } drawing = false; drawStart = null; }; // --- Tutorial button handler --- tutorialBtn.down = function (x, y, obj) { if (tutorialOpen) { tutorialText.visible = false; tutorialOpen = false; } else { tutorialText.visible = true; tutorialOpen = true; } }; tutorialBtnText.down = tutorialBtn.down; // Allow tap anywhere on tutorialText to close it tutorialText.down = function (x, y, obj) { if (tutorialOpen) { tutorialText.visible = false; tutorialOpen = false; } }; // --- Game update loop --- game.update = function () { // Pause timer if tutorial or win is open if ((tutorialOpen || winText && winText.visible) && timerInterval) { LK.clearInterval(timerInterval); timerInterval = null; } // Resume timer if not paused and not ended if (!tutorialOpen && (!winText || !winText.visible) && !timerEnded && !timerInterval) { if (!timerText) { timerText = new Text2("01:00", { size: 80, fill: 0x112266, align: "center" }); timerText.anchor.set(0, 1); timerText.x = 60; timerText.y = LK.gui.height - 60; LK.gui.addChild(timerText); updateTimerText(); } timerInterval = LK.setInterval(function () { if (tutorialOpen || winText && winText.visible) return; if (timerEnded) return; timer--; if (timer < 0) timer = 0; if (timerText) { var min = Math.floor(timer / 60); var sec = Math.floor(timer % 60); var str = (min < 10 ? "0" : "") + min + ":" + (sec < 10 ? "0" : "") + sec; timerText.setText(str); } if (timer === 0 && !timerEnded) { timerEnded = true; LK.effects.flashScreen(0xff0033, 1200); LK.setTimeout(function () { LK.showGameOver(); }, 1200); } }, 1000); } // Update power bar var barW = 600 * power; powerBarFg.width = barW; if (power < 0.18) { powerBarFg.tint = 0xff0033; } else { powerBarFg.tint = 0x00fff7; } // Thieves lose energy if not caught for (var i = 0; i < thieves.length; i++) { var t = thieves[i]; if (!t.caught) { t.energy = clamp(t.energy - THIEF_ENERGY_LOSS_PER_TICK, 0, 1); // If energy runs out, game over (thief escapes) if (t.energy <= 0) { if (timerInterval) { LK.clearInterval(timerInterval); timerInterval = null; } LK.effects.flashScreen(0xff0033, 1200); LK.setTimeout(function () { if (timerText && timerText.parent) { timerText.parent.removeChild(timerText); timerText = null; } LK.showGameOver(); }, 1200); return; } } } // Power regen (optional, comment out to disable) // power = clamp(power + POWER_REGEN_PER_TICK, POWER_MIN, POWER_MAX); // Animate lines for (var i = 0; i < lines.length; i++) { if (lines[i].update) lines[i].update(); } // Animate thieves and police for (var i = 0; i < thieves.length; i++) { if (thieves[i].update) thieves[i].update(); } if (police && police.update) police.update(); if (drawLine && drawLine.update) drawLine.update(); }; // --- End of file ---
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// LaserLine: a segment between two points, visually a rotated rectangle (was NeonLine)
var LaserLine = Container.expand(function () {
var self = Container.call(this);
// Set up endpoints
self.x1 = 0;
self.y1 = 0;
self.x2 = 0;
self.y2 = 0;
// The visual line asset
var lineAsset = self.attachAsset('laserLine', {
anchorX: 0.5,
anchorY: 0.5
});
// Set endpoints and update position/rotation/length
self.setEndpoints = function (x1, y1, x2, y2) {
self.x1 = x1;
self.y1 = y1;
self.x2 = x2;
self.y2 = y2;
// Center of the line
self.x = (x1 + x2) / 2;
self.y = (y1 + y2) / 2;
// Angle
var dx = x2 - x1;
var dy = y2 - y1;
var len = Math.sqrt(dx * dx + dy * dy);
self.rotation = Math.atan2(dy, dx);
// Set length
lineAsset.width = len;
// Simulate glow by alpha pulse
lineAsset.alpha = 0.85 + 0.15 * Math.sin(LK.ticks / 8);
};
// For update pulse
self.update = function () {
// Animate glow
var lineAsset = self.children[0];
lineAsset.alpha = 0.85 + 0.15 * Math.sin(LK.ticks / 8 + self.x * 0.01 + self.y * 0.01);
};
return self;
});
// Police: the starting node for laser lines (was PowerSource)
var Police = Container.expand(function () {
var self = Container.call(this);
var policeAsset = self.attachAsset('police', {
anchorX: 0.5,
anchorY: 0.5
});
// Pulse effect
self.update = function () {
policeAsset.alpha = 0.95 + 0.05 * Math.sin(LK.ticks / 8);
};
return self;
});
// Thief: a thief to catch (was Device)
var Thief = Container.expand(function () {
var self = Container.call(this);
var thiefAsset = self.attachAsset('thief', {
anchorX: 0.5,
anchorY: 0.5
});
// Caught state
self.caught = false;
self.energy = 1; // 1 = not caught, 0 = caught
// For pulsing effect when caught
self.update = function () {
if (self.caught) {
thiefAsset.alpha = 0.95 + 0.05 * Math.sin(LK.ticks / 6 + self.x * 0.01);
} else {
thiefAsset.alpha = 1.0;
}
};
// Flash when caught
self.flash = function () {
tween(thiefAsset, {
tint: 0xffffff
}, {
duration: 120,
onFinish: function onFinish() {
tween(thiefAsset, {
tint: 0x222222
}, {
duration: 200
});
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
name: "Shoot The Thiefs",
backgroundColor: 0x0a0a1a
});
/****
* Game Code
****/
// --- Prison background setup ---
var prisonBg = LK.getAsset('prisonBg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: GAME_WIDTH,
height: GAME_HEIGHT
});
game.addChild(prisonBg);
// --- UI Setup ---
// Police: blue
// Laser/rope: police blue
// Thief: dark/gray
// Thief-Police theme: 'device' is now 'thief', 'powerSource' is 'police', neonLine is a rope/laser
// Tutorial button
// Police node (was power source)
// Thief node (was device)
// Laser line segment (was neon line)
// --- Game constants ---
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var LEVELS = [
// Each level: {thiefCount, positions: [ {x, y}, ... ] }
{
thiefCount: 3,
positions: [{
x: 400,
y: 600
}, {
x: GAME_WIDTH - 400,
y: 600
}, {
x: GAME_WIDTH / 2,
y: 300
}]
}, {
thiefCount: 4,
positions: [{
x: 400,
y: 600
}, {
x: GAME_WIDTH - 400,
y: 600
}, {
x: 400,
y: 1400
}, {
x: GAME_WIDTH - 400,
y: 1400
}]
}, {
thiefCount: 5,
positions: [{
x: 400,
y: 600
}, {
x: GAME_WIDTH - 400,
y: 600
}, {
x: 400,
y: 1400
}, {
x: GAME_WIDTH - 400,
y: 1400
}, {
x: GAME_WIDTH / 2,
y: 300
}]
}, {
thiefCount: 6,
positions: [{
x: 400,
y: 600
}, {
x: GAME_WIDTH - 400,
y: 600
}, {
x: 400,
y: 1400
}, {
x: GAME_WIDTH - 400,
y: 1400
}, {
x: GAME_WIDTH / 2,
y: 300
}, {
x: GAME_WIDTH / 2,
y: 1000
}]
}, {
thiefCount: 7,
positions: [{
x: 400,
y: 600
}, {
x: GAME_WIDTH - 400,
y: 600
}, {
x: 400,
y: 1400
}, {
x: GAME_WIDTH - 400,
y: 1400
}, {
x: GAME_WIDTH / 2,
y: 300
}, {
x: GAME_WIDTH / 2,
y: 1000
}, {
x: GAME_WIDTH / 2,
y: 1800
}]
}];
var currentLevel = 0;
var POWER_DRAIN_PER_PIXEL = 0.00012; // Power cost per pixel of line
var POWER_REGEN_PER_TICK = 0.0002; // Power regen per tick (if desired)
var THIEF_ENERGY_LOSS_PER_TICK = 0.0012; // How fast thieves escape
var THIEF_CATCH_RADIUS = 100; // px, how close a line endpoint must be to catch
var POWER_MIN = 0;
var POWER_MAX = 1;
// --- Game state ---
var power = 1; // Shared power meter (0-1)
var thieves = [];
var lines = [];
var police = null;
var drawing = false;
var drawStart = null; // {x, y}
var drawLine = null; // LaserLine being drawn
var drawFromPolice = false;
var caughtThieves = 0;
var tutorialOpen = false;
// --- Timer state ---
var TIMER_START = 13; // seconds
var timer = TIMER_START;
var timerInterval = null;
var timerText = null;
var timerEnded = false;
// --- UI elements ---
var powerBarBg = null;
var powerBarFg = null;
var tutorialBtn = null;
var tutorialText = null;
var winText = null;
var levelText = null; // Level counter UI
// --- Helper functions ---
// Clamp value between min and max
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Distance between two points
function dist(x1, y1, x2, y2) {
var dx = x2 - x1;
var dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
// Check if a point is inside a device (for connecting)
function pointInDevice(x, y, device) {
var dx = x - device.x;
var dy = y - device.y;
var r = device.children[0].width / 2;
return dx * dx + dy * dy <= r * r;
}
// --- UI Setup ---
// Power bar (top center, not in top left 100x100)
powerBarBg = new Container();
var bgBar = LK.getAsset('neonLine', {
width: 600,
height: 36,
anchorX: 0,
anchorY: 0.5
});
bgBar.tint = 0x222244;
powerBarBg.addChild(bgBar);
powerBarFg = LK.getAsset('neonLine', {
width: 600,
height: 36,
anchorX: 0,
anchorY: 0.5
});
powerBarFg.tint = 0x00fff7;
powerBarBg.addChild(powerBarFg);
powerBarBg.x = (LK.gui.width - 600) / 2;
powerBarBg.y = 110;
LK.gui.top.addChild(powerBarBg);
// Level counter (top right, above tutorial button)
levelText = new Text2("Level " + (currentLevel + 1), {
size: 70,
fill: 0x000000,
align: "right"
});
levelText.anchor.set(1, 0); // anchor to right top
levelText.x = LK.gui.width - 40; // 40px from right edge, avoids top right crowding
levelText.y = 10; // just below the top edge, but not in top left 100x100
LK.gui.top.addChild(levelText);
// Timer UI (bottom left)
timerText = new Text2("01:00", {
size: 80,
fill: 0x112266,
align: "center"
});
timerText.anchor.set(0, 1); // left aligned, bottom edge
timerText.x = 60;
timerText.y = LK.gui.height - 60;
if (timerText && !timerText.parent) {
LK.gui.addChild(timerText);
}
// Tutorial button (top right, not in top left)
tutorialBtn = LK.getAsset('tutorialBtn', {
anchorX: 0.5,
anchorY: 0.5
});
tutorialBtn.x = LK.gui.width - 120;
tutorialBtn.y = 110;
LK.gui.top.addChild(tutorialBtn);
var tutorialBtnText = new Text2('?', {
size: 60,
fill: 0xFFFFFF
});
tutorialBtnText.anchor.set(0.5, 0.5);
tutorialBtnText.x = tutorialBtn.x;
tutorialBtnText.y = tutorialBtn.y;
LK.gui.top.addChild(tutorialBtnText);
// Tutorial popup (hidden by default)
tutorialText = new Text2("How to Play:\n\n" + "1. Draw blue police laser lines from the police (bottom) to catch all the thieves (gray circles).\n" + "2. Every laser drains the police's energy meter at the top.\n" + "3. Catch all thieves before the police run out of energy!\n" + "4. Lasers must touch a thief to catch them.\n\n" + "Tip: Plan your path to use as little energy as possible.\n\n" + "Tap the '?' button anytime for help.", {
size: 70,
fill: 0x1976d2,
align: "center",
wordWrap: true,
wordWrapWidth: 1200
});
tutorialText.anchor.set(0.5, 0.5);
tutorialText.x = LK.gui.width / 2;
tutorialText.y = LK.gui.height / 2;
tutorialText.visible = false;
LK.gui.center.addChild(tutorialText);
// Win text (hidden by default)
// (Removed lower right corner text as requested)
// --- Game Setup ---
// Place police (bottom center)
police = new Police();
police.x = GAME_WIDTH / 2;
police.y = GAME_HEIGHT - 300;
game.addChild(police);
// Start timer countdown
function updateTimerText() {
var min = Math.floor(timer / 60);
var sec = Math.floor(timer % 60);
var str = (min < 10 ? "0" : "") + min + ":" + (sec < 10 ? "0" : "") + sec;
if (timerText) timerText.setText(str);
}
updateTimerText();
if (timerInterval) {
LK.clearInterval(timerInterval);
}
timerEnded = false;
timer = TIMER_START;
timerInterval = LK.setInterval(function () {
if (tutorialOpen || winText && winText.visible) return;
if (timerEnded) return;
timer--;
if (timer < 0) timer = 0;
updateTimerText();
if (timer === 0 && !timerEnded) {
timerEnded = true;
LK.effects.flashScreen(0xff0033, 1200);
if (timerText && timerText.parent) {
timerText.parent.removeChild(timerText);
timerText = null;
}
LK.setTimeout(function () {
LK.showGameOver();
}, 1200);
}
}, 1000);
// Place thieves for the current level, randomizing positions if needed
function shufflePositions(arr) {
// Fisher-Yates shuffle
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
function setupLevel(levelIdx) {
// Remove old thieves
for (var i = 0; i < thieves.length; i++) {
if (thieves[i].parent) thieves[i].parent.removeChild(thieves[i]);
}
// Remove all laserLine assets from previous level
for (var i = 0; i < lines.length; i++) {
if (lines[i] && lines[i].parent) lines[i].parent.removeChild(lines[i]);
}
thieves = [];
lines = [];
caughtThieves = 0;
power = 1;
var level = LEVELS[levelIdx];
var positions = [];
// If there are more thieves than positions, generate random positions
if (level.positions.length >= level.thiefCount) {
// Shuffle and pick first N
positions = shufflePositions(level.positions.slice()).slice(0, level.thiefCount);
} else {
// Use all positions, and add random ones for the rest
positions = level.positions.slice();
while (positions.length < level.thiefCount) {
positions.push({
x: 200 + Math.random() * (GAME_WIDTH - 400),
y: 300 + Math.random() * (GAME_HEIGHT - 1000)
});
}
}
for (var i = 0; i < level.thiefCount; i++) {
var t = new Thief();
t.x = positions[i].x;
t.y = positions[i].y;
thieves.push(t);
game.addChild(t);
}
}
setupLevel(currentLevel);
if (levelText) {
levelText.setText("Level " + (currentLevel + 1));
}
// --- Drawing logic ---
// Helper: find thief at (x, y)
function findThiefAt(x, y) {
for (var i = 0; i < thieves.length; i++) {
if (pointInDevice(x, y, thieves[i])) return thieves[i];
}
return null;
}
// Helper: check if a line endpoint is close enough to a thief to catch
function thiefInRadius(x, y) {
for (var i = 0; i < thieves.length; i++) {
if (!thieves[i].caught && dist(x, y, thieves[i].x, thieves[i].y) < THIEF_CATCH_RADIUS) {
return thieves[i];
}
}
return null;
}
// Helper: check if a point is close to the police
function pointNearPolice(x, y) {
var dx = x - police.x;
var dy = y - police.y;
var r = police.children[0].width / 2 + 30;
return dx * dx + dy * dy <= r * r;
}
// --- Input handlers ---
// Only allow drawing if not in tutorial or win
function canDraw() {
// Block all game input if tutorial is open or win text is visible
return !tutorialOpen && (!winText || !winText.visible) && power > 0;
}
// Start drawing: must start from police or a caught thief
game.down = function (x, y, obj) {
if (tutorialOpen) return;
if (!canDraw()) return;
// Convert to game coordinates
var gx = x,
gy = y;
// Start from police?
if (pointNearPolice(gx, gy)) {
drawFromPolice = true;
drawStart = {
x: police.x,
y: police.y
};
} else {
// Start from caught thief?
for (var i = 0; i < thieves.length; i++) {
if (thieves[i].caught && pointInDevice(gx, gy, thieves[i])) {
drawFromPolice = false;
drawStart = {
x: thieves[i].x,
y: thieves[i].y
};
break;
}
}
}
if (drawStart) {
drawing = true;
drawLine = new LaserLine();
drawLine.setEndpoints(drawStart.x, drawStart.y, gx, gy);
game.addChild(drawLine);
}
};
// Drawing in progress
game.move = function (x, y, obj) {
if (tutorialOpen) return;
if (!drawing || !drawLine) return;
if (!canDraw()) return;
var gx = x,
gy = y;
drawLine.setEndpoints(drawStart.x, drawStart.y, gx, gy);
// Show line in real time, but don't finalize until up
};
// Finish drawing: if endpoint is on an uncaught thief, connect and catch
game.up = function (x, y, obj) {
if (tutorialOpen) return;
if (!drawing || !drawLine) {
drawing = false;
drawStart = null;
return;
}
if (!canDraw()) {
// Remove preview line
if (drawLine) {
drawLine.destroy();
drawLine = null;
}
drawing = false;
drawStart = null;
return;
}
var gx = x,
gy = y;
drawLine.setEndpoints(drawStart.x, drawStart.y, gx, gy);
// Check if endpoint is close to an uncaught thief
var targetThief = thiefInRadius(gx, gy);
// Only allow connecting to uncaught thief
if (targetThief) {
// Calculate line length
var len = dist(drawStart.x, drawStart.y, targetThief.x, targetThief.y);
var cost = len * POWER_DRAIN_PER_PIXEL;
if (power >= cost) {
// Snap endpoint to thief center
drawLine.setEndpoints(drawStart.x, drawStart.y, targetThief.x, targetThief.y);
lines.push(drawLine);
// Drain power
power = clamp(power - cost, POWER_MIN, POWER_MAX);
// Catch thief
targetThief.caught = true;
targetThief.energy = 1;
targetThief.flash();
caughtThieves++;
// Win condition
if (caughtThieves === thieves.length) {
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
if (winText) winText.visible = true;
if (timerText && timerText.parent) {
timerText.parent.removeChild(timerText);
timerText = null;
}
LK.effects.flashScreen(0x1976d2, 1200);
LK.setTimeout(function () {
// Advance to next level if available, else show win
if (currentLevel < LEVELS.length - 1) {
// Remove timerText when moving to next level
if (timerText && timerText.parent) {
timerText.parent.removeChild(timerText);
timerText = null;
}
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
currentLevel++;
setupLevel(currentLevel);
// Update level counter
if (levelText) {
levelText.setText("Level " + (currentLevel + 1));
}
// Reset timer and UI
timer = TIMER_START;
timerEnded = false;
// Add timerText when next level starts
if (!timerText) {
timerText = new Text2("01:00", {
size: 80,
fill: 0x112266,
align: "center"
});
timerText.anchor.set(0, 1);
timerText.x = 60;
timerText.y = LK.gui.height - 60;
LK.gui.addChild(timerText);
}
updateTimerText();
if (timerInterval) LK.clearInterval(timerInterval);
timerInterval = LK.setInterval(function () {
if (tutorialOpen || winText && winText.visible) return;
if (timerEnded) return;
timer--;
if (timer < 0) timer = 0;
updateTimerText();
if (timer === 0 && !timerEnded) {
timerEnded = true;
LK.effects.flashScreen(0xff0033, 1200);
if (timerText && timerText.parent) {
timerText.parent.removeChild(timerText);
timerText = null;
}
LK.setTimeout(function () {
LK.showGameOver();
}, 1200);
}
}, 1000);
} else {
LK.showYouWin();
}
}, 1800);
}
drawLine = null;
drawing = false;
drawStart = null;
return;
} else {
// Not enough power, flash bar
LK.effects.flashObject(powerBarFg, 0xff0000, 400);
}
}
// Not a valid connection, remove preview line
if (drawLine) {
drawLine.destroy();
drawLine = null;
}
drawing = false;
drawStart = null;
};
// --- Tutorial button handler ---
tutorialBtn.down = function (x, y, obj) {
if (tutorialOpen) {
tutorialText.visible = false;
tutorialOpen = false;
} else {
tutorialText.visible = true;
tutorialOpen = true;
}
};
tutorialBtnText.down = tutorialBtn.down;
// Allow tap anywhere on tutorialText to close it
tutorialText.down = function (x, y, obj) {
if (tutorialOpen) {
tutorialText.visible = false;
tutorialOpen = false;
}
};
// --- Game update loop ---
game.update = function () {
// Pause timer if tutorial or win is open
if ((tutorialOpen || winText && winText.visible) && timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
// Resume timer if not paused and not ended
if (!tutorialOpen && (!winText || !winText.visible) && !timerEnded && !timerInterval) {
if (!timerText) {
timerText = new Text2("01:00", {
size: 80,
fill: 0x112266,
align: "center"
});
timerText.anchor.set(0, 1);
timerText.x = 60;
timerText.y = LK.gui.height - 60;
LK.gui.addChild(timerText);
updateTimerText();
}
timerInterval = LK.setInterval(function () {
if (tutorialOpen || winText && winText.visible) return;
if (timerEnded) return;
timer--;
if (timer < 0) timer = 0;
if (timerText) {
var min = Math.floor(timer / 60);
var sec = Math.floor(timer % 60);
var str = (min < 10 ? "0" : "") + min + ":" + (sec < 10 ? "0" : "") + sec;
timerText.setText(str);
}
if (timer === 0 && !timerEnded) {
timerEnded = true;
LK.effects.flashScreen(0xff0033, 1200);
LK.setTimeout(function () {
LK.showGameOver();
}, 1200);
}
}, 1000);
}
// Update power bar
var barW = 600 * power;
powerBarFg.width = barW;
if (power < 0.18) {
powerBarFg.tint = 0xff0033;
} else {
powerBarFg.tint = 0x00fff7;
}
// Thieves lose energy if not caught
for (var i = 0; i < thieves.length; i++) {
var t = thieves[i];
if (!t.caught) {
t.energy = clamp(t.energy - THIEF_ENERGY_LOSS_PER_TICK, 0, 1);
// If energy runs out, game over (thief escapes)
if (t.energy <= 0) {
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
LK.effects.flashScreen(0xff0033, 1200);
LK.setTimeout(function () {
if (timerText && timerText.parent) {
timerText.parent.removeChild(timerText);
timerText = null;
}
LK.showGameOver();
}, 1200);
return;
}
}
}
// Power regen (optional, comment out to disable)
// power = clamp(power + POWER_REGEN_PER_TICK, POWER_MIN, POWER_MAX);
// Animate lines
for (var i = 0; i < lines.length; i++) {
if (lines[i].update) lines[i].update();
}
// Animate thieves and police
for (var i = 0; i < thieves.length; i++) {
if (thieves[i].update) thieves[i].update();
}
if (police && police.update) police.update();
if (drawLine && drawLine.update) drawLine.update();
};
// --- End of file ---
bullets arranged from top to bottom. In-Game asset. 2d. High contrast. No shadows
a topdown policeman and he's pointing a gun. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows
topdown thief. In-Game asset. 2d. High contrast. No shadows
a top down white black theme prison. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows