User prompt
move the shoot button diagonally up left
User prompt
move the shoot button to bottom right corner
User prompt
add a shoot button
Code edit (1 edits merged)
Please save this source code
User prompt
Stickman Sniper: Target Acquired
Initial prompt
Stickman Sniper is a precision shooting game where players control a sniper scope tasked with eliminating various targets across increasingly challenging levels. Player tap and drag to aim, then press a button to fire. Game is level-based and has a storyline that advances with each level. Before the levels start, targets are identified and the player is asked to neutralize the target, while missed shots or hitting civilians result failure of the level. The game features multiple levels, varying targets, and limited time, requiring careful aim and timing to progress and achieve high scores.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Fire Button class var FireButton = Container.expand(function () { var self = Container.call(this); var btn = self.attachAsset('fire_btn', { anchorX: 0.5, anchorY: 0.5 }); var txt = new Text2('FIRE', { size: 60, fill: "#fff" }); txt.anchor.set(0.5, 0.5); txt.x = 0; txt.y = 0; self.addChild(txt); // For press effect self.flash = function () { tween(btn, { alpha: 0.5 }, { duration: 80, onFinish: function onFinish() { tween(btn, { alpha: 1 }, { duration: 80 }); } }); }; return self; }); // Sniper Scope class var Scope = Container.expand(function () { var self = Container.call(this); // Scope ring var ring = self.attachAsset('scope_ring', { anchorX: 0.5, anchorY: 0.5, alpha: 1 }); // Crosshairs var crossV = self.attachAsset('scope_crosshair_v', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5 }); var crossH = self.attachAsset('scope_crosshair_h', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5 }); // For shot flash self.flash = function () { var flash = self.attachAsset('shot_flash', { anchorX: 0.5, anchorY: 0.5, alpha: 0.7 }); tween(flash, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { flash.destroy(); } }); }; return self; }); // Stickman class (base for all stickmen) var Stickman = Container.expand(function () { var self = Container.call(this); // Default: civilian self.isTarget = false; self.isAlive = true; // Set up body and head var body = null; var head = null; self.setType = function (type) { // Remove previous if (body) body.destroy(); if (head) head.destroy(); if (type === 'target') { body = self.attachAsset('stickman_target_body', { anchorX: 0.5, anchorY: 0 }); head = self.attachAsset('stickman_target_head', { anchorX: 0.5, anchorY: 0.5, y: 0 }); head.y = -30; self.isTarget = true; } else if (type === 'civilian') { body = self.attachAsset('stickman_civilian_body', { anchorX: 0.5, anchorY: 0 }); head = self.attachAsset('stickman_civilian_head', { anchorX: 0.5, anchorY: 0.5, y: 0 }); head.y = -30; self.isTarget = false; } else { body = self.attachAsset('stickman_body', { anchorX: 0.5, anchorY: 0 }); head = self.attachAsset('stickman_head', { anchorX: 0.5, anchorY: 0.5, y: 0 }); head.y = -30; self.isTarget = false; } }; // For hit detection, we use the head and body rectangles self.getHitRects = function () { // Returns array of rectangles in game coordinates var rects = []; if (!body || !head) return rects; // Body rects.push(new Rectangle(self.x - body.width / 2, self.y, body.width, body.height)); // Head rects.push(new Rectangle(self.x - head.width / 2, self.y - 30 - head.height / 2, head.width, head.height)); return rects; }; // Animate death self.die = function () { if (!self.isAlive) return; self.isAlive = false; // Fade out and fall tween(self, { rotation: Math.PI / 2, alpha: 0 }, { duration: 700, easing: tween.cubicOut, onFinish: function onFinish() { self.visible = false; } }); }; // --- Random Movement for Stickmen --- self.moveTarget = { x: self.x, y: self.y }; self.moveSpeed = 0.7 + Math.random() * 0.8; // px per frame, slower movement self.moveCooldown = 0; self.lastX = self.x; self.lastY = self.y; // Helper to pick a new random target within bounds self.pickNewTarget = function () { // Stickmen should stay within the visible area var margin = 120; var minX = margin; var maxX = 2048 - margin; var minY = 400; var maxY = 2732 - margin; // Prevent stickmen from moving to the top half of the screen in the first, second, and third missions // Only for level 0 (first mission), level 1 (second mission), or level 2 (third mission) if (typeof currentLevel !== "undefined" && (currentLevel === 0 || currentLevel === 1 || currentLevel === 2)) { minY = Math.max(minY, 1366); // 1366 is half of 2732, so only allow bottom half } self.moveTarget.x = minX + Math.random() * (maxX - minX); self.moveTarget.y = minY + Math.random() * (maxY - minY); self.moveSpeed = 0.7 + Math.random() * 0.8; self.moveCooldown = 60 + Math.floor(Math.random() * 120); // 1-3 seconds before next move }; // Per-frame update for random movement self.update = function () { if (!self.isAlive) return; // Only move if not dying self.lastX = self.x; self.lastY = self.y; // If close to target or cooldown expired, pick a new target var dx = self.moveTarget.x - self.x; var dy = self.moveTarget.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 10 || self.moveCooldown <= 0) { self.pickNewTarget(); } // Move towards target if (dist > 1) { var step = Math.min(self.moveSpeed, dist); self.x += dx / dist * step; self.y += dy / dist * step; } if (self.moveCooldown > 0) self.moveCooldown--; // --- Scale up all stickmen as they approach the bottom of the screen in the first and third missions --- if (typeof currentLevel !== "undefined" && (currentLevel === 0 || currentLevel === 2)) { // y ranges from 1366 (half) to 2732 (bottom) var minY = 1366; var maxY = 2732; var t = (self.y - minY) / (maxY - minY); if (t < 0) t = 0; if (t > 1) t = 1; // Scale from 1.0 at minY to 3.0 at maxY (even greater effect, applies to all stickmen) var scale = 1.0 + 2.0 * t; self.scaleX = scale; self.scaleY = scale; } else { // All stickmen normal size in other levels self.scaleX = 1; self.scaleY = 1; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222222 }); /**** * Game Code ****/ // Background image assets for each environment // --- Scope Mask Overlay --- // --- Setup UI --- var scopeMaskOverlay = null; function updateScopeMask(x, y) { // Remove previous mask if any if (scopeMaskOverlay) { scopeMaskOverlay.destroy(); scopeMaskOverlay = null; } // Create a new container for the mask scopeMaskOverlay = new Container(); // We'll use four black rectangles to cover the area outside the circle, and NOT darken the viewed area var radius = 400; // Scope visible radius var cx = typeof x === "number" ? x : scope ? scope.x : 2048 / 2; var cy = typeof y === "number" ? y : scope ? scope.y : 2732 / 2; // Top var topRect = LK.getAsset('scope_crosshair_h', { width: 2048, height: Math.max(0, cy - radius), anchorX: 0, anchorY: 0, x: 0, y: 0, alpha: 0.98, // darken not-viewed area even more color: 0xf8f8f8 }); topRect.alpha = 0.98; scopeMaskOverlay.addChild(topRect); // Bottom var bottomRect = LK.getAsset('scope_crosshair_h', { width: 2048, height: Math.max(0, 2732 - (cy + radius)), anchorX: 0, anchorY: 0, x: 0, y: cy + radius, alpha: 0.98, // darken not-viewed area even more color: 0xf8f8f8 }); bottomRect.alpha = 0.98; scopeMaskOverlay.addChild(bottomRect); // Left var leftRect = LK.getAsset('scope_crosshair_h', { width: Math.max(0, cx - radius), height: 2 * radius, anchorX: 0, anchorY: 0, x: 0, y: cy - radius, alpha: 0.98, // darken not-viewed area even more color: 0xf8f8f8 }); leftRect.alpha = 0.98; scopeMaskOverlay.addChild(leftRect); // Right var rightRect = LK.getAsset('scope_crosshair_h', { width: Math.max(0, 2048 - (cx + radius)), height: 2 * radius, anchorX: 0, anchorY: 0, x: cx + radius, y: cy - radius, alpha: 0.98, // darken not-viewed area even more color: 0xf8f8f8 }); rightRect.alpha = 0.98; scopeMaskOverlay.addChild(rightRect); // Increase alpha of all assets that start with 'scope' for (var i = 0; i < scopeMaskOverlay.children.length; ++i) { var child = scopeMaskOverlay.children[i]; if (child.assetId && typeof child.assetId === "string" && child.assetId.indexOf("scope") === 0) { child.alpha = 1; } } // No overlay inside the scope; viewed part is not artificially brightened or darkened // Add to game (on top of stickmen, under scope) game.addChild(scopeMaskOverlay); if (scope && scope.parent) { scope.parent.setChildIndex(scope, scope.parent.children.length - 1); } } // Stickman body (used for both targets and civilians, colored differently) // Sniper scope // Fire button // Shot effect // Sound effects // Music var levels = [ // Level 1: City Street { environment: { backgroundColor: 0x222222, name: "City Street", backgroundAssetId: "bg_city" }, targets: [{ x: 600, y: 1200 }], civilians: [{ x: 900, y: 1200 }, { x: 1400, y: 1200 }, { x: 700, y: 1000 }, { x: 1200, y: 1500 }, { x: 1700, y: 1100 }], time: 15, story: "Eliminate the red stickman. Avoid civilians! (City Street)" }, // Level 2: Rooftop { environment: { backgroundColor: 0x1a2637, name: "Rooftop", backgroundAssetId: "bg_rooftop" }, targets: [{ x: 400, y: 1000 }, { x: 1600, y: 1300 }], civilians: [{ x: 900, y: 1200 }, { x: 1200, y: 1400 }, { x: 700, y: 1500 }, { x: 500, y: 1100 }, { x: 1500, y: 1600 }, { x: 1800, y: 1200 }], time: 18, story: "Two targets this time. Don't miss! (Rooftop)" }, // Level 3: Park { environment: { backgroundColor: 0x2e5d2e, name: "Park", backgroundAssetId: "bg_park" }, targets: [{ x: 500, y: 900 }, { x: 1550, y: 1100 }, { x: 1000, y: 1700 }], civilians: [{ x: 800, y: 1200 }, { x: 1200, y: 1300 }, { x: 1700, y: 1500 }, { x: 600, y: 1600 }, { x: 400, y: 1000 }, { x: 1800, y: 1700 }, { x: 1000, y: 900 }], time: 20, story: "Three targets. Watch out for blue civilians! (Park)" }, // Level 4: Desert { environment: { backgroundColor: 0xf7e9b0, name: "Desert", backgroundAssetId: "bg_desert" }, targets: [{ x: 300, y: 1100 }, { x: 1800, y: 1200 }, { x: 1000, y: 1500 }, { x: 1400, y: 900 }], civilians: [{ x: 600, y: 1300 }, { x: 900, y: 1000 }, { x: 1200, y: 1700 }, { x: 1700, y: 1400 }, { x: 400, y: 1500 }, { x: 1600, y: 1700 }], time: 22, story: "Desert operation: Four targets, more civilians. Be careful!" }, // Level 5: Night Forest { environment: { backgroundColor: 0x1a2b1a, name: "Night Forest", backgroundAssetId: "bg_nightforest" }, targets: [{ x: 400, y: 1000 }, { x: 800, y: 1200 }, { x: 1200, y: 1400 }, { x: 1600, y: 1600 }, { x: 1800, y: 1000 }], civilians: [{ x: 600, y: 1100 }, { x: 1000, y: 1300 }, { x: 1400, y: 1500 }, { x: 1700, y: 1700 }, { x: 500, y: 1500 }, { x: 1500, y: 1200 }, { x: 900, y: 1700 }], time: 25, story: "Night Forest: Five targets. It's dark and dangerous!" }]; // --- Game State --- var currentLevel = 0; var stickmen = []; var targetsLeft = 0; var timeLeft = 0; var timerInterval = null; var gameActive = false; var score = 0; var shotsFired = 0; var shotsHit = 0; var levelStartTime = 0; // --- UI Elements --- var scope = null; var fireBtn = null; var timerTxt = null; var scoreTxt = null; var storyTxt = null; // --- Setup UI --- function setupUI() { // Remove previous UI if any if (timerTxt) timerTxt.destroy(); if (scoreTxt) scoreTxt.destroy(); if (storyTxt) storyTxt.destroy(); // Timer (top right) timerTxt = new Text2('00', { size: 90, fill: "#fff" }); timerTxt.anchor.set(1, 0); LK.gui.topRight.addChild(timerTxt); // Score (top center) scoreTxt = new Text2('Score: 0', { size: 90, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Story (top, below score) storyTxt = new Text2('', { size: 60, fill: "#fff" }); storyTxt.anchor.set(0.5, 0); storyTxt.y = 100; LK.gui.top.addChild(storyTxt); } // --- Start Level --- function startLevel(levelIdx) { // Clean up previous for (var i = 0; i < stickmen.length; ++i) { stickmen[i].destroy(); } stickmen = []; targetsLeft = 0; timeLeft = 0; gameActive = false; shotsFired = 0; shotsHit = 0; setupUI(); // Level data var level = levels[levelIdx]; if (!level) { // No more levels: show win LK.showYouWin(); return; } // Remove previous background asset if any if (typeof game.bgAsset !== "undefined" && game.bgAsset) { game.bgAsset.destroy(); game.bgAsset = null; } // Show environment background asset if defined, else fallback to color if (level.environment && typeof level.environment.backgroundAssetId === "string") { var bgAsset = LK.getAsset(level.environment.backgroundAssetId, { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732 }); game.addChildAt(bgAsset, 0); game.bgAsset = bgAsset; // Optionally set background color as fallback if (typeof level.environment.backgroundColor === "number") { game.setBackgroundColor(level.environment.backgroundColor); } } else if (level.environment && typeof level.environment.backgroundColor === "number") { game.setBackgroundColor(level.environment.backgroundColor); } // Show story storyTxt.setText(level.story); // Place stickmen // Targets for (var i = 0; i < level.targets.length; ++i) { var t = new Stickman(); t.setType('target'); t.x = level.targets[i].x; t.y = level.targets[i].y; game.addChild(t); stickmen.push(t); targetsLeft++; } // Civilians for (var i = 0; i < level.civilians.length; ++i) { var c = new Stickman(); c.setType('civilian'); c.x = level.civilians[i].x; c.y = level.civilians[i].y; game.addChild(c); stickmen.push(c); } // Reset scope position to center if (scope) scope.destroy(); scope = new Scope(); scope.x = 2048 / 2; scope.y = 2732 / 2; game.addChild(scope); updateScopeMask(scope.x, scope.y); // Fire button (bottom right, but offset diagonally up left) if (fireBtn) fireBtn.destroy(); fireBtn = new FireButton(); // Offset the button up and left from the bottom right corner fireBtn.x = -180; fireBtn.y = -180; LK.gui.bottomRight.addChild(fireBtn); // Fire button fires shot when pressed fireBtn.down = function (x, y, obj) { if (gameActive) { fireShot(); } }; fireBtn.up = function (x, y, obj) { // No action needed on up }; // Timer timeLeft = level.time; timerTxt.setText(timeLeft.toFixed(1)); scoreTxt.setText('Score: ' + score); // Start timer if (timerInterval) LK.clearInterval(timerInterval); timerInterval = LK.setInterval(function () { if (!gameActive) return; timeLeft -= 0.1; if (timeLeft < 0) timeLeft = 0; timerTxt.setText(timeLeft.toFixed(1)); if (timeLeft <= 0) { failLevel("Time's up!"); } }, 100); // Enable game gameActive = true; levelStartTime = Date.now(); } // --- Fire Shot --- function fireShot() { if (!gameActive) return; fireBtn.flash(); scope.flash(); LK.getSound('shot').play(); shotsFired++; // Check if any stickman is under the scope center var hit = false; var hitTarget = null; var hitCivilian = null; for (var i = 0; i < stickmen.length; ++i) { var s = stickmen[i]; if (!s.isAlive) continue; var rects = s.getHitRects(); for (var j = 0; j < rects.length; ++j) { var r = rects[j]; // Scope center is (scope.x, scope.y) if (scope.x >= r.x && scope.x <= r.x + r.width && scope.y >= r.y && scope.y <= r.y + r.height) { hit = true; if (s.isTarget) hitTarget = s;else hitCivilian = s; break; } } if (hit) break; } if (hitTarget) { // Success: kill target // --- Blood effect --- var blood = LK.getAsset('stickman_head', { anchorX: 0.5, anchorY: 0.5, x: hitTarget.x, y: hitTarget.y - 30, // head center color: 0xb80000, // deep red alpha: 0.8, scaleX: 1.2, scaleY: 1.2 }); game.addChild(blood); tween(blood, { alpha: 0, scaleX: 2.5, scaleY: 2.5, y: blood.y + 80 }, { duration: 700, onFinish: function onFinish() { blood.destroy(); } }); hitTarget.die(); targetsLeft--; shotsHit++; score += 100 + Math.max(0, Math.floor(50 * (timeLeft / levels[currentLevel].time))); scoreTxt.setText('Score: ' + score); // Play success sound when a stickman target is shot LK.getSound('success').play(); if (targetsLeft <= 0) { // Level complete gameActive = false; LK.setTimeout(function () { currentLevel++; startLevel(currentLevel); }, 1200); } } else if (hitCivilian) { // Fail: hit civilian hitCivilian.die(); failLevel("You shot a civilian!"); } else { // Missed shot failLevel("You missed!"); } } // --- Fail Level --- function failLevel(msg) { if (!gameActive) return; gameActive = false; LK.getSound('fail').play(); LK.effects.flashScreen(0xff0000, 800); // Show game over after a short delay LK.setTimeout(function () { LK.showGameOver(); }, 900); } // --- Game Move/Drag Handling --- var draggingScope = false; var dragOffsetX = 0; var dragOffsetY = 0; // Only allow dragging scope, not fire button game.down = function (x, y, obj) { // If touch is inside fireBtn, don't drag scope var local = LK.gui.bottomRight.toLocal({ x: x, y: y }); if (fireBtn && fireBtn.containsPoint && fireBtn.containsPoint(local)) { // Do nothing, handled by fireBtn return; } // Allow dragging scope from anywhere on the screen draggingScope = true; dragOffsetX = x - scope.x; dragOffsetY = y - scope.y; }; game.move = function (x, y, obj) { if (draggingScope && gameActive) { // Clamp scope inside game area var nx = x - dragOffsetX; var ny = y - dragOffsetY; // Keep scope inside screen if (nx < 200) nx = 200; if (nx > 2048 - 200) nx = 2048 - 200; if (ny < 200) ny = 200; if (ny > 2732 - 200) ny = 2732 - 200; scope.x = nx; scope.y = ny; updateScopeMask(scope.x, scope.y); } }; game.up = function (x, y, obj) { draggingScope = false; }; // Fire button press fireBtn = null; // Will be created in startLevel // (Removed: fire is now triggered by releasing the scope, not the button) // fireBtn.down is now always assigned in startLevel after creation // --- Game Update --- game.update = function () { // Move all stickmen randomly for (var i = 0; i < stickmen.length; ++i) { if (stickmen[i] && stickmen[i].update) stickmen[i].update(); } }; // --- Start Game --- currentLevel = 0; score = 0; setupUI(); startLevel(currentLevel); // --- Play music --- LK.playMusic('sniper_bg');
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Fire Button class
var FireButton = Container.expand(function () {
var self = Container.call(this);
var btn = self.attachAsset('fire_btn', {
anchorX: 0.5,
anchorY: 0.5
});
var txt = new Text2('FIRE', {
size: 60,
fill: "#fff"
});
txt.anchor.set(0.5, 0.5);
txt.x = 0;
txt.y = 0;
self.addChild(txt);
// For press effect
self.flash = function () {
tween(btn, {
alpha: 0.5
}, {
duration: 80,
onFinish: function onFinish() {
tween(btn, {
alpha: 1
}, {
duration: 80
});
}
});
};
return self;
});
// Sniper Scope class
var Scope = Container.expand(function () {
var self = Container.call(this);
// Scope ring
var ring = self.attachAsset('scope_ring', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 1
});
// Crosshairs
var crossV = self.attachAsset('scope_crosshair_v', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5
});
var crossH = self.attachAsset('scope_crosshair_h', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5
});
// For shot flash
self.flash = function () {
var flash = self.attachAsset('shot_flash', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.7
});
tween(flash, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
flash.destroy();
}
});
};
return self;
});
// Stickman class (base for all stickmen)
var Stickman = Container.expand(function () {
var self = Container.call(this);
// Default: civilian
self.isTarget = false;
self.isAlive = true;
// Set up body and head
var body = null;
var head = null;
self.setType = function (type) {
// Remove previous
if (body) body.destroy();
if (head) head.destroy();
if (type === 'target') {
body = self.attachAsset('stickman_target_body', {
anchorX: 0.5,
anchorY: 0
});
head = self.attachAsset('stickman_target_head', {
anchorX: 0.5,
anchorY: 0.5,
y: 0
});
head.y = -30;
self.isTarget = true;
} else if (type === 'civilian') {
body = self.attachAsset('stickman_civilian_body', {
anchorX: 0.5,
anchorY: 0
});
head = self.attachAsset('stickman_civilian_head', {
anchorX: 0.5,
anchorY: 0.5,
y: 0
});
head.y = -30;
self.isTarget = false;
} else {
body = self.attachAsset('stickman_body', {
anchorX: 0.5,
anchorY: 0
});
head = self.attachAsset('stickman_head', {
anchorX: 0.5,
anchorY: 0.5,
y: 0
});
head.y = -30;
self.isTarget = false;
}
};
// For hit detection, we use the head and body rectangles
self.getHitRects = function () {
// Returns array of rectangles in game coordinates
var rects = [];
if (!body || !head) return rects;
// Body
rects.push(new Rectangle(self.x - body.width / 2, self.y, body.width, body.height));
// Head
rects.push(new Rectangle(self.x - head.width / 2, self.y - 30 - head.height / 2, head.width, head.height));
return rects;
};
// Animate death
self.die = function () {
if (!self.isAlive) return;
self.isAlive = false;
// Fade out and fall
tween(self, {
rotation: Math.PI / 2,
alpha: 0
}, {
duration: 700,
easing: tween.cubicOut,
onFinish: function onFinish() {
self.visible = false;
}
});
};
// --- Random Movement for Stickmen ---
self.moveTarget = {
x: self.x,
y: self.y
};
self.moveSpeed = 0.7 + Math.random() * 0.8; // px per frame, slower movement
self.moveCooldown = 0;
self.lastX = self.x;
self.lastY = self.y;
// Helper to pick a new random target within bounds
self.pickNewTarget = function () {
// Stickmen should stay within the visible area
var margin = 120;
var minX = margin;
var maxX = 2048 - margin;
var minY = 400;
var maxY = 2732 - margin;
// Prevent stickmen from moving to the top half of the screen in the first, second, and third missions
// Only for level 0 (first mission), level 1 (second mission), or level 2 (third mission)
if (typeof currentLevel !== "undefined" && (currentLevel === 0 || currentLevel === 1 || currentLevel === 2)) {
minY = Math.max(minY, 1366); // 1366 is half of 2732, so only allow bottom half
}
self.moveTarget.x = minX + Math.random() * (maxX - minX);
self.moveTarget.y = minY + Math.random() * (maxY - minY);
self.moveSpeed = 0.7 + Math.random() * 0.8;
self.moveCooldown = 60 + Math.floor(Math.random() * 120); // 1-3 seconds before next move
};
// Per-frame update for random movement
self.update = function () {
if (!self.isAlive) return;
// Only move if not dying
self.lastX = self.x;
self.lastY = self.y;
// If close to target or cooldown expired, pick a new target
var dx = self.moveTarget.x - self.x;
var dy = self.moveTarget.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 10 || self.moveCooldown <= 0) {
self.pickNewTarget();
}
// Move towards target
if (dist > 1) {
var step = Math.min(self.moveSpeed, dist);
self.x += dx / dist * step;
self.y += dy / dist * step;
}
if (self.moveCooldown > 0) self.moveCooldown--;
// --- Scale up all stickmen as they approach the bottom of the screen in the first and third missions ---
if (typeof currentLevel !== "undefined" && (currentLevel === 0 || currentLevel === 2)) {
// y ranges from 1366 (half) to 2732 (bottom)
var minY = 1366;
var maxY = 2732;
var t = (self.y - minY) / (maxY - minY);
if (t < 0) t = 0;
if (t > 1) t = 1;
// Scale from 1.0 at minY to 3.0 at maxY (even greater effect, applies to all stickmen)
var scale = 1.0 + 2.0 * t;
self.scaleX = scale;
self.scaleY = scale;
} else {
// All stickmen normal size in other levels
self.scaleX = 1;
self.scaleY = 1;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// Background image assets for each environment
// --- Scope Mask Overlay ---
// --- Setup UI ---
var scopeMaskOverlay = null;
function updateScopeMask(x, y) {
// Remove previous mask if any
if (scopeMaskOverlay) {
scopeMaskOverlay.destroy();
scopeMaskOverlay = null;
}
// Create a new container for the mask
scopeMaskOverlay = new Container();
// We'll use four black rectangles to cover the area outside the circle, and NOT darken the viewed area
var radius = 400; // Scope visible radius
var cx = typeof x === "number" ? x : scope ? scope.x : 2048 / 2;
var cy = typeof y === "number" ? y : scope ? scope.y : 2732 / 2;
// Top
var topRect = LK.getAsset('scope_crosshair_h', {
width: 2048,
height: Math.max(0, cy - radius),
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
alpha: 0.98,
// darken not-viewed area even more
color: 0xf8f8f8
});
topRect.alpha = 0.98;
scopeMaskOverlay.addChild(topRect);
// Bottom
var bottomRect = LK.getAsset('scope_crosshair_h', {
width: 2048,
height: Math.max(0, 2732 - (cy + radius)),
anchorX: 0,
anchorY: 0,
x: 0,
y: cy + radius,
alpha: 0.98,
// darken not-viewed area even more
color: 0xf8f8f8
});
bottomRect.alpha = 0.98;
scopeMaskOverlay.addChild(bottomRect);
// Left
var leftRect = LK.getAsset('scope_crosshair_h', {
width: Math.max(0, cx - radius),
height: 2 * radius,
anchorX: 0,
anchorY: 0,
x: 0,
y: cy - radius,
alpha: 0.98,
// darken not-viewed area even more
color: 0xf8f8f8
});
leftRect.alpha = 0.98;
scopeMaskOverlay.addChild(leftRect);
// Right
var rightRect = LK.getAsset('scope_crosshair_h', {
width: Math.max(0, 2048 - (cx + radius)),
height: 2 * radius,
anchorX: 0,
anchorY: 0,
x: cx + radius,
y: cy - radius,
alpha: 0.98,
// darken not-viewed area even more
color: 0xf8f8f8
});
rightRect.alpha = 0.98;
scopeMaskOverlay.addChild(rightRect);
// Increase alpha of all assets that start with 'scope'
for (var i = 0; i < scopeMaskOverlay.children.length; ++i) {
var child = scopeMaskOverlay.children[i];
if (child.assetId && typeof child.assetId === "string" && child.assetId.indexOf("scope") === 0) {
child.alpha = 1;
}
}
// No overlay inside the scope; viewed part is not artificially brightened or darkened
// Add to game (on top of stickmen, under scope)
game.addChild(scopeMaskOverlay);
if (scope && scope.parent) {
scope.parent.setChildIndex(scope, scope.parent.children.length - 1);
}
}
// Stickman body (used for both targets and civilians, colored differently)
// Sniper scope
// Fire button
// Shot effect
// Sound effects
// Music
var levels = [
// Level 1: City Street
{
environment: {
backgroundColor: 0x222222,
name: "City Street",
backgroundAssetId: "bg_city"
},
targets: [{
x: 600,
y: 1200
}],
civilians: [{
x: 900,
y: 1200
}, {
x: 1400,
y: 1200
}, {
x: 700,
y: 1000
}, {
x: 1200,
y: 1500
}, {
x: 1700,
y: 1100
}],
time: 15,
story: "Eliminate the red stickman. Avoid civilians! (City Street)"
},
// Level 2: Rooftop
{
environment: {
backgroundColor: 0x1a2637,
name: "Rooftop",
backgroundAssetId: "bg_rooftop"
},
targets: [{
x: 400,
y: 1000
}, {
x: 1600,
y: 1300
}],
civilians: [{
x: 900,
y: 1200
}, {
x: 1200,
y: 1400
}, {
x: 700,
y: 1500
}, {
x: 500,
y: 1100
}, {
x: 1500,
y: 1600
}, {
x: 1800,
y: 1200
}],
time: 18,
story: "Two targets this time. Don't miss! (Rooftop)"
},
// Level 3: Park
{
environment: {
backgroundColor: 0x2e5d2e,
name: "Park",
backgroundAssetId: "bg_park"
},
targets: [{
x: 500,
y: 900
}, {
x: 1550,
y: 1100
}, {
x: 1000,
y: 1700
}],
civilians: [{
x: 800,
y: 1200
}, {
x: 1200,
y: 1300
}, {
x: 1700,
y: 1500
}, {
x: 600,
y: 1600
}, {
x: 400,
y: 1000
}, {
x: 1800,
y: 1700
}, {
x: 1000,
y: 900
}],
time: 20,
story: "Three targets. Watch out for blue civilians! (Park)"
},
// Level 4: Desert
{
environment: {
backgroundColor: 0xf7e9b0,
name: "Desert",
backgroundAssetId: "bg_desert"
},
targets: [{
x: 300,
y: 1100
}, {
x: 1800,
y: 1200
}, {
x: 1000,
y: 1500
}, {
x: 1400,
y: 900
}],
civilians: [{
x: 600,
y: 1300
}, {
x: 900,
y: 1000
}, {
x: 1200,
y: 1700
}, {
x: 1700,
y: 1400
}, {
x: 400,
y: 1500
}, {
x: 1600,
y: 1700
}],
time: 22,
story: "Desert operation: Four targets, more civilians. Be careful!"
},
// Level 5: Night Forest
{
environment: {
backgroundColor: 0x1a2b1a,
name: "Night Forest",
backgroundAssetId: "bg_nightforest"
},
targets: [{
x: 400,
y: 1000
}, {
x: 800,
y: 1200
}, {
x: 1200,
y: 1400
}, {
x: 1600,
y: 1600
}, {
x: 1800,
y: 1000
}],
civilians: [{
x: 600,
y: 1100
}, {
x: 1000,
y: 1300
}, {
x: 1400,
y: 1500
}, {
x: 1700,
y: 1700
}, {
x: 500,
y: 1500
}, {
x: 1500,
y: 1200
}, {
x: 900,
y: 1700
}],
time: 25,
story: "Night Forest: Five targets. It's dark and dangerous!"
}];
// --- Game State ---
var currentLevel = 0;
var stickmen = [];
var targetsLeft = 0;
var timeLeft = 0;
var timerInterval = null;
var gameActive = false;
var score = 0;
var shotsFired = 0;
var shotsHit = 0;
var levelStartTime = 0;
// --- UI Elements ---
var scope = null;
var fireBtn = null;
var timerTxt = null;
var scoreTxt = null;
var storyTxt = null;
// --- Setup UI ---
function setupUI() {
// Remove previous UI if any
if (timerTxt) timerTxt.destroy();
if (scoreTxt) scoreTxt.destroy();
if (storyTxt) storyTxt.destroy();
// Timer (top right)
timerTxt = new Text2('00', {
size: 90,
fill: "#fff"
});
timerTxt.anchor.set(1, 0);
LK.gui.topRight.addChild(timerTxt);
// Score (top center)
scoreTxt = new Text2('Score: 0', {
size: 90,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Story (top, below score)
storyTxt = new Text2('', {
size: 60,
fill: "#fff"
});
storyTxt.anchor.set(0.5, 0);
storyTxt.y = 100;
LK.gui.top.addChild(storyTxt);
}
// --- Start Level ---
function startLevel(levelIdx) {
// Clean up previous
for (var i = 0; i < stickmen.length; ++i) {
stickmen[i].destroy();
}
stickmen = [];
targetsLeft = 0;
timeLeft = 0;
gameActive = false;
shotsFired = 0;
shotsHit = 0;
setupUI();
// Level data
var level = levels[levelIdx];
if (!level) {
// No more levels: show win
LK.showYouWin();
return;
}
// Remove previous background asset if any
if (typeof game.bgAsset !== "undefined" && game.bgAsset) {
game.bgAsset.destroy();
game.bgAsset = null;
}
// Show environment background asset if defined, else fallback to color
if (level.environment && typeof level.environment.backgroundAssetId === "string") {
var bgAsset = LK.getAsset(level.environment.backgroundAssetId, {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChildAt(bgAsset, 0);
game.bgAsset = bgAsset;
// Optionally set background color as fallback
if (typeof level.environment.backgroundColor === "number") {
game.setBackgroundColor(level.environment.backgroundColor);
}
} else if (level.environment && typeof level.environment.backgroundColor === "number") {
game.setBackgroundColor(level.environment.backgroundColor);
}
// Show story
storyTxt.setText(level.story);
// Place stickmen
// Targets
for (var i = 0; i < level.targets.length; ++i) {
var t = new Stickman();
t.setType('target');
t.x = level.targets[i].x;
t.y = level.targets[i].y;
game.addChild(t);
stickmen.push(t);
targetsLeft++;
}
// Civilians
for (var i = 0; i < level.civilians.length; ++i) {
var c = new Stickman();
c.setType('civilian');
c.x = level.civilians[i].x;
c.y = level.civilians[i].y;
game.addChild(c);
stickmen.push(c);
}
// Reset scope position to center
if (scope) scope.destroy();
scope = new Scope();
scope.x = 2048 / 2;
scope.y = 2732 / 2;
game.addChild(scope);
updateScopeMask(scope.x, scope.y);
// Fire button (bottom right, but offset diagonally up left)
if (fireBtn) fireBtn.destroy();
fireBtn = new FireButton();
// Offset the button up and left from the bottom right corner
fireBtn.x = -180;
fireBtn.y = -180;
LK.gui.bottomRight.addChild(fireBtn);
// Fire button fires shot when pressed
fireBtn.down = function (x, y, obj) {
if (gameActive) {
fireShot();
}
};
fireBtn.up = function (x, y, obj) {
// No action needed on up
};
// Timer
timeLeft = level.time;
timerTxt.setText(timeLeft.toFixed(1));
scoreTxt.setText('Score: ' + score);
// Start timer
if (timerInterval) LK.clearInterval(timerInterval);
timerInterval = LK.setInterval(function () {
if (!gameActive) return;
timeLeft -= 0.1;
if (timeLeft < 0) timeLeft = 0;
timerTxt.setText(timeLeft.toFixed(1));
if (timeLeft <= 0) {
failLevel("Time's up!");
}
}, 100);
// Enable game
gameActive = true;
levelStartTime = Date.now();
}
// --- Fire Shot ---
function fireShot() {
if (!gameActive) return;
fireBtn.flash();
scope.flash();
LK.getSound('shot').play();
shotsFired++;
// Check if any stickman is under the scope center
var hit = false;
var hitTarget = null;
var hitCivilian = null;
for (var i = 0; i < stickmen.length; ++i) {
var s = stickmen[i];
if (!s.isAlive) continue;
var rects = s.getHitRects();
for (var j = 0; j < rects.length; ++j) {
var r = rects[j];
// Scope center is (scope.x, scope.y)
if (scope.x >= r.x && scope.x <= r.x + r.width && scope.y >= r.y && scope.y <= r.y + r.height) {
hit = true;
if (s.isTarget) hitTarget = s;else hitCivilian = s;
break;
}
}
if (hit) break;
}
if (hitTarget) {
// Success: kill target
// --- Blood effect ---
var blood = LK.getAsset('stickman_head', {
anchorX: 0.5,
anchorY: 0.5,
x: hitTarget.x,
y: hitTarget.y - 30,
// head center
color: 0xb80000,
// deep red
alpha: 0.8,
scaleX: 1.2,
scaleY: 1.2
});
game.addChild(blood);
tween(blood, {
alpha: 0,
scaleX: 2.5,
scaleY: 2.5,
y: blood.y + 80
}, {
duration: 700,
onFinish: function onFinish() {
blood.destroy();
}
});
hitTarget.die();
targetsLeft--;
shotsHit++;
score += 100 + Math.max(0, Math.floor(50 * (timeLeft / levels[currentLevel].time)));
scoreTxt.setText('Score: ' + score);
// Play success sound when a stickman target is shot
LK.getSound('success').play();
if (targetsLeft <= 0) {
// Level complete
gameActive = false;
LK.setTimeout(function () {
currentLevel++;
startLevel(currentLevel);
}, 1200);
}
} else if (hitCivilian) {
// Fail: hit civilian
hitCivilian.die();
failLevel("You shot a civilian!");
} else {
// Missed shot
failLevel("You missed!");
}
}
// --- Fail Level ---
function failLevel(msg) {
if (!gameActive) return;
gameActive = false;
LK.getSound('fail').play();
LK.effects.flashScreen(0xff0000, 800);
// Show game over after a short delay
LK.setTimeout(function () {
LK.showGameOver();
}, 900);
}
// --- Game Move/Drag Handling ---
var draggingScope = false;
var dragOffsetX = 0;
var dragOffsetY = 0;
// Only allow dragging scope, not fire button
game.down = function (x, y, obj) {
// If touch is inside fireBtn, don't drag scope
var local = LK.gui.bottomRight.toLocal({
x: x,
y: y
});
if (fireBtn && fireBtn.containsPoint && fireBtn.containsPoint(local)) {
// Do nothing, handled by fireBtn
return;
}
// Allow dragging scope from anywhere on the screen
draggingScope = true;
dragOffsetX = x - scope.x;
dragOffsetY = y - scope.y;
};
game.move = function (x, y, obj) {
if (draggingScope && gameActive) {
// Clamp scope inside game area
var nx = x - dragOffsetX;
var ny = y - dragOffsetY;
// Keep scope inside screen
if (nx < 200) nx = 200;
if (nx > 2048 - 200) nx = 2048 - 200;
if (ny < 200) ny = 200;
if (ny > 2732 - 200) ny = 2732 - 200;
scope.x = nx;
scope.y = ny;
updateScopeMask(scope.x, scope.y);
}
};
game.up = function (x, y, obj) {
draggingScope = false;
};
// Fire button press
fireBtn = null; // Will be created in startLevel
// (Removed: fire is now triggered by releasing the scope, not the button)
// fireBtn.down is now always assigned in startLevel after creation
// --- Game Update ---
game.update = function () {
// Move all stickmen randomly
for (var i = 0; i < stickmen.length; ++i) {
if (stickmen[i] && stickmen[i].update) stickmen[i].update();
}
};
// --- Start Game ---
currentLevel = 0;
score = 0;
setupUI();
startLevel(currentLevel);
// --- Play music ---
LK.playMusic('sniper_bg');