/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
// Obstacle class: moves downward, destroys itself off-screen
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obsGfx = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 12 + Math.random() * 6; // pixels per frame, slower base speed
self.update = function () {
self.y += self.speed;
};
return self;
});
// Player class: follows face position
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGfx = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
// For possible future effects
self.flash = function () {
tween(self, {
alpha: 0.3
}, {
duration: 80,
onFinish: function onFinish() {
tween(self, {
alpha: 1
}, {
duration: 120
});
}
});
};
return self;
});
// Powerup class: moves downward, destroys itself off-screen
var Powerup = Container.expand(function () {
var self = Container.call(this);
var powGfx = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 14 + Math.random() * 6;
self.update = function () {
self.y += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181c2c
});
/****
* Game Code
****/
// Powerup: Green ellipse
// Obstacle: Red box
// Character: Player avatar (ellipse, bright color)
// Game area margins (to avoid top-left menu)
var marginTop = 100;
var marginLeft = 100;
var marginRight = 100;
var marginBottom = 100;
// Player setup
var player = new Player();
game.addChild(player);
// Start in center
player.x = 2048 / 2;
player.y = 2732 - 400;
// Score display
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Powerup timer display (hidden by default)
var powerupTxt = new Text2('', {
size: 80,
fill: 0xFFE066
});
powerupTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(powerupTxt);
powerupTxt.visible = false;
// Game state
var obstacles = [];
var powerups = [];
var powerupActive = false;
var powerupTimer = 0;
var powerupDuration = 180; // frames (3 seconds)
var score = 0;
var ticks = 0;
var gameOver = false;
// --- Speed scaling and rare obstacle pass-through ---
var speedScale = 1.0; // Start slow, increases with powerups
var rarePassThroughActive = false;
var rarePassThroughTimer = 0;
var rarePassThroughDuration = 120; // frames (2 seconds)
// Helper: clamp value
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Main update loop
game.update = function () {
if (gameOver) return;
ticks++;
// --- Face tracking: update player position ---
// Use mouth center if available, else fallback to noseTip
var faceX = facekit.mouthCenter && facekit.mouthCenter.x ? facekit.mouthCenter.x : facekit.noseTip && facekit.noseTip.x ? facekit.noseTip.x : 2048 / 2;
var faceY = facekit.mouthCenter && facekit.mouthCenter.y ? facekit.mouthCenter.y : facekit.noseTip && facekit.noseTip.y ? facekit.noseTip.y : 2732 - 400;
// Clamp to game area (avoid top-left menu and screen edges)
var px = clamp(faceX, marginLeft + player.width / 2, 2048 - marginRight - player.width / 2);
var py = clamp(faceY, marginTop + player.height / 2, 2732 - marginBottom - player.height / 2);
// Smooth movement (lerp)
player.x += (px - player.x) * 0.35;
player.y += (py - player.y) * 0.35;
// --- Spawn obstacles ---
// Increase spawn rate over time
var spawnInterval = Math.max(32, 80 - Math.floor(ticks / 600));
if (ticks % spawnInterval === 0) {
var obs = new Obstacle();
// Random X, avoid edges
obs.x = clamp(180 + Math.random() * (2048 - 360), marginLeft + 90, 2048 - marginRight - 90);
obs.y = -80;
// Speed up as score increases and with powerups
obs.speed = (12 + Math.random() * 6 + Math.floor(score / 10) * 1.2) * speedScale;
// 10% chance: rare pass-through obstacle (if not already active)
if (!rarePassThroughActive && Math.random() < 0.10) {
obs.isPassThrough = true;
obs.alpha = 0.45; // visually faded
obs.tint = 0x8e44ad; // purple tint for rare
} else {
obs.isPassThrough = false;
}
obstacles.push(obs);
game.addChild(obs);
}
// --- Spawn powerups ---
if (!powerupActive && ticks > 120 && ticks % 420 === 0) {
var pow = new Powerup();
pow.x = clamp(180 + Math.random() * (2048 - 360), marginLeft + 90, 2048 - marginRight - 90);
pow.y = -60;
pow.speed = (14 + Math.random() * 6) * speedScale;
powerups.push(pow);
game.addChild(pow);
}
// --- Update obstacles ---
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
obs.update();
// Off-screen
if (obs.y - obs.height / 2 > 2732 + 40) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with player
if (!powerupActive && player.intersects(obs)) {
// If rare obstacle and pass-through is active, skip collision
if (obs.isPassThrough && rarePassThroughActive) {
// No effect, pass through
} else {
player.flash();
LK.effects.flashScreen(0xff0000, 600);
gameOver = true;
LK.setTimeout(function () {
LK.showGameOver();
}, 600);
return;
}
}
}
// --- Update powerups ---
for (var j = powerups.length - 1; j >= 0; j--) {
var pow = powerups[j];
pow.update();
// Off-screen
if (pow.y - pow.height / 2 > 2732 + 40) {
pow.destroy();
powerups.splice(j, 1);
continue;
}
// Collect
if (player.intersects(pow)) {
pow.destroy();
powerups.splice(j, 1);
powerupActive = true;
powerupTimer = powerupDuration;
powerupTxt.visible = true;
// --- Speed up game a little every powerup ---
speedScale = Math.min(speedScale + 0.08, 2.5); // Cap max speed
// --- 25% chance: activate rare pass-through mode for a short time ---
if (!rarePassThroughActive && Math.random() < 0.25) {
rarePassThroughActive = true;
rarePassThroughTimer = rarePassThroughDuration;
powerupTxt.setText("Pass Through!");
powerupTxt.visible = true;
// Visual feedback: player turns purple
tween(player, {
tint: 0x8e44ad
}, {
duration: 120
});
}
// Visual feedback
tween(player, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
onFinish: function onFinish() {
tween(player, {
scaleX: 1,
scaleY: 1
}, {
duration: 200
});
}
});
}
}
// --- Powerup effect ---
if (powerupActive) {
powerupTimer--;
if (rarePassThroughActive) {
powerupTxt.setText("Pass Through: " + Math.ceil(rarePassThroughTimer / 60) + "s");
} else {
powerupTxt.setText("Invincible: " + Math.ceil(powerupTimer / 60) + "s");
}
if (powerupTimer <= 0) {
powerupActive = false;
if (!rarePassThroughActive) powerupTxt.visible = false;
}
}
// --- Rare pass-through effect ---
if (rarePassThroughActive) {
rarePassThroughTimer--;
if (rarePassThroughTimer <= 0) {
rarePassThroughActive = false;
powerupTxt.visible = false;
// Restore player color
tween(player, {
tint: 0x3fa9f5
}, {
duration: 120
});
}
}
// --- Score ---
if (ticks % 6 === 0 && !gameOver) {
score++;
LK.setScore(score);
scoreTxt.setText(score);
}
};
// Reset state on game restart
game.on('reset', function () {
// Remove all obstacles and powerups
for (var i = 0; i < obstacles.length; i++) obstacles[i].destroy();
for (var j = 0; j < powerups.length; j++) powerups[j].destroy();
obstacles = [];
powerups = [];
score = 0;
LK.setScore(0);
scoreTxt.setText('0');
powerupActive = false;
powerupTimer = 0;
powerupTxt.visible = false;
player.x = 2048 / 2;
player.y = 2732 - 400;
gameOver = false;
ticks = 0;
speedScale = 1.0;
rarePassThroughActive = false;
rarePassThroughTimer = 0;
});
// No touch controls: face only, but allow drag fallback for debug
var dragNode = null;
game.down = function (x, y, obj) {
// For debug: allow dragging player if facekit not available
if (!facekit.mouthCenter) dragNode = player;
};
game.move = function (x, y, obj) {
if (dragNode) {
player.x = clamp(x, marginLeft + player.width / 2, 2048 - marginRight - player.width / 2);
player.y = clamp(y, marginTop + player.height / 2, 2732 - marginBottom - player.height / 2);
}
};
game.up = function (x, y, obj) {
dragNode = null;
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
// Obstacle class: moves downward, destroys itself off-screen
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obsGfx = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 12 + Math.random() * 6; // pixels per frame, slower base speed
self.update = function () {
self.y += self.speed;
};
return self;
});
// Player class: follows face position
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGfx = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
// For possible future effects
self.flash = function () {
tween(self, {
alpha: 0.3
}, {
duration: 80,
onFinish: function onFinish() {
tween(self, {
alpha: 1
}, {
duration: 120
});
}
});
};
return self;
});
// Powerup class: moves downward, destroys itself off-screen
var Powerup = Container.expand(function () {
var self = Container.call(this);
var powGfx = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 14 + Math.random() * 6;
self.update = function () {
self.y += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181c2c
});
/****
* Game Code
****/
// Powerup: Green ellipse
// Obstacle: Red box
// Character: Player avatar (ellipse, bright color)
// Game area margins (to avoid top-left menu)
var marginTop = 100;
var marginLeft = 100;
var marginRight = 100;
var marginBottom = 100;
// Player setup
var player = new Player();
game.addChild(player);
// Start in center
player.x = 2048 / 2;
player.y = 2732 - 400;
// Score display
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Powerup timer display (hidden by default)
var powerupTxt = new Text2('', {
size: 80,
fill: 0xFFE066
});
powerupTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(powerupTxt);
powerupTxt.visible = false;
// Game state
var obstacles = [];
var powerups = [];
var powerupActive = false;
var powerupTimer = 0;
var powerupDuration = 180; // frames (3 seconds)
var score = 0;
var ticks = 0;
var gameOver = false;
// --- Speed scaling and rare obstacle pass-through ---
var speedScale = 1.0; // Start slow, increases with powerups
var rarePassThroughActive = false;
var rarePassThroughTimer = 0;
var rarePassThroughDuration = 120; // frames (2 seconds)
// Helper: clamp value
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Main update loop
game.update = function () {
if (gameOver) return;
ticks++;
// --- Face tracking: update player position ---
// Use mouth center if available, else fallback to noseTip
var faceX = facekit.mouthCenter && facekit.mouthCenter.x ? facekit.mouthCenter.x : facekit.noseTip && facekit.noseTip.x ? facekit.noseTip.x : 2048 / 2;
var faceY = facekit.mouthCenter && facekit.mouthCenter.y ? facekit.mouthCenter.y : facekit.noseTip && facekit.noseTip.y ? facekit.noseTip.y : 2732 - 400;
// Clamp to game area (avoid top-left menu and screen edges)
var px = clamp(faceX, marginLeft + player.width / 2, 2048 - marginRight - player.width / 2);
var py = clamp(faceY, marginTop + player.height / 2, 2732 - marginBottom - player.height / 2);
// Smooth movement (lerp)
player.x += (px - player.x) * 0.35;
player.y += (py - player.y) * 0.35;
// --- Spawn obstacles ---
// Increase spawn rate over time
var spawnInterval = Math.max(32, 80 - Math.floor(ticks / 600));
if (ticks % spawnInterval === 0) {
var obs = new Obstacle();
// Random X, avoid edges
obs.x = clamp(180 + Math.random() * (2048 - 360), marginLeft + 90, 2048 - marginRight - 90);
obs.y = -80;
// Speed up as score increases and with powerups
obs.speed = (12 + Math.random() * 6 + Math.floor(score / 10) * 1.2) * speedScale;
// 10% chance: rare pass-through obstacle (if not already active)
if (!rarePassThroughActive && Math.random() < 0.10) {
obs.isPassThrough = true;
obs.alpha = 0.45; // visually faded
obs.tint = 0x8e44ad; // purple tint for rare
} else {
obs.isPassThrough = false;
}
obstacles.push(obs);
game.addChild(obs);
}
// --- Spawn powerups ---
if (!powerupActive && ticks > 120 && ticks % 420 === 0) {
var pow = new Powerup();
pow.x = clamp(180 + Math.random() * (2048 - 360), marginLeft + 90, 2048 - marginRight - 90);
pow.y = -60;
pow.speed = (14 + Math.random() * 6) * speedScale;
powerups.push(pow);
game.addChild(pow);
}
// --- Update obstacles ---
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
obs.update();
// Off-screen
if (obs.y - obs.height / 2 > 2732 + 40) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with player
if (!powerupActive && player.intersects(obs)) {
// If rare obstacle and pass-through is active, skip collision
if (obs.isPassThrough && rarePassThroughActive) {
// No effect, pass through
} else {
player.flash();
LK.effects.flashScreen(0xff0000, 600);
gameOver = true;
LK.setTimeout(function () {
LK.showGameOver();
}, 600);
return;
}
}
}
// --- Update powerups ---
for (var j = powerups.length - 1; j >= 0; j--) {
var pow = powerups[j];
pow.update();
// Off-screen
if (pow.y - pow.height / 2 > 2732 + 40) {
pow.destroy();
powerups.splice(j, 1);
continue;
}
// Collect
if (player.intersects(pow)) {
pow.destroy();
powerups.splice(j, 1);
powerupActive = true;
powerupTimer = powerupDuration;
powerupTxt.visible = true;
// --- Speed up game a little every powerup ---
speedScale = Math.min(speedScale + 0.08, 2.5); // Cap max speed
// --- 25% chance: activate rare pass-through mode for a short time ---
if (!rarePassThroughActive && Math.random() < 0.25) {
rarePassThroughActive = true;
rarePassThroughTimer = rarePassThroughDuration;
powerupTxt.setText("Pass Through!");
powerupTxt.visible = true;
// Visual feedback: player turns purple
tween(player, {
tint: 0x8e44ad
}, {
duration: 120
});
}
// Visual feedback
tween(player, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
onFinish: function onFinish() {
tween(player, {
scaleX: 1,
scaleY: 1
}, {
duration: 200
});
}
});
}
}
// --- Powerup effect ---
if (powerupActive) {
powerupTimer--;
if (rarePassThroughActive) {
powerupTxt.setText("Pass Through: " + Math.ceil(rarePassThroughTimer / 60) + "s");
} else {
powerupTxt.setText("Invincible: " + Math.ceil(powerupTimer / 60) + "s");
}
if (powerupTimer <= 0) {
powerupActive = false;
if (!rarePassThroughActive) powerupTxt.visible = false;
}
}
// --- Rare pass-through effect ---
if (rarePassThroughActive) {
rarePassThroughTimer--;
if (rarePassThroughTimer <= 0) {
rarePassThroughActive = false;
powerupTxt.visible = false;
// Restore player color
tween(player, {
tint: 0x3fa9f5
}, {
duration: 120
});
}
}
// --- Score ---
if (ticks % 6 === 0 && !gameOver) {
score++;
LK.setScore(score);
scoreTxt.setText(score);
}
};
// Reset state on game restart
game.on('reset', function () {
// Remove all obstacles and powerups
for (var i = 0; i < obstacles.length; i++) obstacles[i].destroy();
for (var j = 0; j < powerups.length; j++) powerups[j].destroy();
obstacles = [];
powerups = [];
score = 0;
LK.setScore(0);
scoreTxt.setText('0');
powerupActive = false;
powerupTimer = 0;
powerupTxt.visible = false;
player.x = 2048 / 2;
player.y = 2732 - 400;
gameOver = false;
ticks = 0;
speedScale = 1.0;
rarePassThroughActive = false;
rarePassThroughTimer = 0;
});
// No touch controls: face only, but allow drag fallback for debug
var dragNode = null;
game.down = function (x, y, obj) {
// For debug: allow dragging player if facekit not available
if (!facekit.mouthCenter) dragNode = player;
};
game.move = function (x, y, obj) {
if (dragNode) {
player.x = clamp(x, marginLeft + player.width / 2, 2048 - marginRight - player.width / 2);
player.y = clamp(y, marginTop + player.height / 2, 2732 - marginBottom - player.height / 2);
}
};
game.up = function (x, y, obj) {
dragNode = null;
};