/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // GuitarString class: visual representation of a string var GuitarString = Container.expand(function () { var self = Container.call(this); self.stringIndex = 0; // 0-5 self.init = function (stringIndex) { self.stringIndex = stringIndex; var stringAssetId = 'guitarString' + (stringIndex + 1); var string = self.attachAsset(stringAssetId, { anchorX: 0.5, anchorY: 0 }); self.width = string.width; self.height = string.height; }; return self; }); // HoldNote class: represents a note that must be held for a duration var HoldNote = Container.expand(function () { var self = Container.call(this); self.stringIndex = 0; self.hit = false; self.missed = false; self.holdStart = null; // tick when hold started self.holdDuration = 60; // frames to hold (default 1s) self.held = false; // is currently being held self.completed = false; // hold completed self.init = function (stringIndex, holdDuration) { self.stringIndex = stringIndex; self.holdDuration = holdDuration || 60; var noteAssetId = 'note' + (stringIndex + 1); var note = self.attachAsset(noteAssetId, { anchorX: 0.5, anchorY: 0.5 }); self.width = note.width; self.height = note.height; // Add a visual indicator for hold (e.g. stretch vertically) // The holdBar should start at the main note and extend upward self.holdBar = self.attachAsset(noteAssetId, { anchorX: 0.5, anchorY: 1, // anchor at bottom of note scaleY: self.holdDuration * noteSpeed / note.height }); self.holdBar.alpha = 0.3; // Position the holdBar so its bottom aligns with the center of the main note self.holdBar.y = -note.height / 2; }; self.update = function () { if (!self.hit && !self.missed) { self.y += noteSpeed; if (self.held && self.holdStart !== null && !self.completed) { // Check if hold duration is completed if (LK.ticks - self.holdStart >= self.holdDuration) { self.completed = true; self.hit = true; } } } }; return self; }); // Note class: represents a falling note on a string var Note = Container.expand(function () { var self = Container.call(this); self.stringIndex = 0; // 0-5 self.hit = false; self.missed = false; self.init = function (stringIndex) { self.stringIndex = stringIndex; var noteAssetId = 'note' + (stringIndex + 1); var note = self.attachAsset(noteAssetId, { anchorX: 0.5, anchorY: 0.5 }); self.width = note.width; self.height = note.height; }; self.update = function () { if (!self.hit && !self.missed) { self.y += noteSpeed; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181818 }); /**** * Game Code ****/ // Music (background track) // Sound for hit/miss // Hit zone // Note shapes (one per string, colored) // Guitar string shapes (5 strings) // --- Game Constants --- var NUM_STRINGS = 6; var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; var STRING_SPACING = 260; var STRING_TOP = 200; var STRING_BOTTOM = 2000; var NOTE_START_Y = -100; var HIT_ZONE_Y = 2000; var HIT_ZONE_HEIGHT = 40; var NOTE_HIT_WINDOW = 180; // px (was 100) var NOTE_MISS_WINDOW = 200; // px (was 120) var MAX_MISSES = 20; var BASE_NOTE_SPEED = 8; // px per frame (slower notes) var NOTE_SPAWN_OFFSET = 60; // frames between notes (song tempo) var MULTIPLIER_THRESHOLDS = [10, 20, 40, 60]; // streaks for multipliers // --- Game State --- var strings = []; var notes = []; var noteSpeed = BASE_NOTE_SPEED; var score = 0; var streak = 0; var multiplier = 1; var misses = 0; var songTime = 0; var songEnded = false; var lastNoteSpawnTick = 0; var songNotes = []; // Array of {tick, stringIndex} var nextSongNoteIndex = 0; // --- UI Elements --- var scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var streakTxt = new Text2('', { size: 60, fill: "#fff" }); streakTxt.anchor.set(0.5, 0); LK.gui.top.addChild(streakTxt); var missesTxt = new Text2('', { size: 60, fill: 0xFF4444 }); missesTxt.anchor.set(0.5, 0); LK.gui.top.addChild(missesTxt); // --- Background --- var background = LK.getAsset('background', { anchorX: 0, anchorY: 0, x: -1000, // Move background a little bit more to the right y: 0 }); game.addChild(background); // --- Hit Zone --- var hitZone = LK.getAsset('hitZone', { anchorX: 0.5, anchorY: 0.5, x: GAME_WIDTH / 2, y: HIT_ZONE_Y }); game.addChild(hitZone); hitZone.alpha = 0.8; // --- Place Guitar Strings --- // To create an infinite effect, tile the string asset vertically from far above to far below the visible area. var stringStartX = (GAME_WIDTH - (NUM_STRINGS - 1) * STRING_SPACING) / 2; var stringTileStartY = -10000; // much farther above visible area for infinite effect var stringTileEndY = GAME_HEIGHT + 10000; // much farther below visible area for infinite effect for (var i = 0; i < NUM_STRINGS; i++) { // Add shadow under string (infinite effect) var shadowY = stringTileStartY + 16; while (shadowY < stringTileEndY) { var shadow = LK.getAsset('guitarString' + (i + 1), { anchorX: 0.5, anchorY: 0, x: stringStartX + i * STRING_SPACING + 10, y: shadowY, scaleX: 1.05, scaleY: 1.01, alpha: 0.22 // subtle shadow }); shadow.tint = 0x000000; game.addChild(shadow); shadowY += shadow.height; } // Add main string (infinite effect) var stringY = stringTileStartY; while (stringY < stringTileEndY) { var gs = new GuitarString(); gs.init(i); gs.x = stringStartX + i * STRING_SPACING; gs.y = stringY; game.addChild(gs); if (stringY === stringTileStartY) { // Only push the first instance for hit/miss logic strings.push(gs); } stringY += gs.height; } } // --- Song Data (joyful, variable note pattern) --- // Each entry: {tick: frame to spawn, stringIndex: 0-5} songNotes = [ // Joyful intro: bouncy up and down, then playful skips { tick: 30, stringIndex: 0 }, // Example hold note (must hold for 90 frames = 1.5s) { tick: 100, stringIndex: 2, type: "hold", holdDuration: 90 }, // E { tick: 60, stringIndex: 2 }, // D { tick: 90, stringIndex: 4 }, // B { tick: 120, stringIndex: 1 }, // A { tick: 150, stringIndex: 3 }, // G { tick: 180, stringIndex: 5 }, // E2 { tick: 210, stringIndex: 2 }, // D { tick: 240, stringIndex: 4 }, // B { tick: 270, stringIndex: 0 }, // E { tick: 300, stringIndex: 3 }, // G { tick: 330, stringIndex: 1 }, // A { tick: 360, stringIndex: 5 }, // E2 // Playful double notes and chords { tick: 400, stringIndex: 0 }, { tick: 400, stringIndex: 2, type: "hold", holdDuration: 60 }, { tick: 430, stringIndex: 1 }, { tick: 430, stringIndex: 3 }, { tick: 460, stringIndex: 4, type: "hold", holdDuration: 80 }, { tick: 460, stringIndex: 5 }, // Quick zig-zag { tick: 500, stringIndex: 0, type: "hold", holdDuration: 70 }, { tick: 520, stringIndex: 5 }, { tick: 540, stringIndex: 1 }, { tick: 560, stringIndex: 4 }, { tick: 580, stringIndex: 2, type: "hold", holdDuration: 60 }, { tick: 600, stringIndex: 3 }, // Joyful run up and down { tick: 650, stringIndex: 0 }, { tick: 670, stringIndex: 1, type: "hold", holdDuration: 60 }, { tick: 690, stringIndex: 2 }, { tick: 710, stringIndex: 3 }, { tick: 730, stringIndex: 4 }, { tick: 750, stringIndex: 5, type: "hold", holdDuration: 80 }, { tick: 770, stringIndex: 4 }, { tick: 790, stringIndex: 3 }, { tick: 810, stringIndex: 2 }, { tick: 830, stringIndex: 1 }, { tick: 850, stringIndex: 0 }, // Chord burst { tick: 900, stringIndex: 0 }, { tick: 900, stringIndex: 2, type: "hold", holdDuration: 90 }, { tick: 900, stringIndex: 4 }, { tick: 930, stringIndex: 1 }, { tick: 930, stringIndex: 3 }, { tick: 930, stringIndex: 5, type: "hold", holdDuration: 70 }, // Playful syncopation { tick: 970, stringIndex: 2 }, { tick: 990, stringIndex: 4, type: "hold", holdDuration: 60 }, { tick: 1010, stringIndex: 1 }, { tick: 1030, stringIndex: 3 }, { tick: 1050, stringIndex: 5 }, { tick: 1070, stringIndex: 0, type: "hold", holdDuration: 80 }, // Fast run, alternating { tick: 1100, stringIndex: 0 }, { tick: 1110, stringIndex: 2 }, { tick: 1120, stringIndex: 4, type: "hold", holdDuration: 60 }, { tick: 1130, stringIndex: 1 }, { tick: 1140, stringIndex: 3 }, { tick: 1150, stringIndex: 5 }, { tick: 1160, stringIndex: 2 }, { tick: 1170, stringIndex: 4 }, { tick: 1180, stringIndex: 0 }, { tick: 1190, stringIndex: 3 }, // Joyful chord { tick: 1230, stringIndex: 0 }, { tick: 1230, stringIndex: 2, type: "hold", holdDuration: 90 }, { tick: 1230, stringIndex: 4 }, { tick: 1230, stringIndex: 5 }, // Bouncy outro { tick: 1280, stringIndex: 1 }, { tick: 1300, stringIndex: 3, type: "hold", holdDuration: 60 }, { tick: 1320, stringIndex: 5 }, { tick: 1340, stringIndex: 2 }, { tick: 1360, stringIndex: 4 }, { tick: 1380, stringIndex: 0, type: "hold", holdDuration: 80 }, { tick: 1400, stringIndex: 3 }, { tick: 1420, stringIndex: 1 }, { tick: 1440, stringIndex: 5, type: "hold", holdDuration: 70 }, // Final joyful chord { tick: 1500, stringIndex: 0 }, { tick: 1500, stringIndex: 2, type: "hold", holdDuration: 100 }, { tick: 1500, stringIndex: 4 }, { tick: 1500, stringIndex: 5 }]; // Song ends after last note + 3 seconds var songEndTick = songNotes[songNotes.length - 1].tick + 180; // --- Helper Functions --- function updateScoreUI() { scoreTxt.setText(score); streakTxt.setText(streak > 0 ? "Streak: " + streak + " x" + multiplier : ""); missesTxt.setText(misses > 0 ? "Misses: " + misses + "/" + MAX_MISSES : ""); } function getStringIndexFromX(x) { // Find which string x is closest to var minDist = 99999; var idx = 0; for (var i = 0; i < strings.length; i++) { var dist = Math.abs(x - strings[i].x); if (dist < minDist) { minDist = dist; idx = i; } } return idx; } function tryHitNote(stringIndex) { // Find the first note on this string within the hit window for (var i = 0; i < notes.length; i++) { var n = notes[i]; if (n.stringIndex === stringIndex && !n.hit && !n.missed) { var dy = Math.abs(n.y - HIT_ZONE_Y); if (dy <= NOTE_HIT_WINDOW) { n.hit = true; // Play the correct guitar note sound for each string if (stringIndex === 0) { LK.getSound('E').play(); } else if (stringIndex === 1) { LK.getSound('A').play(); } else if (stringIndex === 2) { LK.getSound('D').play(); } else if (stringIndex === 3) { LK.getSound('G').play(); } else if (stringIndex === 4) { LK.getSound('B').play(); } else if (stringIndex === 5) { LK.getSound('E2').play(); } // Play the music only for the first note hit, then let it play continuously if (typeof LK.resumeMusic === "function") { if (!window._musicStarted) { LK.resumeMusic(); window._musicStarted = true; } } score += 100 * multiplier; LK.setScore(LK.getScore() + 1); streak += 1; // Multiplier logic if (streak >= MULTIPLIER_THRESHOLDS[3]) { multiplier = 5; } else if (streak >= MULTIPLIER_THRESHOLDS[2]) { multiplier = 4; } else if (streak >= MULTIPLIER_THRESHOLDS[1]) { multiplier = 3; } else if (streak >= MULTIPLIER_THRESHOLDS[0]) { multiplier = 2; } else { multiplier = 1; } updateScoreUI(); // Animate note tween(n, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { n.destroy(); } }); return true; } } } // Missed (no note in window) LK.getSound('noteMiss').play(); streak = 0; multiplier = 1; misses += 1; updateScoreUI(); LK.effects.flashObject(strings[stringIndex], 0xff0000, 200); if (misses >= MAX_MISSES) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); } return false; } // --- Input Handling --- game.down = function (x, y, obj) { // Only allow input in lower 2/3 of screen (where strings are) if (y < STRING_TOP) return; var stringIdx = getStringIndexFromX(x); // Check for hold note first for (var i = 0; i < notes.length; i++) { var n = notes[i]; if (n instanceof HoldNote && !n.hit && !n.missed && !n.completed && n.stringIndex === stringIdx) { var dy = Math.abs(n.y - HIT_ZONE_Y); if (dy <= NOTE_HIT_WINDOW) { n.held = true; n.holdStart = LK.ticks; // Play the correct guitar note sound for each string if (stringIdx === 0) { LK.getSound('E').play(); } else if (stringIdx === 1) { LK.getSound('A').play(); } else if (stringIdx === 2) { LK.getSound('D').play(); } else if (stringIdx === 3) { LK.getSound('G').play(); } else if (stringIdx === 4) { LK.getSound('B').play(); } else if (stringIdx === 5) { LK.getSound('E2').play(); } // Play the music only for the first note hit, then let it play continuously if (typeof LK.resumeMusic === "function") { if (!window._musicStarted) { LK.resumeMusic(); window._musicStarted = true; } } return; } } } // Otherwise, normal note tryHitNote(stringIdx); }; game.up = function (x, y, obj) { // On release, check if any hold note is being held and not completed for (var i = 0; i < notes.length; i++) { var n = notes[i]; if (n instanceof HoldNote && n.held && !n.completed && !n.missed) { // If released before holdDuration, mark as missed if (LK.ticks - n.holdStart < n.holdDuration) { n.missed = true; n.held = false; streak = 0; multiplier = 1; misses += 1; updateScoreUI(); LK.getSound('noteMiss').play(); LK.effects.flashObject(strings[n.stringIndex], 0xff0000, 200); if (misses >= MAX_MISSES) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); } } } } }; // --- Game Update Loop --- game.update = function () { // Spawn notes according to songNotes if (!songEnded && nextSongNoteIndex < songNotes.length) { while (nextSongNoteIndex < songNotes.length && LK.ticks >= songNotes[nextSongNoteIndex].tick) { // Count currently active (not hit/missed) notes var activeNotes = 0; for (var i = 0; i < notes.length; i++) { if (!notes[i].hit && !notes[i].missed) { activeNotes++; } } // Only allow up to 3 notes sliding down at the same time if (activeNotes >= 3) { break; } var noteData = songNotes[nextSongNoteIndex]; var n; if (noteData.type === "hold") { n = new HoldNote(); n.init(noteData.stringIndex, noteData.holdDuration); } else { n = new Note(); n.init(noteData.stringIndex); } n.x = strings[noteData.stringIndex].x; n.y = NOTE_START_Y; n.alpha = 1; n.scaleX = 1; n.scaleY = 1; notes.push(n); game.addChild(n); nextSongNoteIndex += 1; } } // Update notes for (var i = notes.length - 1; i >= 0; i--) { var n = notes[i]; n.update(); // Missed note? if (!n.hit && !n.missed && n.y > HIT_ZONE_Y + NOTE_MISS_WINDOW) { n.missed = true; streak = 0; multiplier = 1; misses += 1; updateScoreUI(); LK.getSound('noteMiss').play(); LK.effects.flashObject(strings[n.stringIndex], 0xff0000, 200); tween(n, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { n.destroy(); } }); if (misses >= MAX_MISSES) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); } } // Completed hold note if (n instanceof HoldNote && n.completed && !n.hit) { n.hit = true; score += 200 * multiplier; LK.setScore(LK.getScore() + 2); streak += 1; // Multiplier logic if (streak >= MULTIPLIER_THRESHOLDS[3]) { multiplier = 5; } else if (streak >= MULTIPLIER_THRESHOLDS[2]) { multiplier = 4; } else if (streak >= MULTIPLIER_THRESHOLDS[1]) { multiplier = 3; } else if (streak >= MULTIPLIER_THRESHOLDS[0]) { multiplier = 2; } else { multiplier = 1; } updateScoreUI(); tween(n, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { n.destroy(); } }); } // Remove notes that are hit and faded out if ((n.hit || n.missed) && n.alpha <= 0.01) { notes.splice(i, 1); continue; } // Make columns infinite: if note goes off bottom, respawn at top (unless hit/missed) if (!n.hit && !n.missed && n.y > GAME_HEIGHT + n.height) { n.y = NOTE_START_Y; n.hit = false; n.missed = false; n.alpha = 1; n.scaleX = 1; n.scaleY = 1; } } // End of song if (!songEnded && LK.ticks > songEndTick) { songEnded = true; LK.showYouWin(); } }; // --- UI Placement --- scoreTxt.position.set(GAME_WIDTH / 2, 60); streakTxt.position.set(GAME_WIDTH / 2, 160); missesTxt.position.set(GAME_WIDTH / 2, 240); // --- Start Music --- // Start the music but immediately pause it so it only plays when notes are hit LK.playMusic('song1', { loop: true }); if (typeof LK.pauseMusic === "function") { LK.pauseMusic(); } // --- Initialize UI --- updateScoreUI();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// GuitarString class: visual representation of a string
var GuitarString = Container.expand(function () {
var self = Container.call(this);
self.stringIndex = 0; // 0-5
self.init = function (stringIndex) {
self.stringIndex = stringIndex;
var stringAssetId = 'guitarString' + (stringIndex + 1);
var string = self.attachAsset(stringAssetId, {
anchorX: 0.5,
anchorY: 0
});
self.width = string.width;
self.height = string.height;
};
return self;
});
// HoldNote class: represents a note that must be held for a duration
var HoldNote = Container.expand(function () {
var self = Container.call(this);
self.stringIndex = 0;
self.hit = false;
self.missed = false;
self.holdStart = null; // tick when hold started
self.holdDuration = 60; // frames to hold (default 1s)
self.held = false; // is currently being held
self.completed = false; // hold completed
self.init = function (stringIndex, holdDuration) {
self.stringIndex = stringIndex;
self.holdDuration = holdDuration || 60;
var noteAssetId = 'note' + (stringIndex + 1);
var note = self.attachAsset(noteAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.width = note.width;
self.height = note.height;
// Add a visual indicator for hold (e.g. stretch vertically)
// The holdBar should start at the main note and extend upward
self.holdBar = self.attachAsset(noteAssetId, {
anchorX: 0.5,
anchorY: 1,
// anchor at bottom of note
scaleY: self.holdDuration * noteSpeed / note.height
});
self.holdBar.alpha = 0.3;
// Position the holdBar so its bottom aligns with the center of the main note
self.holdBar.y = -note.height / 2;
};
self.update = function () {
if (!self.hit && !self.missed) {
self.y += noteSpeed;
if (self.held && self.holdStart !== null && !self.completed) {
// Check if hold duration is completed
if (LK.ticks - self.holdStart >= self.holdDuration) {
self.completed = true;
self.hit = true;
}
}
}
};
return self;
});
// Note class: represents a falling note on a string
var Note = Container.expand(function () {
var self = Container.call(this);
self.stringIndex = 0; // 0-5
self.hit = false;
self.missed = false;
self.init = function (stringIndex) {
self.stringIndex = stringIndex;
var noteAssetId = 'note' + (stringIndex + 1);
var note = self.attachAsset(noteAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.width = note.width;
self.height = note.height;
};
self.update = function () {
if (!self.hit && !self.missed) {
self.y += noteSpeed;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Music (background track)
// Sound for hit/miss
// Hit zone
// Note shapes (one per string, colored)
// Guitar string shapes (5 strings)
// --- Game Constants ---
var NUM_STRINGS = 6;
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var STRING_SPACING = 260;
var STRING_TOP = 200;
var STRING_BOTTOM = 2000;
var NOTE_START_Y = -100;
var HIT_ZONE_Y = 2000;
var HIT_ZONE_HEIGHT = 40;
var NOTE_HIT_WINDOW = 180; // px (was 100)
var NOTE_MISS_WINDOW = 200; // px (was 120)
var MAX_MISSES = 20;
var BASE_NOTE_SPEED = 8; // px per frame (slower notes)
var NOTE_SPAWN_OFFSET = 60; // frames between notes (song tempo)
var MULTIPLIER_THRESHOLDS = [10, 20, 40, 60]; // streaks for multipliers
// --- Game State ---
var strings = [];
var notes = [];
var noteSpeed = BASE_NOTE_SPEED;
var score = 0;
var streak = 0;
var multiplier = 1;
var misses = 0;
var songTime = 0;
var songEnded = false;
var lastNoteSpawnTick = 0;
var songNotes = []; // Array of {tick, stringIndex}
var nextSongNoteIndex = 0;
// --- UI Elements ---
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var streakTxt = new Text2('', {
size: 60,
fill: "#fff"
});
streakTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(streakTxt);
var missesTxt = new Text2('', {
size: 60,
fill: 0xFF4444
});
missesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(missesTxt);
// --- Background ---
var background = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: -1000,
// Move background a little bit more to the right
y: 0
});
game.addChild(background);
// --- Hit Zone ---
var hitZone = LK.getAsset('hitZone', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_WIDTH / 2,
y: HIT_ZONE_Y
});
game.addChild(hitZone);
hitZone.alpha = 0.8;
// --- Place Guitar Strings ---
// To create an infinite effect, tile the string asset vertically from far above to far below the visible area.
var stringStartX = (GAME_WIDTH - (NUM_STRINGS - 1) * STRING_SPACING) / 2;
var stringTileStartY = -10000; // much farther above visible area for infinite effect
var stringTileEndY = GAME_HEIGHT + 10000; // much farther below visible area for infinite effect
for (var i = 0; i < NUM_STRINGS; i++) {
// Add shadow under string (infinite effect)
var shadowY = stringTileStartY + 16;
while (shadowY < stringTileEndY) {
var shadow = LK.getAsset('guitarString' + (i + 1), {
anchorX: 0.5,
anchorY: 0,
x: stringStartX + i * STRING_SPACING + 10,
y: shadowY,
scaleX: 1.05,
scaleY: 1.01,
alpha: 0.22 // subtle shadow
});
shadow.tint = 0x000000;
game.addChild(shadow);
shadowY += shadow.height;
}
// Add main string (infinite effect)
var stringY = stringTileStartY;
while (stringY < stringTileEndY) {
var gs = new GuitarString();
gs.init(i);
gs.x = stringStartX + i * STRING_SPACING;
gs.y = stringY;
game.addChild(gs);
if (stringY === stringTileStartY) {
// Only push the first instance for hit/miss logic
strings.push(gs);
}
stringY += gs.height;
}
}
// --- Song Data (joyful, variable note pattern) ---
// Each entry: {tick: frame to spawn, stringIndex: 0-5}
songNotes = [
// Joyful intro: bouncy up and down, then playful skips
{
tick: 30,
stringIndex: 0
},
// Example hold note (must hold for 90 frames = 1.5s)
{
tick: 100,
stringIndex: 2,
type: "hold",
holdDuration: 90
},
// E
{
tick: 60,
stringIndex: 2
},
// D
{
tick: 90,
stringIndex: 4
},
// B
{
tick: 120,
stringIndex: 1
},
// A
{
tick: 150,
stringIndex: 3
},
// G
{
tick: 180,
stringIndex: 5
},
// E2
{
tick: 210,
stringIndex: 2
},
// D
{
tick: 240,
stringIndex: 4
},
// B
{
tick: 270,
stringIndex: 0
},
// E
{
tick: 300,
stringIndex: 3
},
// G
{
tick: 330,
stringIndex: 1
},
// A
{
tick: 360,
stringIndex: 5
},
// E2
// Playful double notes and chords
{
tick: 400,
stringIndex: 0
}, {
tick: 400,
stringIndex: 2,
type: "hold",
holdDuration: 60
}, {
tick: 430,
stringIndex: 1
}, {
tick: 430,
stringIndex: 3
}, {
tick: 460,
stringIndex: 4,
type: "hold",
holdDuration: 80
}, {
tick: 460,
stringIndex: 5
},
// Quick zig-zag
{
tick: 500,
stringIndex: 0,
type: "hold",
holdDuration: 70
}, {
tick: 520,
stringIndex: 5
}, {
tick: 540,
stringIndex: 1
}, {
tick: 560,
stringIndex: 4
}, {
tick: 580,
stringIndex: 2,
type: "hold",
holdDuration: 60
}, {
tick: 600,
stringIndex: 3
},
// Joyful run up and down
{
tick: 650,
stringIndex: 0
}, {
tick: 670,
stringIndex: 1,
type: "hold",
holdDuration: 60
}, {
tick: 690,
stringIndex: 2
}, {
tick: 710,
stringIndex: 3
}, {
tick: 730,
stringIndex: 4
}, {
tick: 750,
stringIndex: 5,
type: "hold",
holdDuration: 80
}, {
tick: 770,
stringIndex: 4
}, {
tick: 790,
stringIndex: 3
}, {
tick: 810,
stringIndex: 2
}, {
tick: 830,
stringIndex: 1
}, {
tick: 850,
stringIndex: 0
},
// Chord burst
{
tick: 900,
stringIndex: 0
}, {
tick: 900,
stringIndex: 2,
type: "hold",
holdDuration: 90
}, {
tick: 900,
stringIndex: 4
}, {
tick: 930,
stringIndex: 1
}, {
tick: 930,
stringIndex: 3
}, {
tick: 930,
stringIndex: 5,
type: "hold",
holdDuration: 70
},
// Playful syncopation
{
tick: 970,
stringIndex: 2
}, {
tick: 990,
stringIndex: 4,
type: "hold",
holdDuration: 60
}, {
tick: 1010,
stringIndex: 1
}, {
tick: 1030,
stringIndex: 3
}, {
tick: 1050,
stringIndex: 5
}, {
tick: 1070,
stringIndex: 0,
type: "hold",
holdDuration: 80
},
// Fast run, alternating
{
tick: 1100,
stringIndex: 0
}, {
tick: 1110,
stringIndex: 2
}, {
tick: 1120,
stringIndex: 4,
type: "hold",
holdDuration: 60
}, {
tick: 1130,
stringIndex: 1
}, {
tick: 1140,
stringIndex: 3
}, {
tick: 1150,
stringIndex: 5
}, {
tick: 1160,
stringIndex: 2
}, {
tick: 1170,
stringIndex: 4
}, {
tick: 1180,
stringIndex: 0
}, {
tick: 1190,
stringIndex: 3
},
// Joyful chord
{
tick: 1230,
stringIndex: 0
}, {
tick: 1230,
stringIndex: 2,
type: "hold",
holdDuration: 90
}, {
tick: 1230,
stringIndex: 4
}, {
tick: 1230,
stringIndex: 5
},
// Bouncy outro
{
tick: 1280,
stringIndex: 1
}, {
tick: 1300,
stringIndex: 3,
type: "hold",
holdDuration: 60
}, {
tick: 1320,
stringIndex: 5
}, {
tick: 1340,
stringIndex: 2
}, {
tick: 1360,
stringIndex: 4
}, {
tick: 1380,
stringIndex: 0,
type: "hold",
holdDuration: 80
}, {
tick: 1400,
stringIndex: 3
}, {
tick: 1420,
stringIndex: 1
}, {
tick: 1440,
stringIndex: 5,
type: "hold",
holdDuration: 70
},
// Final joyful chord
{
tick: 1500,
stringIndex: 0
}, {
tick: 1500,
stringIndex: 2,
type: "hold",
holdDuration: 100
}, {
tick: 1500,
stringIndex: 4
}, {
tick: 1500,
stringIndex: 5
}];
// Song ends after last note + 3 seconds
var songEndTick = songNotes[songNotes.length - 1].tick + 180;
// --- Helper Functions ---
function updateScoreUI() {
scoreTxt.setText(score);
streakTxt.setText(streak > 0 ? "Streak: " + streak + " x" + multiplier : "");
missesTxt.setText(misses > 0 ? "Misses: " + misses + "/" + MAX_MISSES : "");
}
function getStringIndexFromX(x) {
// Find which string x is closest to
var minDist = 99999;
var idx = 0;
for (var i = 0; i < strings.length; i++) {
var dist = Math.abs(x - strings[i].x);
if (dist < minDist) {
minDist = dist;
idx = i;
}
}
return idx;
}
function tryHitNote(stringIndex) {
// Find the first note on this string within the hit window
for (var i = 0; i < notes.length; i++) {
var n = notes[i];
if (n.stringIndex === stringIndex && !n.hit && !n.missed) {
var dy = Math.abs(n.y - HIT_ZONE_Y);
if (dy <= NOTE_HIT_WINDOW) {
n.hit = true;
// Play the correct guitar note sound for each string
if (stringIndex === 0) {
LK.getSound('E').play();
} else if (stringIndex === 1) {
LK.getSound('A').play();
} else if (stringIndex === 2) {
LK.getSound('D').play();
} else if (stringIndex === 3) {
LK.getSound('G').play();
} else if (stringIndex === 4) {
LK.getSound('B').play();
} else if (stringIndex === 5) {
LK.getSound('E2').play();
}
// Play the music only for the first note hit, then let it play continuously
if (typeof LK.resumeMusic === "function") {
if (!window._musicStarted) {
LK.resumeMusic();
window._musicStarted = true;
}
}
score += 100 * multiplier;
LK.setScore(LK.getScore() + 1);
streak += 1;
// Multiplier logic
if (streak >= MULTIPLIER_THRESHOLDS[3]) {
multiplier = 5;
} else if (streak >= MULTIPLIER_THRESHOLDS[2]) {
multiplier = 4;
} else if (streak >= MULTIPLIER_THRESHOLDS[1]) {
multiplier = 3;
} else if (streak >= MULTIPLIER_THRESHOLDS[0]) {
multiplier = 2;
} else {
multiplier = 1;
}
updateScoreUI();
// Animate note
tween(n, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
n.destroy();
}
});
return true;
}
}
}
// Missed (no note in window)
LK.getSound('noteMiss').play();
streak = 0;
multiplier = 1;
misses += 1;
updateScoreUI();
LK.effects.flashObject(strings[stringIndex], 0xff0000, 200);
if (misses >= MAX_MISSES) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
}
return false;
}
// --- Input Handling ---
game.down = function (x, y, obj) {
// Only allow input in lower 2/3 of screen (where strings are)
if (y < STRING_TOP) return;
var stringIdx = getStringIndexFromX(x);
// Check for hold note first
for (var i = 0; i < notes.length; i++) {
var n = notes[i];
if (n instanceof HoldNote && !n.hit && !n.missed && !n.completed && n.stringIndex === stringIdx) {
var dy = Math.abs(n.y - HIT_ZONE_Y);
if (dy <= NOTE_HIT_WINDOW) {
n.held = true;
n.holdStart = LK.ticks;
// Play the correct guitar note sound for each string
if (stringIdx === 0) {
LK.getSound('E').play();
} else if (stringIdx === 1) {
LK.getSound('A').play();
} else if (stringIdx === 2) {
LK.getSound('D').play();
} else if (stringIdx === 3) {
LK.getSound('G').play();
} else if (stringIdx === 4) {
LK.getSound('B').play();
} else if (stringIdx === 5) {
LK.getSound('E2').play();
}
// Play the music only for the first note hit, then let it play continuously
if (typeof LK.resumeMusic === "function") {
if (!window._musicStarted) {
LK.resumeMusic();
window._musicStarted = true;
}
}
return;
}
}
}
// Otherwise, normal note
tryHitNote(stringIdx);
};
game.up = function (x, y, obj) {
// On release, check if any hold note is being held and not completed
for (var i = 0; i < notes.length; i++) {
var n = notes[i];
if (n instanceof HoldNote && n.held && !n.completed && !n.missed) {
// If released before holdDuration, mark as missed
if (LK.ticks - n.holdStart < n.holdDuration) {
n.missed = true;
n.held = false;
streak = 0;
multiplier = 1;
misses += 1;
updateScoreUI();
LK.getSound('noteMiss').play();
LK.effects.flashObject(strings[n.stringIndex], 0xff0000, 200);
if (misses >= MAX_MISSES) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
}
}
}
}
};
// --- Game Update Loop ---
game.update = function () {
// Spawn notes according to songNotes
if (!songEnded && nextSongNoteIndex < songNotes.length) {
while (nextSongNoteIndex < songNotes.length && LK.ticks >= songNotes[nextSongNoteIndex].tick) {
// Count currently active (not hit/missed) notes
var activeNotes = 0;
for (var i = 0; i < notes.length; i++) {
if (!notes[i].hit && !notes[i].missed) {
activeNotes++;
}
}
// Only allow up to 3 notes sliding down at the same time
if (activeNotes >= 3) {
break;
}
var noteData = songNotes[nextSongNoteIndex];
var n;
if (noteData.type === "hold") {
n = new HoldNote();
n.init(noteData.stringIndex, noteData.holdDuration);
} else {
n = new Note();
n.init(noteData.stringIndex);
}
n.x = strings[noteData.stringIndex].x;
n.y = NOTE_START_Y;
n.alpha = 1;
n.scaleX = 1;
n.scaleY = 1;
notes.push(n);
game.addChild(n);
nextSongNoteIndex += 1;
}
}
// Update notes
for (var i = notes.length - 1; i >= 0; i--) {
var n = notes[i];
n.update();
// Missed note?
if (!n.hit && !n.missed && n.y > HIT_ZONE_Y + NOTE_MISS_WINDOW) {
n.missed = true;
streak = 0;
multiplier = 1;
misses += 1;
updateScoreUI();
LK.getSound('noteMiss').play();
LK.effects.flashObject(strings[n.stringIndex], 0xff0000, 200);
tween(n, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
n.destroy();
}
});
if (misses >= MAX_MISSES) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
}
}
// Completed hold note
if (n instanceof HoldNote && n.completed && !n.hit) {
n.hit = true;
score += 200 * multiplier;
LK.setScore(LK.getScore() + 2);
streak += 1;
// Multiplier logic
if (streak >= MULTIPLIER_THRESHOLDS[3]) {
multiplier = 5;
} else if (streak >= MULTIPLIER_THRESHOLDS[2]) {
multiplier = 4;
} else if (streak >= MULTIPLIER_THRESHOLDS[1]) {
multiplier = 3;
} else if (streak >= MULTIPLIER_THRESHOLDS[0]) {
multiplier = 2;
} else {
multiplier = 1;
}
updateScoreUI();
tween(n, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
n.destroy();
}
});
}
// Remove notes that are hit and faded out
if ((n.hit || n.missed) && n.alpha <= 0.01) {
notes.splice(i, 1);
continue;
}
// Make columns infinite: if note goes off bottom, respawn at top (unless hit/missed)
if (!n.hit && !n.missed && n.y > GAME_HEIGHT + n.height) {
n.y = NOTE_START_Y;
n.hit = false;
n.missed = false;
n.alpha = 1;
n.scaleX = 1;
n.scaleY = 1;
}
}
// End of song
if (!songEnded && LK.ticks > songEndTick) {
songEnded = true;
LK.showYouWin();
}
};
// --- UI Placement ---
scoreTxt.position.set(GAME_WIDTH / 2, 60);
streakTxt.position.set(GAME_WIDTH / 2, 160);
missesTxt.position.set(GAME_WIDTH / 2, 240);
// --- Start Music ---
// Start the music but immediately pause it so it only plays when notes are hit
LK.playMusic('song1', {
loop: true
});
if (typeof LK.pauseMusic === "function") {
LK.pauseMusic();
}
// --- Initialize UI ---
updateScoreUI();