/**** * 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;
};