/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var facekit = LK.import("@upit/facekit.v1"); /**** * Classes ****/ // Player character controlled by face var FaceChar = Container.expand(function () { var self = Container.call(this); var _char = self.attachAsset('faceChar', { anchorX: 0.5, anchorY: 0.5 }); // For hit feedback self.flash = function () { tween(self, { alpha: 0.3 }, { duration: 80, onFinish: function onFinish() { tween(self, { alpha: 1 }, { duration: 120 }); } }); }; return self; }); // Obstacle class var Obstacle = Container.expand(function () { var self = Container.call(this); var obs = self.attachAsset('obstacle', { anchorX: 0.5, anchorY: 0.5 }); // Speed is set on creation self.speed = 0; self.update = function () { self.y += self.speed; }; return self; }); // Point collectible class var PointItem = Container.expand(function () { var self = Container.call(this); var pt = self.attachAsset('point', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 0; self.update = function () { self.y += self.speed; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181c24 }); /**** * Game Code ****/ /* We use simple shapes for the character, obstacles, and points. */ // Game area: 2048x2732 // Center Y for spawn var GAME_W = 2048; var GAME_H = 2732; // Main character var faceChar = new FaceChar(); faceChar.x = GAME_W / 2; faceChar.y = GAME_H - 400; game.addChild(faceChar); // Arrays for obstacles and points var obstacles = []; var points = []; // Score display var scoreTxt = new Text2('0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Best score display (optional, not persistent) var bestScore = 0; // Game state var isDead = false; var lastScore = 0; // Spawning timers var obstacleTimer = 0; var pointTimer = 0; // For face smoothing var lastFaceX = faceChar.x; var lastFaceY = faceChar.y; // Helper: clamp function clamp(val, min, max) { return Math.max(min, Math.min(max, val)); } // Main update loop game.update = function () { if (isDead) return; // --- Face tracking movement --- // Use mouthCenter if available, else fallback to noseTip var fx = facekit.mouthCenter && facekit.mouthCenter.x ? facekit.mouthCenter.x : facekit.noseTip && facekit.noseTip.x ? facekit.noseTip.x : GAME_W / 2; var fy = facekit.mouthCenter && facekit.mouthCenter.y ? facekit.mouthCenter.y : facekit.noseTip && facekit.noseTip.y ? facekit.noseTip.y : GAME_H - 400; // Clamp to game area, keep character fully visible var charR = faceChar.width / 2; fx = clamp(fx, charR, GAME_W - charR); fy = clamp(fy, charR + 120, GAME_H - charR - 40); // Smooth movement (lerp) lastFaceX += (fx - lastFaceX) * 0.35; lastFaceY += (fy - lastFaceY) * 0.35; faceChar.x = lastFaceX; faceChar.y = lastFaceY; // --- Spawning obstacles --- obstacleTimer--; if (obstacleTimer <= 0) { var obs = new Obstacle(); // Random X, avoid left 100px (menu) var minX = 120 + obs.width / 2; var maxX = GAME_W - obs.width / 2; obs.x = minX + Math.random() * (maxX - minX); obs.y = -obs.height / 2; // Speed increases with score obs.speed = 12 + LK.getScore() * 0.12; obstacles.push(obs); game.addChild(obs); // Next spawn: 36-60 ticks obstacleTimer = 36 + Math.floor(Math.random() * 24); } // --- Spawning points --- pointTimer--; if (pointTimer <= 0) { var pt = new PointItem(); var minX = 120 + pt.width / 2; var maxX = GAME_W - pt.width / 2; pt.x = minX + Math.random() * (maxX - minX); pt.y = -pt.height / 2; pt.speed = 10 + LK.getScore() * 0.10; points.push(pt); game.addChild(pt); // Next spawn: 60-120 ticks pointTimer = 60 + Math.floor(Math.random() * 60); } // --- Update obstacles --- for (var i = obstacles.length - 1; i >= 0; i--) { var o = obstacles[i]; o.update(); // Remove if off screen if (o.y - o.height / 2 > GAME_H + 40) { o.destroy(); obstacles.splice(i, 1); continue; } // Collision with player if (o.intersects(faceChar)) { // Game over isDead = true; faceChar.flash(); LK.effects.flashScreen(0xff0000, 800); LK.showGameOver(); return; } } // --- Update points --- for (var j = points.length - 1; j >= 0; j--) { var p = points[j]; p.update(); // Remove if off screen if (p.y - p.height / 2 > GAME_H + 40) { p.destroy(); points.splice(j, 1); continue; } // Collect if (p.intersects(faceChar)) { LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); // Play point collect sound LK.getSound('pointCollect').play(); // Feedback tween(p, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 180, onFinish: function onFinish() { p.destroy(); } }); points.splice(j, 1); continue; } } }; // Reset state on new game game.on('reset', function () { // Remove all obstacles and points for (var i = 0; i < obstacles.length; i++) obstacles[i].destroy(); for (var j = 0; j < points.length; j++) points[j].destroy(); obstacles = []; points = []; isDead = false; LK.setScore(0); scoreTxt.setText('0'); // Reset faceChar position faceChar.x = GAME_W / 2; faceChar.y = GAME_H - 400; lastFaceX = faceChar.x; lastFaceY = faceChar.y; obstacleTimer = 30; pointTimer = 60; }); // Also reset on game over (for safety) game.on('gameover', function () { isDead = true; }); // Touch fallback: allow dragging character if facekit not available var dragNode = null; function handleMove(x, y, obj) { if (dragNode) { // Clamp to game area var charR = faceChar.width / 2; var nx = clamp(x, charR, GAME_W - charR); var ny = clamp(y, charR + 120, GAME_H - charR - 40); faceChar.x = nx; faceChar.y = ny; lastFaceX = nx; lastFaceY = ny; } } game.move = handleMove; game.down = function (x, y, obj) { // Only allow drag if facekit not available if (!facekit.mouthCenter || !facekit.mouthCenter.x) { dragNode = faceChar; handleMove(x, y, obj); } }; game.up = function (x, y, obj) { dragNode = null; };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
// Player character controlled by face
var FaceChar = Container.expand(function () {
var self = Container.call(this);
var _char = self.attachAsset('faceChar', {
anchorX: 0.5,
anchorY: 0.5
});
// For hit feedback
self.flash = function () {
tween(self, {
alpha: 0.3
}, {
duration: 80,
onFinish: function onFinish() {
tween(self, {
alpha: 1
}, {
duration: 120
});
}
});
};
return self;
});
// Obstacle class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obs = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
// Speed is set on creation
self.speed = 0;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Point collectible class
var PointItem = Container.expand(function () {
var self = Container.call(this);
var pt = self.attachAsset('point', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 0;
self.update = function () {
self.y += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181c24
});
/****
* Game Code
****/
/*
We use simple shapes for the character, obstacles, and points.
*/
// Game area: 2048x2732
// Center Y for spawn
var GAME_W = 2048;
var GAME_H = 2732;
// Main character
var faceChar = new FaceChar();
faceChar.x = GAME_W / 2;
faceChar.y = GAME_H - 400;
game.addChild(faceChar);
// Arrays for obstacles and points
var obstacles = [];
var points = [];
// Score display
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Best score display (optional, not persistent)
var bestScore = 0;
// Game state
var isDead = false;
var lastScore = 0;
// Spawning timers
var obstacleTimer = 0;
var pointTimer = 0;
// For face smoothing
var lastFaceX = faceChar.x;
var lastFaceY = faceChar.y;
// Helper: clamp
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Main update loop
game.update = function () {
if (isDead) return;
// --- Face tracking movement ---
// Use mouthCenter if available, else fallback to noseTip
var fx = facekit.mouthCenter && facekit.mouthCenter.x ? facekit.mouthCenter.x : facekit.noseTip && facekit.noseTip.x ? facekit.noseTip.x : GAME_W / 2;
var fy = facekit.mouthCenter && facekit.mouthCenter.y ? facekit.mouthCenter.y : facekit.noseTip && facekit.noseTip.y ? facekit.noseTip.y : GAME_H - 400;
// Clamp to game area, keep character fully visible
var charR = faceChar.width / 2;
fx = clamp(fx, charR, GAME_W - charR);
fy = clamp(fy, charR + 120, GAME_H - charR - 40);
// Smooth movement (lerp)
lastFaceX += (fx - lastFaceX) * 0.35;
lastFaceY += (fy - lastFaceY) * 0.35;
faceChar.x = lastFaceX;
faceChar.y = lastFaceY;
// --- Spawning obstacles ---
obstacleTimer--;
if (obstacleTimer <= 0) {
var obs = new Obstacle();
// Random X, avoid left 100px (menu)
var minX = 120 + obs.width / 2;
var maxX = GAME_W - obs.width / 2;
obs.x = minX + Math.random() * (maxX - minX);
obs.y = -obs.height / 2;
// Speed increases with score
obs.speed = 12 + LK.getScore() * 0.12;
obstacles.push(obs);
game.addChild(obs);
// Next spawn: 36-60 ticks
obstacleTimer = 36 + Math.floor(Math.random() * 24);
}
// --- Spawning points ---
pointTimer--;
if (pointTimer <= 0) {
var pt = new PointItem();
var minX = 120 + pt.width / 2;
var maxX = GAME_W - pt.width / 2;
pt.x = minX + Math.random() * (maxX - minX);
pt.y = -pt.height / 2;
pt.speed = 10 + LK.getScore() * 0.10;
points.push(pt);
game.addChild(pt);
// Next spawn: 60-120 ticks
pointTimer = 60 + Math.floor(Math.random() * 60);
}
// --- Update obstacles ---
for (var i = obstacles.length - 1; i >= 0; i--) {
var o = obstacles[i];
o.update();
// Remove if off screen
if (o.y - o.height / 2 > GAME_H + 40) {
o.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with player
if (o.intersects(faceChar)) {
// Game over
isDead = true;
faceChar.flash();
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
return;
}
}
// --- Update points ---
for (var j = points.length - 1; j >= 0; j--) {
var p = points[j];
p.update();
// Remove if off screen
if (p.y - p.height / 2 > GAME_H + 40) {
p.destroy();
points.splice(j, 1);
continue;
}
// Collect
if (p.intersects(faceChar)) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
// Play point collect sound
LK.getSound('pointCollect').play();
// Feedback
tween(p, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 180,
onFinish: function onFinish() {
p.destroy();
}
});
points.splice(j, 1);
continue;
}
}
};
// Reset state on new game
game.on('reset', function () {
// Remove all obstacles and points
for (var i = 0; i < obstacles.length; i++) obstacles[i].destroy();
for (var j = 0; j < points.length; j++) points[j].destroy();
obstacles = [];
points = [];
isDead = false;
LK.setScore(0);
scoreTxt.setText('0');
// Reset faceChar position
faceChar.x = GAME_W / 2;
faceChar.y = GAME_H - 400;
lastFaceX = faceChar.x;
lastFaceY = faceChar.y;
obstacleTimer = 30;
pointTimer = 60;
});
// Also reset on game over (for safety)
game.on('gameover', function () {
isDead = true;
});
// Touch fallback: allow dragging character if facekit not available
var dragNode = null;
function handleMove(x, y, obj) {
if (dragNode) {
// Clamp to game area
var charR = faceChar.width / 2;
var nx = clamp(x, charR, GAME_W - charR);
var ny = clamp(y, charR + 120, GAME_H - charR - 40);
faceChar.x = nx;
faceChar.y = ny;
lastFaceX = nx;
lastFaceY = ny;
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
// Only allow drag if facekit not available
if (!facekit.mouthCenter || !facekit.mouthCenter.x) {
dragNode = faceChar;
handleMove(x, y, obj);
}
};
game.up = function (x, y, obj) {
dragNode = null;
};