/**** * 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');