/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var facekit = LK.import("@upit/facekit.v1"); /**** * Classes ****/ // Note class var Note = Container.expand(function () { var self = Container.call(this); self.direction = 'up'; // will be set after creation self.speed = 12; // pixels per frame, reduced for easier gameplay self.hit = false; self.active = true; // Attach asset self.noteAsset = null; // will be set after direction is set // For hit animation self.flashTween = null; // Called every tick self.update = function () { if (!self.active) { return; } var vec = getNoteTravelVec(self.direction); self.x += vec.x * self.speed; self.y += vec.y * self.speed; }; // Animate on hit self.animateHit = function () { self.active = false; if (self.flashTween) { tween.stop(self, { alpha: true, scaleX: true, scaleY: true }); } tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 180, easing: tween.cubicOut, onFinish: function onFinish() { self.destroy(); } }); }; // Animate on miss self.animateMiss = function () { self.active = false; if (self.flashTween) { tween.stop(self, { alpha: true, scaleX: true, scaleY: true }); } tween(self, { alpha: 0.2 }, { duration: 180, easing: tween.cubicOut, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); // Particle effect for center hit var ParticleEffect = Container.expand(function () { var self = Container.call(this); self.particles = []; self.duration = 350; self.numParticles = 18; self.radius = 240; // 3x original (was 80) self.createParticles = function (x, y) { for (var i = 0; i < self.numParticles; i++) { var angle = 2 * Math.PI * i / self.numParticles; var px = x + Math.cos(angle) * 30; // 3x original (was 10) var py = y + Math.sin(angle) * 30; // 3x original (was 10) var color = i % 2 === 0 ? 0xfff176 : 0xff8a65; var part = LK.getAsset('centerTarget', { anchorX: 0.5, anchorY: 0.5, x: px, y: py, scaleX: (0.18 + Math.random() * 0.12) * 3, // 3x original scaleY: (0.18 + Math.random() * 0.12) * 3, // 3x original alpha: 0.85, tint: color }); self.addChild(part); // Animate outward and fade var tx = x + Math.cos(angle) * (self.radius + Math.random() * 90); // 3x original (was 30) var ty = y + Math.sin(angle) * (self.radius + Math.random() * 90); // 3x original (was 30) tween(part, { x: tx, y: ty, alpha: 0, scaleX: part.scaleX * 1.5, scaleY: part.scaleY * 1.5 }, { duration: self.duration, easing: tween.cubicOut, onFinish: function (p) { return function () { if (p.parent) p.parent.removeChild(p); }; }(part) }); self.particles.push(part); } // Remove effect container after animation tween(self, {}, { duration: self.duration + 10, onFinish: function onFinish() { if (self.parent) self.parent.removeChild(self); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181c20 }); /**** * Game Code ****/ // Heart image for life display // Center of play area // Four note shapes for each direction // Sounds for hit and miss // Music (placeholder, actual music asset id will be auto-loaded) // Note directions // Left note images (6 total) // Right note images (6 total) var NOTE_DIRECTIONS = ['left', 'right']; var NOTE_ASSET = { left: ['noteLeft', 'noteLeft2', 'noteLeft3', 'noteLeft4', 'noteLeft5', 'noteLeft6'], right: ['noteRight', 'noteRight2', 'noteRight3', 'noteRight4', 'noteRight5', 'noteRight6'] }; // Note spawn positions (offscreen, moving toward center) function getNoteSpawnPos(direction) { var centerX = 2048 / 2, centerY = 2732 / 2; var offset = 900; // How far from center to spawn if (direction === 'left') { return { x: centerX - offset, y: centerY }; } if (direction === 'right') { return { x: centerX + offset, y: centerY }; } return { x: centerX, y: centerY }; } // Note travel vector (unit vector toward center) function getNoteTravelVec(direction) { if (direction === 'left') { return { x: 1, y: 0 }; } if (direction === 'right') { return { x: -1, y: 0 }; } return { x: 0, y: 0 }; } var centerX = 2048 / 2, centerY = 2732 / 2; // Add transparent center note path line (asset: centerLine) var pathLineAlpha = 0.25; var pathLineWidth = 300; var pathLineLength = 2000; var pathLineColor = 0xffffff; // Center path line (asset: centerLine) var centerPathLine = LK.getAsset('centerLine', { anchorX: 0.5, anchorY: 0.5, width: pathLineLength, height: pathLineWidth, x: centerX, y: centerY, alpha: pathLineAlpha, reference: 'centerPathLine' // asset reference slot }); centerPathLine.rotation = 0; // horizontal game.addChild(centerPathLine); // Add center target var centerTarget = LK.getAsset('centerTarget', { anchorX: 0.5, anchorY: 0.5, x: centerX, y: centerY, scaleX: 1, scaleY: 1 }); game.addChild(centerTarget); // Score display var score = 0; var scoreTxt = new Text2('0', { size: 170, fill: { type: 'linear', colors: ['#ffecb3', '#ffd740', '#ff6f00'], stops: [0, 0.5, 1] }, stroke: '#ffb300', strokeThickness: 12, dropShadow: true, dropShadowColor: '#ff6f00', dropShadowDistance: 6, dropShadowAngle: Math.PI / 2, dropShadowAlpha: 0.7, fontWeight: 'bold' }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Combo display var combo = 0; var comboTxt = new Text2('', { size: 110, fill: { type: 'linear', colors: ['#b2ff59', '#00e676', '#00bfae'], stops: [0, 0.5, 1] }, stroke: '#00bfae', strokeThickness: 10, dropShadow: true, dropShadowColor: '#00bfae', dropShadowDistance: 5, dropShadowAngle: Math.PI / 2, dropShadowAlpha: 0.6, fontWeight: 'bold' }); comboTxt.anchor.set(0.5, 0); LK.gui.top.addChild(comboTxt); comboTxt.y = 150; // Remaining life display (top right) - as heart images var maxHearts = 5; var heartImages = []; var heartSpacing = 18; var heartStartY = 30; var heartStartX = LK.gui.topRight.width - 30; var maxMisses = maxHearts; // always 5 function updateLifeNotes() { // Remove all old hearts for (var i = 0; i < heartImages.length; i++) { if (heartImages[i].parent) { heartImages[i].parent.removeChild(heartImages[i]); } } heartImages = []; // Add hearts for remaining life var remaining = maxHearts - misses; for (var i = 0; i < remaining; i++) { var heart = LK.getAsset('heart', { anchorX: 1, anchorY: 0, x: heartStartX, y: heartStartY + i * (120 + heartSpacing), scaleX: 1, scaleY: 1, alpha: 1 }); LK.gui.topRight.addChild(heart); heartImages.push(heart); } } updateLifeNotes(); // Miss feedback var missTxt = new Text2('', { size: 100, fill: '#e57373' }); missTxt.anchor.set(0.5, 0.5); LK.gui.center.addChild(missTxt); // Notes array var notes = []; // Timing var noteInterval = 38; // frames between notes (about 1.5 notes/sec at 60fps) var noteTimer = 0; // Game state var isGameOver = false; var isYouWin = false; var maxMisses = 5; var misses = 0; var targetScore = 50; // Tutorial state var isTutorial = true; var tutorialStep = 0; // 0: show left, 1: wait left, 2: show right, 3: wait right, 4: done var tutorialTxt = new Text2('', { size: 120, fill: { type: 'linear', colors: ['#fffde7', '#ffd54f', '#ff7043'], stops: [0, 0.5, 1] }, stroke: '#ffb300', strokeThickness: 10, dropShadow: true, dropShadowColor: '#ff6f00', dropShadowDistance: 6, dropShadowAngle: Math.PI / 2, dropShadowAlpha: 0.7, fontWeight: 'bold' }); tutorialTxt.anchor.set(0.5, 0.5); LK.gui.center.addChild(tutorialTxt); tutorialTxt.x = LK.gui.center.width / 2; tutorialTxt.y = LK.gui.center.height / 2 - 200; tutorialTxt.visible = true; // Show left arrow image for tutorial var tutLeftImg = LK.getAsset('noteLeft', { anchorX: 0.5, anchorY: 0.5, x: LK.gui.center.width / 2 - 300, y: LK.gui.center.height / 2 - 200, scaleX: 1.2, scaleY: 1.2, alpha: 0.9 }); tutLeftImg.rotation = -Math.PI / 2; LK.gui.center.addChild(tutLeftImg); tutLeftImg.visible = false; // Show right arrow image for tutorial var tutRightImg = LK.getAsset('noteRight', { anchorX: 0.5, anchorY: 0.5, x: LK.gui.center.width / 2 + 300, y: LK.gui.center.height / 2 - 200, scaleX: 1.2, scaleY: 1.2, alpha: 0.9 }); tutRightImg.rotation = Math.PI / 2; LK.gui.center.addChild(tutRightImg); tutRightImg.visible = false; // Face direction detection function getFaceDirection() { // Use noseTip and chin to estimate tilt // If facekit is not ready, return null if (!facekit.noseTip || !facekit.chin || !facekit.leftEye || !facekit.rightEye) { return null; } var dx = facekit.rightEye.x - facekit.leftEye.x; var dy = facekit.rightEye.y - facekit.leftEye.y; var angle = Math.atan2(dy, dx) * 180 / Math.PI; // horizontal head tilt // Left/right: compare noseTip.x to center var horizontal = facekit.noseTip.x - centerX; // Heuristics: prioritize strong tilts if (horizontal < -80) { return 'left'; } if (horizontal > 80) { return 'right'; } // If head is turned, use angle if (angle < -25) { return 'right'; } if (angle > 25) { return 'left'; } return null; } // Show combo function showCombo() { if (combo > 1) { comboTxt.setText(combo + 'x Combo!'); // Animate combo pop comboTxt.scale.set(1.3, 1.3); tween(comboTxt, { scaleX: 1, scaleY: 1 }, { duration: 180, easing: tween.cubicOut }); } else { comboTxt.setText(''); } } // Show miss function showMiss() { missTxt.setText('Miss!'); tween(missTxt, { alpha: 0 }, { duration: 600, onFinish: function onFinish() { missTxt.setText(''); missTxt.alpha = 1; } }); } // Spawn a note function spawnNote() { var dir = NOTE_DIRECTIONS[Math.floor(Math.random() * NOTE_DIRECTIONS.length)]; var note = new Note(); note.direction = dir; var spawn = getNoteSpawnPos(dir); note.x = spawn.x; note.y = spawn.y; // Randomly select a note image for this direction var assetList = NOTE_ASSET[dir]; var assetId = assetList[Math.floor(Math.random() * assetList.length)]; note.noteAsset = note.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Rotate asset to point toward center if (dir === 'left') { note.noteAsset.rotation = -Math.PI / 2; } if (dir === 'right') { note.noteAsset.rotation = Math.PI / 2; } notes.push(note); game.addChild(note); } // Check if note is in hit window (distance to center) function isNoteHittable(note) { var dx = note.x - centerX; var dy = note.y - centerY; var dist = Math.sqrt(dx * dx + dy * dy); return dist < 180; // hit window radius (increased for easier hits) } // Check if note is missed (passed center) function isNoteMissed(note) { var dx = note.x - centerX; var dy = note.y - centerY; var dist = Math.sqrt(dx * dx + dy * dy); return dist < 20; // too close, missed (smaller, so player has more time to hit) } // Main update loop game.update = function () { if (isGameOver || isYouWin) { return; } // Tutorial logic if (isTutorial) { var faceDir = getFaceDirection(); // Step 0: Show "Tilt your face LEFT" and left arrow if (tutorialStep === 0) { tutorialTxt.setText('Tilt your face LEFT'); tutLeftImg.visible = true; tutRightImg.visible = false; // Wait for left tilt if (faceDir === 'left') { tutorialStep = 1; // Animate arrow and text tween(tutLeftImg, { scaleX: 1.7, scaleY: 1.7, alpha: 0.2 }, { duration: 350, onFinish: function onFinish() { tutLeftImg.visible = false; tutLeftImg.scaleX = 1.2; tutLeftImg.scaleY = 1.2; tutLeftImg.alpha = 0.9; } }); tween(tutorialTxt, { scaleX: 1.2, scaleY: 1.2 }, { duration: 180, yoyo: true, repeat: 1, onFinish: function onFinish() { tutorialTxt.scaleX = 1; tutorialTxt.scaleY = 1; } }); // Short delay before next step LK.setTimeout(function () { tutorialStep = 2; }, 400); } return; } // Step 2: Show "Tilt your face RIGHT" and right arrow if (tutorialStep === 2) { tutorialTxt.setText('Tilt your face RIGHT'); tutLeftImg.visible = false; tutRightImg.visible = true; if (faceDir === 'right') { tutorialStep = 3; tween(tutRightImg, { scaleX: 1.7, scaleY: 1.7, alpha: 0.2 }, { duration: 350, onFinish: function onFinish() { tutRightImg.visible = false; tutRightImg.scaleX = 1.2; tutRightImg.scaleY = 1.2; tutRightImg.alpha = 0.9; } }); tween(tutorialTxt, { scaleX: 1.2, scaleY: 1.2 }, { duration: 180, yoyo: true, repeat: 1, onFinish: function onFinish() { tutorialTxt.scaleX = 1; tutorialTxt.scaleY = 1; } }); LK.setTimeout(function () { tutorialStep = 4; }, 400); } return; } // Step 4: Done, hide tutorial and start game if (tutorialStep === 4) { tutorialTxt.setText('Great! Get ready...'); tutLeftImg.visible = false; tutRightImg.visible = false; LK.setTimeout(function () { tutorialTxt.visible = false; isTutorial = false; }, 700); tutorialStep = 5; return; } // During tutorial, do not run game logic return; } // Spawn notes noteTimer++; if (noteTimer >= noteInterval) { spawnNote(); noteTimer = 0; } // Get current face direction var faceDir = getFaceDirection(); // For each note: move, check for hit/miss for (var i = notes.length - 1; i >= 0; i--) { var note = notes[i]; note.update(); if (!note.active) { notes.splice(i, 1); continue; } // If note is in hit window and faceDir matches, hit! if (!note.hit && isNoteHittable(note) && faceDir === note.direction) { note.hit = true; note.animateHit(); // Particle effect at center var effect = new ParticleEffect(); effect.createParticles(centerX, centerY); game.addChild(effect); // On hit, resume music LK.playMusic(currentMusicId); score++; combo++; showCombo(); scoreTxt.setText(score); if (score >= targetScore) { isYouWin = true; LK.showYouWin(); return; } continue; } // If note passes center and not hit, miss // Only trigger miss the moment the note crosses the miss threshold (not repeatedly) if (!note.hit && note.lastMissed !== true && isNoteMissed(note)) { note.hit = false; note.lastMissed = true; note.animateMiss(); // On miss, stop music LK.stopMusic(); misses++; combo = 0; showCombo(); showMiss(); updateLifeNotes(); if (misses >= maxMisses) { isGameOver = true; LK.effects.flashScreen(0xff0000, 800); LK.showGameOver(); return; } continue; } else if (!note.hit && note.lastMissed !== true) { // Track that note is not yet missed note.lastMissed = false; } } }; // Music pool var BGMUSIC_IDS = ['bgmusic', 'bgmusic2', 'bgmusic3', 'bgmusic4', 'bgmusic5', 'bgmusic6']; var currentMusicId = BGMUSIC_IDS[Math.floor(Math.random() * BGMUSIC_IDS.length)]; LK.playMusic(currentMusicId, { fade: { start: 0.1, end: 1, duration: 100 } }); // Reset state on game restart game.on('reset', function () { score = 0; combo = 0; misses = 0; isGameOver = false; isYouWin = false; notes = []; scoreTxt.setText('0'); comboTxt.setText(''); missTxt.setText(''); noteTimer = 0; updateLifeNotes(); // Reset tutorial state isTutorial = true; tutorialStep = 0; tutorialTxt.visible = true; tutorialTxt.setText(''); tutLeftImg.visible = false; tutRightImg.visible = false; // Pick a new random music for the next level currentMusicId = BGMUSIC_IDS[Math.floor(Math.random() * BGMUSIC_IDS.length)]; LK.playMusic(currentMusicId, { fade: { start: 0.1, end: 1, duration: 100 } }); }); // No touch controls needed; game is face-only // Make sure all elements are visible and not in top left 100x100 centerTarget.x = centerX; centerTarget.y = centerY; scoreTxt.x = LK.gui.top.width / 2; scoreTxt.y = 30; comboTxt.x = LK.gui.top.width / 2; comboTxt.y = scoreTxt.y + scoreTxt.height + 10; missTxt.x = LK.gui.center.width / 2; missTxt.y = LK.gui.center.height / 2; ;
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
// Note class
var Note = Container.expand(function () {
var self = Container.call(this);
self.direction = 'up'; // will be set after creation
self.speed = 12; // pixels per frame, reduced for easier gameplay
self.hit = false;
self.active = true;
// Attach asset
self.noteAsset = null; // will be set after direction is set
// For hit animation
self.flashTween = null;
// Called every tick
self.update = function () {
if (!self.active) {
return;
}
var vec = getNoteTravelVec(self.direction);
self.x += vec.x * self.speed;
self.y += vec.y * self.speed;
};
// Animate on hit
self.animateHit = function () {
self.active = false;
if (self.flashTween) {
tween.stop(self, {
alpha: true,
scaleX: true,
scaleY: true
});
}
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 180,
easing: tween.cubicOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
// Animate on miss
self.animateMiss = function () {
self.active = false;
if (self.flashTween) {
tween.stop(self, {
alpha: true,
scaleX: true,
scaleY: true
});
}
tween(self, {
alpha: 0.2
}, {
duration: 180,
easing: tween.cubicOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Particle effect for center hit
var ParticleEffect = Container.expand(function () {
var self = Container.call(this);
self.particles = [];
self.duration = 350;
self.numParticles = 18;
self.radius = 240; // 3x original (was 80)
self.createParticles = function (x, y) {
for (var i = 0; i < self.numParticles; i++) {
var angle = 2 * Math.PI * i / self.numParticles;
var px = x + Math.cos(angle) * 30; // 3x original (was 10)
var py = y + Math.sin(angle) * 30; // 3x original (was 10)
var color = i % 2 === 0 ? 0xfff176 : 0xff8a65;
var part = LK.getAsset('centerTarget', {
anchorX: 0.5,
anchorY: 0.5,
x: px,
y: py,
scaleX: (0.18 + Math.random() * 0.12) * 3,
// 3x original
scaleY: (0.18 + Math.random() * 0.12) * 3,
// 3x original
alpha: 0.85,
tint: color
});
self.addChild(part);
// Animate outward and fade
var tx = x + Math.cos(angle) * (self.radius + Math.random() * 90); // 3x original (was 30)
var ty = y + Math.sin(angle) * (self.radius + Math.random() * 90); // 3x original (was 30)
tween(part, {
x: tx,
y: ty,
alpha: 0,
scaleX: part.scaleX * 1.5,
scaleY: part.scaleY * 1.5
}, {
duration: self.duration,
easing: tween.cubicOut,
onFinish: function (p) {
return function () {
if (p.parent) p.parent.removeChild(p);
};
}(part)
});
self.particles.push(part);
}
// Remove effect container after animation
tween(self, {}, {
duration: self.duration + 10,
onFinish: function onFinish() {
if (self.parent) self.parent.removeChild(self);
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181c20
});
/****
* Game Code
****/
// Heart image for life display
// Center of play area
// Four note shapes for each direction
// Sounds for hit and miss
// Music (placeholder, actual music asset id will be auto-loaded)
// Note directions
// Left note images (6 total)
// Right note images (6 total)
var NOTE_DIRECTIONS = ['left', 'right'];
var NOTE_ASSET = {
left: ['noteLeft', 'noteLeft2', 'noteLeft3', 'noteLeft4', 'noteLeft5', 'noteLeft6'],
right: ['noteRight', 'noteRight2', 'noteRight3', 'noteRight4', 'noteRight5', 'noteRight6']
};
// Note spawn positions (offscreen, moving toward center)
function getNoteSpawnPos(direction) {
var centerX = 2048 / 2,
centerY = 2732 / 2;
var offset = 900; // How far from center to spawn
if (direction === 'left') {
return {
x: centerX - offset,
y: centerY
};
}
if (direction === 'right') {
return {
x: centerX + offset,
y: centerY
};
}
return {
x: centerX,
y: centerY
};
}
// Note travel vector (unit vector toward center)
function getNoteTravelVec(direction) {
if (direction === 'left') {
return {
x: 1,
y: 0
};
}
if (direction === 'right') {
return {
x: -1,
y: 0
};
}
return {
x: 0,
y: 0
};
}
var centerX = 2048 / 2,
centerY = 2732 / 2;
// Add transparent center note path line (asset: centerLine)
var pathLineAlpha = 0.25;
var pathLineWidth = 300;
var pathLineLength = 2000;
var pathLineColor = 0xffffff;
// Center path line (asset: centerLine)
var centerPathLine = LK.getAsset('centerLine', {
anchorX: 0.5,
anchorY: 0.5,
width: pathLineLength,
height: pathLineWidth,
x: centerX,
y: centerY,
alpha: pathLineAlpha,
reference: 'centerPathLine' // asset reference slot
});
centerPathLine.rotation = 0; // horizontal
game.addChild(centerPathLine);
// Add center target
var centerTarget = LK.getAsset('centerTarget', {
anchorX: 0.5,
anchorY: 0.5,
x: centerX,
y: centerY,
scaleX: 1,
scaleY: 1
});
game.addChild(centerTarget);
// Score display
var score = 0;
var scoreTxt = new Text2('0', {
size: 170,
fill: {
type: 'linear',
colors: ['#ffecb3', '#ffd740', '#ff6f00'],
stops: [0, 0.5, 1]
},
stroke: '#ffb300',
strokeThickness: 12,
dropShadow: true,
dropShadowColor: '#ff6f00',
dropShadowDistance: 6,
dropShadowAngle: Math.PI / 2,
dropShadowAlpha: 0.7,
fontWeight: 'bold'
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Combo display
var combo = 0;
var comboTxt = new Text2('', {
size: 110,
fill: {
type: 'linear',
colors: ['#b2ff59', '#00e676', '#00bfae'],
stops: [0, 0.5, 1]
},
stroke: '#00bfae',
strokeThickness: 10,
dropShadow: true,
dropShadowColor: '#00bfae',
dropShadowDistance: 5,
dropShadowAngle: Math.PI / 2,
dropShadowAlpha: 0.6,
fontWeight: 'bold'
});
comboTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(comboTxt);
comboTxt.y = 150;
// Remaining life display (top right) - as heart images
var maxHearts = 5;
var heartImages = [];
var heartSpacing = 18;
var heartStartY = 30;
var heartStartX = LK.gui.topRight.width - 30;
var maxMisses = maxHearts; // always 5
function updateLifeNotes() {
// Remove all old hearts
for (var i = 0; i < heartImages.length; i++) {
if (heartImages[i].parent) {
heartImages[i].parent.removeChild(heartImages[i]);
}
}
heartImages = [];
// Add hearts for remaining life
var remaining = maxHearts - misses;
for (var i = 0; i < remaining; i++) {
var heart = LK.getAsset('heart', {
anchorX: 1,
anchorY: 0,
x: heartStartX,
y: heartStartY + i * (120 + heartSpacing),
scaleX: 1,
scaleY: 1,
alpha: 1
});
LK.gui.topRight.addChild(heart);
heartImages.push(heart);
}
}
updateLifeNotes();
// Miss feedback
var missTxt = new Text2('', {
size: 100,
fill: '#e57373'
});
missTxt.anchor.set(0.5, 0.5);
LK.gui.center.addChild(missTxt);
// Notes array
var notes = [];
// Timing
var noteInterval = 38; // frames between notes (about 1.5 notes/sec at 60fps)
var noteTimer = 0;
// Game state
var isGameOver = false;
var isYouWin = false;
var maxMisses = 5;
var misses = 0;
var targetScore = 50;
// Tutorial state
var isTutorial = true;
var tutorialStep = 0; // 0: show left, 1: wait left, 2: show right, 3: wait right, 4: done
var tutorialTxt = new Text2('', {
size: 120,
fill: {
type: 'linear',
colors: ['#fffde7', '#ffd54f', '#ff7043'],
stops: [0, 0.5, 1]
},
stroke: '#ffb300',
strokeThickness: 10,
dropShadow: true,
dropShadowColor: '#ff6f00',
dropShadowDistance: 6,
dropShadowAngle: Math.PI / 2,
dropShadowAlpha: 0.7,
fontWeight: 'bold'
});
tutorialTxt.anchor.set(0.5, 0.5);
LK.gui.center.addChild(tutorialTxt);
tutorialTxt.x = LK.gui.center.width / 2;
tutorialTxt.y = LK.gui.center.height / 2 - 200;
tutorialTxt.visible = true;
// Show left arrow image for tutorial
var tutLeftImg = LK.getAsset('noteLeft', {
anchorX: 0.5,
anchorY: 0.5,
x: LK.gui.center.width / 2 - 300,
y: LK.gui.center.height / 2 - 200,
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.9
});
tutLeftImg.rotation = -Math.PI / 2;
LK.gui.center.addChild(tutLeftImg);
tutLeftImg.visible = false;
// Show right arrow image for tutorial
var tutRightImg = LK.getAsset('noteRight', {
anchorX: 0.5,
anchorY: 0.5,
x: LK.gui.center.width / 2 + 300,
y: LK.gui.center.height / 2 - 200,
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.9
});
tutRightImg.rotation = Math.PI / 2;
LK.gui.center.addChild(tutRightImg);
tutRightImg.visible = false;
// Face direction detection
function getFaceDirection() {
// Use noseTip and chin to estimate tilt
// If facekit is not ready, return null
if (!facekit.noseTip || !facekit.chin || !facekit.leftEye || !facekit.rightEye) {
return null;
}
var dx = facekit.rightEye.x - facekit.leftEye.x;
var dy = facekit.rightEye.y - facekit.leftEye.y;
var angle = Math.atan2(dy, dx) * 180 / Math.PI; // horizontal head tilt
// Left/right: compare noseTip.x to center
var horizontal = facekit.noseTip.x - centerX;
// Heuristics: prioritize strong tilts
if (horizontal < -80) {
return 'left';
}
if (horizontal > 80) {
return 'right';
}
// If head is turned, use angle
if (angle < -25) {
return 'right';
}
if (angle > 25) {
return 'left';
}
return null;
}
// Show combo
function showCombo() {
if (combo > 1) {
comboTxt.setText(combo + 'x Combo!');
// Animate combo pop
comboTxt.scale.set(1.3, 1.3);
tween(comboTxt, {
scaleX: 1,
scaleY: 1
}, {
duration: 180,
easing: tween.cubicOut
});
} else {
comboTxt.setText('');
}
}
// Show miss
function showMiss() {
missTxt.setText('Miss!');
tween(missTxt, {
alpha: 0
}, {
duration: 600,
onFinish: function onFinish() {
missTxt.setText('');
missTxt.alpha = 1;
}
});
}
// Spawn a note
function spawnNote() {
var dir = NOTE_DIRECTIONS[Math.floor(Math.random() * NOTE_DIRECTIONS.length)];
var note = new Note();
note.direction = dir;
var spawn = getNoteSpawnPos(dir);
note.x = spawn.x;
note.y = spawn.y;
// Randomly select a note image for this direction
var assetList = NOTE_ASSET[dir];
var assetId = assetList[Math.floor(Math.random() * assetList.length)];
note.noteAsset = note.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Rotate asset to point toward center
if (dir === 'left') {
note.noteAsset.rotation = -Math.PI / 2;
}
if (dir === 'right') {
note.noteAsset.rotation = Math.PI / 2;
}
notes.push(note);
game.addChild(note);
}
// Check if note is in hit window (distance to center)
function isNoteHittable(note) {
var dx = note.x - centerX;
var dy = note.y - centerY;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < 180; // hit window radius (increased for easier hits)
}
// Check if note is missed (passed center)
function isNoteMissed(note) {
var dx = note.x - centerX;
var dy = note.y - centerY;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < 20; // too close, missed (smaller, so player has more time to hit)
}
// Main update loop
game.update = function () {
if (isGameOver || isYouWin) {
return;
}
// Tutorial logic
if (isTutorial) {
var faceDir = getFaceDirection();
// Step 0: Show "Tilt your face LEFT" and left arrow
if (tutorialStep === 0) {
tutorialTxt.setText('Tilt your face LEFT');
tutLeftImg.visible = true;
tutRightImg.visible = false;
// Wait for left tilt
if (faceDir === 'left') {
tutorialStep = 1;
// Animate arrow and text
tween(tutLeftImg, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0.2
}, {
duration: 350,
onFinish: function onFinish() {
tutLeftImg.visible = false;
tutLeftImg.scaleX = 1.2;
tutLeftImg.scaleY = 1.2;
tutLeftImg.alpha = 0.9;
}
});
tween(tutorialTxt, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 180,
yoyo: true,
repeat: 1,
onFinish: function onFinish() {
tutorialTxt.scaleX = 1;
tutorialTxt.scaleY = 1;
}
});
// Short delay before next step
LK.setTimeout(function () {
tutorialStep = 2;
}, 400);
}
return;
}
// Step 2: Show "Tilt your face RIGHT" and right arrow
if (tutorialStep === 2) {
tutorialTxt.setText('Tilt your face RIGHT');
tutLeftImg.visible = false;
tutRightImg.visible = true;
if (faceDir === 'right') {
tutorialStep = 3;
tween(tutRightImg, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0.2
}, {
duration: 350,
onFinish: function onFinish() {
tutRightImg.visible = false;
tutRightImg.scaleX = 1.2;
tutRightImg.scaleY = 1.2;
tutRightImg.alpha = 0.9;
}
});
tween(tutorialTxt, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 180,
yoyo: true,
repeat: 1,
onFinish: function onFinish() {
tutorialTxt.scaleX = 1;
tutorialTxt.scaleY = 1;
}
});
LK.setTimeout(function () {
tutorialStep = 4;
}, 400);
}
return;
}
// Step 4: Done, hide tutorial and start game
if (tutorialStep === 4) {
tutorialTxt.setText('Great! Get ready...');
tutLeftImg.visible = false;
tutRightImg.visible = false;
LK.setTimeout(function () {
tutorialTxt.visible = false;
isTutorial = false;
}, 700);
tutorialStep = 5;
return;
}
// During tutorial, do not run game logic
return;
}
// Spawn notes
noteTimer++;
if (noteTimer >= noteInterval) {
spawnNote();
noteTimer = 0;
}
// Get current face direction
var faceDir = getFaceDirection();
// For each note: move, check for hit/miss
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
note.update();
if (!note.active) {
notes.splice(i, 1);
continue;
}
// If note is in hit window and faceDir matches, hit!
if (!note.hit && isNoteHittable(note) && faceDir === note.direction) {
note.hit = true;
note.animateHit();
// Particle effect at center
var effect = new ParticleEffect();
effect.createParticles(centerX, centerY);
game.addChild(effect);
// On hit, resume music
LK.playMusic(currentMusicId);
score++;
combo++;
showCombo();
scoreTxt.setText(score);
if (score >= targetScore) {
isYouWin = true;
LK.showYouWin();
return;
}
continue;
}
// If note passes center and not hit, miss
// Only trigger miss the moment the note crosses the miss threshold (not repeatedly)
if (!note.hit && note.lastMissed !== true && isNoteMissed(note)) {
note.hit = false;
note.lastMissed = true;
note.animateMiss();
// On miss, stop music
LK.stopMusic();
misses++;
combo = 0;
showCombo();
showMiss();
updateLifeNotes();
if (misses >= maxMisses) {
isGameOver = true;
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
return;
}
continue;
} else if (!note.hit && note.lastMissed !== true) {
// Track that note is not yet missed
note.lastMissed = false;
}
}
};
// Music pool
var BGMUSIC_IDS = ['bgmusic', 'bgmusic2', 'bgmusic3', 'bgmusic4', 'bgmusic5', 'bgmusic6'];
var currentMusicId = BGMUSIC_IDS[Math.floor(Math.random() * BGMUSIC_IDS.length)];
LK.playMusic(currentMusicId, {
fade: {
start: 0.1,
end: 1,
duration: 100
}
});
// Reset state on game restart
game.on('reset', function () {
score = 0;
combo = 0;
misses = 0;
isGameOver = false;
isYouWin = false;
notes = [];
scoreTxt.setText('0');
comboTxt.setText('');
missTxt.setText('');
noteTimer = 0;
updateLifeNotes();
// Reset tutorial state
isTutorial = true;
tutorialStep = 0;
tutorialTxt.visible = true;
tutorialTxt.setText('');
tutLeftImg.visible = false;
tutRightImg.visible = false;
// Pick a new random music for the next level
currentMusicId = BGMUSIC_IDS[Math.floor(Math.random() * BGMUSIC_IDS.length)];
LK.playMusic(currentMusicId, {
fade: {
start: 0.1,
end: 1,
duration: 100
}
});
});
// No touch controls needed; game is face-only
// Make sure all elements are visible and not in top left 100x100
centerTarget.x = centerX;
centerTarget.y = centerY;
scoreTxt.x = LK.gui.top.width / 2;
scoreTxt.y = 30;
comboTxt.x = LK.gui.top.width / 2;
comboTxt.y = scoreTxt.y + scoreTxt.height + 10;
missTxt.x = LK.gui.center.width / 2;
missTxt.y = LK.gui.center.height / 2;
;