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