User prompt
bro thats is so easy logic to understand if we have doing streak the notes greatly make a combo text animation and u have to show letters by letters c then co then com then comb then combo and if we have taken the combo word give me multiply to score ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
combo text animation is always stuck in c letter fix it its easy we need to take other letters to make combo word ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
in combo text animation u have to write just a word first c then co then com then comb then combo fix it do it ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Fix combo text animation to animate letter-by-letter: C, CO, COM, COMB, COMBO u said that but it didnt fix it ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
combo text animation should first c letter then co letter then com then comb then combo just it make it ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
combo text animation is not working well when i did great catch i have to get get streak and that streak stacking show me combo text animation but that animation stays always in c letter fix it ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'ReferenceError: comboSteps is not defined' in or related to this line: 'if (comboSteps.length > 0) {' Line Number: 1768
User prompt
combo text animation should do the what did u do in later its should get letter by letter to be combo word ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Fix combo text animation to show correct letters as combo builds, not always 'C' u said this but there is still a problem fix it i wanna do the streak ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
combo text animation is always stuck in c letter brooo fix it ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
combo text animation shows c latter in always fix it ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
i can do great catch for combo text animation but when im trying to streak for get combo word and mutiply it is just a c letter on that animation fix it and make it easier to do get other letters ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
i cannot get the othet letter to make streak fix it i can just do c letter fix it ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
make notes spawn in more in y axiss pitch they have to close spawn for slide 9
User prompt
in combo text animation i could go further than c letter make it easier to build co then com then comb then combo ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'ReferenceError: comboSteps is not defined' in or related to this line: 'if (comboSteps.length > 0) {' Line Number: 1712
User prompt
combo text animation is always showing c letter fix it i want to write combo word letter by letter
User prompt
combo text animation is cant getting the other letter for get combo word fix it ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Loading Fix combo text animation to show C, CO, COM, COMB, COMBO letter by letter and reset to nothing on every good catch ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
combo text animation should be c then co then com then comb then combo ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
u did great combo text animation but it resets and stars from c letter fix it and go co then com then comb then comb then comb ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
u did great combo text animation but it resets and stars from c letter fix it to the nothing it should get start of word ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
in combo text animation should go on with correct hold if first got done give c letter then second correct catch give co and go like that
User prompt
in combo rext animation have to be letter by letter when i got first good caught just give c letter then good catch make it co then good catch make it com then good catch make it comb then good catch make it combo ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
in combo rext animation when i will got next streak for combo's letters c is always on screen the word should go c then co then com then comb then combo and if i have got a combo word fully give me multiply to score point ↪💡 Consider importing and using the following plugins: @upit/tween.v1
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Confetti for success
var Confetti = Container.expand(function () {
var self = Container.call(this);
var asset = self.attachAsset('confetti', {
anchorX: 0.5,
anchorY: 0.5
});
self.vx = (Math.random() - 0.5) * 20;
self.vy = -Math.random() * 20 - 10;
self.life = 40 + Math.random() * 20;
self.update = function () {
self.x += self.vx;
self.y += self.vy;
self.vy += 1.2;
self.life--;
asset.rotation += 0.2;
if (self.life <= 0) {
self.destroy();
}
};
return self;
});
// Fail splash for miss
var FailSplash = Container.expand(function () {
var self = Container.call(this);
var asset = self.attachAsset('failSplash', {
anchorX: 0.5,
anchorY: 0.5
});
asset.alpha = 0.7;
self.life = 20;
self.update = function () {
asset.scaleX += 0.1;
asset.scaleY += 0.1;
asset.alpha -= 0.03;
self.life--;
if (self.life <= 0) {
self.destroy();
}
};
return self;
});
// Note: All note types (tap, hold, glide) are handled by the Note class with type property.
var Note = Container.expand(function () {
var self = Container.call(this);
// Properties: type ('tap', 'hold', 'glide'), pitch (0-1), time, duration (for hold/glide), glideTo (for glide)
self.type = 'tap';
self.pitch = 0.5; // 0 (bottom) to 1 (top)
self.time = 0; // When the note should be hit (in ms)
self.duration = 0; // For hold/glide notes (ms)
self.glideTo = null; // For glide notes: target pitch (0-1)
// Visuals: Remove the long bar for all notes, only use the note head and curveBars for visual
// Create a tiny invisible asset to keep .noteAsset for hit/miss feedback, but don't show a long bar
var noteAsset = self.attachAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
noteAsset.width = 1;
noteAsset.height = 1;
noteAsset.alpha = 0; // Hide the main bar, only use for scaling feedback
self.noteAsset = noteAsset;
// Add a note head (noteTap) at the start of every note for good catch feedback
var noteHead = self.attachAsset('noteTap', {
anchorX: 0.5,
anchorY: 0.5
});
// Showdown: make note head larger and more distinct for musical section
noteHead.width = 100;
noteHead.height = 100;
noteHead.x = -noteAsset.width / 2; // Start at the left end of the bar
noteHead.y = 0;
self.noteHead = noteHead;
// Curvy bar for duration: approximate with a sequence of small rectangles to simulate a curve
self.curveBars = [];
var curveBarCount = 22; // More = smoother, more musical
for (var i = 0; i < curveBarCount; i++) {
var seg = self.attachAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
// Showdown: make curve bars more musical and visually clear
seg.width = 22;
seg.height = 30;
seg.alpha = 0.8;
if (self.type === 'tap') {
seg.tint = 0x66ccff;
} else if (self.type === 'hold') {
seg.tint = 0x44dd88;
} else if (self.type === 'glide') {
seg.tint = 0xff66aa;
} else {
seg.tint = 0xffffff;
}
self.curveBars.push(seg);
}
// State
self.hit = false; // Whether the note has been hit
self.missed = false; // Whether the note was missed
// For glide: cache start/end pitch
self.glideStart = null;
self.glideEnd = null;
// Update visuals per frame
self.update = function () {
// All notes are now click-and-hold for a variable duration (not just tap)
// The bar is always horizontal and its length reflects the duration
// For tap: treat as a short hold, so bar length is based on tapHoldDuration
var tapHoldDuration = 400; // ms, how long you must hold for a tap note (difficulty can adjust this)
var barLen = 0;
var startPitch = self.pitch;
var endPitch = self.pitch;
if (self.type === 'tap') {
barLen = Math.max(80, tapHoldDuration * noteSpeed);
self.duration = tapHoldDuration;
} else if (self.type === 'hold') {
barLen = Math.max(80, (self.duration || 1) * noteSpeed);
} else if (self.type === 'glide') {
barLen = Math.max(80, (self.duration || 1) * noteSpeed);
startPitch = self.glideStart !== null ? self.glideStart : self.pitch;
endPitch = self.glideEnd !== null ? self.glideEnd : self.pitch;
}
// The main bar is hidden, so only show the note head and curveBars as small segments
self.noteAsset.x = 0;
self.noteAsset.y = 0;
// Place the note head at the start of the bar
if (self.noteHead) {
self.noteHead.x = -barLen / 2;
self.noteHead.y = 0;
}
// Only show curveBars for hold and glide notes, hide for tap
for (var i = 0; i < self.curveBars.length; i++) {
var seg = self.curveBars[i];
var t = i / (self.curveBars.length - 1);
var x = t * barLen;
var y = 0;
var show = false;
if (self.type === 'tap') {
// Hide curveBars for tap notes
seg.visible = false;
continue;
} else if (self.type === 'hold') {
// Use holdShape and holdShapeParams for custom shapes (curvy, zigzag, diagonal, flat)
var shape = self.holdShape || (self.holdShapeParams ? 'custom' : null);
var params = self.holdShapeParams || {};
if (shape === 'zigzag') {
// Zigzag: alternate up/down with frequency and amplitude
var amp = typeof params.amplitude === "number" ? params.amplitude : 0.12;
var freq = typeof params.frequency === "number" ? params.frequency : 3;
var phase = typeof params.phase === "number" ? params.phase : 0;
// Zigzag in pitch space, then convert to y
var pitchOffset = Math.sin(t * Math.PI * freq + phase) * amp;
y = pitchToY(self.pitch + pitchOffset) - pitchToY(self.pitch);
} else if (shape === 'curvy') {
// Curvy: smooth sine wave
var amp = typeof params.amplitude === "number" ? params.amplitude : 0.13;
var freq = typeof params.frequency === "number" ? params.frequency : 2.2;
var phase = typeof params.phase === "number" ? params.phase : 0;
var pitchOffset = Math.sin(t * Math.PI * freq + phase) * amp;
y = pitchToY(self.pitch + pitchOffset) - pitchToY(self.pitch);
} else if (shape === 'diagonal') {
// Diagonal: linear pitch change
var slope = typeof params.slope === "number" ? params.slope : 0.1;
var pitchOffset = t * slope;
y = pitchToY(self.pitch + pitchOffset) - pitchToY(self.pitch);
} else if (shape === 'custom') {
// fallback: flat
y = 0;
} else {
// fallback: flat
y = 0;
}
show = true;
} else if (self.type === 'glide') {
// Interpolate pitch for glide
var pitch = lerp(startPitch, endPitch, t);
var y0 = pitchToY(startPitch);
var y1 = pitchToY(endPitch);
y = lerp(y0, y1, t) - pitchToY(self.pitch);
show = true;
}
// Only show every other segment for a "dotted" look, or randomize for zigzag
if (show) {
if (self.type === 'hold' && self.holdShape === 'curvy') {
// For curvy, show every other segment for a dotted curve
seg.visible = i % 2 === 0;
} else if (self.type === 'hold' && self.holdShape === 'zigzag') {
// For zigzag, show all segments for a solid zigzag
seg.visible = true;
} else if (self.type === 'hold' && self.holdShape === 'diagonal') {
// For diagonal, show every segment for a dashed line
seg.visible = i % 2 === 0;
} else if (self.type === 'hold') {
// For flat/custom, show all segments
seg.visible = true;
} else if (self.type === 'glide') {
// For glide, show every other segment for a dotted line
seg.visible = i % 2 === 0;
} else {
seg.visible = false;
}
} else {
seg.visible = false;
}
seg.x = x - barLen / 2 + 60;
seg.y = y;
seg.rotation = 0;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222244
});
/****
* Game Code
****/
// Trombone pitch sound assets for different pitch levels
// --- Constants ---
// Trombone Slide Showdown - Asset Initialization
// Note: Asset creation is handled automatically by LK based on usage below.
// Trombone slide bar (vertical pitch bar)
// Note types
// Hold bar (for hold notes)
// Hit zone
// Comedic feedback
// Sounds (placeholders, actual sound assets will be loaded by LK)
// Music (background track)
// 10 different slide up and 10 different slide down assets
// 10 slide down sounds, evenly split across the sample
// 10 slide up sounds, evenly split across the sample
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: !0,
configurable: !0,
writable: !0
}) : e[r] = t, e;
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : i + "";
}
function _toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
var gameWidth = 2048;
var gameHeight = 2732;
// Pitch bar (vertical, left side)
var pitchBarHeight = gameHeight;
var pitchBarY = 0;
var pitchBarX = 120;
// Hit zone (where notes should be hit)
var hitZoneX = 340;
var hitZoneWidth = 140;
// Note scroll area
var noteStartX = gameWidth + 1000; // Offscreen right (even further than before)
var noteEndX = hitZoneX; // Hit zone
// Note speed (pixels per ms)
var noteSpeed = 1.2; // px/ms (tune for difficulty)
// --- State ---
var notes = []; // All notes in the song
var activeNotes = []; // Notes currently on screen
var currentTime = 0; // ms since song start
var songStarted = false;
var songEnded = false;
var score = 0;
var combo = 0;
var maxCombo = 0;
var accuracySum = 0;
var accuracyCount = 0;
// --- Song Selection Menu ---
// Rebuild hold note data to generate zigzag, curvy, diagonal, flat, and random horizontal hold bars
function randomHoldShape(pitch, duration, idx) {
// Avoid too vertical, favor horizontal/curvy/zigzag/diagonal/flat
var shapes = ['zigzag', 'curvy', 'diagonal', 'flat'];
// Weighted: more horizontal/curvy/zigzag
var weights = [2, 2, 1, 1];
var total = 0;
for (var i = 0; i < weights.length; i++) total += weights[i];
var r = Math.random() * total;
var acc = 0;
var shape = 'curvy';
for (var i = 0; i < shapes.length; i++) {
acc += weights[i];
if (r < acc) {
shape = shapes[i];
break;
}
}
// Parameters for each shape
if (shape === 'zigzag') {
return {
holdShape: 'zigzag',
holdShapeParams: {
amplitude: 0.09 + Math.random() * 0.07,
// 0.09-0.16
frequency: 2 + Math.random() * 2.5,
// 2-4.5
phase: Math.random() * Math.PI * 2
}
};
} else if (shape === 'curvy') {
return {
holdShape: 'curvy',
holdShapeParams: {
amplitude: 0.08 + Math.random() * 0.08,
// 0.08-0.16
frequency: 1.2 + Math.random() * 1.8,
// 1.2-3
phase: Math.random() * Math.PI * 2
}
};
} else if (shape === 'diagonal') {
// Slope: -0.18 to 0.18, but not too steep
var slope = (Math.random() - 0.5) * 0.28;
if (Math.abs(slope) < 0.08) slope = slope < 0 ? -0.12 : 0.12;
return {
holdShape: 'diagonal',
holdShapeParams: {
slope: slope
}
};
} else if (shape === 'flat') {
return {
holdShape: 'diagonal',
holdShapeParams: {
slope: 0
}
};
}
// fallback
return {
holdShape: 'curvy',
holdShapeParams: {
amplitude: 0.1,
frequency: 2,
phase: 0
}
};
}
// Build a new songList with more horizontal and diagonal notes, and ensure the Showdown music is exactly 1 minute (60s)
var songList = [{
name: "Showdown (Default)",
id: "bgmusic",
notes: function () {
// --- 1-minute (60s) song, all notes spaced min 120px, max 200px apart, some at 120px, some at 150px, some at 200px ---
// All notes are 'hold' for click-hold mechanic
var notes = [];
var baseTime = 2000;
var totalNotes = 38; // Remove the last 4 notes so they cannot be held or seen in the 1-minute showdown
var songDuration = 60000; // 60s in ms
var minPx = 120,
midPx = 150,
maxPx = 200;
var minMs = Math.ceil(minPx / noteSpeed);
var midMs = Math.ceil(midPx / noteSpeed);
var maxMs = Math.floor(maxPx / noteSpeed);
// Pitches for variety (cycled)
var pitches = [0.25, 0.35, 0.45, 0.60, 0.52, 0.70, 0.80, 0.65, 0.40, 0.30, 0.22, 0.50, 0.60, 0.75, 0.55, 0.38, 0.28, 0.45, 0.60, 0.78, 0.65, 0.50, 0.35, 0.22, 0.30, 0.45, 0.60, 0.75, 0.80, 0.65, 0.50, 0.35, 0.22, 0.30, 0.45, 0.60, 0.55, 0.40]; // Only 38 pitches
// Shapes: more horizontal and diagonal, less curvy
var shapes = [{
holdShape: "diagonal",
holdShapeParams: {
slope: 0.0
}
}, {
holdShape: "diagonal",
holdShapeParams: {
slope: 0.12
}
}, {
holdShape: "diagonal",
holdShapeParams: {
slope: -0.12
}
}, {
holdShape: "diagonal",
holdShapeParams: {
slope: 0.18
}
}, {
holdShape: "diagonal",
holdShapeParams: {
slope: -0.18
}
}, {
holdShape: "curvy",
holdShapeParams: {
amplitude: 0.08,
frequency: 1.2,
phase: 0.0
}
}, {
holdShape: "curvy",
holdShapeParams: {
amplitude: 0.10,
frequency: 2.0,
phase: 1.0
}
}, {
holdShape: "curvy",
holdShapeParams: {
amplitude: 0.09,
frequency: 1.5,
phase: 2.0
}
}, {
holdShape: "diagonal",
holdShapeParams: {
slope: 0.08
}
}, {
holdShape: "diagonal",
holdShapeParams: {
slope: -0.08
}
}]; //{2X} // Only 10 shapes, will cycle for 38 notes
// Duration pattern for musicality (make some notes shorter, but keep total time unchanged)
// Shorter notes: 500ms, 600ms, 700ms, 800ms, 900ms, 1000ms, 1100ms, 1200ms, 1300ms
// More short notes, but sum of all durations and gaps will still fit 1 minute
// Shorten the gap between note12 and note13 by reducing the pxGap between them, and adjust durations to keep total song length 1 minute
var durations = [600, 500, 1200, 700, 1100, 1300, 600, 800, 700, 500, 1000, 540, 600, 1100, 500, 700, 800, 1200, 600, 800, 700, 500, 1000, 700, 600, 800, 500, 700, 800, 700, 600, 900, 500, 700, 800, 900, 800, 700]; // Only 38 durations
// Build a pxGap pattern: make all gaps very close (random between 18px and 36px) for a difficult game
// For slide 9 (top pitch region), make Y positions even closer together
var pxGapChoices = [];
for (var i = 0; i < totalNotes - 1; i++) {
// If the note is in the top pitch region (slide 9), make the gap even smaller
// slide 9 is pitch >= 0.9 (top 10%)
var isSlide9 = false;
if (pitches[i % pitches.length] >= 0.9) {
isSlide9 = true;
}
// For slide 9, use a much smaller gap (6-14px), otherwise 18-36px
var closeGap = isSlide9 ? 6 + Math.floor(Math.random() * 9) // 6-14 inclusive
: 18 + Math.floor(Math.random() * 19); // 18-36 inclusive
pxGapChoices.push(closeGap);
}
var time = baseTime;
var debugNoteTable = [];
for (var i = 0; i < totalNotes; i++) {
// Skip note40 (i = 39) and note41 (i = 40), since i is 0-based
if (i === 39 || i === 40) {
continue;
}
var pxGap = 0;
if (i === 0) {
pxGap = 0;
} else {
pxGap = pxGapChoices[i - 1];
}
var gapMs = Math.round(pxGap / noteSpeed);
time += gapMs;
// Only add notes that will spawn before or at 1 minute (60000ms)
if (time <= baseTime + songDuration) {
// Custom override for note2 (i==1)
if (i === 1) {
notes.push({
name: "note2",
type: "hold",
time: 2040,
pitch: 1772 / 2732,
// convert Y to pitch (pitch = 1 - (Y / pitchBarHeight))
duration: 550,
holdShape: shapes[i % shapes.length].holdShape,
holdShapeParams: shapes[i % shapes.length].holdShapeParams
});
debugNoteTable.push({
noteNumber: "note2",
time: 2040,
distance: 30
});
} else {
notes.push({
name: "note" + (i + 1),
type: "hold",
time: time,
pitch: pitches[i % pitches.length],
duration: durations[i % durations.length],
holdShape: shapes[i % shapes.length].holdShape,
holdShapeParams: shapes[i % shapes.length].holdShapeParams
});
// Build debug table row
debugNoteTable.push({
noteNumber: "note" + (i + 1),
time: time,
distance: i === 0 ? "— (first note)" : pxGap
});
}
}
}
// If last note is too early, stretch all notes to fit songDuration
var lastTime = notes.length ? notes[notes.length - 1].time : baseTime;
if (lastTime < baseTime + songDuration) {
var stretch = (baseTime + songDuration - lastTime) / (notes.length - 1);
for (var i = 1; i < notes.length; i++) {
notes[i].time += Math.round(i * stretch);
debugNoteTable[i].time = notes[i].time; // update debug table as well
}
}
// Clamp last note to end exactly at 1 minute (baseTime + 60000)
if (notes.length) {
var lastIdx = notes.length - 1;
notes[lastIdx].time = baseTime + songDuration;
debugNoteTable[lastIdx].time = notes[lastIdx].time;
// Also clamp last note's duration so it ends at 1 minute
var lastNoteStart = notes[lastIdx].time;
var lastNoteDuration = Math.max(100, baseTime + songDuration - lastNoteStart);
notes[lastIdx].duration = lastNoteDuration;
}
// Print debug table to console in markdown format
if (typeof console !== "undefined" && typeof console.log === "function") {
var table = "| Note Number | Time (ms) | Distance from Previous Note (px) |\n";
table += "|-------------|-----------|-----------------------------------|\n";
for (var i = 0; i < debugNoteTable.length; i++) {
var row = debugNoteTable[i];
table += "| " + row.noteNumber + " | " + row.time + " | " + row.distance + " |\n";
}
// Print the full table to the console for all notes
console.log(table);
// Also print each row individually for easier copy-paste if needed
for (var i = 0; i < debugNoteTable.length; i++) {
var row = debugNoteTable[i];
console.log("| " + row.noteNumber + " | " + row.time + " | " + row.distance + " |");
}
}
// Expose debugNoteTable globally for inspection if needed
if (typeof window !== "undefined") {
window.debugNoteTable = debugNoteTable;
}
return notes;
}()
}];
var menuContainer = new Container();
game.addChild(menuContainer);
// Menu background REMOVED (pitchBarBg)
var menuTitle = new Text2("Select a Song", {
size: 120,
fill: 0xFFE066
});
menuTitle.anchor.set(0.5, 0);
menuTitle.x = gameWidth / 2;
menuTitle.y = gameHeight / 2 - 350;
menuContainer.addChild(menuTitle);
var menuButtons = [];
for (var i = 0; i < songList.length; i++) {
(function (idx) {
var song = songList[idx];
var btn = new Text2(song.name, {
size: 90,
fill: "#fff"
});
btn.anchor.set(0.5, 0.5);
btn.x = gameWidth / 2;
btn.y = gameHeight / 2 - 100 + idx * 180;
btn.interactive = true;
btn.buttonMode = true;
btn.songIndex = idx;
btn.alpha = 0.92;
btn.bg = LK.getAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
btn.bg.width = 700;
btn.bg.height = 140;
btn.bg.x = btn.x;
btn.bg.y = btn.y;
btn.bg.alpha = 0.35;
menuContainer.addChild(btn.bg);
menuContainer.addChild(btn);
menuButtons.push(btn);
btn.down = function (x, y, obj) {
selectSong(idx);
};
})(i);
}
function selectSong(idx) {
// Set up the selected song
selectedSong = songList[idx];
notes = [];
for (var n = 0; n < selectedSong.notes.length; n++) {
// Deep copy to avoid mutation
var noteData = {};
for (var k in selectedSong.notes[n]) noteData[k] = selectedSong.notes[n][k];
// Reposition pitch (Y) by +50px
if (typeof noteData.pitch === "number") {
// Convert pitch to Y, add 50, then convert back to pitch
var y = pitchToY(noteData.pitch) + 50;
// Clamp y to pitchBarY..pitchBarY+pitchBarHeight
if (y < pitchBarY) y = pitchBarY;
if (y > pitchBarY + pitchBarHeight) y = pitchBarY + pitchBarHeight;
noteData.pitch = yToPitch(y);
}
notes.push(noteData);
}
// Remove menu
menuContainer.visible = false;
menuContainer.interactive = false;
menuActive = false;
// Start game
songStarted = false;
songEnded = false;
currentTime = 0;
score = 0;
combo = 0;
maxCombo = 0;
accuracySum = 0;
accuracyCount = 0;
// Show UI/gameplay elements
pitchBarActive.visible = true;
hitZone.visible = true;
scoreTxt.visible = true;
comboAnimText.visible = false; // Only show when animating
comboTxt.visible = true;
feedbackTxt.visible = true;
}
var selectedSong = null;
var menuActive = true;
// --- UI Elements ---
// Pitch bar background REMOVED
// Pitch bar vertical line (full height)
var pitchBarLine = LK.getAsset('pitchBarLine', {
anchorX: 0.5,
anchorY: 0
});
pitchBarLine.x = pitchBarX;
pitchBarLine.y = pitchBarY;
game.addChild(pitchBarLine);
// Pitch bar active marker (shows current player pitch)
var pitchBarActive = LK.getAsset('pitchBarActive', {
anchorX: 0.5,
anchorY: 0.5
});
pitchBarActive.x = pitchBarX;
pitchBarActive.y = pitchBarY + pitchBarHeight / 2;
game.addChild(pitchBarActive);
// Perfect zone indicator (shows the "perfect" pitch window)
var perfectZoneBar = LK.getAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
perfectZoneBar.width = 80;
perfectZoneBar.height = 36;
perfectZoneBar.alpha = 0.5;
perfectZoneBar.tint = 0xfff700;
perfectZoneBar.x = pitchBarX - 60;
perfectZoneBar.y = pitchBarActive.y;
game.addChild(perfectZoneBar);
// Pitch deviation indicator (shows how far off pitch the player is)
var pitchDeviationBar = LK.getAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
pitchDeviationBar.width = 24;
pitchDeviationBar.height = 120;
pitchDeviationBar.alpha = 0.7;
pitchDeviationBar.tint = 0xff3333;
pitchDeviationBar.x = pitchBarX + 60;
pitchDeviationBar.y = pitchBarActive.y;
game.addChild(pitchDeviationBar);
// Avatar reaction (mirrors performance quality)
var avatarFace = LK.getAsset('noteTap', {
anchorX: 0.5,
anchorY: 0.5
});
avatarFace.x = gameWidth - 220;
avatarFace.y = gameHeight - 320;
avatarFace.scaleX = 2.2;
avatarFace.scaleY = 2.2;
avatarFace.alpha = 0.92;
game.addChild(avatarFace);
// Hit zone
var hitZone = LK.getAsset('hitZone', {
anchorX: 0.5,
anchorY: 0.5
});
hitZone.x = hitZoneX;
hitZone.y = gameHeight / 2;
hitZone.alpha = 0.13;
game.addChild(hitZone);
// Score text
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
scoreTxt.x = LK.gui.top.width / 2; // Center score horizontally in GUI
scoreTxt.y = 0;
LK.gui.top.addChild(scoreTxt);
// Combo text (now animated, to the left of score)
var comboWord = "COMBO";
var comboAnimIndex = 0;
var comboAnimActive = false;
var comboAnimTimer = null;
var comboAnimScoreToAdd = 0;
var comboAnimScorePerCombo = 200; // Score to add per full combo word
var comboAnimText = new Text2('', {
size: 110,
fill: 0xFFE066
});
// Place comboAnimText to the left of the score, with enough space for the combo word
comboAnimText.anchor.set(1, 0); // right aligned
comboAnimText.x = scoreTxt.x - 220; // further left of score to avoid overlap
comboAnimText.y = scoreTxt.y + 10;
LK.gui.top.addChild(comboAnimText);
// Time counter text (rightmost upper edge)
var timeCounterTxt = new Text2('0:00', {
size: 110,
fill: "#fff"
});
// Place at the rightmost upper edge, with a small margin from the edge
timeCounterTxt.anchor.set(1, 0); // right aligned, top
timeCounterTxt.x = 700; // set to 700 instead of LK.gui.top.width
timeCounterTxt.y = 0;
LK.gui.top.addChild(timeCounterTxt);
// Dummy comboTxt for legacy compatibility (prevents ReferenceError)
var comboTxt = {
setText: function setText() {},
visible: false
};
// Dummy feedbackTxt for legacy compatibility (prevents ReferenceError)
var feedbackTxt = {
setText: function setText() {},
visible: false
};
// Hide gameplay UI until song is selected
pitchBarActive.visible = false;
hitZone.visible = false;
scoreTxt.visible = false;
comboAnimText.visible = false;
// --- Player Pitch State ---
var playerPitch = 0.5; // 0 (bottom) to 1 (top)
var isSliding = false;
// --- PitchBarActive Sound State ---
var pitchBarSound = null;
var pitchBarSoundId = null;
var pitchBarSoundPlaying = false;
var lastPitchBarPitch = null;
var pitchBarSoundType = null; // "up" or "down"
// Array for 10 slide sounds (slide0 to slide9)
var slideSounds = [];
for (var i = 0; i < 10; i++) {
slideSounds.push(LK.getSound('slide' + i));
}
// --- Song Data (set by menu) ---
// --- Helper Functions ---
function clamp(val, min, max) {
return val < min ? min : val > max ? max : val;
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
// Returns a list of all note durations (ms) for the current song
function getAllNoteDurations() {
var durations = [];
for (var i = 0; i < notes.length; i++) {
if (typeof notes[i].duration !== "undefined") {
durations.push(notes[i].duration);
}
}
return durations;
}
// Returns a list of all notes with their duration (note number and duration)
function getAllNotesWithDuration() {
var noteList = [];
for (var i = 0; i < notes.length; i++) {
noteList.push({
noteNumber: "note" + (i + 1),
duration: notes[i].duration
});
}
return noteList;
}
// Print all notes with their duration to the console in a readable list
if (typeof console !== "undefined" && typeof console.log === "function") {
var allNotesWithDuration = getAllNotesWithDuration();
var durationTable = "| Note Number | Duration (ms) |\n";
durationTable += "|-------------|---------------|\n";
for (var i = 0; i < allNotesWithDuration.length; i++) {
var row = allNotesWithDuration[i];
durationTable += "| " + row.noteNumber + " | " + row.duration + " |\n";
}
console.log(durationTable);
// Also print as array of objects for easy copy-paste
console.log(allNotesWithDuration);
}
// Converts pitch (0-1) to y position on pitch bar
function pitchToY(pitch) {
return pitchBarY + (1 - pitch) * pitchBarHeight;
}
// Converts y position to pitch (0-1)
function yToPitch(y) {
var rel = (y - pitchBarY) / pitchBarHeight;
return clamp(1 - rel, 0, 1);
}
// Spawns a note object and adds to game
function spawnNote(noteData) {
// All notes are now click-and-hold, and cannot cross (no overlap in time)
var note = new Note();
// Force all notes to be 'hold' type for click-hold mechanic
note.type = 'hold';
note.pitch = noteData.pitch;
note.time = noteData.time;
// For tap notes, treat as short hold
if (noteData.type === 'tap') {
note.duration = 400;
} else {
note.duration = noteData.duration || 0;
}
note.glideTo = null;
note.hit = false;
note.missed = false;
// Always spawn notes fully offscreen right, never in the middle
note.x = noteStartX;
note.y = pitchToY(note.pitch) + 2;
// Defensive: for noteTap with holdBar, force x to noteStartX as well
if (note.type === 'tap' || note.type === 'hold' || note.type === 'glide') {
note.x = noteStartX;
}
// Assign hold shape if present
if (noteData.holdShape) {
note.holdShape = noteData.holdShape;
note.holdShapeParams = noteData.holdShapeParams;
}
// --- Enforce minimum X distance between note head (noteTap) and holdBar for each note ---
// The note head is at -barLen/2, the holdBar is at +barLen/2 (centered at note.x)
// We want the distance between the right edge of noteTap and the left edge of holdBar to be at least 15px
// Get the barLen for this note (should match Note.update logic)
var tapHoldDuration = 400; // ms, how long you must hold for a tap note (difficulty can adjust this)
var barLen = 0;
if (note.type === 'tap') {
barLen = Math.max(80, tapHoldDuration * noteSpeed);
note.duration = tapHoldDuration;
} else if (note.type === 'hold') {
barLen = Math.max(80, (note.duration || 1) * noteSpeed);
} else if (note.type === 'glide') {
barLen = Math.max(80, (note.duration || 1) * noteSpeed);
}
// Get note head and holdBar asset widths
var noteHeadWidth = note.noteHead ? note.noteHead.width : 0;
var holdBarWidth = note.noteAsset ? note.noteAsset.width : 0;
// Calculate the right edge of note head and left edge of holdBar
var noteHeadRight = note.x - barLen / 2 + noteHeadWidth / 2;
var holdBarLeft = note.x + barLen / 2 - holdBarWidth / 2;
// If the distance is less than 15, increase barLen to enforce minimum distance
var minXDist = 15;
var actualDist = holdBarLeft - noteHeadRight;
if (actualDist < minXDist) {
var needed = minXDist - actualDist;
barLen += needed;
// Update duration to match new barLen
if (note.type === 'tap') {
note.duration = Math.round(barLen / noteSpeed);
} else {
note.duration = Math.round(barLen / noteSpeed);
}
}
// Store the barLen for use in Note.update (optional, for consistency)
note.barLen = barLen;
// --- Enforce minimum X distance between consecutive notes (head to head) ---
// Find the last active note (if any) and check its head position
if (activeNotes.length > 0) {
var lastNote = activeNotes[activeNotes.length - 1];
var lastBarLen = lastNote.barLen || 0;
var lastNoteHeadWidth = lastNote.noteHead ? lastNote.noteHead.width : 0;
var lastNoteHeadX = lastNote.x - lastBarLen / 2;
var thisNoteHeadX = note.x - barLen / 2;
if (Math.abs(thisNoteHeadX - lastNoteHeadX) < minXDist) {
// Shift this note further right to enforce minimum distance
var shift = minXDist - Math.abs(thisNoteHeadX - lastNoteHeadX);
note.x += shift;
}
}
// --- Add a visible label to each note with its name (note1, note2, ...) ---
var noteLabel = null;
var noteIndex = notes.indexOf(noteData) + 1;
noteLabel = new Text2("note" + noteIndex, {
size: 48,
fill: "#fff"
});
noteLabel.anchor.set(0.5, 1);
noteLabel.x = 0;
noteLabel.y = -70;
note.addChild(noteLabel);
activeNotes.push(note);
game.addChild(note);
}
// Combo letter-by-letter animation and multiplier logic (step by step: C, CO, COM, COMB, COMBO)
var comboWord = "COMBO";
// Define comboSteps as the array of combo word steps for animation logic
var comboSteps = [];
for (var i = 1; i <= comboWord.length; i++) {
comboSteps.push(comboWord.substring(0, i));
}
var comboStreak = 0; // 0 = no combo, 1 = C, 2 = CO, ..., 5 = COMBO
var comboMultiplier = 1; // 1x by default, 2x when full combo
var comboAnimActive = false;
var comboAnimTimer = null;
var comboEasyCounter = 0; // Track consecutive good catches for easier combo
// Show the current step of the combo word, animating letter-by-letter
function startComboAnim() {
comboAnimActive = true;
if (comboStreak > 0 && comboStreak <= comboWord.length) {
comboAnimText.setText(comboWord.substring(0, comboStreak));
comboAnimText.visible = true;
} else {
comboAnimText.setText('');
comboAnimText.visible = false;
}
// If full combo, set multiplier
if (comboStreak === comboWord.length) {
comboMultiplier = 2;
comboAnimText.setText(comboWord);
comboAnimText.visible = true;
} else {
comboMultiplier = 1;
}
if (comboAnimTimer) {
LK.clearTimeout(comboAnimTimer);
comboAnimTimer = null;
}
}
// Call this when a combo is successfully continued (good catch)
function advanceCombo() {
// Make combo easier: advance to next letter every 1 consecutive good catch (so every good catch advances)
comboEasyCounter++;
var easyStep = 1; // Number of consecutive good catches needed to advance a letter
// Only allow to advance to the next letter if the previous letter is already shown
if (comboStreak < comboWord.length) {
if (comboEasyCounter >= easyStep) {
var _animateNextLetter = function animateNextLetter() {
if (animIndex < targetLength) {
comboAnimText.setText(comboWord.substring(0, animIndex + 1));
comboAnimText.visible = true;
// Animate scale up and back for pop effect
comboAnimText.scaleX = 1.4;
comboAnimText.scaleY = 1.4;
tween(comboAnimText, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.cubicOut
});
animIndex++;
comboAnimTimer = LK.setTimeout(_animateNextLetter, 60);
} else {
comboAnimTimer = null;
// If full combo, do a bigger pop
if (comboStreak === comboWord.length) {
comboMultiplier = 2;
comboAnimText.setText(comboWord);
comboAnimText.visible = true;
comboAnimText.scaleX = 1.6;
comboAnimText.scaleY = 1.6;
tween(comboAnimText, {
scaleX: 1,
scaleY: 1
}, {
duration: 220,
easing: tween.elasticInOut
});
} else if (comboStreak > 0 && comboStreak < comboWord.length) {
comboMultiplier = 1;
comboAnimText.setText(comboWord.substring(0, comboStreak));
comboAnimText.visible = true;
} else {
comboMultiplier = 1;
comboAnimText.setText('');
comboAnimText.visible = false;
}
}
};
comboStreak++;
comboEasyCounter = 0;
// Animate the new letter appearing, letter-by-letter
if (comboAnimTimer) {
LK.clearTimeout(comboAnimTimer);
comboAnimTimer = null;
}
var animIndex = 0;
var targetLength = comboStreak;
_animateNextLetter();
}
} else if (comboStreak === comboWord.length) {
// Already at full combo, keep showing full word and multiplier
comboAnimText.setText(comboWord);
comboAnimText.visible = true;
comboMultiplier = 2;
}
}
// Call this when combo is broken (miss or wrong hold)
function resetComboProgress() {
// Always reset combo text animation to '' immediately on miss/fault/miss notes
comboStreak = 0; // Set to 0 so that next good catch starts at 'C'
comboMultiplier = 1;
comboAnimActive = false;
comboAnimText.setText('');
comboAnimText.visible = false;
comboEasyCounter = 0; // Reset easy counter as well
if (comboAnimTimer) {
LK.clearTimeout(comboAnimTimer);
comboAnimTimer = null;
}
}
// Patch all places where combo is increased or reset to use new logic
// Spawns confetti at (x, y)
function spawnConfetti(x, y) {
for (var i = 0; i < 10; i++) {
var c = new Confetti();
c.x = x;
c.y = y;
game.addChild(c);
}
}
// Spawns fail splash at (x, y)
function spawnFailSplash(x, y) {
var s = new FailSplash();
s.x = x;
s.y = y;
game.addChild(s);
}
// --- Input Handling ---
// The player can press/hold anywhere on the vertical pitch field to set pitch.
// The vertical position of the cursor/finger determines the pitch (0-1).
// The player must press/hold as the note enters the hit zone, and track pitch for glides/holds.
var pitchFieldActive = false; // True if player is pressing/holding
var pitchFieldY = pitchBarY + pitchBarHeight / 2; // Last y position of input
game.down = function (x, y, obj) {
if (menuActive) return;
// Allow input anywhere on the screen to control pitch
pitchFieldActive = true;
isSliding = true;
// Defensive: combo animation should never block pitchBarActive movement
pitchFieldY = y;
playerPitch = yToPitch(y);
updatePitchBar();
// --- Slide region sound logic: replay slide0-slide9 sound as long as user is holding, like Trombone Champ ---
// Calculate which slide region we're in
var idx = 0;
if (pitchBarActive && typeof pitchBarActive.y === "number") {
var rel = (pitchBarActive.y - pitchBarY) / pitchBarHeight;
rel = Math.max(0, Math.min(1, rel));
idx = Math.floor((1 - rel) * 10); // 0 = bottom, 9 = top
if (idx < 0) idx = 0;
if (idx > 9) idx = 9;
}
if (typeof game.lastSlideSoundIdx === "undefined") {
game.lastSlideSoundIdx = -1;
}
if (typeof game.lastSlideSoundType === "undefined") {
game.lastSlideSoundType = null;
}
// Always stop all slide sounds before playing the new one to prevent overlap
for (var i = 0; i < slideSounds.length; i++) {
if (slideSounds[i] && typeof slideSounds[i].stop === "function") {
slideSounds[i].stop();
}
}
pitchBarSound = slideSounds[idx];
pitchBarSoundType = "down"; // Default to down on click
var minRate = 0.7;
var maxRate = 1.3;
var rate = minRate + (maxRate - minRate) * playerPitch;
var playOptions = {
loop: false,
volume: 1.0,
rate: rate
};
pitchBarSoundId = pitchBarSound.play(playOptions);
pitchBarSoundPlaying = true;
game.lastSlideSoundIdx = idx;
game.lastSlideSoundType = "down";
if (pitchBarSound && typeof pitchBarSound.setRate === "function") {
pitchBarSound.setRate(pitchBarSoundId, rate);
}
};
game.move = function (x, y, obj) {
if (menuActive) return;
// Always allow pitchBarActive to be moved while holding, regardless of note state
// Defensive: always set isSliding true if move is called and pitchFieldActive or isSliding
if (pitchFieldActive || isSliding) {
isSliding = true;
pitchFieldY = y;
playerPitch = yToPitch(y);
updatePitchBar();
// Update slide sound pitch (rate) in real time
if (pitchBarSound && typeof pitchBarSound.setRate === "function" && pitchBarSoundId !== null) {
var minRate = 0.7;
var maxRate = 1.3;
var rate = minRate + (maxRate - minRate) * playerPitch;
pitchBarSound.setRate(pitchBarSoundId, rate);
}
}
// Defensive: combo animation should never block pitchBarActive movement
// If comboAnimActive is true, still allow pitchBarActive to move as above
};
game.up = function (x, y, obj) {
if (menuActive) return;
pitchFieldActive = false;
isSliding = false;
// Defensive: always allow pitchBarActive to move again on next down
// Stop all slide region sounds to prevent stuck sounds
for (var i = 0; i < slideSounds.length; i++) {
if (slideSounds[i] && typeof slideSounds[i].stop === "function") {
slideSounds[i].stop();
}
}
pitchBarSoundPlaying = false;
pitchBarSoundId = null;
};
// Update pitch bar marker position
function updatePitchBar() {
pitchBarActive.y = pitchToY(playerPitch);
}
// --- Game Loop ---
game.update = function () {
if (menuActive) {
// Hide time counter in menu
if (typeof timeCounterTxt !== "undefined") timeCounterTxt.visible = false;
// Stop pitchBarActive sound if playing
// Stop all slide region sounds to prevent stuck sounds when menu is active
for (var i = 0; i < slideSounds.length; i++) {
if (slideSounds[i] && typeof slideSounds[i].stop === "function") {
slideSounds[i].stop();
}
}
pitchBarSoundPlaying = false;
pitchBarSoundId = null;
// Hide combo and time counter in menu
if (typeof comboAnimText !== "undefined") comboAnimText.visible = false;
if (typeof timeCounterTxt !== "undefined") timeCounterTxt.visible = false;
// Block game update until song is selected
return;
}
// Show and update time counter
if (typeof timeCounterTxt !== "undefined") {
timeCounterTxt.visible = true;
// Clamp to 60s (1 minute) for showdown
var totalMs = Math.max(0, Math.min(currentTime, 60000));
var min = Math.floor(totalMs / 60000);
var sec = Math.floor(totalMs % 60000 / 1000);
var secStr = sec < 10 ? "0" + sec : "" + sec;
timeCounterTxt.setText(min + ":" + secStr);
// If we reach exactly 1:01 (61000ms), force the episode complete menu after 2s
// Defensive: Only trigger after 1:01, not before
if (!songEnded && currentTime >= 61000) {
if (typeof game.lastShowdownEndTime === "undefined" || game.lastShowdownEndTime === null) {
game.lastShowdownEndTime = currentTime;
}
}
}
// --- PitchBarActive Sound Logic ---
// Defensive: always allow isSliding to be set by input, never block by combo animation
// Only play slide sound while holding (isSliding) and pitchBarActive is visible
if (pitchBarActive.visible && isSliding) {
// Track last Y for pitchBarActive to determine direction
if (typeof pitchBarActive.lastY === "undefined") {
pitchBarActive.lastY = pitchBarActive.y;
}
var movingDown = false;
var movingUp = false;
if (pitchBarActive.y > pitchBarActive.lastY + 1) {
movingDown = true;
} else if (pitchBarActive.y < pitchBarActive.lastY - 1) {
movingUp = true;
}
// Choose sound type and volume
var slideVolume = 0.7;
var newSoundType = null;
if (movingDown) {
slideVolume = 1.0;
newSoundType = "down";
} else if (movingUp) {
slideVolume = 0.35;
newSoundType = "up";
} else {
// Not moving, keep last type or default to down
newSoundType = pitchBarSoundType || "down";
}
// Allow pitchBarActive to reach lower pitch and divide pitch bar into 10 equal regions including the new lower section
var idx = 0;
if (pitchBarActive && typeof pitchBarActive.y === "number") {
// Extend pitch bar to allow more downward pitch (e.g. allow pitch to go to -0.1, so rel can be up to 1.1)
var rel = (pitchBarActive.y - pitchBarY) / pitchBarHeight;
rel = Math.max(-0.1, Math.min(1.1, rel)); // allow a bit below 0 and above 1
// Use 10 equal divisions for idx, so each region is exactly pitchBarHeight/10 tall
// If rel < 0, treat as region 0 (lowest), if rel > 1, treat as region 9 (highest)
idx = Math.floor((1 - rel) * 10); // 0 = bottom, 9 = top
if (idx < 0) idx = 0;
if (idx > 9) idx = 9;
// Label the region for debug/UX
if (!game.slideSoundTxt) {
game.slideSoundTxt = new Text2('', {
size: 60,
fill: "#fff"
});
game.slideSoundTxt.anchor.set(0.5, 0.5);
game.slideSoundTxt.x = pitchBarActive.x + 220;
game.slideSoundTxt.y = pitchBarActive.y;
game.addChild(game.slideSoundTxt);
}
var soundLabel = "slide" + idx;
game.slideSoundTxt.setText(soundLabel);
game.slideSoundTxt.x = pitchBarActive.x + 220;
game.slideSoundTxt.y = pitchBarActive.y;
game.slideSoundTxt.visible = true;
}
if (typeof game.lastSlideSoundIdx === "undefined") {
game.lastSlideSoundIdx = -1;
}
if (typeof game.lastSlideSoundType === "undefined") {
game.lastSlideSoundType = null;
}
// Only play the slide sound if region or direction changed, and do not replay if still in same region
if (pitchBarSoundType !== newSoundType || game.lastSlideSoundIdx !== idx) {
// Stop previous sound if playing
if (pitchBarSound && pitchBarSoundPlaying && typeof pitchBarSound.stop === "function" && pitchBarSoundId !== null) {
pitchBarSound.stop(pitchBarSoundId);
pitchBarSoundPlaying = false;
pitchBarSoundId = null;
}
// Set new sound asset: pick one of 10 based on pitchBarActive position
pitchBarSound = slideSounds[idx];
pitchBarSoundType = newSoundType;
// Show which slide sound is mapped to pitchBarActive
if (!game.slideSoundTxt) {
game.slideSoundTxt = new Text2('', {
size: 60,
fill: "#fff"
});
game.slideSoundTxt.anchor.set(0.5, 0.5);
game.slideSoundTxt.x = pitchBarActive.x + 220;
game.slideSoundTxt.y = pitchBarActive.y;
game.addChild(game.slideSoundTxt);
}
var soundLabel = "slide" + idx;
game.slideSoundTxt.setText(soundLabel);
game.slideSoundTxt.x = pitchBarActive.x + 220;
game.slideSoundTxt.y = pitchBarActive.y;
game.slideSoundTxt.visible = true;
// Play the new sound immediately if holding and region changed
if (isSliding && pitchBarActive.visible) {
var minRate = 0.7;
var maxRate = 1.3;
var rate = minRate + (maxRate - minRate) * playerPitch;
var playOptions = {
loop: false,
volume: slideVolume,
rate: rate
};
pitchBarSoundId = pitchBarSound.play(playOptions);
pitchBarSoundPlaying = true;
game.lastSlideSoundIdx = idx;
game.lastSlideSoundType = newSoundType;
}
// Do not replay sound here if still holding in the same region
} else {
// If still in the same region, do not replay or restart the sound, just keep it as is.
// This prevents replaying or restarting the sound if the region hasn't changed.
// (No sound restart here)
}
pitchBarActive.lastY = pitchBarActive.y;
// If the sound has ended, replay it at the current position while holding in the same region (Y coordinate)
if (typeof game.slideSoundLastReplay === "undefined") {
game.slideSoundLastReplay = {};
}
if (isSliding && pitchBarSound && typeof pitchBarSound.isPlaying === "function" && pitchBarSoundId !== null) {
var regionKey = "" + game.lastSlideSoundIdx;
// If the sound is not playing, replay it immediately (fixes bug where it sometimes doesn't play)
if (!pitchBarSound.isPlaying(pitchBarSoundId)) {
// Stop any previous instance before replaying to prevent overlap
if (typeof pitchBarSound.stop === "function" && pitchBarSoundId !== null) {
pitchBarSound.stop(pitchBarSoundId);
}
// Replay the sound if ended and still holding, at the last known position if possible
var minRate = 0.7;
var maxRate = 1.3;
var rate = minRate + (maxRate - minRate) * playerPitch;
var playOptions = {
loop: false,
volume: slideVolume,
rate: rate
};
// Always restart from the beginning for infinite replay while holding
pitchBarSoundId = pitchBarSound.play(playOptions);
pitchBarSoundPlaying = true;
game.slideSoundLastReplay[regionKey] = Date.now();
// game.lastSlideSoundIdx and game.lastSlideSoundType remain unchanged
} else {
pitchBarSoundPlaying = true;
// Adjust volume in real time if possible
if (typeof pitchBarSound.setVolume === "function") {
pitchBarSound.setVolume(pitchBarSoundId, slideVolume);
}
}
}
// Do not start a new sound here if already playing or paused; only replay when ended and still holding in the same region
// Only set rate if sound is playing or paused, do not start a new one here
if (typeof pitchBarSound.setRate === "function" && pitchBarSoundId !== null) {
// playerPitch is 0 (bottom) to 1 (top)
// Map to rate: 0.7 (low) to 1.3 (high)
var minRate = 0.7;
var maxRate = 1.3;
var rate = minRate + (maxRate - minRate) * playerPitch;
pitchBarSound.setRate(pitchBarSoundId, rate);
}
} else {
// Stop all slide region sounds immediately if not holding or not visible (no delay)
for (var i = 0; i < slideSounds.length; i++) {
if (slideSounds[i] && typeof slideSounds[i].stop === "function") {
slideSounds[i].stop();
}
}
pitchBarSoundPlaying = false;
pitchBarSoundId = null;
// Hide slide sound label if present
if (game.slideSoundTxt) {
game.slideSoundTxt.visible = false;
}
}
if (!songStarted) {
// Start music and timer
LK.playMusic(selectedSong ? selectedSong.id : 'bgmusic');
songStarted = true;
currentTime = 0;
score = 0;
combo = 0;
maxCombo = 0;
accuracySum = 0;
accuracyCount = 0;
scoreTxt.setText('0');
comboTxt.setText('');
feedbackTxt.setText('');
// Remove any old notes
for (var i = activeNotes.length - 1; i >= 0; i--) {
activeNotes[i].destroy();
activeNotes.splice(i, 1);
}
}
// Advance time
currentTime += 1000 / 60; // 60 FPS
// --- Visual pitch deviation indicator and avatar reaction ---
var minPitchDiff = 1.0;
var bestType = null;
var bestPitch = null;
for (var i = 0; i < activeNotes.length; i++) {
var note = activeNotes[i];
if (!note.hit && !note.missed) {
// Only consider notes in the hit zone
var inHitZone = Math.abs(note.x - hitZoneX) < hitZoneWidth / 2;
if (inHitZone) {
var targetPitch = note.pitch;
if (note.type === 'glide') {
var glideT = clamp((currentTime - note.time) / note.duration, 0, 1);
targetPitch = lerp(note.glideStart, note.glideEnd, glideT);
}
var diff = Math.abs(playerPitch - targetPitch);
if (diff < minPitchDiff) {
minPitchDiff = diff;
bestType = note.type;
bestPitch = targetPitch;
}
}
}
}
// Show deviation bar only if a note is in the hit zone
if (minPitchDiff < 1.0) {
pitchDeviationBar.visible = true;
pitchDeviationBar.y = pitchBarActive.y;
// Color: green if close, yellow if moderate, red if far
// Match easier catch: widen perfect/ok windows for feedback
if (minPitchDiff < 0.10) {
pitchDeviationBar.tint = 0x44dd88;
} else if (minPitchDiff < 0.20) {
pitchDeviationBar.tint = 0xffe066;
} else {
pitchDeviationBar.tint = 0xff3333;
}
// Height: larger if more off
pitchDeviationBar.height = 120 + minPitchDiff * 400;
// Perfect zone indicator follows the current note's target pitch
if (bestPitch !== null) {
perfectZoneBar.visible = true;
perfectZoneBar.y = pitchToY(bestPitch);
} else {
perfectZoneBar.visible = false;
}
} else {
pitchDeviationBar.visible = false;
perfectZoneBar.visible = false;
}
// Avatar reaction: happy if close, worried if off, shocked if very off
// Match easier catch: widen avatar feedback windows
if (minPitchDiff < 0.10) {
avatarFace.tint = 0x44dd88; // happy
avatarFace.scaleX = 2.2;
avatarFace.scaleY = 2.2;
} else if (minPitchDiff < 0.20) {
avatarFace.tint = 0xffe066; // worried
avatarFace.scaleX = 2.0;
avatarFace.scaleY = 2.0;
} else if (minPitchDiff < 1.0) {
avatarFace.tint = 0xff3333; // shocked
avatarFace.scaleX = 2.4;
avatarFace.scaleY = 2.4;
} else {
avatarFace.tint = 0xffffff;
avatarFace.scaleX = 2.2;
avatarFace.scaleY = 2.2;
}
// Spawn notes as they come into view, only when their time is reached (like Trombone Champ)
var lastSpawnedNoteX = null;
for (var i = 0; i < notes.length; i++) {
var noteData = notes[i];
// Remove any notes that would spawn after 1 minute (60000ms)
if (noteData.time > 60000 + 3400) {
if (noteData.spawned) {
for (var j = activeNotes.length - 1; j >= 0; j--) {
if (activeNotes[j].time === noteData.time) {
activeNotes[j].destroy();
activeNotes.splice(j, 1);
}
}
}
continue;
}
// Defensive: also remove any notes with time > 60000, even if they somehow exist
if (noteData.time > 60000) {
if (noteData.spawned) {
for (var j = activeNotes.length - 1; j >= 0; j--) {
if (activeNotes[j].time === noteData.time) {
activeNotes[j].destroy();
activeNotes.splice(j, 1);
}
}
}
continue;
}
// Only spawn if not already spawned and the note's scheduled time minus travel time is reached
// This ensures notes start offscreen right and scroll in, not appear in the middle
var noteTravelTime = (noteStartX - hitZoneX) / noteSpeed;
// Calculate the X position where this note would spawn
var spawnX = noteStartX;
// Always spawn notes as soon as they are eligible, never skip
if (!noteData.spawned && currentTime >= noteData.time - noteTravelTime) {
// Enforce minimum X distance between notes
var canSpawn = true;
// Check last spawned note's X position
if (lastSpawnedNoteX !== null && Math.abs(spawnX - lastSpawnedNoteX) < 15) {
canSpawn = false;
}
// Prevent spawning in the middle of the screen (only allow if at noteStartX)
if (spawnX !== noteStartX) {
canSpawn = false;
}
if (canSpawn) {
spawnNote(noteData);
noteData.spawned = true;
lastSpawnedNoteX = spawnX;
}
}
}
// Update notes
for (var i = activeNotes.length - 1; i >= 0; i--) {
var note = activeNotes[i];
// Calculate note's current x based on time
var noteTravelTime = (noteStartX - hitZoneX) / noteSpeed;
var t = currentTime - (note.time - noteTravelTime);
// Notes start at the right and move left toward the hit zone
if (t < 0) {
// Not yet on screen, keep fully offscreen right
note.x = noteStartX;
} else {
note.x = noteStartX - t * noteSpeed;
// Clamp so note never appears in the middle before scrolling in
if (note.x > noteStartX) note.x = noteStartX;
// Defensive: never allow any note to appear left of noteStartX before scrolling in
if (t < 0 && note.x !== noteStartX) note.x = noteStartX;
}
// Clamp to hitZoneX and continue left after
if (note.x < hitZoneX) note.x = hitZoneX + (note.x - hitZoneX);
// For all notes: y position is fixed to their initial pitch, even for glides (no vertical movement)
if (!note.hit && !note.missed) {
note.y = pitchToY(note.pitch);
}
// If hit or missed, do not update y (freeze at last value)
// Remove notes that have gone far enough offscreen left
if (note.x < -650) {
note.destroy();
activeNotes.splice(i, 1);
continue;
}
// Check for hit/miss
if (!note.hit && !note.missed) {
// Tap note: now must be click-and-hold in hit zone for a short time (like a mini-hold)
if (note.type === 'tap') {
var tapHoldDuration = 400; // ms, must match Note class
var hitWindow = tapHoldDuration; // ms, tap is now a short hold
var timeDiff = Math.abs(currentTime - note.time);
var inHitZone = Math.abs(note.x - hitZoneX) < hitZoneWidth / 2;
var pitchDiff = Math.abs(playerPitch - note.pitch);
// --- Tap notes do not give points, only visual feedback for holding ---
if (inHitZone && timeDiff < hitWindow) {
if (!note.holdStarted) {
note.holdStarted = true;
note.holdReleased = false;
}
note.noteAsset.scaleX = 1.2;
note.noteAsset.scaleY = 1.2;
} else if (note.holdStarted) {
note.noteAsset.scaleX = 1;
note.noteAsset.scaleY = 1;
}
// Track if player released before end of tap hold
if (note.holdStarted && !isSliding && !note.holdReleased) {
note.holdReleased = true;
}
// End of tap "hold" (after hitWindow)
if (currentTime > note.time + hitWindow && !note.hit) {
note.hit = true;
// No points for tap notes, just combo/accuracy for holding and releasing
var tapAcc = 1; // Always 1 for holding
if (note.holdStarted) {
// Make combo easier: advance combo on any correct catch
advanceCombo();
combo = comboStreak;
if (combo > maxCombo) maxCombo = combo;
accuracySum += tapAcc;
accuracyCount++;
// Apply multiplier if combo is complete
var addScore = 0;
if (comboStreak === comboSteps.length && comboMultiplier > 1) {
addScore = Math.round(100 * comboMultiplier);
score += addScore;
} else {
addScore = 100;
score += addScore;
}
score = Math.round(score);
scoreTxt.setText(score + '');
spawnConfetti(note.x, note.y);
LK.getSound('hit').play();
} else {
// Combo broken, reset to 'C' and allow restart on next correct catch
resetComboProgress();
combo = 0;
comboAnimText.setText('');
comboAnimText.visible = false;
spawnFailSplash(note.x, note.y);
LK.getSound('miss').play();
}
}
// Missed if passed hit window and not started
if (currentTime > note.time + hitWindow + 200 && !note.hit) {
note.missed = true;
resetComboProgress();
combo = 0;
spawnFailSplash(note.x, note.y);
LK.getSound('miss').play();
}
}
// Hold note: must hold correct pitch during duration (horizontal, click-and-hold)
else if (note.type === 'hold') {
var holdStart = note.time;
var holdEnd = note.time + note.duration;
var inHoldZone = currentTime >= holdStart - 180 && currentTime <= holdEnd + 180;
var inHitZone = Math.abs(note.x - hitZoneX) < hitZoneWidth / 2;
// --- HEAD CATCH LOGIC ---
// Only allow hold scoring if the head of the note is caught in the hit zone at the correct time and pitch
// Only allow one hold bar to be caught at a time (like Trombone Champ)
if (!note.headCaught) {
// Check if any other hold note is currently being held/caught
var anotherHoldCaught = false;
for (var j = 0; j < activeNotes.length; j++) {
var other = activeNotes[j];
if (other !== note && other.type === 'hold' && other.headCaught && !other.hit && !other.missed) {
anotherHoldCaught = true;
break;
}
}
// Only allow catching this hold bar if no other is currently caught
if (!anotherHoldCaught) {
// Check if the head of the note (the first frame it enters the hit zone) is caught
var headInZone = Math.abs(note.x - hitZoneX) < hitZoneWidth / 2;
var headTimeOk = Math.abs(currentTime - holdStart) < 180;
// Make it easier to catch the note's head for combo: widen pitch window from 0.13 to 0.22, and allow short release
var headPitchOk = Math.abs(playerPitch - note.pitch) < 0.22 && (isSliding || note.holdStarted && !note.holdReleased && currentTime - (note.headCatchTime || 0) < 120);
if (headInZone && headTimeOk && headPitchOk) {
// Before catching, forcibly release all other hold notes' headCaught state
for (var j = 0; j < activeNotes.length; j++) {
var other = activeNotes[j];
if (other !== note && other.type === 'hold' && other.headCaught && !other.hit && !other.missed) {
other.headCaught = false;
other.holdStarted = false;
other.holdReleased = true;
}
}
note.headCaught = true;
note.holdStarted = true;
note.holdScore = 0;
note.holdTicks = 0;
// Do not play slide sound when catching hold note head; only focus on click/release for sound
if (!note.slideSoundPlayed) {
note.slideSoundPlayed = true;
}
note.headCatchTime = currentTime;
note.noteAsset.scaleX = 1.2;
note.noteAsset.scaleY = 1.2;
}
}
}
// Only allow hold scoring if head was caught
if (note.headCaught && inHoldZone && inHitZone) {
// --- Begin: Like Trombone Champ, update points in real time while holding note ---
if (typeof note.lastScoreFrame === "undefined" || note.lastScoreFrame !== Math.floor(currentTime * 1000)) {
// Make it easier to keep holding: widen pitch window from 0.13 to 0.18
if (isSliding && Math.abs(playerPitch - note.pitch) < 0.18) {
// Give more points for correct catch (e.g. 6 per frame, easier scoring)
if (typeof note.lastScoreIncrementFrame === "undefined" || note.lastScoreIncrementFrame !== Math.floor(currentTime * 1000)) {
note.holdScore += 6;
score += 6;
note.lastScoreIncrementFrame = Math.floor(currentTime * 1000);
}
// Removed showFeedback('Good!', "#66ccff");
} else {
// Give a smaller penalty for fault (e.g. -0.1 per frame, less punishing)
note.holdScore += 0;
score -= 0.1;
if (score < 0) score = 0;
// Removed showFeedback('Bad!', "#ffcc66");
}
// Always round score to nearest integer to avoid floating point artifacts
score = Math.round(score);
scoreTxt.setText(score + '');
note.lastScoreFrame = Math.floor(currentTime * 1000);
}
note.holdTicks += 1;
// Match the easier catch window for visual feedback
note.noteAsset.scaleX = isSliding && Math.abs(playerPitch - note.pitch) < 0.18 ? 1.2 : 1;
note.noteAsset.scaleY = isSliding && Math.abs(playerPitch - note.pitch) < 0.18 ? 1.2 : 1;
// --- End: Like Trombone Champ, update points in real time while holding note ---
// Track if player released before end of hold
if (!isSliding && !note.holdReleased) {
// Allow a short release (up to 120ms) without breaking combo
if (!note.releaseGraceStart) note.releaseGraceStart = currentTime;
if (currentTime - note.releaseGraceStart > 120) {
note.holdReleased = true;
}
} else if (isSliding) {
note.releaseGraceStart = null;
}
} else if (note.holdStarted) {
note.noteAsset.scaleX = 1;
note.noteAsset.scaleY = 1;
// --- Decrease score if player is holding the holdbar at the wrong time (not in correct window) ---
if (isSliding) {
// Make it even easier to get a fault: trigger combo fault on every frame of wrong hold, and only allow combo if holding at the right time
score -= 0.4; // Stronger penalty
if (score < 0) score = 0;
// Always round score to nearest integer to avoid floating point artifacts
score = Math.round(score);
scoreTxt.setText(score + '');
// --- Combo system: reset combo if player is holding at wrong time (fault) ---
// Only break combo if not in grace period
// Reset combo text animation to 'C' immediately on every frame of wrong time hold
resetComboProgress();
combo = 0;
if (comboSteps.length > 0) {
// Show the correct combo step based on current comboStreak
var showIdx = Math.max(0, Math.min(comboStreak - 1, comboSteps.length - 1));
comboAnimText.setText(comboSteps[showIdx]);
comboAnimText.visible = true;
comboAnimIndex = showIdx;
} else {
comboAnimText.setText('');
comboAnimText.visible = false;
comboAnimIndex = 0;
}
// No need to throttle with lastWrongHoldFrame, always fault on every frame of wrong hold
}
}
// End of hold: only if head was caught
if (currentTime > holdEnd && !note.hit) {
note.hit = true;
// Do NOT force player to release hold or stop isSliding when note ends
// (fix: allow pitchBarActive to keep moving and playing slide sounds after a note ends)
// Only stop slide region sounds if player is not holding anymore (handled in game.up)
// (no pitchFieldActive = false, no isSliding = false, no forced sound stop here)
var holdAcc = note.holdTicks ? note.holdScore / note.holdTicks : 0;
// Make combo easier: advance combo as soon as headCaught, regardless of holdTicks
if (note.headCaught) {
advanceCombo();
combo = comboStreak;
if (combo > maxCombo) maxCombo = combo;
accuracySum += holdAcc;
accuracyCount++;
// Apply multiplier if combo is complete
var addScore = 0;
if (comboStreak === comboSteps.length && comboMultiplier > 1) {
addScore = Math.round(note.holdScore * comboMultiplier);
score += addScore;
} else {
addScore = Math.round(note.holdScore);
score += addScore;
}
score = Math.round(score);
scoreTxt.setText(score + '');
spawnConfetti(note.x, note.y);
LK.getSound('tromboneGood').play();
} else {
// Combo broken, reset to 'C' and allow restart on next correct catch
resetComboProgress();
combo = 0;
comboAnimText.setText('');
comboAnimText.visible = false;
spawnFailSplash(note.x, note.y);
LK.getSound('miss').play();
}
}
// Missed if passed hold window and not started or head not caught
if (currentTime > holdEnd + 200 && !note.hit) {
note.missed = true;
// Reset combo text animation to 'C' immediately on miss notes
resetComboProgress();
combo = 0;
spawnFailSplash(note.x, note.y);
LK.getSound('miss').play();
}
}
// Glide note: must follow pitch from start to end (horizontal, click-and-hold)
else if (note.type === 'glide') {
var glideStart = note.time;
var glideEnd = note.time + note.duration;
var inGlideZone = currentTime >= glideStart - 180 && currentTime <= glideEnd + 180;
var inHitZone = Math.abs(note.x - hitZoneX) < hitZoneWidth / 2;
if (inGlideZone && inHitZone) {
var glideT = clamp((currentTime - glideStart) / note.duration, 0, 1);
var targetPitch = lerp(note.glideStart, note.glideEnd, glideT);
var pitchDiff = Math.abs(playerPitch - targetPitch);
// Make it easier to catch glide notes: widen pitch window from 0.15 to 0.20
if (isSliding && pitchDiff < 0.20) {
// Good glide
if (!note.glideStarted) {
note.glideStarted = true;
note.glideScore = 0;
note.glideTicks = 0;
note.glideReleased = false; // Track if player released at end
// Do not play slide sound when catching glide note start; only focus on click/release for sound
if (!note.slideSoundPlayed) {
note.slideSoundPlayed = true;
}
}
note.glideScore += 1;
note.glideTicks += 1;
// Match the easier catch window for visual feedback
note.noteAsset.scaleX = 1.2;
note.noteAsset.scaleY = 1.2;
// Track if player released before end of glide
if (note.glideStarted && !isSliding && !note.glideReleased) {
note.glideReleased = true;
}
} else if (note.glideStarted) {
note.noteAsset.scaleX = 1;
note.noteAsset.scaleY = 1;
}
}
// End of glide
if (currentTime > glideEnd && !note.hit) {
note.hit = true;
var glideAcc = note.glideTicks ? note.glideScore / note.glideTicks : 0;
// Make combo easier: advance combo as soon as glideStarted, regardless of glideTicks
if (note.glideStarted) {
advanceCombo();
combo = comboStreak;
if (combo > maxCombo) maxCombo = combo;
accuracySum += glideAcc;
accuracyCount++;
// Apply multiplier if combo is complete
var addScore = 0;
if (comboStreak === comboSteps.length && comboMultiplier > 1) {
addScore = Math.round(note.glideScore * comboMultiplier);
score += addScore;
} else {
addScore = Math.round(note.glideScore);
score += addScore;
}
score = Math.round(score);
scoreTxt.setText(score + '');
spawnConfetti(note.x, note.y);
LK.getSound('tromboneGood').play();
} else {
// Combo broken, reset to 'C' and allow restart on next correct catch
resetComboProgress();
combo = 0;
comboAnimText.setText('');
comboAnimText.visible = false;
spawnFailSplash(note.x, note.y);
LK.getSound('miss').play();
}
}
// Missed if passed glide window and not started
if (currentTime > glideEnd + 200 && !note.hit) {
note.missed = true;
resetComboProgress();
combo = 0;
spawnFailSplash(note.x, note.y);
LK.getSound('miss').play();
}
}
}
}
// Remove hit/missed notes after a delay
for (var i = activeNotes.length - 1; i >= 0; i--) {
var note = activeNotes[i];
if ((note.hit || note.missed) && currentTime - note.time > 800) {
// Reset slideSoundPlayed so it can be triggered again for new notes
note.slideSoundPlayed = false;
note.destroy();
activeNotes.splice(i, 1);
}
}
// --- Showdown End: Wait 2 seconds after last note is hit or missed, then show 'Episode Complete!' ---
// Track the time when the last note is hit or missed
if (typeof game.lastShowdownEndTime === "undefined") {
game.lastShowdownEndTime = null;
}
// --- NEW: End episode exactly at 1 minute (60000ms) in showdown music ---
if (!songEnded && currentTime >= 60000) {
songEnded = true;
// Calculate accuracy and rank
var acc = accuracyCount ? Math.round(Math.min(accuracySum / accuracyCount, 1) * 100) : 0;
var rank = "D";
// Optimize rank thresholds for new scoring (much easier to get higher rank)
if (acc >= 90 && score > 800 && maxCombo >= notes.length * 0.7) {
rank = "S";
} else if (acc >= 80 && score > 600 && maxCombo >= notes.length * 0.5) {
rank = "A";
} else if (acc >= 65 && score > 400 && maxCombo >= notes.length * 0.3) {
rank = "B";
} else if (acc >= 40 && score > 150 && maxCombo >= notes.length * 0.1) {
rank = "C";
}
// Hide gameplay UI
pitchBarActive.visible = false;
hitZone.visible = false;
scoreTxt.visible = false;
comboAnimText.visible = false;
comboTxt.visible = false;
feedbackTxt.visible = false;
// Hide time counter on result overlay
if (typeof timeCounterTxt !== "undefined") timeCounterTxt.visible = false;
// Show result overlay
if (typeof resultOverlay !== "undefined" && resultOverlay) {
resultOverlay.destroy();
}
var resultOverlay = new Container();
game.addChild(resultOverlay);
// Title
var resultTitle = new Text2("Episode Complete!", {
size: 120,
fill: 0xFFE066
});
resultTitle.anchor.set(0.5, 0);
resultTitle.x = gameWidth / 2;
resultTitle.y = gameHeight / 2 - 480;
resultOverlay.addChild(resultTitle);
// Rank
var rankText = new Text2("Rank: " + rank, {
size: 200,
fill: rank === "S" ? "#ffd700" : rank === "A" ? "#aaffaa" : rank === "B" ? "#66ccff" : rank === "C" ? "#ffcc66" : "#ff6666"
});
rankText.anchor.set(0.5, 0);
rankText.x = gameWidth / 2;
rankText.y = gameHeight / 2 - 260;
resultOverlay.addChild(rankText);
// Score, Combo, Accuracy
var resultStats = new Text2("Score: " + score + "\n\nMax Combo: " + maxCombo + "\n\nAccuracy: " + acc + "%", {
size: 90,
fill: "#fff"
});
resultStats.anchor.set(0.5, 0);
resultStats.x = gameWidth / 2;
resultStats.y = gameHeight / 2 - 10;
resultOverlay.addChild(resultStats);
// Menu button
var menuBtnBg = LK.getAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
menuBtnBg.width = 500;
menuBtnBg.height = 120;
menuBtnBg.x = gameWidth / 2;
menuBtnBg.y = gameHeight / 2 + 350;
menuBtnBg.alpha = 0.4;
resultOverlay.addChild(menuBtnBg);
var menuBtn = new Text2("Back to Menu", {
size: 90,
fill: 0xFFE066
});
menuBtn.anchor.set(0.5, 0.5);
menuBtn.x = gameWidth / 2;
menuBtn.y = gameHeight / 2 + 350;
menuBtn.interactive = true;
menuBtn.buttonMode = true;
resultOverlay.addChild(menuBtn);
menuBtn.down = function (x, y, obj) {
// Remove overlay, show menu, reset state
resultOverlay.destroy();
menuContainer.visible = true;
menuContainer.interactive = true;
menuActive = true;
// Hide gameplay UI
pitchBarActive.visible = false;
hitZone.visible = false;
scoreTxt.visible = false;
comboAnimText.visible = false;
comboTxt.visible = false;
feedbackTxt.visible = false;
// Remove all notes
for (var i = activeNotes.length - 1; i >= 0; i--) {
activeNotes[i].destroy();
activeNotes.splice(i, 1);
}
// Reset song state
songStarted = false;
songEnded = false;
currentTime = 0;
score = 0;
combo = 0;
maxCombo = 0;
accuracySum = 0;
accuracyCount = 0;
game.lastShowdownEndTime = null;
};
// Block further game update until menu is shown again
return;
}
// --- (Legacy fallback: if for some reason the above doesn't trigger, keep the original fallback for > 3s after last note time, but not before 1:01) ---
if (!songEnded) {
// Find the last note in the song
var lastNoteData = notes.length ? notes[notes.length - 1] : null;
var lastNoteObj = null;
// Find the corresponding active note object (if still present)
for (var i = 0; i < activeNotes.length; i++) {
if (activeNotes[i].time === (lastNoteData ? lastNoteData.time : -1)) {
lastNoteObj = activeNotes[i];
break;
}
}
// If the last note exists and is hit or missed, record the time
if (lastNoteObj && (lastNoteObj.hit || lastNoteObj.missed)) {
if (game.lastShowdownEndTime === null) {
game.lastShowdownEndTime = currentTime;
}
}
// If the last note is no longer in activeNotes (already removed), but all notes are hit/missed, also record the time
if (!lastNoteObj && notes.length && notes[notes.length - 1].spawned) {
// Check if all notes are hit or missed
var allDone = true;
for (var i = 0; i < notes.length; i++) {
var found = false;
for (var j = 0; j < activeNotes.length; j++) {
if (activeNotes[j].time === notes[i].time) {
found = true;
break;
}
}
if (found) {
// Still active, not done
allDone = false;
break;
}
}
if (allDone && game.lastShowdownEndTime === null) {
game.lastShowdownEndTime = currentTime;
}
}
// If 2 seconds have passed since last note was hit/missed, show result
if (game.lastShowdownEndTime !== null && currentTime - game.lastShowdownEndTime > 2000) {
songEnded = true;
// Calculate accuracy and rank
var acc = accuracyCount ? Math.round(Math.min(accuracySum / accuracyCount, 1) * 100) : 0;
var rank = "D";
// Optimize rank thresholds for new scoring (much easier to get higher rank)
if (acc >= 90 && score > 800 && maxCombo >= notes.length * 0.7) {
rank = "S";
} else if (acc >= 80 && score > 600 && maxCombo >= notes.length * 0.5) {
rank = "A";
} else if (acc >= 65 && score > 400 && maxCombo >= notes.length * 0.3) {
rank = "B";
} else if (acc >= 40 && score > 150 && maxCombo >= notes.length * 0.1) {
rank = "C";
}
// --- LIKE POINT END SECTION ---
// This is the "end section of like point" where you can finalize points, score, and show results.
// (You can add any additional logic here for 'like point' if needed.)
// Hide gameplay UI
pitchBarActive.visible = false;
hitZone.visible = false;
scoreTxt.visible = false;
comboAnimText.visible = false;
comboTxt.visible = false;
feedbackTxt.visible = false;
// Hide time counter on result overlay
if (typeof timeCounterTxt !== "undefined") timeCounterTxt.visible = false;
// Show result overlay
if (typeof resultOverlay !== "undefined" && resultOverlay) {
resultOverlay.destroy();
}
var resultOverlay = new Container();
game.addChild(resultOverlay);
// Dim background REMOVED (pitchBarBg)
// Title
var resultTitle = new Text2("Episode Complete!", {
size: 120,
fill: 0xFFE066
});
resultTitle.anchor.set(0.5, 0);
resultTitle.x = gameWidth / 2;
resultTitle.y = gameHeight / 2 - 480;
resultOverlay.addChild(resultTitle);
// Rank
var rankText = new Text2("Rank: " + rank, {
size: 200,
fill: rank === "S" ? "#ffd700" : rank === "A" ? "#aaffaa" : rank === "B" ? "#66ccff" : rank === "C" ? "#ffcc66" : "#ff6666"
});
rankText.anchor.set(0.5, 0);
rankText.x = gameWidth / 2;
rankText.y = gameHeight / 2 - 260;
resultOverlay.addChild(rankText);
// Score, Combo, Accuracy
var resultStats = new Text2("Score: " + score + "\n\nMax Combo: " + maxCombo + "\n\nAccuracy: " + acc + "%", {
size: 90,
fill: "#fff"
});
resultStats.anchor.set(0.5, 0);
resultStats.x = gameWidth / 2;
resultStats.y = gameHeight / 2 - 10;
// Add a semi-transparent background for stats for readability
var statsBg = LK.getAsset('holdBar', {
anchorX: 0.5,
anchorY: 0
});
statsBg.width = 800;
statsBg.height = 480;
statsBg.x = resultStats.x;
statsBg.y = resultStats.y + 80;
statsBg.alpha = 0.28;
resultOverlay.addChild(statsBg);
resultOverlay.addChild(resultStats);
// Menu button
var menuBtnBg = LK.getAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
menuBtnBg.width = 500;
menuBtnBg.height = 120;
menuBtnBg.x = gameWidth / 2;
menuBtnBg.y = gameHeight / 2 + 350;
menuBtnBg.alpha = 0.4;
resultOverlay.addChild(menuBtnBg);
var menuBtn = new Text2("Back to Menu", {
size: 90,
fill: 0xFFE066
});
menuBtn.anchor.set(0.5, 0.5);
menuBtn.x = gameWidth / 2;
menuBtn.y = gameHeight / 2 + 350;
menuBtn.interactive = true;
menuBtn.buttonMode = true;
resultOverlay.addChild(menuBtn);
menuBtn.down = function (x, y, obj) {
// Remove overlay, show menu, reset state
resultOverlay.destroy();
menuContainer.visible = true;
menuContainer.interactive = true;
menuActive = true;
// Hide gameplay UI
pitchBarActive.visible = false;
hitZone.visible = false;
scoreTxt.visible = false;
comboAnimText.visible = false;
comboTxt.visible = false;
feedbackTxt.visible = false;
// Remove all notes
for (var i = activeNotes.length - 1; i >= 0; i--) {
activeNotes[i].destroy();
activeNotes.splice(i, 1);
}
// Reset song state
songStarted = false;
songEnded = false;
currentTime = 0;
score = 0;
combo = 0;
maxCombo = 0;
accuracySum = 0;
accuracyCount = 0;
game.lastShowdownEndTime = null;
};
// Block further game update until menu is shown again
return;
}
}
// (Legacy fallback: if for some reason the above doesn't trigger, keep the original fallback for > 3s after last note time, but not before 1:01)
if (!songEnded && currentTime > notes[notes.length - 1].time + 3000 && currentTime >= 61000) {
// Only allow fallback after 1:01 (61000ms)
songEnded = true;
// Calculate accuracy and rank
var acc = accuracyCount ? Math.round(Math.min(accuracySum / accuracyCount, 1) * 100) : 0;
var rank = "D";
// Optimize rank thresholds for new scoring (much easier to get higher rank)
if (acc >= 90 && score > 800 && maxCombo >= notes.length * 0.7) {
rank = "S";
} else if (acc >= 80 && score > 600 && maxCombo >= notes.length * 0.5) {
rank = "A";
} else if (acc >= 65 && score > 400 && maxCombo >= notes.length * 0.3) {
rank = "B";
} else if (acc >= 40 && score > 150 && maxCombo >= notes.length * 0.1) {
rank = "C";
}
// --- LIKE POINT END SECTION ---
// This is the "end section of like point" where you can finalize points, score, and show results.
// (You can add any additional logic here for 'like point' if needed.)
// Hide gameplay UI
pitchBarActive.visible = false;
hitZone.visible = false;
scoreTxt.visible = false;
comboAnimText.visible = false;
comboTxt.visible = false;
feedbackTxt.visible = false;
// Hide time counter on result overlay
if (typeof timeCounterTxt !== "undefined") timeCounterTxt.visible = false;
// Show result overlay
if (typeof resultOverlay !== "undefined" && resultOverlay) {
resultOverlay.destroy();
}
var resultOverlay = new Container();
game.addChild(resultOverlay);
// Dim background REMOVED (pitchBarBg)
// Title
var resultTitle = new Text2("Episode Complete!", {
size: 120,
fill: 0xFFE066
});
resultTitle.anchor.set(0.5, 0);
resultTitle.x = gameWidth / 2;
resultTitle.y = gameHeight / 2 - 480;
resultOverlay.addChild(resultTitle);
// Rank
var rankText = new Text2("Rank: " + rank, {
size: 200,
fill: rank === "S" ? "#ffd700" : rank === "A" ? "#aaffaa" : rank === "B" ? "#66ccff" : rank === "C" ? "#ffcc66" : "#ff6666"
});
rankText.anchor.set(0.5, 0);
rankText.x = gameWidth / 2;
rankText.y = gameHeight / 2 - 260;
resultOverlay.addChild(rankText);
// Score, Combo, Accuracy
var resultStats = new Text2("Score: " + score + "\n\nMax Combo: " + maxCombo + "\n\nAccuracy: " + acc + "%", {
size: 90,
fill: "#fff"
});
resultStats.anchor.set(0.5, 0);
resultStats.x = gameWidth / 2;
resultStats.y = gameHeight / 2 - 10;
// Add a semi-transparent background for stats for readability
var statsBg = LK.getAsset('holdBar', {
anchorX: 0.5,
anchorY: 0
});
statsBg.width = 800;
statsBg.height = 480;
statsBg.x = resultStats.x;
statsBg.y = resultStats.y + 80;
statsBg.alpha = 0.28;
resultOverlay.addChild(statsBg);
resultOverlay.addChild(resultStats);
// Menu button
var menuBtnBg = LK.getAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
menuBtnBg.width = 500;
menuBtnBg.height = 120;
menuBtnBg.x = gameWidth / 2;
menuBtnBg.y = gameHeight / 2 + 350;
menuBtnBg.alpha = 0.4;
resultOverlay.addChild(menuBtnBg);
var menuBtn = new Text2("Back to Menu", {
size: 90,
fill: 0xFFE066
});
menuBtn.anchor.set(0.5, 0.5);
menuBtn.x = gameWidth / 2;
menuBtn.y = gameHeight / 2 + 350;
menuBtn.interactive = true;
menuBtn.buttonMode = true;
resultOverlay.addChild(menuBtn);
menuBtn.down = function (x, y, obj) {
// Remove overlay, show menu, reset state
resultOverlay.destroy();
menuContainer.visible = true;
menuContainer.interactive = true;
menuActive = true;
// Hide gameplay UI
pitchBarActive.visible = false;
hitZone.visible = false;
scoreTxt.visible = false;
comboAnimText.visible = false;
comboTxt.visible = false;
feedbackTxt.visible = false;
// Remove all notes
for (var i = activeNotes.length - 1; i >= 0; i--) {
activeNotes[i].destroy();
activeNotes.splice(i, 1);
}
// Reset song state
songStarted = false;
songEnded = false;
currentTime = 0;
score = 0;
combo = 0;
maxCombo = 0;
accuracySum = 0;
accuracyCount = 0;
game.lastShowdownEndTime = null;
};
// Block further game update until menu is shown again
return;
}
};
// --- Start music ---
// Now handled in game.update after song selection ===================================================================
--- original.js
+++ change.js
@@ -953,18 +953,9 @@
var easyStep = 1; // Number of consecutive good catches needed to advance a letter
// Only allow to advance to the next letter if the previous letter is already shown
if (comboStreak < comboWord.length) {
if (comboEasyCounter >= easyStep) {
- comboStreak++;
- comboEasyCounter = 0;
- // Animate the new letter appearing, letter-by-letter
- if (comboAnimTimer) {
- LK.clearTimeout(comboAnimTimer);
- comboAnimTimer = null;
- }
- var animIndex = 0;
- var targetLength = comboStreak;
- var _animateNextLetter2 = function _animateNextLetter() {
+ var _animateNextLetter = function animateNextLetter() {
if (animIndex < targetLength) {
comboAnimText.setText(comboWord.substring(0, animIndex + 1));
comboAnimText.visible = true;
// Animate scale up and back for pop effect
@@ -977,9 +968,9 @@
duration: 120,
easing: tween.cubicOut
});
animIndex++;
- comboAnimTimer = LK.setTimeout(_animateNextLetter2, 60);
+ comboAnimTimer = LK.setTimeout(_animateNextLetter, 60);
} else {
comboAnimTimer = null;
// If full combo, do a bigger pop
if (comboStreak === comboWord.length) {
@@ -1005,9 +996,18 @@
comboAnimText.visible = false;
}
}
};
- _animateNextLetter2();
+ comboStreak++;
+ comboEasyCounter = 0;
+ // Animate the new letter appearing, letter-by-letter
+ if (comboAnimTimer) {
+ LK.clearTimeout(comboAnimTimer);
+ comboAnimTimer = null;
+ }
+ var animIndex = 0;
+ var targetLength = comboStreak;
+ _animateNextLetter();
}
} else if (comboStreak === comboWord.length) {
// Already at full combo, keep showing full word and multiplier
comboAnimText.setText(comboWord);
make a circle red. In-Game asset. 2d. High contrast. No shadows
give me black circle. In-Game asset. 2d. High contrast. No shadows
make a episode complete background for trambone game but without any text.. In-Game asset. 2d. High contrast. No shadows
make a backgroud shaped pear but dont do pear. In-Game asset. 2d. High contrast. No shadows
make a fully white eye like a just oval but without pupil. In-Game asset. 2d. High contrast. No shadows
nose. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
make a just face witout eyes and eyebrows and hairs and nose and mouth. In-Game asset. 2d. High contrast. No shadows
make a straight closed mouth. In-Game asset. 2d. High contrast. No shadows
make a laughed mouth. In-Game asset. 2d. High contrast. No shadows
make a red curtain but just curtain like real 3d. In-Game asset. High contrast. shadow
make a selection menu background but without any text.. In-Game asset. 2d. High contrast. No shadows
make a background like trambone champ game. In-Game asset. 2d. High contrast. No shadows
make a 2d trambone image. In-Game asset. 2d. High contrast