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