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