/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Note = Container.expand(function (beat, track) {
var self = Container.call(this);
var noteGraphics = self.attachAsset('note', {
anchorX: 0.5,
anchorY: 0.5
});
self.beat = beat;
self.track = track;
self.speed = 4;
self.hit = false;
self.missed = false;
self.scaledUp = false;
self.update = function () {
self.y += self.speed;
// Check if note is entering hit zone (scale up)
if (!self.scaledUp && self.y >= hitZoneY - 150 && self.y <= hitZoneY + 150) {
self.scaledUp = true;
tween(noteGraphics, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.easeOut
});
}
// Check if note passed hit zone without being hit
if (!self.hit && !self.missed && self.y > hitZoneY + 100) {
self.missed = true;
missedNotes++;
}
};
return self;
});
var SongButton = Container.expand(function (songId, songName, yPos) {
var self = Container.call(this);
var buttonBg = self.attachAsset('songButton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.875,
scaleY: 1.875
});
var buttonText = new Text2(songName, {
size: 50,
fill: '#e4f4f5',
font: "'Arial Black'",
stroke: '#000000',
strokeThickness: 3
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.songId = songId;
self.x = 1024;
self.y = yPos;
self.down = function (x, y, obj) {
selectSong(self.songId);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1A1A2E
});
/****
* Game Code
****/
// Game states
var GAME_STATE = {
SONG_SELECT: 0,
PATTERN_CREATE: 1,
PATTERN_RECREATE: 2,
AI_CHALLENGE: 3,
GAME_OVER: 4
};
var currentState = GAME_STATE.SONG_SELECT;
var selectedSong = null;
var currentMusic = null;
// Pattern data
var playerPattern = [];
var currentPattern = [];
var patternStartTime = 0;
var passNumber = 1;
// Timing variables
var bpm = 120;
var beatDuration = 60000 / bpm; // milliseconds per beat
var songStartTime = 0;
var lastBeatTime = 0;
var phaseOneTimer = 30; // 30 seconds for phase one
var timerInterval = null;
// Game objects
var notes = [];
var tracks = [];
var hitZones = [];
var uiElements = [];
// Scoring
var score = 0;
var hitNotes = 0;
var missedNotes = 0;
// Track positions - spaced wider to prevent hit zone overlap
var trackPositions = [350, 650, 950, 1250, 1550];
var hitZoneY = 2200;
// UI Title elements
var titleText = LK.getAsset('Title', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 500
});
var instructionText = new Text2('Select a Song', {
size: 80,
fill: '#ECFF86',
font: "'Arial Black'"
});
instructionText.anchor.set(0.5, 0.5);
instructionText.x = 1024;
instructionText.y = 775;
var scoreText = new Text2('Score: 0', {
size: 60,
fill: '#FFFFFF',
font: "'Arial Black'"
});
scoreText.anchor.set(0.5, 0);
var passText = new Text2('Pass 1/3', {
size: 40,
fill: '#FFFFFF',
font: "'Arial Black'"
});
passText.anchor.set(0.5, 0);
var timerText = new Text2('Time left\nto create: 30', {
size: 80,
fill: '#FFD700',
font: "'Arial Black'",
align: 'center'
});
timerText.anchor.set(0.5, 0.5);
timerText.alpha = 0; // Hidden initially
LK.gui.top.addChild(scoreText);
LK.gui.top.addChild(passText);
LK.gui.top.addChild(timerText);
passText.x = 0;
passText.y = 80;
timerText.x = 0;
timerText.y = 900;
// Initialize game elements
function initializeGame() {
// Create background
var bg = game.addChild(LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
// Create tracks
for (var i = 0; i < trackPositions.length; i++) {
var track = game.addChild(LK.getAsset('trackLine', {
anchorX: 0.5,
anchorY: 0,
x: trackPositions[i],
y: 0
}));
track.alpha = 0.3;
tracks.push(track);
}
// Create hit zones
var hitZoneLabels = ['Sound', 'Cymbal', 'Drumkick', 'Base', 'Shaker'];
for (var i = 0; i < trackPositions.length; i++) {
var hitZone = game.addChild(LK.getAsset('hitZone', {
anchorX: 0.5,
anchorY: 0.5,
x: trackPositions[i],
y: hitZoneY
}));
hitZone.alpha = 0;
hitZones.push(hitZone);
// Add text label for each hit zone
var hitZoneText = new Text2(hitZoneLabels[i], {
size: 48,
fill: '#FFFFFF',
font: "'Arial Black'",
stroke: '#000000',
strokeThickness: 3
});
hitZoneText.anchor.set(0.5, 0.5);
hitZoneText.x = trackPositions[i];
hitZoneText.y = hitZoneY + 80;
hitZoneText.alpha = 0;
game.addChild(hitZoneText);
hitZones.push(hitZoneText);
}
showSongSelection();
}
function showSongSelection() {
currentState = GAME_STATE.SONG_SELECT;
// Ensure title is visible
titleText.alpha = 1;
game.addChild(titleText);
game.addChild(instructionText);
// Hide score and pass text during menu
scoreText.alpha = 0;
passText.alpha = 0;
// Play menu music
LK.playMusic('menu_music');
// Apply wobble function to instruction text
wobbleText(instructionText, 0.1, 800, tween.easeInOut, 300, function () {
return currentState !== GAME_STATE.SONG_SELECT;
});
// Create song buttons with much increased spacing
var songButton1 = game.addChild(new SongButton('song1', 'Electronic Beat', 1000));
var songButton2 = game.addChild(new SongButton('song2', 'Rock Anthem', 1300));
var songButton3 = game.addChild(new SongButton('song3', 'Jazz Fusion', 1600));
var songButton4 = game.addChild(new SongButton('song4', 'Hip Hop', 1900));
var songButton5 = game.addChild(new SongButton('song5', '8-Bit', 2200));
uiElements.push(songButton1, songButton2, songButton3, songButton4, songButton5);
}
function selectSong(songId) {
// Stop menu music
LK.stopMusic();
// Map song IDs to actual music asset names
var songMapping = {
'song1': 'electronic_beat',
'song2': 'rock_anthem',
'song3': 'jazz',
'song4': 'hip_hop',
'song5': 'eight_bit'
};
selectedSong = songMapping[songId];
// Show score and pass text when game starts
scoreText.alpha = 1;
passText.alpha = 1;
// Animate hit zones to visible
for (var i = 0; i < hitZones.length; i++) {
tween(hitZones[i], {
alpha: 1
}, {
duration: 500,
easing: tween.easeOut
});
}
// Clear UI elements
for (var i = 0; i < uiElements.length; i++) {
uiElements[i].destroy();
}
uiElements = [];
titleText.destroy();
instructionText.destroy();
startPatternCreation();
}
function startPatternCreation() {
currentState = GAME_STATE.PATTERN_CREATE;
passNumber = 1;
updatePassText();
var createText = new Text2('Tap to create\nyour rhythm pattern!', {
size: 100,
fill: '#ECFF86',
font: "'Arial Black'",
stroke: '#000000',
strokeThickness: 4,
align: 'center'
});
createText.anchor.set(0.5, 0.5);
createText.x = 1024;
createText.y = 400;
game.addChild(createText);
uiElements.push(createText);
// Apply wobble function to create text
wobbleText(createText, 0.1, 800, tween.easeInOut, 300, function () {
return currentState !== GAME_STATE.PATTERN_CREATE;
});
showCountdown(function () {
// Start music
currentMusic = selectedSong;
LK.playMusic(selectedSong, {
volume: 0.25,
fade: {
start: 0,
end: 0.25,
duration: 500
}
});
songStartTime = Date.now();
patternStartTime = songStartTime;
playerPattern = [];
// Start phase one timer
phaseOneTimer = 30;
timerText.alpha = 1;
updateTimerDisplay();
timerInterval = LK.setInterval(function () {
if (currentState === GAME_STATE.PATTERN_CREATE && phaseOneTimer > 0) {
phaseOneTimer--;
updateTimerDisplay();
if (phaseOneTimer <= 0) {
LK.clearInterval(timerInterval);
timerText.alpha = 0;
}
}
}, 1000);
// Auto advance after 30 seconds
LK.setTimeout(function () {
if (currentState === GAME_STATE.PATTERN_CREATE) {
startPatternRecreation();
}
}, 30000);
});
}
function startPatternRecreation() {
currentState = GAME_STATE.PATTERN_RECREATE;
passNumber = 2;
updatePassText();
// Clear timer
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
timerText.alpha = 0;
// Clear UI
for (var i = 0; i < uiElements.length; i++) {
uiElements[i].destroy();
}
uiElements = [];
var recreateText = new Text2('Recreate your pattern\nto score points!', {
size: 100,
fill: '#ECFF86',
font: "'Arial Black'",
stroke: '#000000',
strokeThickness: 4,
align: 'center'
});
recreateText.anchor.set(0.5, 0.5);
recreateText.x = 1024;
recreateText.y = 400;
game.addChild(recreateText);
uiElements.push(recreateText);
// Apply wobble function to recreate text
wobbleText(recreateText, 0.1, 800, tween.easeInOut, 300, function () {
return currentState !== GAME_STATE.PATTERN_RECREATE;
});
showCountdown(function () {
// Restart music and prepare pattern
LK.stopMusic();
LK.setTimeout(function () {
LK.playMusic(selectedSong, {
volume: 0.25,
fade: {
start: 0,
end: 0.25,
duration: 500
}
});
songStartTime = Date.now();
currentPattern = playerPattern.slice();
spawnNotesForPattern();
}, 100);
// Auto advance after pattern completion + buffer
LK.setTimeout(function () {
if (currentState === GAME_STATE.PATTERN_RECREATE) {
startAIChallenge();
}
}, 35000);
});
}
function startAIChallenge() {
currentState = GAME_STATE.AI_CHALLENGE;
passNumber = 3;
updatePassText();
// Clear UI
for (var i = 0; i < uiElements.length; i++) {
uiElements[i].destroy();
}
uiElements = [];
var challengeText = new Text2('AI Enhanced\nChallenge!', {
size: 100,
fill: '#FF6B6B',
font: "'Arial Black'",
stroke: '#000000',
strokeThickness: 4,
align: 'center'
});
challengeText.anchor.set(0.5, 0.5);
challengeText.x = 1024;
challengeText.y = 400;
game.addChild(challengeText);
uiElements.push(challengeText);
// Apply wobble function to challenge text
wobbleText(challengeText, 0.1, 800, tween.easeInOut, 300, function () {
return currentState !== GAME_STATE.AI_CHALLENGE;
});
// Add AI beats to pattern
var enhancedPattern = playerPattern.slice();
var aiPercentage = Math.random() * 0.10 + 0.10; // Random between 10% and 20%
var aiBeats = Math.floor(playerPattern.length * aiPercentage);
for (var i = 0; i < aiBeats; i++) {
var randomTime = Math.random() * 30000; // Random time within 30 seconds
var randomTrack = Math.floor(Math.random() * trackPositions.length);
enhancedPattern.push({
time: randomTime,
track: randomTrack,
isAI: true
});
}
// Sort by time
enhancedPattern.sort(function (a, b) {
return a.time - b.time;
});
showCountdown(function () {
// Restart music
LK.stopMusic();
LK.setTimeout(function () {
LK.playMusic(selectedSong, {
volume: 0.25
});
songStartTime = Date.now();
currentPattern = enhancedPattern;
spawnNotesForPattern();
}, 100);
// Auto end after completion
LK.setTimeout(function () {
if (currentState === GAME_STATE.AI_CHALLENGE) {
endGame();
}
}, 40000);
});
}
function spawnNotesForPattern() {
for (var i = 0; i < currentPattern.length; i++) {
var beat = currentPattern[i];
var spawnTime = beat.time - 2000; // Spawn 2 seconds before hit time
LK.setTimeout(function (beatData) {
return function () {
if (currentState === GAME_STATE.PATTERN_RECREATE || currentState === GAME_STATE.AI_CHALLENGE) {
var note = new Note(beatData, beatData.track);
note.x = trackPositions[beatData.track];
note.y = -50;
if (beatData.isAI) {
note.attachAsset('note', {}).tint = 0xFF6B6B; // Red for AI beats
}
game.addChild(note);
notes.push(note);
}
};
}(beat), Math.max(0, spawnTime));
}
}
function updatePassText() {
passText.setText('Pass ' + passNumber + '/3');
}
function updateTimerDisplay() {
timerText.setText('Time left\nto create\n' + phaseOneTimer);
if (phaseOneTimer <= 10) {
timerText.fill = '#FF6B6B'; // Red when time is running out
} else {
timerText.fill = '#FFD700'; // Gold normally
}
}
function pulseText(textElement, scaleFrom, scaleTo, duration, easing, delay, stopCondition) {
// Set default values if not provided
scaleFrom = scaleFrom || 1.0;
scaleTo = scaleTo || 1.3;
duration = duration || 800;
easing = easing || tween.easeInOut;
delay = delay || 200;
function doPulse() {
// Check stop condition if provided
if (stopCondition && stopCondition()) {
return;
}
// Scale up
tween(textElement, {
scaleX: scaleTo,
scaleY: scaleTo
}, {
duration: duration,
easing: easing,
onFinish: function onFinish() {
// Check stop condition again
if (stopCondition && stopCondition()) {
return;
}
// Scale back down
tween(textElement, {
scaleX: scaleFrom,
scaleY: scaleFrom
}, {
duration: duration,
easing: easing,
onFinish: function onFinish() {
// Check if we should continue pulsing
if (!stopCondition || !stopCondition()) {
LK.setTimeout(doPulse, delay);
}
}
});
}
});
}
doPulse();
}
function wobbleText(textElement, rotationAngle, duration, easing, delay, stopCondition) {
// Set default values if not provided
rotationAngle = rotationAngle || 0.1;
duration = duration || 800;
easing = easing || tween.easeInOut;
delay = delay || 300;
function doWobble() {
// Check stop condition if provided
if (stopCondition && stopCondition()) {
return;
}
// Wobble left
tween(textElement, {
rotation: -rotationAngle
}, {
duration: duration,
easing: easing,
onFinish: function onFinish() {
// Check stop condition again
if (stopCondition && stopCondition()) {
return;
}
// Wobble right
tween(textElement, {
rotation: rotationAngle
}, {
duration: duration,
easing: easing,
onFinish: function onFinish() {
// Check stop condition again
if (stopCondition && stopCondition()) {
return;
}
// Return to center for smooth loop
tween(textElement, {
rotation: 0
}, {
duration: duration,
easing: easing,
onFinish: function onFinish() {
// Check if we should continue wobbling
if (!stopCondition || !stopCondition()) {
LK.setTimeout(doWobble, delay);
}
}
});
}
});
}
});
}
doWobble();
}
function showCountdown(callback) {
var countdownText = new Text2('3', {
size: 200,
fill: '#FF6B6B',
font: "'Arial Black'"
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 1024;
countdownText.y = 1366;
countdownText.alpha = 0;
game.addChild(countdownText);
// Animate countdown from 3 to 1
var count = 3;
function animateCount() {
countdownText.setText(count.toString());
countdownText.alpha = 1;
countdownText.scaleX = 2;
countdownText.scaleY = 2;
tween(countdownText, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
count--;
if (count > 0) {
animateCount();
} else {
// Show "GO!" for final countdown
countdownText.setText('GO!');
countdownText.alpha = 1;
countdownText.scaleX = 2;
countdownText.scaleY = 2;
tween(countdownText, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
countdownText.destroy();
callback();
}
});
}
}
});
}
animateCount();
}
function updateScore(points) {
score += points;
scoreText.setText('Score: ' + score);
}
function endGame() {
currentState = GAME_STATE.GAME_OVER;
LK.stopMusic();
var accuracy = hitNotes > 0 ? Math.round(hitNotes / (hitNotes + missedNotes) * 100) : 0;
var totalNotes = hitNotes + missedNotes;
LK.setScore(score);
// Clear any remaining UI elements
for (var i = 0; i < uiElements.length; i++) {
uiElements[i].destroy();
}
uiElements = [];
// Create comprehensive final score display
var gameCompleteText = new Text2('GAME COMPLETE!', {
size: 80,
fill: '#ECFF86',
font: "'Arial Black'"
});
gameCompleteText.anchor.set(0.5, 0.5);
gameCompleteText.x = 1024;
gameCompleteText.y = 800;
game.addChild(gameCompleteText);
var finalScoreText = new Text2('Final Score: ' + score, {
size: 70,
fill: '#FFFFFF',
font: "'Arial Black'"
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 1024;
finalScoreText.y = 1000;
game.addChild(finalScoreText);
var accuracyText = new Text2('Accuracy: ' + accuracy + '%', {
size: 60,
fill: '#FFFFFF',
font: "'Arial Black'"
});
accuracyText.anchor.set(0.5, 0.5);
accuracyText.x = 1024;
accuracyText.y = 1150;
game.addChild(accuracyText);
var statsText = new Text2('Notes Hit: ' + hitNotes + ' / ' + totalNotes, {
size: 50,
fill: '#FFFFFF',
font: "'Arial Black'"
});
statsText.anchor.set(0.5, 0.5);
statsText.x = 1024;
statsText.y = 1300;
game.addChild(statsText);
var continueText = new Text2('Tap to continue...', {
size: 40,
fill: '#ECFF86',
font: "'Arial Black'"
});
continueText.anchor.set(0.5, 0.5);
continueText.x = 1024;
continueText.y = 1500;
game.addChild(continueText);
// Add pulsing animation to continue text
function pulseContinue() {
if (currentState !== GAME_STATE.GAME_OVER) {
return;
}
tween(continueText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (currentState !== GAME_STATE.GAME_OVER) {
return;
}
tween(continueText, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (currentState === GAME_STATE.GAME_OVER) {
LK.setTimeout(pulseContinue, 200);
}
}
});
}
});
}
pulseContinue();
// Store UI elements for cleanup
uiElements.push(gameCompleteText, finalScoreText, accuracyText, statsText, continueText);
}
function resetGame() {
// Clear timer
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
timerText.alpha = 0;
phaseOneTimer = 30;
// Clear all game objects
for (var i = notes.length - 1; i >= 0; i--) {
notes[i].destroy();
}
notes = [];
for (var i = 0; i < uiElements.length; i++) {
uiElements[i].destroy();
}
uiElements = [];
// Reset hit zones to invisible
for (var i = 0; i < hitZones.length; i++) {
tween(hitZones[i], {
alpha: 0
}, {
duration: 300,
easing: tween.easeOut
});
}
// Reset game variables
playerPattern = [];
currentPattern = [];
passNumber = 1;
score = 0;
hitNotes = 0;
missedNotes = 0;
selectedSong = null;
currentMusic = null;
// Clear score display
scoreText.setText('Score: 0');
// Return to song selection
showSongSelection();
}
function handleTap(x, y) {
var currentTime = Date.now();
if (currentState === GAME_STATE.GAME_OVER) {
// Return to song selection from final score screen
resetGame();
return;
}
if (currentState === GAME_STATE.PATTERN_CREATE) {
// Record tap in pattern
var relativeTime = currentTime - patternStartTime;
var nearestTrack = findNearestTrack(x);
playerPattern.push({
time: relativeTime,
track: nearestTrack,
isAI: false
});
LK.getSound('hit_track' + nearestTrack).play();
// Visual feedback
var hitZone = hitZones[nearestTrack];
tween(hitZone, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100
});
tween(hitZone, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
} else if (currentState === GAME_STATE.PATTERN_RECREATE || currentState === GAME_STATE.AI_CHALLENGE) {
// Check for note hits
var nearestTrack = findNearestTrack(x);
var hitNote = null;
var minDistance = Infinity;
for (var i = 0; i < notes.length; i++) {
var note = notes[i];
if (!note.hit && !note.missed && note.track === nearestTrack) {
var distance = Math.abs(note.y - hitZoneY);
if (distance < minDistance && distance < 100) {
minDistance = distance;
hitNote = note;
}
}
}
if (hitNote) {
hitNote.hit = true;
hitNotes++;
var points = Math.max(10, 100 - Math.floor(minDistance));
updateScore(points);
LK.getSound('hit_track' + nearestTrack).play();
// Visual feedback
tween(hitNote, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 200
});
LK.setTimeout(function () {
if (hitNote && hitNote.parent) {
hitNote.destroy();
var index = notes.indexOf(hitNote);
if (index > -1) {
notes.splice(index, 1);
}
}
}, 200);
} else {
LK.getSound('miss').play();
}
}
}
function findNearestTrack(x) {
var minDistance = Infinity;
var nearestTrack = 0;
for (var i = 0; i < trackPositions.length; i++) {
var distance = Math.abs(x - trackPositions[i]);
if (distance < minDistance) {
minDistance = distance;
nearestTrack = i;
}
}
return nearestTrack;
}
// Event handlers
game.down = function (x, y, obj) {
handleTap(x, y);
};
game.update = function () {
// Update notes
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
// Remove notes that are off screen
if (note.y > 2800) {
note.destroy();
notes.splice(i, 1);
}
}
// Auto-end game logic removed - endGame() is now only called after phase 3 completion
// Check for missed notes and update score display
if (LK.ticks % 60 === 0) {
// Update every second
scoreText.setText('Score: ' + score);
}
};
// Initialize the game
initializeGame(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Note = Container.expand(function (beat, track) {
var self = Container.call(this);
var noteGraphics = self.attachAsset('note', {
anchorX: 0.5,
anchorY: 0.5
});
self.beat = beat;
self.track = track;
self.speed = 4;
self.hit = false;
self.missed = false;
self.scaledUp = false;
self.update = function () {
self.y += self.speed;
// Check if note is entering hit zone (scale up)
if (!self.scaledUp && self.y >= hitZoneY - 150 && self.y <= hitZoneY + 150) {
self.scaledUp = true;
tween(noteGraphics, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.easeOut
});
}
// Check if note passed hit zone without being hit
if (!self.hit && !self.missed && self.y > hitZoneY + 100) {
self.missed = true;
missedNotes++;
}
};
return self;
});
var SongButton = Container.expand(function (songId, songName, yPos) {
var self = Container.call(this);
var buttonBg = self.attachAsset('songButton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.875,
scaleY: 1.875
});
var buttonText = new Text2(songName, {
size: 50,
fill: '#e4f4f5',
font: "'Arial Black'",
stroke: '#000000',
strokeThickness: 3
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.songId = songId;
self.x = 1024;
self.y = yPos;
self.down = function (x, y, obj) {
selectSong(self.songId);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1A1A2E
});
/****
* Game Code
****/
// Game states
var GAME_STATE = {
SONG_SELECT: 0,
PATTERN_CREATE: 1,
PATTERN_RECREATE: 2,
AI_CHALLENGE: 3,
GAME_OVER: 4
};
var currentState = GAME_STATE.SONG_SELECT;
var selectedSong = null;
var currentMusic = null;
// Pattern data
var playerPattern = [];
var currentPattern = [];
var patternStartTime = 0;
var passNumber = 1;
// Timing variables
var bpm = 120;
var beatDuration = 60000 / bpm; // milliseconds per beat
var songStartTime = 0;
var lastBeatTime = 0;
var phaseOneTimer = 30; // 30 seconds for phase one
var timerInterval = null;
// Game objects
var notes = [];
var tracks = [];
var hitZones = [];
var uiElements = [];
// Scoring
var score = 0;
var hitNotes = 0;
var missedNotes = 0;
// Track positions - spaced wider to prevent hit zone overlap
var trackPositions = [350, 650, 950, 1250, 1550];
var hitZoneY = 2200;
// UI Title elements
var titleText = LK.getAsset('Title', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 500
});
var instructionText = new Text2('Select a Song', {
size: 80,
fill: '#ECFF86',
font: "'Arial Black'"
});
instructionText.anchor.set(0.5, 0.5);
instructionText.x = 1024;
instructionText.y = 775;
var scoreText = new Text2('Score: 0', {
size: 60,
fill: '#FFFFFF',
font: "'Arial Black'"
});
scoreText.anchor.set(0.5, 0);
var passText = new Text2('Pass 1/3', {
size: 40,
fill: '#FFFFFF',
font: "'Arial Black'"
});
passText.anchor.set(0.5, 0);
var timerText = new Text2('Time left\nto create: 30', {
size: 80,
fill: '#FFD700',
font: "'Arial Black'",
align: 'center'
});
timerText.anchor.set(0.5, 0.5);
timerText.alpha = 0; // Hidden initially
LK.gui.top.addChild(scoreText);
LK.gui.top.addChild(passText);
LK.gui.top.addChild(timerText);
passText.x = 0;
passText.y = 80;
timerText.x = 0;
timerText.y = 900;
// Initialize game elements
function initializeGame() {
// Create background
var bg = game.addChild(LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
// Create tracks
for (var i = 0; i < trackPositions.length; i++) {
var track = game.addChild(LK.getAsset('trackLine', {
anchorX: 0.5,
anchorY: 0,
x: trackPositions[i],
y: 0
}));
track.alpha = 0.3;
tracks.push(track);
}
// Create hit zones
var hitZoneLabels = ['Sound', 'Cymbal', 'Drumkick', 'Base', 'Shaker'];
for (var i = 0; i < trackPositions.length; i++) {
var hitZone = game.addChild(LK.getAsset('hitZone', {
anchorX: 0.5,
anchorY: 0.5,
x: trackPositions[i],
y: hitZoneY
}));
hitZone.alpha = 0;
hitZones.push(hitZone);
// Add text label for each hit zone
var hitZoneText = new Text2(hitZoneLabels[i], {
size: 48,
fill: '#FFFFFF',
font: "'Arial Black'",
stroke: '#000000',
strokeThickness: 3
});
hitZoneText.anchor.set(0.5, 0.5);
hitZoneText.x = trackPositions[i];
hitZoneText.y = hitZoneY + 80;
hitZoneText.alpha = 0;
game.addChild(hitZoneText);
hitZones.push(hitZoneText);
}
showSongSelection();
}
function showSongSelection() {
currentState = GAME_STATE.SONG_SELECT;
// Ensure title is visible
titleText.alpha = 1;
game.addChild(titleText);
game.addChild(instructionText);
// Hide score and pass text during menu
scoreText.alpha = 0;
passText.alpha = 0;
// Play menu music
LK.playMusic('menu_music');
// Apply wobble function to instruction text
wobbleText(instructionText, 0.1, 800, tween.easeInOut, 300, function () {
return currentState !== GAME_STATE.SONG_SELECT;
});
// Create song buttons with much increased spacing
var songButton1 = game.addChild(new SongButton('song1', 'Electronic Beat', 1000));
var songButton2 = game.addChild(new SongButton('song2', 'Rock Anthem', 1300));
var songButton3 = game.addChild(new SongButton('song3', 'Jazz Fusion', 1600));
var songButton4 = game.addChild(new SongButton('song4', 'Hip Hop', 1900));
var songButton5 = game.addChild(new SongButton('song5', '8-Bit', 2200));
uiElements.push(songButton1, songButton2, songButton3, songButton4, songButton5);
}
function selectSong(songId) {
// Stop menu music
LK.stopMusic();
// Map song IDs to actual music asset names
var songMapping = {
'song1': 'electronic_beat',
'song2': 'rock_anthem',
'song3': 'jazz',
'song4': 'hip_hop',
'song5': 'eight_bit'
};
selectedSong = songMapping[songId];
// Show score and pass text when game starts
scoreText.alpha = 1;
passText.alpha = 1;
// Animate hit zones to visible
for (var i = 0; i < hitZones.length; i++) {
tween(hitZones[i], {
alpha: 1
}, {
duration: 500,
easing: tween.easeOut
});
}
// Clear UI elements
for (var i = 0; i < uiElements.length; i++) {
uiElements[i].destroy();
}
uiElements = [];
titleText.destroy();
instructionText.destroy();
startPatternCreation();
}
function startPatternCreation() {
currentState = GAME_STATE.PATTERN_CREATE;
passNumber = 1;
updatePassText();
var createText = new Text2('Tap to create\nyour rhythm pattern!', {
size: 100,
fill: '#ECFF86',
font: "'Arial Black'",
stroke: '#000000',
strokeThickness: 4,
align: 'center'
});
createText.anchor.set(0.5, 0.5);
createText.x = 1024;
createText.y = 400;
game.addChild(createText);
uiElements.push(createText);
// Apply wobble function to create text
wobbleText(createText, 0.1, 800, tween.easeInOut, 300, function () {
return currentState !== GAME_STATE.PATTERN_CREATE;
});
showCountdown(function () {
// Start music
currentMusic = selectedSong;
LK.playMusic(selectedSong, {
volume: 0.25,
fade: {
start: 0,
end: 0.25,
duration: 500
}
});
songStartTime = Date.now();
patternStartTime = songStartTime;
playerPattern = [];
// Start phase one timer
phaseOneTimer = 30;
timerText.alpha = 1;
updateTimerDisplay();
timerInterval = LK.setInterval(function () {
if (currentState === GAME_STATE.PATTERN_CREATE && phaseOneTimer > 0) {
phaseOneTimer--;
updateTimerDisplay();
if (phaseOneTimer <= 0) {
LK.clearInterval(timerInterval);
timerText.alpha = 0;
}
}
}, 1000);
// Auto advance after 30 seconds
LK.setTimeout(function () {
if (currentState === GAME_STATE.PATTERN_CREATE) {
startPatternRecreation();
}
}, 30000);
});
}
function startPatternRecreation() {
currentState = GAME_STATE.PATTERN_RECREATE;
passNumber = 2;
updatePassText();
// Clear timer
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
timerText.alpha = 0;
// Clear UI
for (var i = 0; i < uiElements.length; i++) {
uiElements[i].destroy();
}
uiElements = [];
var recreateText = new Text2('Recreate your pattern\nto score points!', {
size: 100,
fill: '#ECFF86',
font: "'Arial Black'",
stroke: '#000000',
strokeThickness: 4,
align: 'center'
});
recreateText.anchor.set(0.5, 0.5);
recreateText.x = 1024;
recreateText.y = 400;
game.addChild(recreateText);
uiElements.push(recreateText);
// Apply wobble function to recreate text
wobbleText(recreateText, 0.1, 800, tween.easeInOut, 300, function () {
return currentState !== GAME_STATE.PATTERN_RECREATE;
});
showCountdown(function () {
// Restart music and prepare pattern
LK.stopMusic();
LK.setTimeout(function () {
LK.playMusic(selectedSong, {
volume: 0.25,
fade: {
start: 0,
end: 0.25,
duration: 500
}
});
songStartTime = Date.now();
currentPattern = playerPattern.slice();
spawnNotesForPattern();
}, 100);
// Auto advance after pattern completion + buffer
LK.setTimeout(function () {
if (currentState === GAME_STATE.PATTERN_RECREATE) {
startAIChallenge();
}
}, 35000);
});
}
function startAIChallenge() {
currentState = GAME_STATE.AI_CHALLENGE;
passNumber = 3;
updatePassText();
// Clear UI
for (var i = 0; i < uiElements.length; i++) {
uiElements[i].destroy();
}
uiElements = [];
var challengeText = new Text2('AI Enhanced\nChallenge!', {
size: 100,
fill: '#FF6B6B',
font: "'Arial Black'",
stroke: '#000000',
strokeThickness: 4,
align: 'center'
});
challengeText.anchor.set(0.5, 0.5);
challengeText.x = 1024;
challengeText.y = 400;
game.addChild(challengeText);
uiElements.push(challengeText);
// Apply wobble function to challenge text
wobbleText(challengeText, 0.1, 800, tween.easeInOut, 300, function () {
return currentState !== GAME_STATE.AI_CHALLENGE;
});
// Add AI beats to pattern
var enhancedPattern = playerPattern.slice();
var aiPercentage = Math.random() * 0.10 + 0.10; // Random between 10% and 20%
var aiBeats = Math.floor(playerPattern.length * aiPercentage);
for (var i = 0; i < aiBeats; i++) {
var randomTime = Math.random() * 30000; // Random time within 30 seconds
var randomTrack = Math.floor(Math.random() * trackPositions.length);
enhancedPattern.push({
time: randomTime,
track: randomTrack,
isAI: true
});
}
// Sort by time
enhancedPattern.sort(function (a, b) {
return a.time - b.time;
});
showCountdown(function () {
// Restart music
LK.stopMusic();
LK.setTimeout(function () {
LK.playMusic(selectedSong, {
volume: 0.25
});
songStartTime = Date.now();
currentPattern = enhancedPattern;
spawnNotesForPattern();
}, 100);
// Auto end after completion
LK.setTimeout(function () {
if (currentState === GAME_STATE.AI_CHALLENGE) {
endGame();
}
}, 40000);
});
}
function spawnNotesForPattern() {
for (var i = 0; i < currentPattern.length; i++) {
var beat = currentPattern[i];
var spawnTime = beat.time - 2000; // Spawn 2 seconds before hit time
LK.setTimeout(function (beatData) {
return function () {
if (currentState === GAME_STATE.PATTERN_RECREATE || currentState === GAME_STATE.AI_CHALLENGE) {
var note = new Note(beatData, beatData.track);
note.x = trackPositions[beatData.track];
note.y = -50;
if (beatData.isAI) {
note.attachAsset('note', {}).tint = 0xFF6B6B; // Red for AI beats
}
game.addChild(note);
notes.push(note);
}
};
}(beat), Math.max(0, spawnTime));
}
}
function updatePassText() {
passText.setText('Pass ' + passNumber + '/3');
}
function updateTimerDisplay() {
timerText.setText('Time left\nto create\n' + phaseOneTimer);
if (phaseOneTimer <= 10) {
timerText.fill = '#FF6B6B'; // Red when time is running out
} else {
timerText.fill = '#FFD700'; // Gold normally
}
}
function pulseText(textElement, scaleFrom, scaleTo, duration, easing, delay, stopCondition) {
// Set default values if not provided
scaleFrom = scaleFrom || 1.0;
scaleTo = scaleTo || 1.3;
duration = duration || 800;
easing = easing || tween.easeInOut;
delay = delay || 200;
function doPulse() {
// Check stop condition if provided
if (stopCondition && stopCondition()) {
return;
}
// Scale up
tween(textElement, {
scaleX: scaleTo,
scaleY: scaleTo
}, {
duration: duration,
easing: easing,
onFinish: function onFinish() {
// Check stop condition again
if (stopCondition && stopCondition()) {
return;
}
// Scale back down
tween(textElement, {
scaleX: scaleFrom,
scaleY: scaleFrom
}, {
duration: duration,
easing: easing,
onFinish: function onFinish() {
// Check if we should continue pulsing
if (!stopCondition || !stopCondition()) {
LK.setTimeout(doPulse, delay);
}
}
});
}
});
}
doPulse();
}
function wobbleText(textElement, rotationAngle, duration, easing, delay, stopCondition) {
// Set default values if not provided
rotationAngle = rotationAngle || 0.1;
duration = duration || 800;
easing = easing || tween.easeInOut;
delay = delay || 300;
function doWobble() {
// Check stop condition if provided
if (stopCondition && stopCondition()) {
return;
}
// Wobble left
tween(textElement, {
rotation: -rotationAngle
}, {
duration: duration,
easing: easing,
onFinish: function onFinish() {
// Check stop condition again
if (stopCondition && stopCondition()) {
return;
}
// Wobble right
tween(textElement, {
rotation: rotationAngle
}, {
duration: duration,
easing: easing,
onFinish: function onFinish() {
// Check stop condition again
if (stopCondition && stopCondition()) {
return;
}
// Return to center for smooth loop
tween(textElement, {
rotation: 0
}, {
duration: duration,
easing: easing,
onFinish: function onFinish() {
// Check if we should continue wobbling
if (!stopCondition || !stopCondition()) {
LK.setTimeout(doWobble, delay);
}
}
});
}
});
}
});
}
doWobble();
}
function showCountdown(callback) {
var countdownText = new Text2('3', {
size: 200,
fill: '#FF6B6B',
font: "'Arial Black'"
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 1024;
countdownText.y = 1366;
countdownText.alpha = 0;
game.addChild(countdownText);
// Animate countdown from 3 to 1
var count = 3;
function animateCount() {
countdownText.setText(count.toString());
countdownText.alpha = 1;
countdownText.scaleX = 2;
countdownText.scaleY = 2;
tween(countdownText, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
count--;
if (count > 0) {
animateCount();
} else {
// Show "GO!" for final countdown
countdownText.setText('GO!');
countdownText.alpha = 1;
countdownText.scaleX = 2;
countdownText.scaleY = 2;
tween(countdownText, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
countdownText.destroy();
callback();
}
});
}
}
});
}
animateCount();
}
function updateScore(points) {
score += points;
scoreText.setText('Score: ' + score);
}
function endGame() {
currentState = GAME_STATE.GAME_OVER;
LK.stopMusic();
var accuracy = hitNotes > 0 ? Math.round(hitNotes / (hitNotes + missedNotes) * 100) : 0;
var totalNotes = hitNotes + missedNotes;
LK.setScore(score);
// Clear any remaining UI elements
for (var i = 0; i < uiElements.length; i++) {
uiElements[i].destroy();
}
uiElements = [];
// Create comprehensive final score display
var gameCompleteText = new Text2('GAME COMPLETE!', {
size: 80,
fill: '#ECFF86',
font: "'Arial Black'"
});
gameCompleteText.anchor.set(0.5, 0.5);
gameCompleteText.x = 1024;
gameCompleteText.y = 800;
game.addChild(gameCompleteText);
var finalScoreText = new Text2('Final Score: ' + score, {
size: 70,
fill: '#FFFFFF',
font: "'Arial Black'"
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 1024;
finalScoreText.y = 1000;
game.addChild(finalScoreText);
var accuracyText = new Text2('Accuracy: ' + accuracy + '%', {
size: 60,
fill: '#FFFFFF',
font: "'Arial Black'"
});
accuracyText.anchor.set(0.5, 0.5);
accuracyText.x = 1024;
accuracyText.y = 1150;
game.addChild(accuracyText);
var statsText = new Text2('Notes Hit: ' + hitNotes + ' / ' + totalNotes, {
size: 50,
fill: '#FFFFFF',
font: "'Arial Black'"
});
statsText.anchor.set(0.5, 0.5);
statsText.x = 1024;
statsText.y = 1300;
game.addChild(statsText);
var continueText = new Text2('Tap to continue...', {
size: 40,
fill: '#ECFF86',
font: "'Arial Black'"
});
continueText.anchor.set(0.5, 0.5);
continueText.x = 1024;
continueText.y = 1500;
game.addChild(continueText);
// Add pulsing animation to continue text
function pulseContinue() {
if (currentState !== GAME_STATE.GAME_OVER) {
return;
}
tween(continueText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (currentState !== GAME_STATE.GAME_OVER) {
return;
}
tween(continueText, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (currentState === GAME_STATE.GAME_OVER) {
LK.setTimeout(pulseContinue, 200);
}
}
});
}
});
}
pulseContinue();
// Store UI elements for cleanup
uiElements.push(gameCompleteText, finalScoreText, accuracyText, statsText, continueText);
}
function resetGame() {
// Clear timer
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
timerText.alpha = 0;
phaseOneTimer = 30;
// Clear all game objects
for (var i = notes.length - 1; i >= 0; i--) {
notes[i].destroy();
}
notes = [];
for (var i = 0; i < uiElements.length; i++) {
uiElements[i].destroy();
}
uiElements = [];
// Reset hit zones to invisible
for (var i = 0; i < hitZones.length; i++) {
tween(hitZones[i], {
alpha: 0
}, {
duration: 300,
easing: tween.easeOut
});
}
// Reset game variables
playerPattern = [];
currentPattern = [];
passNumber = 1;
score = 0;
hitNotes = 0;
missedNotes = 0;
selectedSong = null;
currentMusic = null;
// Clear score display
scoreText.setText('Score: 0');
// Return to song selection
showSongSelection();
}
function handleTap(x, y) {
var currentTime = Date.now();
if (currentState === GAME_STATE.GAME_OVER) {
// Return to song selection from final score screen
resetGame();
return;
}
if (currentState === GAME_STATE.PATTERN_CREATE) {
// Record tap in pattern
var relativeTime = currentTime - patternStartTime;
var nearestTrack = findNearestTrack(x);
playerPattern.push({
time: relativeTime,
track: nearestTrack,
isAI: false
});
LK.getSound('hit_track' + nearestTrack).play();
// Visual feedback
var hitZone = hitZones[nearestTrack];
tween(hitZone, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100
});
tween(hitZone, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
} else if (currentState === GAME_STATE.PATTERN_RECREATE || currentState === GAME_STATE.AI_CHALLENGE) {
// Check for note hits
var nearestTrack = findNearestTrack(x);
var hitNote = null;
var minDistance = Infinity;
for (var i = 0; i < notes.length; i++) {
var note = notes[i];
if (!note.hit && !note.missed && note.track === nearestTrack) {
var distance = Math.abs(note.y - hitZoneY);
if (distance < minDistance && distance < 100) {
minDistance = distance;
hitNote = note;
}
}
}
if (hitNote) {
hitNote.hit = true;
hitNotes++;
var points = Math.max(10, 100 - Math.floor(minDistance));
updateScore(points);
LK.getSound('hit_track' + nearestTrack).play();
// Visual feedback
tween(hitNote, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 200
});
LK.setTimeout(function () {
if (hitNote && hitNote.parent) {
hitNote.destroy();
var index = notes.indexOf(hitNote);
if (index > -1) {
notes.splice(index, 1);
}
}
}, 200);
} else {
LK.getSound('miss').play();
}
}
}
function findNearestTrack(x) {
var minDistance = Infinity;
var nearestTrack = 0;
for (var i = 0; i < trackPositions.length; i++) {
var distance = Math.abs(x - trackPositions[i]);
if (distance < minDistance) {
minDistance = distance;
nearestTrack = i;
}
}
return nearestTrack;
}
// Event handlers
game.down = function (x, y, obj) {
handleTap(x, y);
};
game.update = function () {
// Update notes
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
// Remove notes that are off screen
if (note.y > 2800) {
note.destroy();
notes.splice(i, 1);
}
}
// Auto-end game logic removed - endGame() is now only called after phase 3 completion
// Check for missed notes and update score display
if (LK.ticks % 60 === 0) {
// Update every second
scoreText.setText('Score: ' + score);
}
};
// Initialize the game
initializeGame();
dark neon lit background in the tints of green orange yellow and blue. In-Game asset. 2d. High contrast. No shadows
light neon orange yellow green and blue line. In-Game asset. 2d. High contrast. No shadows
text in neon outline no background that says "Rhythm Echo Challenge". In-Game asset. 2d. High contrast. No shadows
neon pink purple red orange oval filled in blue nuaces. In-Game asset. 2d. High contrast. No shadows