/**** * Plugins ****/ var facekit = LK.import("@upit/facekit.v1"); var storage = LK.import("@upit/storage.v1", { currentLevel: 1, totalHearts: 0, heartsNeeded: 5 }); var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var SongButton = Container.expand(function (buttonType) { var self = Container.call(this); var buttonAsset = buttonType === 'chanter' ? 'chanterButton' : 'arreterButton'; var buttonGraphics = self.attachAsset(buttonAsset, { anchorX: 0.5, anchorY: 0.5 }); var buttonText = new Text2(buttonType === 'chanter' ? 'CHANTER' : 'ARRÊTER', { size: 40, fill: 0xFFFFFF }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.buttonType = buttonType; self.isPressed = false; self.down = function (x, y, obj) { self.isPressed = true; buttonGraphics.alpha = 0.8; }; self.up = function (x, y, obj) { if (self.isPressed) { if (self.buttonType === 'chanter') { startSinging(); } else { stopSinging(); } } self.isPressed = false; buttonGraphics.alpha = 1.0; }; return self; }); var TinyHeart = Container.expand(function () { var self = Container.call(this); var heartGraphics = self.attachAsset('tinyHeart', { anchorX: 0.5, anchorY: 0.5 }); self.floatSpeed = Math.random() * 2 + 1; self.bobOffset = Math.random() * Math.PI * 2; self.update = function () { self.y -= self.floatSpeed; self.x += Math.sin(LK.ticks * 0.05 + self.bobOffset) * 0.5; if (self.y < -50) { self.destroy(); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xffeef8 }); /**** * Game Code ****/ // Add custom background var customBackground = game.addChild(LK.getAsset('customBackground', { anchorX: 0, anchorY: 0, x: 0, y: 0 })); // Song list var songList = ["Au clair de la lune", "Frère Jacques", "Chandelier", "Blue (Da Ba Dee)", "Diamonds", "On écrit sur les murs", "Tout le bonheur du monde", "L'Oiseau et l'Enfant", "La Marseillaise", "Happy Birthday"]; var currentSong = ""; var isSinging = false; var singingStartTime = 0; var singingDuration = 15000; // 15 seconds per song var volumeHistory = []; var pitchHistory = []; var tinyHearts = []; // UI Elements var heartContainerBorder = game.addChild(LK.getAsset('heartContainerBorder', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 800 })); var heartContainer = game.addChild(LK.getAsset('heartContainer', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 800, alpha: 0.3 })); var chanterButton = game.addChild(new SongButton('chanter')); chanterButton.x = 1024; chanterButton.y = 1500; var arreterButton = game.addChild(new SongButton('arreter')); arreterButton.x = 1024; arreterButton.y = 1600; arreterButton.alpha = 0.5; // Text displays var levelText = new Text2('Niveau: ' + storage.currentLevel, { size: 60, fill: 0x000000 }); levelText.anchor.set(0.5, 0); LK.gui.top.addChild(levelText); levelText.y = 100; var heartsText = new Text2('Cœurs: ' + storage.totalHearts + '/' + storage.heartsNeeded, { size: 50, fill: 0x000000 }); heartsText.anchor.set(0.5, 0); LK.gui.top.addChild(heartsText); heartsText.y = 180; var songText = new Text2(currentSong, { size: 45, fill: 0x333333 }); songText.anchor.set(0.5, 0.5); game.addChild(songText); songText.x = 1024; songText.y = 1200; var progressText = new Text2('', { size: 40, fill: 0x666666 }); progressText.anchor.set(0.5, 0.5); game.addChild(progressText); progressText.x = 1024; progressText.y = 1300; var debugText = new Text2('', { size: 30, fill: 0x444444 }); debugText.anchor.set(0.5, 0.5); game.addChild(debugText); debugText.x = 1024; debugText.y = 1370; function getRandomSong() { return songList[Math.floor(Math.random() * songList.length)]; } function startSinging() { if (isSinging) return; currentSong = getRandomSong(); songText.setText(currentSong); isSinging = true; singingStartTime = LK.ticks; volumeHistory = []; pitchHistory = []; chanterButton.alpha = 0.5; arreterButton.alpha = 1.0; progressText.setText('Chantez maintenant! 🎤'); } function stopSinging() { if (!isSinging) return; isSinging = false; var accuracy = calculateAccuracy(); var heartsEarned = getHeartsFromAccuracy(accuracy); // Always add earned hearts (even if 0) to maintain accurate tracking storage.totalHearts += heartsEarned; // Only create visual effects and play sound if hearts were actually earned if (heartsEarned > 0) { // Create visual hearts only for earned hearts for (var i = 0; i < heartsEarned; i++) { createTinyHeart(); } LK.getSound('heartCollect').play(); } // Check for level up only if player has enough total hearts while (storage.totalHearts >= storage.heartsNeeded) { // Calculate remaining hearts after leveling up var remainingHearts = storage.totalHearts - storage.heartsNeeded; levelUp(); // Set remaining hearts for the new level storage.totalHearts = remainingHearts; } updateUI(); chanterButton.alpha = 1.0; arreterButton.alpha = 0.5; progressText.setText('Score: ' + Math.round(accuracy) + '% - ' + heartsEarned + ' cœurs!'); // Clear song after 3 seconds LK.setTimeout(function () { if (!isSinging) { songText.setText(''); progressText.setText(''); } }, 3000); } function calculateAccuracy() { if (volumeHistory.length === 0) return 0; // Enhanced song patterns with timing and pitch sequences var songPatterns = { "Au clair de la lune": { pitches: [261.63, 261.63, 261.63, 293.66, 329.63, 293.66, 261.63, 329.63, 293.66, 293.66, 261.63], timing: [1, 1, 1, 1, 2, 1, 1, 2, 2, 1, 3], // Relative note durations rhythm: [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1] // 1=sing, 0=pause }, "Frère Jacques": { pitches: [261.63, 293.66, 329.63, 261.63, 261.63, 293.66, 329.63, 261.63], timing: [1, 1, 1, 1, 1, 1, 1, 1], rhythm: [1, 1, 1, 1, 0, 1, 1, 1] }, "Chandelier": { pitches: [220, 246.94, 261.63, 293.66, 329.63, 293.66, 261.63, 246.94], timing: [1, 1, 2, 1, 2, 1, 1, 2], rhythm: [1, 1, 1, 1, 1, 1, 1, 1] }, "Blue (Da Ba Dee)": { pitches: [246.94, 261.63, 293.66, 329.63, 293.66, 261.63, 246.94], timing: [1, 1, 1, 2, 1, 1, 2], rhythm: [1, 1, 1, 1, 0, 1, 1] }, "Diamonds": { pitches: [261.63, 329.63, 392, 440, 392, 329.63, 261.63], timing: [1, 1, 2, 2, 1, 1, 2], rhythm: [1, 1, 1, 1, 1, 1, 1] }, "On écrit sur les murs": { pitches: [261.63, 293.66, 329.63, 349.23, 329.63, 293.66, 261.63], timing: [1, 1, 1, 2, 1, 1, 2], rhythm: [1, 1, 1, 1, 1, 1, 1] }, "Tout le bonheur du monde": { pitches: [246.94, 261.63, 293.66, 329.63, 293.66, 261.63, 246.94], timing: [1, 1, 1, 2, 1, 1, 2], rhythm: [1, 1, 1, 1, 1, 1, 1] }, "L'Oiseau et l'Enfant": { pitches: [261.63, 293.66, 329.63, 349.23, 329.63, 293.66, 261.63], timing: [1, 1, 2, 1, 1, 1, 2], rhythm: [1, 1, 1, 1, 1, 1, 1] }, "La Marseillaise": { pitches: [261.63, 293.66, 329.63, 392, 440, 392, 329.63, 293.66], timing: [1, 1, 1, 2, 2, 1, 1, 2], rhythm: [1, 1, 1, 1, 1, 1, 1, 1] }, "Happy Birthday": { pitches: [261.63, 261.63, 293.66, 261.63, 349.23, 329.63], timing: [1, 1, 1, 1, 2, 3], rhythm: [1, 0, 1, 1, 1, 1] } }; var pattern = songPatterns[currentSong] || songPatterns["Au clair de la lune"]; var targetPitches = pattern.pitches; var targetTiming = pattern.timing; var targetRhythm = pattern.rhythm; var volumeScore = 0; var pitchScore = 0; var rhythmScore = 0; var timingScore = 0; var validSamples = 0; // Calculate average volume and consistency var avgVolume = 0; var volumeVariance = 0; for (var i = 0; i < volumeHistory.length; i++) { avgVolume += volumeHistory[i]; } avgVolume /= volumeHistory.length; // Volume score: reward appropriate singing volume (0.2-0.8 range) if (avgVolume > 0.15) { volumeScore = Math.min(avgVolume / 0.5 * 25, 25); // Calculate volume consistency for (var i = 0; i < volumeHistory.length; i++) { volumeVariance += Math.abs(volumeHistory[i] - avgVolume); } volumeVariance /= volumeHistory.length; // Penalize excessive volume variation volumeScore = Math.max(0, volumeScore - volumeVariance * 30); } // Enhanced pitch analysis with sequential matching var segmentLength = Math.floor(pitchHistory.length / targetPitches.length); var pitchMatches = 0; var totalPitchChecks = 0; for (var segment = 0; segment < targetPitches.length; segment++) { var startIdx = segment * segmentLength; var endIdx = Math.min(startIdx + segmentLength, pitchHistory.length); var targetPitch = targetPitches[segment]; var segmentMatches = 0; var segmentChecks = 0; var segmentAvgPitch = 0; var segmentAvgVolume = 0; // Analyze this segment for (var i = startIdx; i < endIdx; i++) { if (volumeHistory[i] > 0.15) { // Only when actually singing segmentChecks++; segmentAvgPitch += pitchHistory[i]; segmentAvgVolume += volumeHistory[i]; if (pitchHistory[i] > 15) { var pitchDistance = Math.abs(pitchHistory[i] - targetPitch); if (pitchDistance <= 15) { // Very close segmentMatches += 3; } else if (pitchDistance <= 30) { // Close segmentMatches += 2; } else if (pitchDistance <= 50) { // Acceptable segmentMatches += 1; } } } } if (segmentChecks > 0) { segmentAvgPitch /= segmentChecks; segmentAvgVolume /= segmentChecks; totalPitchChecks += segmentChecks; pitchMatches += segmentMatches; // Bonus for maintaining consistent pitch in segment var pitchConsistency = 0; for (var i = startIdx; i < endIdx; i++) { if (volumeHistory[i] > 0.15 && pitchHistory[i] > 15) { var deviation = Math.abs(pitchHistory[i] - segmentAvgPitch); if (deviation <= 20) pitchConsistency++; } } if (segmentChecks > 0) { pitchMatches += pitchConsistency / segmentChecks * 2; } } } if (totalPitchChecks > 0) { pitchScore = Math.min(pitchMatches / totalPitchChecks * 40, 40); } // Enhanced rhythm analysis based on expected singing/silence patterns var rhythmMatches = 0; var rhythmSegmentLength = Math.floor(volumeHistory.length / targetRhythm.length); for (var r = 0; r < targetRhythm.length; r++) { var rStartIdx = r * rhythmSegmentLength; var rEndIdx = Math.min(rStartIdx + rhythmSegmentLength, volumeHistory.length); var shouldSing = targetRhythm[r] === 1; var actualSinging = 0; var segmentSamples = rEndIdx - rStartIdx; for (var i = rStartIdx; i < rEndIdx; i++) { if (volumeHistory[i] > 0.15) { actualSinging++; } } var singingRatio = actualSinging / segmentSamples; if (shouldSing && singingRatio > 0.6) { rhythmMatches += 2; // Good singing when expected } else if (!shouldSing && singingRatio < 0.3) { rhythmMatches += 1; // Good silence when expected } else if (shouldSing && singingRatio > 0.3) { rhythmMatches += 1; // Partial singing when expected } } rhythmScore = Math.min(rhythmMatches / targetRhythm.length * 20, 20); // Timing score: reward sustained notes and proper pacing var sustainedNotes = 0; var properPacing = 0; var lastVolume = 0; var noteTransitions = 0; for (var i = 1; i < volumeHistory.length; i++) { var currentVol = volumeHistory[i]; var previousVol = volumeHistory[i - 1]; // Detect note transitions if (previousVol <= 0.15 && currentVol > 0.15) { noteTransitions++; // Note start } else if (previousVol > 0.15 && currentVol <= 0.15) { noteTransitions++; // Note end } // Reward sustained singing if (currentVol > 0.2 && previousVol > 0.2) { sustainedNotes++; } } var expectedTransitions = targetPitches.length * 2; // Each note has start and end var transitionAccuracy = Math.min(noteTransitions / expectedTransitions, 1.0); var sustainRatio = sustainedNotes / volumeHistory.length; timingScore = transitionAccuracy * 7.5 + sustainRatio * 7.5; // Total accuracy with improved weighting var totalAccuracy = volumeScore + pitchScore + rhythmScore + timingScore; return Math.min(Math.max(totalAccuracy, 0), 100); } function getHeartsFromAccuracy(accuracy) { // More granular heart distribution based on real performance if (accuracy < 10) return 0; // Very poor performance if (accuracy < 25) return 1; // Poor performance if (accuracy < 40) return 2; // Below average if (accuracy < 55) return 3; // Average performance if (accuracy < 70) return 5; // Good performance if (accuracy < 85) return 7; // Very good performance if (accuracy < 95) return 9; // Excellent performance return 12; // Perfect performance } function createTinyHeart() { var heart = new TinyHeart(); heart.x = heartContainer.x + (Math.random() - 0.5) * 400; heart.y = heartContainer.y + (Math.random() - 0.5) * 400; tinyHearts.push(heart); game.addChild(heart); // Animate heart appearing heart.alpha = 0; heart.scaleX = 0; heart.scaleY = 0; tween(heart, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 500 }); } function levelUp() { storage.currentLevel++; storage.heartsNeeded *= 2; // Don't reset hearts - they are handled in stopSinging function LK.getSound('levelUp').play(); // Flash effect LK.effects.flashScreen(0xffb6c1, 1000); // Check win condition if (storage.currentLevel > 220) { LK.showYouWin(); return; } } function updateUI() { levelText.setText('Niveau: ' + storage.currentLevel); heartsText.setText('Cœurs: ' + storage.totalHearts + '/' + storage.heartsNeeded); // Update heart container fill based on progress var fillProgress = Math.min(storage.totalHearts / storage.heartsNeeded, 1.0); heartContainer.alpha = 0.3 + fillProgress * 0.4; } // Initialize UI updateUI(); game.update = function () { // Handle singing session if (isSinging) { var currentTime = LK.ticks - singingStartTime; var timeLeft = Math.max(0, singingDuration - currentTime * 16.67); // Convert ticks to ms if (timeLeft <= 0) { stopSinging(); } else { progressText.setText('Temps restant: ' + Math.ceil(timeLeft / 1000) + 's'); // Show real-time feedback with enhanced analysis var currentVolume = facekit.volume; var currentPitch = facekit.pitch; var volumeStatus = currentVolume > 0.15 ? 'CHANT' : currentVolume > 0.05 ? 'FAIBLE' : 'SILENCE'; var pitchDisplay = currentPitch > 15 ? Math.round(currentPitch) + 'Hz' : 'Aucun son'; // Calculate current pitch accuracy if we have target var currentAccuracy = ''; if (currentPitch > 15 && currentVolume > 0.15) { var songPatterns = { "Au clair de la lune": [261.63, 293.66, 329.63, 349.23], "Frère Jacques": [261.63, 293.66, 329.63, 261.63], "Chandelier": [220, 246.94, 261.63, 293.66], "Blue (Da Ba Dee)": [246.94, 261.63, 293.66, 329.63], "Diamonds": [261.63, 329.63, 392, 440], "On écrit sur les murs": [261.63, 293.66, 329.63, 349.23], "Tout le bonheur du monde": [246.94, 261.63, 293.66, 329.63], "L'Oiseau et l'Enfant": [261.63, 293.66, 329.63, 349.23], "La Marseillaise": [261.63, 293.66, 329.63, 392], "Happy Birthday": [261.63, 261.63, 293.66, 261.63] }; var targets = songPatterns[currentSong] || [261.63, 293.66, 329.63, 349.23]; var minDist = Infinity; for (var t = 0; t < targets.length; t++) { var dist = Math.abs(currentPitch - targets[t]); if (dist < minDist) minDist = dist; } if (minDist <= 15) currentAccuracy = '✓ Parfait';else if (minDist <= 30) currentAccuracy = '~ Bien';else if (minDist <= 50) currentAccuracy = '- Moyen';else currentAccuracy = 'X Faux'; } debugText.setText('Vol: ' + Math.round(currentVolume * 100) + '% (' + volumeStatus + ') | Pitch: ' + pitchDisplay + ' ' + currentAccuracy); } // Record audio data every few ticks if (LK.ticks % 3 === 0) { volumeHistory.push(facekit.volume); pitchHistory.push(facekit.pitch); } } else { debugText.setText(''); } // Update tiny hearts for (var i = tinyHearts.length - 1; i >= 0; i--) { var heart = tinyHearts[i]; if (heart.destroyed) { tinyHearts.splice(i, 1); } } // Add floating hearts periodically if we have collected hearts if (storage.totalHearts > 0 && LK.ticks % 120 === 0) { if (Math.random() < 0.3) { createTinyHeart(); } } };
/****
* Plugins
****/
var facekit = LK.import("@upit/facekit.v1");
var storage = LK.import("@upit/storage.v1", {
currentLevel: 1,
totalHearts: 0,
heartsNeeded: 5
});
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var SongButton = Container.expand(function (buttonType) {
var self = Container.call(this);
var buttonAsset = buttonType === 'chanter' ? 'chanterButton' : 'arreterButton';
var buttonGraphics = self.attachAsset(buttonAsset, {
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2(buttonType === 'chanter' ? 'CHANTER' : 'ARRÊTER', {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.buttonType = buttonType;
self.isPressed = false;
self.down = function (x, y, obj) {
self.isPressed = true;
buttonGraphics.alpha = 0.8;
};
self.up = function (x, y, obj) {
if (self.isPressed) {
if (self.buttonType === 'chanter') {
startSinging();
} else {
stopSinging();
}
}
self.isPressed = false;
buttonGraphics.alpha = 1.0;
};
return self;
});
var TinyHeart = Container.expand(function () {
var self = Container.call(this);
var heartGraphics = self.attachAsset('tinyHeart', {
anchorX: 0.5,
anchorY: 0.5
});
self.floatSpeed = Math.random() * 2 + 1;
self.bobOffset = Math.random() * Math.PI * 2;
self.update = function () {
self.y -= self.floatSpeed;
self.x += Math.sin(LK.ticks * 0.05 + self.bobOffset) * 0.5;
if (self.y < -50) {
self.destroy();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xffeef8
});
/****
* Game Code
****/
// Add custom background
var customBackground = game.addChild(LK.getAsset('customBackground', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
}));
// Song list
var songList = ["Au clair de la lune", "Frère Jacques", "Chandelier", "Blue (Da Ba Dee)", "Diamonds", "On écrit sur les murs", "Tout le bonheur du monde", "L'Oiseau et l'Enfant", "La Marseillaise", "Happy Birthday"];
var currentSong = "";
var isSinging = false;
var singingStartTime = 0;
var singingDuration = 15000; // 15 seconds per song
var volumeHistory = [];
var pitchHistory = [];
var tinyHearts = [];
// UI Elements
var heartContainerBorder = game.addChild(LK.getAsset('heartContainerBorder', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 800
}));
var heartContainer = game.addChild(LK.getAsset('heartContainer', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 800,
alpha: 0.3
}));
var chanterButton = game.addChild(new SongButton('chanter'));
chanterButton.x = 1024;
chanterButton.y = 1500;
var arreterButton = game.addChild(new SongButton('arreter'));
arreterButton.x = 1024;
arreterButton.y = 1600;
arreterButton.alpha = 0.5;
// Text displays
var levelText = new Text2('Niveau: ' + storage.currentLevel, {
size: 60,
fill: 0x000000
});
levelText.anchor.set(0.5, 0);
LK.gui.top.addChild(levelText);
levelText.y = 100;
var heartsText = new Text2('Cœurs: ' + storage.totalHearts + '/' + storage.heartsNeeded, {
size: 50,
fill: 0x000000
});
heartsText.anchor.set(0.5, 0);
LK.gui.top.addChild(heartsText);
heartsText.y = 180;
var songText = new Text2(currentSong, {
size: 45,
fill: 0x333333
});
songText.anchor.set(0.5, 0.5);
game.addChild(songText);
songText.x = 1024;
songText.y = 1200;
var progressText = new Text2('', {
size: 40,
fill: 0x666666
});
progressText.anchor.set(0.5, 0.5);
game.addChild(progressText);
progressText.x = 1024;
progressText.y = 1300;
var debugText = new Text2('', {
size: 30,
fill: 0x444444
});
debugText.anchor.set(0.5, 0.5);
game.addChild(debugText);
debugText.x = 1024;
debugText.y = 1370;
function getRandomSong() {
return songList[Math.floor(Math.random() * songList.length)];
}
function startSinging() {
if (isSinging) return;
currentSong = getRandomSong();
songText.setText(currentSong);
isSinging = true;
singingStartTime = LK.ticks;
volumeHistory = [];
pitchHistory = [];
chanterButton.alpha = 0.5;
arreterButton.alpha = 1.0;
progressText.setText('Chantez maintenant! 🎤');
}
function stopSinging() {
if (!isSinging) return;
isSinging = false;
var accuracy = calculateAccuracy();
var heartsEarned = getHeartsFromAccuracy(accuracy);
// Always add earned hearts (even if 0) to maintain accurate tracking
storage.totalHearts += heartsEarned;
// Only create visual effects and play sound if hearts were actually earned
if (heartsEarned > 0) {
// Create visual hearts only for earned hearts
for (var i = 0; i < heartsEarned; i++) {
createTinyHeart();
}
LK.getSound('heartCollect').play();
}
// Check for level up only if player has enough total hearts
while (storage.totalHearts >= storage.heartsNeeded) {
// Calculate remaining hearts after leveling up
var remainingHearts = storage.totalHearts - storage.heartsNeeded;
levelUp();
// Set remaining hearts for the new level
storage.totalHearts = remainingHearts;
}
updateUI();
chanterButton.alpha = 1.0;
arreterButton.alpha = 0.5;
progressText.setText('Score: ' + Math.round(accuracy) + '% - ' + heartsEarned + ' cœurs!');
// Clear song after 3 seconds
LK.setTimeout(function () {
if (!isSinging) {
songText.setText('');
progressText.setText('');
}
}, 3000);
}
function calculateAccuracy() {
if (volumeHistory.length === 0) return 0;
// Enhanced song patterns with timing and pitch sequences
var songPatterns = {
"Au clair de la lune": {
pitches: [261.63, 261.63, 261.63, 293.66, 329.63, 293.66, 261.63, 329.63, 293.66, 293.66, 261.63],
timing: [1, 1, 1, 1, 2, 1, 1, 2, 2, 1, 3],
// Relative note durations
rhythm: [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1] // 1=sing, 0=pause
},
"Frère Jacques": {
pitches: [261.63, 293.66, 329.63, 261.63, 261.63, 293.66, 329.63, 261.63],
timing: [1, 1, 1, 1, 1, 1, 1, 1],
rhythm: [1, 1, 1, 1, 0, 1, 1, 1]
},
"Chandelier": {
pitches: [220, 246.94, 261.63, 293.66, 329.63, 293.66, 261.63, 246.94],
timing: [1, 1, 2, 1, 2, 1, 1, 2],
rhythm: [1, 1, 1, 1, 1, 1, 1, 1]
},
"Blue (Da Ba Dee)": {
pitches: [246.94, 261.63, 293.66, 329.63, 293.66, 261.63, 246.94],
timing: [1, 1, 1, 2, 1, 1, 2],
rhythm: [1, 1, 1, 1, 0, 1, 1]
},
"Diamonds": {
pitches: [261.63, 329.63, 392, 440, 392, 329.63, 261.63],
timing: [1, 1, 2, 2, 1, 1, 2],
rhythm: [1, 1, 1, 1, 1, 1, 1]
},
"On écrit sur les murs": {
pitches: [261.63, 293.66, 329.63, 349.23, 329.63, 293.66, 261.63],
timing: [1, 1, 1, 2, 1, 1, 2],
rhythm: [1, 1, 1, 1, 1, 1, 1]
},
"Tout le bonheur du monde": {
pitches: [246.94, 261.63, 293.66, 329.63, 293.66, 261.63, 246.94],
timing: [1, 1, 1, 2, 1, 1, 2],
rhythm: [1, 1, 1, 1, 1, 1, 1]
},
"L'Oiseau et l'Enfant": {
pitches: [261.63, 293.66, 329.63, 349.23, 329.63, 293.66, 261.63],
timing: [1, 1, 2, 1, 1, 1, 2],
rhythm: [1, 1, 1, 1, 1, 1, 1]
},
"La Marseillaise": {
pitches: [261.63, 293.66, 329.63, 392, 440, 392, 329.63, 293.66],
timing: [1, 1, 1, 2, 2, 1, 1, 2],
rhythm: [1, 1, 1, 1, 1, 1, 1, 1]
},
"Happy Birthday": {
pitches: [261.63, 261.63, 293.66, 261.63, 349.23, 329.63],
timing: [1, 1, 1, 1, 2, 3],
rhythm: [1, 0, 1, 1, 1, 1]
}
};
var pattern = songPatterns[currentSong] || songPatterns["Au clair de la lune"];
var targetPitches = pattern.pitches;
var targetTiming = pattern.timing;
var targetRhythm = pattern.rhythm;
var volumeScore = 0;
var pitchScore = 0;
var rhythmScore = 0;
var timingScore = 0;
var validSamples = 0;
// Calculate average volume and consistency
var avgVolume = 0;
var volumeVariance = 0;
for (var i = 0; i < volumeHistory.length; i++) {
avgVolume += volumeHistory[i];
}
avgVolume /= volumeHistory.length;
// Volume score: reward appropriate singing volume (0.2-0.8 range)
if (avgVolume > 0.15) {
volumeScore = Math.min(avgVolume / 0.5 * 25, 25);
// Calculate volume consistency
for (var i = 0; i < volumeHistory.length; i++) {
volumeVariance += Math.abs(volumeHistory[i] - avgVolume);
}
volumeVariance /= volumeHistory.length;
// Penalize excessive volume variation
volumeScore = Math.max(0, volumeScore - volumeVariance * 30);
}
// Enhanced pitch analysis with sequential matching
var segmentLength = Math.floor(pitchHistory.length / targetPitches.length);
var pitchMatches = 0;
var totalPitchChecks = 0;
for (var segment = 0; segment < targetPitches.length; segment++) {
var startIdx = segment * segmentLength;
var endIdx = Math.min(startIdx + segmentLength, pitchHistory.length);
var targetPitch = targetPitches[segment];
var segmentMatches = 0;
var segmentChecks = 0;
var segmentAvgPitch = 0;
var segmentAvgVolume = 0;
// Analyze this segment
for (var i = startIdx; i < endIdx; i++) {
if (volumeHistory[i] > 0.15) {
// Only when actually singing
segmentChecks++;
segmentAvgPitch += pitchHistory[i];
segmentAvgVolume += volumeHistory[i];
if (pitchHistory[i] > 15) {
var pitchDistance = Math.abs(pitchHistory[i] - targetPitch);
if (pitchDistance <= 15) {
// Very close
segmentMatches += 3;
} else if (pitchDistance <= 30) {
// Close
segmentMatches += 2;
} else if (pitchDistance <= 50) {
// Acceptable
segmentMatches += 1;
}
}
}
}
if (segmentChecks > 0) {
segmentAvgPitch /= segmentChecks;
segmentAvgVolume /= segmentChecks;
totalPitchChecks += segmentChecks;
pitchMatches += segmentMatches;
// Bonus for maintaining consistent pitch in segment
var pitchConsistency = 0;
for (var i = startIdx; i < endIdx; i++) {
if (volumeHistory[i] > 0.15 && pitchHistory[i] > 15) {
var deviation = Math.abs(pitchHistory[i] - segmentAvgPitch);
if (deviation <= 20) pitchConsistency++;
}
}
if (segmentChecks > 0) {
pitchMatches += pitchConsistency / segmentChecks * 2;
}
}
}
if (totalPitchChecks > 0) {
pitchScore = Math.min(pitchMatches / totalPitchChecks * 40, 40);
}
// Enhanced rhythm analysis based on expected singing/silence patterns
var rhythmMatches = 0;
var rhythmSegmentLength = Math.floor(volumeHistory.length / targetRhythm.length);
for (var r = 0; r < targetRhythm.length; r++) {
var rStartIdx = r * rhythmSegmentLength;
var rEndIdx = Math.min(rStartIdx + rhythmSegmentLength, volumeHistory.length);
var shouldSing = targetRhythm[r] === 1;
var actualSinging = 0;
var segmentSamples = rEndIdx - rStartIdx;
for (var i = rStartIdx; i < rEndIdx; i++) {
if (volumeHistory[i] > 0.15) {
actualSinging++;
}
}
var singingRatio = actualSinging / segmentSamples;
if (shouldSing && singingRatio > 0.6) {
rhythmMatches += 2; // Good singing when expected
} else if (!shouldSing && singingRatio < 0.3) {
rhythmMatches += 1; // Good silence when expected
} else if (shouldSing && singingRatio > 0.3) {
rhythmMatches += 1; // Partial singing when expected
}
}
rhythmScore = Math.min(rhythmMatches / targetRhythm.length * 20, 20);
// Timing score: reward sustained notes and proper pacing
var sustainedNotes = 0;
var properPacing = 0;
var lastVolume = 0;
var noteTransitions = 0;
for (var i = 1; i < volumeHistory.length; i++) {
var currentVol = volumeHistory[i];
var previousVol = volumeHistory[i - 1];
// Detect note transitions
if (previousVol <= 0.15 && currentVol > 0.15) {
noteTransitions++; // Note start
} else if (previousVol > 0.15 && currentVol <= 0.15) {
noteTransitions++; // Note end
}
// Reward sustained singing
if (currentVol > 0.2 && previousVol > 0.2) {
sustainedNotes++;
}
}
var expectedTransitions = targetPitches.length * 2; // Each note has start and end
var transitionAccuracy = Math.min(noteTransitions / expectedTransitions, 1.0);
var sustainRatio = sustainedNotes / volumeHistory.length;
timingScore = transitionAccuracy * 7.5 + sustainRatio * 7.5;
// Total accuracy with improved weighting
var totalAccuracy = volumeScore + pitchScore + rhythmScore + timingScore;
return Math.min(Math.max(totalAccuracy, 0), 100);
}
function getHeartsFromAccuracy(accuracy) {
// More granular heart distribution based on real performance
if (accuracy < 10) return 0; // Very poor performance
if (accuracy < 25) return 1; // Poor performance
if (accuracy < 40) return 2; // Below average
if (accuracy < 55) return 3; // Average performance
if (accuracy < 70) return 5; // Good performance
if (accuracy < 85) return 7; // Very good performance
if (accuracy < 95) return 9; // Excellent performance
return 12; // Perfect performance
}
function createTinyHeart() {
var heart = new TinyHeart();
heart.x = heartContainer.x + (Math.random() - 0.5) * 400;
heart.y = heartContainer.y + (Math.random() - 0.5) * 400;
tinyHearts.push(heart);
game.addChild(heart);
// Animate heart appearing
heart.alpha = 0;
heart.scaleX = 0;
heart.scaleY = 0;
tween(heart, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 500
});
}
function levelUp() {
storage.currentLevel++;
storage.heartsNeeded *= 2;
// Don't reset hearts - they are handled in stopSinging function
LK.getSound('levelUp').play();
// Flash effect
LK.effects.flashScreen(0xffb6c1, 1000);
// Check win condition
if (storage.currentLevel > 220) {
LK.showYouWin();
return;
}
}
function updateUI() {
levelText.setText('Niveau: ' + storage.currentLevel);
heartsText.setText('Cœurs: ' + storage.totalHearts + '/' + storage.heartsNeeded);
// Update heart container fill based on progress
var fillProgress = Math.min(storage.totalHearts / storage.heartsNeeded, 1.0);
heartContainer.alpha = 0.3 + fillProgress * 0.4;
}
// Initialize UI
updateUI();
game.update = function () {
// Handle singing session
if (isSinging) {
var currentTime = LK.ticks - singingStartTime;
var timeLeft = Math.max(0, singingDuration - currentTime * 16.67); // Convert ticks to ms
if (timeLeft <= 0) {
stopSinging();
} else {
progressText.setText('Temps restant: ' + Math.ceil(timeLeft / 1000) + 's');
// Show real-time feedback with enhanced analysis
var currentVolume = facekit.volume;
var currentPitch = facekit.pitch;
var volumeStatus = currentVolume > 0.15 ? 'CHANT' : currentVolume > 0.05 ? 'FAIBLE' : 'SILENCE';
var pitchDisplay = currentPitch > 15 ? Math.round(currentPitch) + 'Hz' : 'Aucun son';
// Calculate current pitch accuracy if we have target
var currentAccuracy = '';
if (currentPitch > 15 && currentVolume > 0.15) {
var songPatterns = {
"Au clair de la lune": [261.63, 293.66, 329.63, 349.23],
"Frère Jacques": [261.63, 293.66, 329.63, 261.63],
"Chandelier": [220, 246.94, 261.63, 293.66],
"Blue (Da Ba Dee)": [246.94, 261.63, 293.66, 329.63],
"Diamonds": [261.63, 329.63, 392, 440],
"On écrit sur les murs": [261.63, 293.66, 329.63, 349.23],
"Tout le bonheur du monde": [246.94, 261.63, 293.66, 329.63],
"L'Oiseau et l'Enfant": [261.63, 293.66, 329.63, 349.23],
"La Marseillaise": [261.63, 293.66, 329.63, 392],
"Happy Birthday": [261.63, 261.63, 293.66, 261.63]
};
var targets = songPatterns[currentSong] || [261.63, 293.66, 329.63, 349.23];
var minDist = Infinity;
for (var t = 0; t < targets.length; t++) {
var dist = Math.abs(currentPitch - targets[t]);
if (dist < minDist) minDist = dist;
}
if (minDist <= 15) currentAccuracy = '✓ Parfait';else if (minDist <= 30) currentAccuracy = '~ Bien';else if (minDist <= 50) currentAccuracy = '- Moyen';else currentAccuracy = 'X Faux';
}
debugText.setText('Vol: ' + Math.round(currentVolume * 100) + '% (' + volumeStatus + ') | Pitch: ' + pitchDisplay + ' ' + currentAccuracy);
}
// Record audio data every few ticks
if (LK.ticks % 3 === 0) {
volumeHistory.push(facekit.volume);
pitchHistory.push(facekit.pitch);
}
} else {
debugText.setText('');
}
// Update tiny hearts
for (var i = tinyHearts.length - 1; i >= 0; i--) {
var heart = tinyHearts[i];
if (heart.destroyed) {
tinyHearts.splice(i, 1);
}
}
// Add floating hearts periodically if we have collected hearts
if (storage.totalHearts > 0 && LK.ticks % 120 === 0) {
if (Math.random() < 0.3) {
createTinyHeart();
}
}
};