User prompt
a little bit more
User prompt
move it to the right
User prompt
a little little bit right
User prompt
a little bit more
User prompt
move it more
User prompt
move backgrond to the left
User prompt
make the backgorund an assetr
User prompt
make the note hitbox bigger
User prompt
when a note is played on: guitarString1, play E sound guitarString2, play A sound guitarString3, play D sound guitarString4, play G sound guitarString5, play B sound guitarString6, play E2 sound
User prompt
play the notes of a popular rock song when a note is pressed
User prompt
make the columns infinite
User prompt
add one more line to ball fall
Code edit (1 edits merged)
Please save this source code
User prompt
Strum Hero: Guitar Rhythm Challenge
Initial prompt
guitar playing game
/**** * 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();