User prompt
make it easier
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 't.length')' in or related to this line: 'note.noteAsset = note.attachAsset(NOTE_ASSET[dir], {' Line Number: 252
User prompt
convert 2 way if left tilt or right tilt
User prompt
make it easier
Code edit (1 edits merged)
Please save this source code
User prompt
FaceBeat: 4-Way Rhythm Challenge
User prompt
lets make it 4 way rhythm game if face up hit up if face down hit down if face left hit left and if face right hit right. and notes coming like piano tile from 4 direction and player try to hit them
User prompt
old one better
User prompt
Please continue polishing my design document.
User prompt
lets make it 4 way rhythm game if face up hit up if face down hit down if face left hit left and if face right hit right. and notes coming like piano tile from 4 direction and player try to hit them
User prompt
do u track hands
User prompt
reverse it each time hit some notes played
User prompt
ı want to create wıth face trackıng
Initial prompt
know the game called osu
/**** * 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;
;