User prompt
in select a song menu reposition the showdown (default) text to little bit down
User prompt
in select a song menu reposition the showdown (default) text to little bit down
User prompt
in select a song menu reposition the showdown (default) text to little bit down
User prompt
in select a song menu reposition the showdown (default) text to little bit down
User prompt
in select a song menu reposition the showdown (default) text to little bit down right
User prompt
in select a song menu reposition the showdown (default) text to little bit down right
User prompt
in select a song menu reposition the showdown (default) text to little bit down right
User prompt
in select a song menu reposition the showdown (default) text to up left
User prompt
in select a song menu reposition the showdown text to up left
User prompt
move select a song text to a little bit upper
User prompt
can u write a diffirent font that select a song text
User prompt
in select a song menu change the color of select a song text to black
User prompt
Add a new background image for the song selection menu and move the 'Select a Song' text to the top of the screen u did this but i wanna new background for it and name it selection for assets folder
User prompt
lets make a good select a song menu first give a new background for it and name it selection and select a song text shoulf be toppest side of the screen
User prompt
when showdown selection got ended stop the showdown son aswell
User prompt
i cannot see the notes fix it and optimize just give it showdown music as a background song
User prompt
i have to hear that showdown music when im playing showdown fix it
User prompt
if i have selected a showdown song play the showdown asset from music folder and that song has to play 3 times
User prompt
as u can see showdown asset from music play that song when i have selected the showdown song and that song has 20 seconds replay it 3 times
User prompt
as u can see showdown asset from music play that song when i have selected the showdown song and that song has 20 seconds replay it 3 times when it got ended
User prompt
make a new song asset and name it showdown
User prompt
make a new sound asset and name it showdown
User prompt
all notes' holdbar images have a gap between each holdbar images make more holdbar image for fill the holdbar gap
User prompt
all notes' holdbar images have a gap between each holdbar images make more holdbar image for fill the holdbar gap
User prompt
can u understand when any slide sound got ended if u understand make replay the sound when it was finished
/****
* 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)
// Use only holdBar image for all note visuals (no other bar assets)
// Create a circular, fully opaque holdBar asset for hit/miss feedback
var noteAsset = self.attachAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
// Make the holdBar image a perfect circle and fully opaque
noteAsset.width = 100;
noteAsset.height = 100;
noteAsset.alpha = 1;
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 circles to simulate a curve
self.curveBars = [];
// Increase the number of holdbar images to fill the gap between holdbar images for all notes
var curveBarCount = 88; // Increased from 44 to 88 for even denser, gapless holdbar visuals
for (var i = 0; i < curveBarCount; i++) {
// Use only holdBar image for all curve segments, as circles and fully opaque
var seg = self.attachAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
// Make each segment a perfect circle and fully opaque
seg.width = 36;
seg.height = 36;
seg.alpha = 1;
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) {
// Move noteTap position next to holdBar for specific notes to remove gap and make it look like one piece
var noteLabelText = self.children && self.children.length > 0 && self.children[self.children.length - 1] && self.children[self.children.length - 1].text;
var moveHeadNotes = ["note7", "note8", "note17", "note27", "note28", "note39", "note47", "note57", "note48"];
if (noteLabelText && moveHeadNotes.indexOf(noteLabelText) !== -1) {
// Place noteTap and holdBar at the same starting position for seamless look
self.noteHead.x = -barLen / 2;
self.noteAsset.x = -barLen / 2;
// Move note7 and note8's noteTap even more upper and move holdBar down to get closer to holdBar image
if (noteLabelText === "note7" || noteLabelText === "note8") {
// Move noteTap further up
self.noteHead.y = -82;
// Move holdBar further down
self.noteAsset.y = 24;
} else {
// Only set noteHead.y = 0 if not already set for note7 or note8 above
if (!(noteLabelText === "note7" || noteLabelText === "note8" && moveHeadNotes.indexOf(noteLabelText) !== -1)) {
self.noteHead.y = 0;
self.noteAsset.y = 0;
}
}
// Also ensure extraHeadBars (if any) are aligned
if (self.extraHeadBars) {
for (var eb = 0; eb < self.extraHeadBars.length; eb++) {
self.extraHeadBars[eb].x = -barLen / 2;
}
}
} else {
// For all other notes, ensure noteTap and holdBar start at the same position
self.noteHead.x = -barLen / 2;
self.noteAsset.x = -barLen / 2;
}
self.noteHead.y = 0;
// For note7, note8, note17, note27, note28, note39, note47, note57, and note48, fill the gap between holdBar and noteTap by extending the holdBar under the noteTap
var fillGapNotes = ["note7", "note8", "note17", "note27", "note28", "note39", "note47", "note57", "note48"];
if (noteLabelText && fillGapNotes.indexOf(noteLabelText) !== -1) {
// Add multiple extra holdBar segments under the noteTap to fill the gap and make it look like one piece
if (!self.extraHeadBars) {
self.extraHeadBars = [];
var numExtraBars = 8; // Add 8 extra holdBar images for a more solid connection
for (var eb = 0; eb < numExtraBars; eb++) {
var extraBar = self.attachAsset('holdBar', {
anchorX: 0.5,
anchorY: 0.5
});
extraBar.width = 100;
extraBar.height = 100;
extraBar.alpha = 1;
self.extraHeadBars.push(extraBar);
}
}
// Position the extra holdBars so they overlap and fill the gap between noteTap and the first curveBar
var spacing = 13; // overlap them more for a solid look
for (var eb = 0; eb < self.extraHeadBars.length; eb++) {
var bar = self.extraHeadBars[eb];
bar.x = self.noteHead.x + eb * spacing - spacing * (self.extraHeadBars.length - 1) / 2;
bar.y = self.noteHead.y;
bar.visible = true;
}
} else if (self.extraHeadBars) {
for (var eb = 0; eb < self.extraHeadBars.length; eb++) {
self.extraHeadBars[eb].visible = false;
}
}
}
// Only show curveBars for hold and glide notes, hide for tap
// Make holdBar images closer together by reducing the spacing between them
var closerSpacing = 0.65; // 0.65 = 35% closer than default (was 1.0)
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 * closerSpacing;
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
****/
// Create a container for all face features
// --- Face asset for right bottom during song ---
// --- Face asset for right bottom during song ---
// --- UI Elements ---
// new face asset
// New background for song selection menu
var faceContainer = new Container();
// Add faceContainer to the game as the first child so it is the bottom-most layer for all face features
game.addChildAt(faceContainer, 0);
// Hide faceContainer by default (hidden in menu, only shown during music)
faceContainer.visible = false;
// --- Logical face layout ---
// Face image (should be at the bottom of the faceContainer)
var faceAsset = LK.getAsset('face', {
anchorX: 1,
anchorY: 1
});
faceAsset.x = 1928;
faceAsset.y = 2552;
faceContainer.addChild(faceAsset);
// --- Declare all face feature assets before using them ---
var hairAsset = LK.getAsset('hair', {
anchorX: 0.5,
anchorY: 1
});
var leftHairEdge = LK.getAsset('hairedge', {
anchorX: 1,
anchorY: 1
});
var rightHairEdge = LK.getAsset('hairedge', {
anchorX: 0,
anchorY: 1
});
var leftEye = LK.getAsset('2eyes', {
anchorX: 0.5,
anchorY: 0.5
});
var rightEye = LK.getAsset('2eyes', {
anchorX: 0.5,
anchorY: 0.5
});
var leftPupil = LK.getAsset('pupils', {
anchorX: 0.5,
anchorY: 0.5
});
var rightPupil = LK.getAsset('pupils', {
anchorX: 0.5,
anchorY: 0.5
});
var noseAsset = LK.getAsset('nose', {
anchorX: 0.5,
anchorY: 0.5
});
var mouthAsset = LK.getAsset('mouth', {
anchorX: 0.5,
anchorY: 0.5
});
var laughMouthAsset = LK.getAsset('laugh', {
anchorX: 0.5,
anchorY: 0.5
});
var leftEyebrow = LK.getAsset('eyebrow', {
anchorX: 0.5,
anchorY: 0.5
});
var rightEyebrow = LK.getAsset('eyebrow', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide all face features by default (and in select song menu)
hairAsset.visible = false;
leftHairEdge.visible = false;
rightHairEdge.visible = false;
leftEye.visible = false;
rightEye.visible = false;
leftPupil.visible = false;
rightPupil.visible = false;
noseAsset.visible = false;
mouthAsset.visible = false;
laughMouthAsset.visible = false;
leftEyebrow.visible = false;
rightEyebrow.visible = false;
faceAsset.visible = false;
// Ensure face features remain hidden in select song menu (menuContainer is visible)
function hideFaceFeaturesInMenu() {
hairAsset.visible = false;
leftHairEdge.visible = false;
rightHairEdge.visible = false;
leftEye.visible = false;
rightEye.visible = false;
leftPupil.visible = false;
rightPupil.visible = false;
noseAsset.visible = false;
mouthAsset.visible = false;
laughMouthAsset.visible = false;
leftEyebrow.visible = false;
rightEyebrow.visible = false;
faceAsset.visible = false;
}
hideFaceFeaturesInMenu();
if (typeof menuContainer !== "undefined") {
menuContainer.visible = true;
menuContainer.interactive = true;
}
// All other features are positioned relative to the faceAsset's anchor (bottom-right at 1928,2552)
// Face image is 400x400, so its center is at (1928-200, 2552-200) = (1728, 2352)
var faceCenterX = 1928 - 200;
var faceCenterY = 2552 - 200;
// Hair (above face image)
var hairAsset = LK.getAsset('hair', {
anchorX: 0.5,
anchorY: 1
});
hairAsset.x = faceCenterX;
hairAsset.y = faceCenterY - 140; // moved 30px further down for better alignment
faceContainer.addChild(hairAsset);
// Left edge hair
var leftHairEdge = LK.getAsset('hairedge', {
anchorX: 1,
anchorY: 1
});
leftHairEdge.x = faceCenterX - 100;
leftHairEdge.y = faceCenterY - 10; // moved 30px further down for better alignment
faceContainer.addChild(leftHairEdge);
// Right edge hair
var rightHairEdge = LK.getAsset('hairedge', {
anchorX: 0,
anchorY: 1
});
rightHairEdge.x = faceCenterX + 100;
rightHairEdge.y = faceCenterY - 10; // moved 30px further down for better alignment
faceContainer.addChild(rightHairEdge);
// Left eye (white part)
var leftEye = LK.getAsset('2eyes', {
anchorX: 0.5,
anchorY: 0.5
});
leftEye.x = faceCenterX - 55;
leftEye.y = faceCenterY - 45;
faceContainer.addChild(leftEye);
// Right eye (white part)
var rightEye = LK.getAsset('2eyes', {
anchorX: 0.5,
anchorY: 0.5
});
rightEye.x = faceCenterX + 55;
rightEye.y = faceCenterY - 45;
faceContainer.addChild(rightEye);
// Left pupil
var leftPupil = LK.getAsset('pupils', {
anchorX: 0.5,
anchorY: 0.5
});
leftPupil.x = leftEye.x;
leftPupil.y = leftEye.y;
faceContainer.addChild(leftPupil);
// Right pupil
var rightPupil = LK.getAsset('pupils', {
anchorX: 0.5,
anchorY: 0.5
});
rightPupil.x = rightEye.x;
rightPupil.y = rightEye.y;
faceContainer.addChild(rightPupil);
// Nose
var noseAsset = LK.getAsset('nose', {
anchorX: 0.5,
anchorY: 0.5
});
noseAsset.x = faceCenterX;
noseAsset.y = faceCenterY + 20;
faceContainer.addChild(noseAsset);
// Mouth
var mouthAsset = LK.getAsset('mouth', {
anchorX: 0.5,
anchorY: 0.5
});
mouthAsset.x = faceCenterX;
mouthAsset.y = faceCenterY + 80;
faceContainer.addChild(mouthAsset);
// Laugh mouth (hidden by default, same position as mouth)
var laughMouthAsset = LK.getAsset('laugh', {
anchorX: 0.5,
anchorY: 0.5
});
laughMouthAsset.x = mouthAsset.x;
laughMouthAsset.y = mouthAsset.y;
laughMouthAsset.visible = false;
faceContainer.addChild(laughMouthAsset);
// Left eyebrow
var leftEyebrow = LK.getAsset('eyebrow', {
anchorX: 0.5,
anchorY: 0.5
});
leftEyebrow.x = faceCenterX - 60;
leftEyebrow.y = faceCenterY - 90;
leftEyebrow.rotation = -0.18;
faceContainer.addChild(leftEyebrow);
// Right eyebrow
var rightEyebrow = LK.getAsset('eyebrow', {
anchorX: 0.5,
anchorY: 0.5
});
rightEyebrow.x = faceCenterX + 60;
rightEyebrow.y = faceCenterY - 90;
rightEyebrow.rotation = 0.18;
faceContainer.addChild(rightEyebrow);
// Move the faceContainer to (0,0) (already correct)
faceContainer.x = 0;
faceContainer.y = 0;
// --- Showdown Background Image ---
// Make showdown background image fill the entire screen, always as the bottom-most background
// New background asset for episode complete
// Small background for episode complete text
var showdownBg = LK.getAsset('showdown', {
anchorX: 0.5,
anchorY: 0.5
});
// Center the image to the screen at (1024, 1366)
showdownBg.x = 1024;
showdownBg.y = 1366;
// Always scale to fill the screen exactly, even if asset size changes
showdownBg.scaleX = gameWidth / showdownBg.width;
showdownBg.scaleY = gameHeight / showdownBg.height;
showdownBg.visible = false;
// Remove and re-add to ensure it's always at the back
if (showdownBg.parent) {
showdownBg.parent.removeChild(showdownBg);
}
game.addChildAt(showdownBg, 0); // Always at the back
// --- Combo System State (needed for combo logic) ---
// Combo animation removed: no comboStreak or comboAnimTimer to reset
var comboStreak = 0;
var comboWord = "COMBO";
comboMultiplier = 1;
// 10 slide down sounds, evenly split across the sample
// 10 different slide up and 10 different slide down assets
// Music (background track)
// Sounds (placeholders, actual sound assets will be loaded by LK)
// Comedic feedback
// Hit zone
// Hold bar (for hold notes)
// Note types
// Trombone slide bar (vertical pitch bar)
// Note: Asset creation is handled automatically by LK based on usage below.
// Trombone Slide Showdown - Asset Initialization
// --- Constants ---
// Trombone pitch sound assets for different pitch levels
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 with a visible gap for easier catching, and total song duration is 1 minute ---
// All notes are 'hold' for click-hold mechanic
var notes = [];
var baseTime = 2000;
// Calculate totalNotes so that the sum of all note durations and gaps fits 1 minute
var songDuration = 60000; // 60s in ms
// Estimate average duration and gap to fit as close as possible to 1 minute
var avgNoteDuration = 900; // will be overridden by durations array above
var avgGapPx = 75;
var avgGapMs = Math.round(avgGapPx / noteSpeed);
var totalNotes = Math.floor(songDuration / (avgNoteDuration + avgGapMs));
if (totalNotes < 40) totalNotes = 40;
if (totalNotes > 60) totalNotes = 60;
// Pitches for variety (cycled, extended for more notes)
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, 0.32, 0.48, 0.62, 0.72, 0.58, 0.68, 0.82, 0.67]; // 48 pitches
// Shapes: more horizontal and diagonal, less curvy, cycle for 48 notes
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 48 notes
// Duration pattern for musicality (make some notes shorter, but keep total time unchanged, extended for 48 notes)
// Shorter notes: every 3rd note is much shorter, others are a mix of short and medium
var durations = [];
for (var i = 0; i < totalNotes; i++) {
if (i % 3 === 0) {
durations.push(500); // much shorter
} else if (i % 4 === 0) {
durations.push(700); // short
} else if (i % 5 === 0) {
durations.push(850); // short-medium
} else {
durations.push(1100); // medium
}
}
// Build a pxGap pattern: set all gaps to be either very close (30-50px) or normal close (60-90px) for all notes
var pxGapChoices = [];
for (var i = 0; i < totalNotes - 1; i++) {
var isSlide9 = false;
if (pitches[i % pitches.length] >= 0.9) {
isSlide9 = true;
}
// Randomly choose between very close and normal close
var useVeryClose = Math.random() < 0.33; // ~33% chance for very close
var closeGap;
if (useVeryClose) {
closeGap = 30 + Math.floor(Math.random() * 21); // 30-50 inclusive
} else {
closeGap = isSlide9 ? 48 + Math.floor(Math.random() * 13) // 48-60 inclusive
: 60 + Math.floor(Math.random() * 31); // 60-90 inclusive
}
pxGapChoices.push(closeGap);
}
var time = baseTime;
var debugNoteTable = [];
for (var i = 0; i < totalNotes; i++) {
var pxGap = 0;
if (i === 0) {
pxGap = 0;
} else if (i === 7) {
// Add a more visible gap between note7 and note8
pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap
} else if (i === 12) {
// Add a more visible gap between note12 and note13
pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap
} else if (i === 14) {
// Add a more visible gap between note14 and note15
pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap
} else if (i === 17) {
// Add a more visible gap between note17 and note18
pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap
} else if (i === 23) {
// Add a visible gap between note23 and note24
pxGap = pxGapChoices[i - 1] + 60; // add extra 60px gap
} else if (i === 32) {
// Add a more visible gap between note32 and note33
pxGap = pxGapChoices[i - 1] + 220; // add extra 220px gap for more visibility
} else if (i === 37) {
// Add a more visible gap between note37 and note38
pxGap = pxGapChoices[i - 1] + 260; // add extra 260px gap for even more visibility
} else if (i === 38) {
// Add a more visible gap between note38 and note39
pxGap = pxGapChoices[i - 1] + 260; // add extra 260px gap for even more visibility
} else if (i === 44) {
// Add a more visible gap between note44 and note45
pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap for more visibility
} else if (i === 47) {
// Add a more visible gap between note47 and note48
pxGap = pxGapChoices[i - 1] + 420; // add extra 420px gap for even more visibility
} else if (i === 48) {
// Add a more visible gap between note48 and note49
pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap for more visibility
} else if (i === 54) {
// Add a more visible gap between note53 and note54
pxGap = pxGapChoices[i - 1] + 320; // add extra 320px gap for even more visibility
} 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,
// Lower the pitch of note2 so it is not too high
pitch: 0.65,
// slide9 region (top 10%) changed to lower value for less height
duration: 700,
// shorter note for variety
holdShape: shapes[i % shapes.length].holdShape,
holdShapeParams: shapes[i % shapes.length].holdShapeParams
});
debugNoteTable.push({
noteNumber: "note2",
time: 2040,
distance: 48
});
} else {
// For all other notes, if in slide9 region, force pitch to be in upper side (0.90-0.99)
var pitchVal = pitches[i % pitches.length];
var isSlide9Region = false;
if (pitchVal >= 0.9) {
pitchVal = 0.90 + Math.random() * 0.09; // 0.90-0.99
isSlide9Region = true;
}
// Make some notes shorter for variety (every 4th note is shorter)
var noteDuration = i % 4 === 0 ? 700 : durations[i % durations.length];
// Store candidate slide9 notes for later random selection
if (!window._slide9Candidates) window._slide9Candidates = [];
if (isSlide9Region) {
window._slide9Candidates.push({
i: i,
time: time,
pitchVal: pitchVal,
noteDuration: noteDuration
});
}
// For now, push a placeholder; will rebuild after loop
if (i === 5 || i === 11 || i === 17 || i === 23 || i === 35 || i === 36 || i === 42) {
// note6, note12, note18, note24, note36, note37, note43 (0-based indices 5,11,17,23,35,36,42)
// Flat slide9 note: flat = holdShape: 'diagonal', slope: 0, slide9 region = pitch 0.92-0.99
var slide9Pitch = 0.92 + Math.random() * 0.07; // 0.92-0.99
notes.push({
name: "note" + (i + 1),
type: "hold",
time: time,
pitch: slide9Pitch,
duration: noteDuration,
holdShape: "diagonal",
holdShapeParams: {
slope: 0
},
slideSound: "slide9"
});
debugNoteTable.push({
noteNumber: "note" + (i + 1),
time: time,
distance: i === 0 ? "— (first note)" : pxGap
});
} else {
notes.push({
name: "note" + (i + 1),
type: "hold",
time: time,
pitch: pitchVal,
duration: noteDuration,
holdShape: shapes[i % shapes.length].holdShape,
holdShapeParams: shapes[i % shapes.length].holdShapeParams
});
debugNoteTable.push({
noteNumber: "note" + (i + 1),
time: time,
distance: i === 0 ? "— (first note)" : pxGap
});
}
}
}
}
// Rebuild note4, note11, note22, and note24 as diagonal notes between slide0 and slide1 pitchs
var diagonalIndices = [3, 10, 21, 23]; // 0-based indices for note4, note11, note22, note24
// Pick pitches between slide0 and slide1 (bottom 20% of pitch bar)
var diagonalPitches = [0.02 + Math.random() * 0.18, 0.02 + Math.random() * 0.18, 0.02 + Math.random() * 0.18, 0.02 + Math.random() * 0.18];
// Use a moderate positive slope for diagonal (upwards)
var diagonalSlope = 0.16;
for (var j = 0; j < diagonalIndices.length; j++) {
var idx = diagonalIndices[j];
if (notes[idx]) {
notes[idx] = {
name: "note" + (idx + 1),
type: "hold",
time: notes[idx].time,
// Pitch between slide0 and slide1
pitch: diagonalPitches[j],
// Make these notes a bit shorter: 350ms (shorter than most)
duration: 350,
holdShape: "diagonal",
holdShapeParams: {
slope: diagonalSlope
}
// No slideSound property, as requested
};
}
}
// Clean up
window._slide9Candidates = undefined;
// Remove any notes after 1 minute (60000ms) to ensure song is 1 minute
while (notes.length > 0 && notes[notes.length - 1].time > baseTime + songDuration) {
notes.pop();
debugNoteTable.pop();
}
// 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;
// Make every 5th note even shorter for more variety
for (var i = 0; i < notes.length; i++) {
if (i % 5 === 0) {
notes[i].duration = Math.min(notes[i].duration, 500);
}
}
}
// 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);
// Add a new background image for the selection menu
var selectionBg = LK.getAsset('selection', {
anchorX: 0.5,
anchorY: 0.5
});
selectionBg.x = gameWidth / 2;
selectionBg.y = gameHeight / 2;
selectionBg.scaleX = gameWidth / selectionBg.width;
selectionBg.scaleY = gameHeight / selectionBg.height;
menuContainer.addChildAt(selectionBg, 0);
// 'Select a Song' text at the very top of the screen, centered
var menuTitle = new Text2("Select a Song", {
size: 120,
fill: "#000",
font: "Comic Sans MS, Comic Sans, cursive, Arial Black, Impact, sans-serif"
});
menuTitle.anchor.set(0.5, 0);
menuTitle.x = gameWidth / 2;
menuTitle.y = 10; // Move even closer to the top
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"
});
// Move the first song ("Showdown (Default)") to the upper left
if (idx === 0) {
btn.anchor.set(0, 0);
btn.x = 40; // 40px from left edge, avoid top left menu
btn.y = 40; // 40px from top edge, avoid top left menu
} else {
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;
menuContainer.addChild(btn);
menuButtons.push(btn);
btn.down = function (x, y, obj) {
selectSong(idx);
};
})(i);
}
function selectSong(idx) {
// --- Red Curtain Animation ---
// Create curtain container if not already present
if (!game.redCurtainContainer) {
var curtainContainer = new Container();
curtainContainer.x = 0;
curtainContainer.y = 0;
curtainContainer.visible = false;
game.redCurtainContainer = curtainContainer;
game.addChildAt(curtainContainer, game.children.length); // On top
} else {
var curtainContainer = game.redCurtainContainer;
// Remove any old children
while (curtainContainer.children.length > 0) curtainContainer.removeChild(curtainContainer.children[0]);
}
curtainContainer.visible = true;
// Create two curtain halves (left and right)
var curtainLeft = LK.getAsset('redcurtain', {
anchorX: 0,
anchorY: 0
});
var curtainRight = LK.getAsset('redcurtain', {
anchorX: 1,
anchorY: 0
});
// --- Red Curtain Size Multipliers ---
// You can easily change these multipliers to adjust the curtain size!
var redCurtainWidthMultiplierLeft = 2.7; // Make this larger to make the left curtain wider
var redCurtainWidthMultiplierRight = 2.2; // Make this larger to make the right curtain wider
var redCurtainHeightMultiplier = 1.2; // Make this larger to make both curtains taller
// Scale curtains to be much wider and cover the whole screen (with overlap)
// Make curtains extremely wide to ensure full coverage on all devices
// Make left curtain even wider and shift further left to guarantee full coverage
curtainLeft.width = gameWidth * redCurtainWidthMultiplierLeft;
curtainLeft.height = gameHeight * redCurtainHeightMultiplier;
curtainLeft.x = -curtainLeft.width * 0.18;
curtainLeft.y = -curtainLeft.height * 0.1;
// Right curtain remains wide for overlap
curtainRight.width = gameWidth * redCurtainWidthMultiplierRight;
curtainRight.height = gameHeight * redCurtainHeightMultiplier;
curtainRight.x = gameWidth + curtainRight.width * 0.05;
curtainRight.y = -curtainRight.height * 0.1;
curtainContainer.addChild(curtainLeft);
curtainContainer.addChild(curtainRight);
// Animate curtains closing (move both curtains to the left)
curtainLeft.x = -curtainLeft.width;
curtainRight.x = gameWidth + curtainRight.width;
// Animate in (close) - 0.5s
tween(curtainLeft, {
x: -curtainLeft.width * 0.18 // Move left curtain to its original left position
}, {
duration: 500,
easing: tween.cubicInOut
});
tween(curtainRight, {
x: gameWidth - curtainRight.width - curtainRight.width * 0.18 // Move right curtain to the left as well
}, {
duration: 500,
easing: tween.cubicInOut,
onFinish: function onFinish() {
// After closed, wait 0.1s, then continue with song setup and animate out
LK.setTimeout(function () {
// 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];
// Add a little bit of height for all notes (+30px)
if (typeof noteData.pitch === "number") {
// Convert pitch to Y, add 30 for a little more height, then convert back to pitch
var y = pitchToY(noteData.pitch) + 30;
// 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;
comboTxt.visible = true;
feedbackTxt.visible = true;
// Hide combo balloon at song start (will only show on catch)
if (typeof comboBalloonContainer !== "undefined") {
comboBalloonContainer.visible = false;
comboBalloonTxt.setText('x1');
}
// Show faceContainer and all face features during the song
faceContainer.visible = true;
faceAsset.visible = true;
hairAsset.visible = true;
leftHairEdge.visible = true;
rightHairEdge.visible = true;
leftEye.visible = true;
rightEye.visible = true;
leftPupil.visible = true;
rightPupil.visible = true;
noseAsset.visible = true;
mouthAsset.visible = true;
laughMouthAsset.visible = false;
// Always show mouth by default during song, only show laugh on great/correct catch
leftEyebrow.visible = true;
rightEyebrow.visible = true;
// No face features in select song menu; only show during music
// Ensure faceContainer and all face features are positioned and anchored exactly as in the select song menu
// (All positions and anchors are already set in the initial faceContainer setup, so we do not need to re-apply them here)
faceContainer.x = 0;
faceContainer.y = 0;
faceAsset.x = 1928;
faceAsset.y = 2552;
faceAsset.anchorX = 1;
faceAsset.anchorY = 1;
hairAsset.x = 1728;
hairAsset.y = 2352 - 140; // moved 30px further down for better alignment
hairAsset.anchorX = 0.5;
hairAsset.anchorY = 1;
leftHairEdge.x = 1728 - 100;
leftHairEdge.y = 2352 - 30; // moved 30px further down for better alignment
leftHairEdge.anchorX = 1;
leftHairEdge.anchorY = 1;
rightHairEdge.x = 1728 + 100;
rightHairEdge.y = 2352 - 30; // moved 30px further down for better alignment
rightHairEdge.anchorX = 0;
rightHairEdge.anchorY = 1;
leftEye.x = 1728 - 55;
leftEye.y = 2352 - 45;
leftEye.anchorX = 0.5;
leftEye.anchorY = 0.5;
rightEye.x = 1728 + 55;
rightEye.y = 2352 - 45;
rightEye.anchorX = 0.5;
rightEye.anchorY = 0.5;
leftPupil.x = 1728 - 55;
leftPupil.y = 2352 - 45;
leftPupil.anchorX = 0.5;
leftPupil.anchorY = 0.5;
rightPupil.x = 1728 + 55;
rightPupil.y = 2352 - 45;
rightPupil.anchorX = 0.5;
rightPupil.anchorY = 0.5;
noseAsset.x = 1728;
noseAsset.y = 2352 + 20;
noseAsset.anchorX = 0.5;
noseAsset.anchorY = 0.5;
mouthAsset.x = 1728;
mouthAsset.y = 2352 + 80;
mouthAsset.anchorX = 0.5;
mouthAsset.anchorY = 0.5;
laughMouthAsset.x = 1728;
laughMouthAsset.y = 2352 + 80;
laughMouthAsset.anchorX = 0.5;
laughMouthAsset.anchorY = 0.5;
leftEyebrow.x = 1728 - 60;
leftEyebrow.y = 2352 - 90;
leftEyebrow.anchorX = 0.5;
leftEyebrow.anchorY = 0.5;
leftEyebrow.rotation = -0.18;
rightEyebrow.x = 1728 + 60;
rightEyebrow.y = 2352 - 90;
rightEyebrow.anchorX = 0.5;
rightEyebrow.anchorY = 0.5;
rightEyebrow.rotation = 0.18;
// Show showdown background only for showdown music
if (selectedSong && (selectedSong.id === "bgmusic" || selectedSong.id === "showdown")) {
showdownBg.visible = true;
// Always ensure showdownBg is at the back and centered
if (showdownBg.parent) {
showdownBg.parent.removeChild(showdownBg);
}
showdownBg.x = 1024;
showdownBg.y = 1366;
showdownBg.scaleX = gameWidth / showdownBg.width;
showdownBg.scaleY = gameHeight / showdownBg.height;
game.addChildAt(showdownBg, 0);
// For debug: print center coordinates
if (typeof console !== "undefined" && typeof console.log === "function") {
console.log("Showdown Background Center Coordinates: (x, y) = (1024, 1366)");
}
} else {
showdownBg.visible = false;
}
// Animate curtains opening (drag out) - 1s
tween(curtainLeft, {
x: -curtainLeft.width
}, {
duration: 1000,
easing: tween.cubicInOut
});
tween(curtainRight, {
x: gameWidth + curtainRight.width
}, {
duration: 1000,
easing: tween.cubicInOut,
onFinish: function onFinish() {
curtainContainer.visible = false;
// Remove curtain children for next time
while (curtainContainer.children.length > 0) curtainContainer.removeChild(curtainContainer.children[0]);
}
});
}, 100);
}
});
}
var selectedSong = null;
var menuActive = true;
// --- UI Elements ---
// --- Catch Feedback Notification (OK/Nice/Perfect/Nasty) ---
var catchFeedbackTxt = new Text2('', {
size: 180,
//{7Z} // Make it much bigger
fill: "#fff"
});
catchFeedbackTxt.anchor.set(0.5, 0.5);
// Move to right side, vertically centered higher up
catchFeedbackTxt.x = gameWidth - 420;
catchFeedbackTxt.y = gameHeight / 2 - 350;
catchFeedbackTxt.visible = false;
game.addChild(catchFeedbackTxt);
// Helper to show catch feedback
function showCatchFeedback(type) {
var text = "";
if (type === "perfect") text = "perfect";else if (type === "nice") text = "nice";else if (type === "ok") text = "ok";else if (type === "nasty") text = "nasty";else text = "";
// Only show notification if a valid catch type is provided
if (text !== "") {
// Helper to run the shake sequence recursively
var _runShakeStep = function runShakeStep(idx) {
if (idx >= shakeSequence.length) return;
tween(catchFeedbackTxt, shakeSequence[idx], {
duration: shakeStep,
easing: tween.cubicInOut,
onFinish: function onFinish() {
_runShakeStep(idx + 1);
}
});
};
catchFeedbackTxt.setText(text);
catchFeedbackTxt.visible = true;
catchFeedbackTxt.alpha = 1;
catchFeedbackTxt.scaleX = 1;
catchFeedbackTxt.scaleY = 1;
// Always clear any previous timeout to prevent overlap
if (typeof game.catchFeedbackTimeout !== "undefined") {
LK.clearTimeout(game.catchFeedbackTimeout);
}
// Shaking animation: quick left-right shake using tween
// We'll shake 3 times (left-right-left) over 0.3s
var shakeTimes = 3;
var shakeDistance = 32;
var shakeDuration = 300;
var shakeStep = shakeDuration / (shakeTimes * 2);
var originalX = catchFeedbackTxt.x;
var shakeSequence = [];
for (var i = 0; i < shakeTimes; i++) {
shakeSequence.push({
x: originalX + shakeDistance
});
shakeSequence.push({
x: originalX - shakeDistance
});
}
shakeSequence.push({
x: originalX
});
_runShakeStep(0);
// Always hide the notification after 1.5 seconds, regardless of type
game.catchFeedbackTimeout = LK.setTimeout(function () {
tween(catchFeedbackTxt, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
catchFeedbackTxt.visible = false;
catchFeedbackTxt.alpha = 1;
catchFeedbackTxt.setText('');
catchFeedbackTxt.x = originalX; // Defensive: reset position
}
});
// Defensive: also reset text and visibility in case tween is interrupted
LK.setTimeout(function () {
catchFeedbackTxt.visible = false;
catchFeedbackTxt.alpha = 1;
catchFeedbackTxt.setText('');
catchFeedbackTxt.x = originalX;
}, 450);
}, 1500);
} else {
// If no valid catch, hide notification immediately
catchFeedbackTxt.visible = false;
catchFeedbackTxt.alpha = 1;
}
}
// --- Combo Balloon (Message Balloon) ---
// Create a container for the balloon and text
var comboBalloonContainer = new Container();
// The balloon background (ellipse shape, white, semi-transparent)
var comboBalloonBg = LK.getAsset('confetti', {
anchorX: 0.5,
anchorY: 0.5
});
comboBalloonBg.width = 170;
comboBalloonBg.height = 110;
comboBalloonBg.tint = 0xffffff;
comboBalloonBg.alpha = 0.92;
comboBalloonContainer.addChild(comboBalloonBg);
// The combo text (e.g. "x1", "x2", ...)
var comboBalloonTxt = new Text2('x1', {
size: 70,
fill: "#222"
});
comboBalloonTxt.anchor.set(0.5, 0.5);
comboBalloonTxt.x = 0;
comboBalloonTxt.y = 0;
comboBalloonContainer.addChild(comboBalloonTxt);
// Position the balloon to the left of the face (relative to faceCenterX, faceCenterY)
comboBalloonContainer.x = faceCenterX - 220;
comboBalloonContainer.y = faceCenterY - 40;
comboBalloonContainer.visible = false; // Hide by default (show only during song)
game.addChild(comboBalloonContainer);
// 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)
// Remove perfectZoneBar and pitchDeviationBar holdBar images, only use tint feedback (no visual bar)
var perfectZoneBar = {
visible: false,
y: 0
};
var pitchDeviationBar = {
visible: false,
y: 0,
tint: 0xff3333,
height: 0
};
// Avatar reaction (mirrors performance quality) - REMOVED
// 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 animation removed: no comboAnimText UI element.
// 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;//{4A} // Removed: combo text animation is no longer used
// --- 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++) {
var snd = LK.getSound('slide' + i);
// Attach an onEnd handler to each slide sound to replay if needed
(function (idx, sndRef) {
if (sndRef && typeof sndRef.onEnd === "function") {
sndRef.onEnd(function (id) {
// Only replay if this is the currently playing sound and user is still sliding in this region
if (typeof pitchBarSoundId !== "undefined" && pitchBarSoundId === id && typeof isSliding !== "undefined" && isSliding && typeof game !== "undefined" && typeof game.lastSlideSoundIdx !== "undefined" && game.lastSlideSoundIdx === idx) {
// Defensive: stop all slide sounds before replaying
for (var j = 0; j < slideSounds.length; j++) {
if (slideSounds[j] && typeof slideSounds[j].stop === "function") {
slideSounds[j].stop();
}
}
// Replay the sound with the same options as before
var minRate = 0.7;
var maxRate = 1.3;
var rate = minRate + (maxRate - minRate) * playerPitch;
var playOptions = {
loop: false,
volume: 1.0,
rate: rate
};
pitchBarSoundId = sndRef.play(playOptions);
pitchBarSoundPlaying = true;
// Set rate again in case playOptions is not respected
if (sndRef && typeof sndRef.setRate === "function") {
sndRef.setRate(pitchBarSoundId, rate);
}
}
});
}
})(i, snd);
slideSounds.push(snd);
}
// --- 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 logic removed: combo text animation and related functions are no longer used.
var comboMultiplier = 1; // 1x by default, 2x when full combo
function advanceCombo() {
// Prevent advancing combo if lockout is active
if (game.comboBalloonLockout) return;
// No combo animation, just set multiplier if needed
comboMultiplier = 1;
// Show a happy face (smile) when combo increases
if (typeof laughMouthAsset !== "undefined" && typeof mouthAsset !== "undefined") {
laughMouthAsset.visible = true;
mouthAsset.visible = false;
}
// Update combo balloon text and show only on catch
if (typeof comboBalloonContainer !== "undefined" && typeof comboBalloonTxt !== "undefined") {
// Always increment streak and show x1, x2, x3, ... for every consecutive catch (no cap)
comboStreak = (typeof comboStreak === "number" ? comboStreak : 0) + 1;
comboBalloonTxt.setText('x' + comboStreak);
// Show the balloon immediately on every catch, no delay
comboBalloonContainer.visible = true;
// Hide the balloon after 2 seconds (2000ms) for more visible streaks
if (typeof game.comboBalloonTimeout !== "undefined") {
LK.clearTimeout(game.comboBalloonTimeout);
}
game.comboBalloonTimeout = LK.setTimeout(function () {
comboBalloonContainer.visible = false;
}, 2000);
// Add a lockout to prevent next catch for a bit longer after showing the balloon (easier to get higher streaks)
if (typeof game.comboBalloonLockoutTimeout !== "undefined") {
LK.clearTimeout(game.comboBalloonLockoutTimeout);
}
game.comboBalloonLockout = true;
game.comboBalloonLockoutTimeout = LK.setTimeout(function () {
game.comboBalloonLockout = false;
}, 900); // 900ms lockout before next catch can be registered
}
}
function resetComboProgress() {
comboMultiplier = 1;
// Show a worried face (normal mouth) when combo breaks
if (typeof laughMouthAsset !== "undefined" && typeof mouthAsset !== "undefined") {
laughMouthAsset.visible = false;
mouthAsset.visible = true;
}
// On miss, reset comboStreak and set comboBalloonTxt to x1, and hide the balloon
if (typeof comboBalloonContainer !== "undefined" && typeof comboBalloonTxt !== "undefined") {
comboStreak = 0;
comboBalloonTxt.setText('x1');
comboBalloonContainer.visible = false;
if (typeof game.comboBalloonTimeout !== "undefined") {
LK.clearTimeout(game.comboBalloonTimeout);
game.comboBalloonTimeout = undefined;
}
}
}
// Spawns confetti at (x, y)
function spawnConfetti(x, y) {
for (var i = 0; i < 22; i++) {
var c = new Confetti();
c.x = x;
c.y = y;
game.addChild(c);
}
}
// Spawns fail splash at (x, y)
function spawnFailSplash(x, y) {
// Fail splash animation removed: do nothing
}
// --- 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: play slide0-slide9 sound only on region change or new press ---
// 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 play the slide sound on down, even if the region or type has not changed
// 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();
// --- Slide region sound logic: play slide0-slide9 sound only on region change, and only once per region until finger is lifted ---
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;
}
// Only play the slide sound if the region has changed since last time
if (game.lastSlideSoundIdx !== idx) {
// 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 move
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);
}
}
// Update slide sound pitch (rate) in real time, but do not replay sound
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 combo balloon in menu
if (typeof comboBalloonContainer !== "undefined") comboBalloonContainer.visible = false;
if (typeof game.comboBalloonTimeout !== "undefined") {
LK.clearTimeout(game.comboBalloonTimeout);
game.comboBalloonTimeout = undefined;
}
// 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;
}
}
}
// --- Ensure all face features are always above the background and visible during the song ---
if (faceContainer && faceContainer.parent) {
// Always keep faceContainer above showdownBg (background)
if (showdownBg && showdownBg.parent) {
// Remove and re-add faceContainer after showdownBg to ensure correct order
if (faceContainer.parent.children.indexOf(faceContainer) < faceContainer.parent.children.indexOf(showdownBg)) {
faceContainer.parent.removeChild(faceContainer);
faceContainer.parent.addChild(faceContainer);
}
}
// Ensure all face features are visible and above the face image
var faceFeatures = [hairAsset, leftHairEdge, rightHairEdge, leftEye, rightEye, leftPupil, rightPupil, noseAsset, mouthAsset, laughMouthAsset, leftEyebrow, rightEyebrow];
for (var i = 0; i < faceFeatures.length; i++) {
var feature = faceFeatures[i];
if (feature && feature.parent !== faceContainer) {
faceContainer.addChild(feature);
}
if (feature) feature.visible = true;
}
// Always keep faceAsset as the bottom-most child in faceContainer
if (faceAsset && faceAsset.parent === faceContainer && faceContainer.children[0] !== faceAsset) {
faceContainer.removeChild(faceAsset);
faceContainer.addChildAt(faceAsset, 0);
}
// Always show mouth asset by default unless laugh is being shown for a great/correct catch
if (typeof game.laughTimeout !== "undefined" && laughMouthAsset.visible) {
// If the timeout expired, hide laugh and show mouth
if (Date.now() > game.laughTimeout) {
laughMouthAsset.visible = false;
mouthAsset.visible = true;
game.laughTimeout = undefined;
} else {
laughMouthAsset.visible = true;
mouthAsset.visible = false;
}
} else {
laughMouthAsset.visible = false;
mouthAsset.visible = true;
}
}
// --- PitchBarActive Sound Logic ---
// 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";
}
// Divide pitch bar into 10 equal regions
var idx = 0;
if (pitchBarActive && typeof pitchBarActive.y === "number") {
var rel = (pitchBarActive.y - pitchBarY) / pitchBarHeight;
rel = Math.max(-0.1, Math.min(1.1, rel));
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 the region has changed since last time (like Trombone Champ)
if (game.lastSlideSoundIdx !== idx) {
// Stop all slide sounds before playing the new one
for (var i = 0; i < slideSounds.length; i++) {
if (slideSounds[i] && typeof slideSounds[i].stop === "function") {
slideSounds[i].stop();
}
}
pitchBarSound = slideSounds[idx];
pitchBarSoundType = newSoundType;
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;
if (pitchBarSound && typeof pitchBarSound.setRate === "function") {
pitchBarSound.setRate(pitchBarSoundId, rate);
}
if (pitchBarSound && typeof pitchBarSound.setVolume === "function") {
pitchBarSound.setVolume(pitchBarSoundId, slideVolume);
}
} else {
// If still in the same region, only update pitch/volume for smoothness, do not replay sound
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);
}
if (pitchBarSound && typeof pitchBarSound.setVolume === "function" && pitchBarSoundId !== null) {
pitchBarSound.setVolume(pitchBarSoundId, slideVolume);
}
}
pitchBarActive.lastY = pitchBarActive.y;
} 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) {
// Delay the start of the showdown song by 2 seconds
if (typeof game.showdownSongDelayTimer === "undefined") {
game.showdownSongDelayTimer = 120; // 2 seconds at 60 FPS
game.showdownSongStarted = false;
}
if (!game.showdownSongStarted) {
game.showdownSongDelayTimer--;
if (game.showdownSongDelayTimer <= 0) {
// Always play showdown music as background when showdown is selected
if (selectedSong && (selectedSong.id === 'showdown' || selectedSong.id === 'bgmusic')) {
LK.playMusic('showdown', {
loop: true
});
game.showdownSongStarted = true;
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);
}
} else {
LK.playMusic(selectedSong ? selectedSong.id : 'bgmusic');
game.showdownSongStarted = true;
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);
}
}
}
}
// Block further update until song actually starts
if (!songStarted) return;
}
// 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
// (avatarFace logic removed)
// 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) {
// Defensive: always spawn notes at noteStartX
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) {
// --- Show catch feedback for tap note ---
// Logical mapping: perfect (very rare), nice (rare), ok (common), nasty (miss)
var tapType = "ok";
var tapDiff = Math.abs(playerPitch - note.pitch);
var r = Math.random();
if (tapDiff < 0.025 && r < 0.12) tapType = "perfect";else if (tapDiff < 0.06 && r < 0.25) tapType = "nice";else if (tapDiff < 0.18) tapType = "ok";else tapType = "nasty";
showCatchFeedback(tapType);
// Only advance combo when the tap note ends (on note.hit), not on every correct catch or hold
advanceCombo();
combo = comboStreak;
if (combo > maxCombo) maxCombo = combo;
accuracySum += tapAcc;
accuracyCount++;
// Apply multiplier if combo is complete
var addScore = 0;
if (comboStreak === comboWord.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();
// Show laugh mouth for great catch
if (typeof laughMouthAsset !== "undefined" && typeof mouthAsset !== "undefined") {
laughMouthAsset.visible = true;
mouthAsset.visible = false;
// Set a much longer timeout (3500ms) to revert to mouth asset
game.laughTimeout = Date.now() + 3500;
}
} else {
// Combo broken, reset to 'C' and allow restart on next correct catch
resetComboProgress();
combo = 0;
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 combo balloon streak much easier: much wider pitch window and allow streak even if not sliding when balloon is visible ---
var balloonEasier = typeof comboBalloonContainer !== "undefined" && comboBalloonContainer.visible;
// --- Make combo balloon streak much easier: much wider pitch window and allow streak even if not sliding when balloon is visible, but only if player is actually sliding ---
// Make 'perfect' catch much harder: use a much tighter window for head catch
// Only allow catch if player is actually sliding (isSliding) at the moment of catch, regardless of balloon
var headPitchWindow = balloonEasier ? 0.65 : 0.13; // was 0.28, now 0.13 for normal, still wide for balloon
var headPitchOk = Math.abs(playerPitch - note.pitch) < headPitchWindow && isSliding;
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;
// Play slide9 sound for flat slide9 notes
if (!note.slideSoundPlayed) {
note.slideSoundPlayed = true;
if (note.slideSound === "slide9") {
var slide9Sound = LK.getSound("slide9");
if (slide9Sound) slide9Sound.play();
}
}
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 even easier to keep holding: widen pitch window from 0.18 to 0.22
// --- Make streak much easier while combo balloon is visible ---
var balloonEasier = typeof comboBalloonContainer !== "undefined" && comboBalloonContainer.visible;
var sustainPitchWindow = balloonEasier ? 0.55 : 0.22;
// Only allow sustain animation/score if player is actually sliding (isSliding)
if (isSliding && Math.abs(playerPitch - note.pitch) < sustainPitchWindow) {
// 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;
// Combo text animation removed: no comboAnimText update.
// 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;
// Only advance combo when the note ends (on note.hit), not on every correct catch or hold
if (note.headCaught && note.holdStarted) {
// --- Show catch feedback for hold note ---
// Only show feedback if player actually started sliding (holdStarted)
var holdType = "ok";
// Logical mapping: perfect (very rare), nice (rare), ok (common), nasty (miss)
var holdRatio = note.holdTicks ? note.holdScore / note.holdTicks : 0;
var r = Math.random();
if (holdRatio >= 0.98 && r < 0.10) holdType = "perfect";else if (holdRatio >= 0.93 && r < 0.22) holdType = "nice";else if (holdRatio >= 0.40) holdType = "ok";else holdType = "nasty";
showCatchFeedback(holdType);
advanceCombo();
combo = comboStreak;
if (combo > maxCombo) maxCombo = combo;
accuracySum += holdAcc;
accuracyCount++;
// Apply multiplier if combo is complete
var addScore = 0;
if (comboStreak === comboWord.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();
// Show laugh mouth for great catch
if (typeof laughMouthAsset !== "undefined" && typeof mouthAsset !== "undefined") {
laughMouthAsset.visible = true;
mouthAsset.visible = false;
// Set a much longer timeout (3500ms) to revert to mouth asset
game.laughTimeout = Date.now() + 3500;
}
} else {
// Combo broken, reset to 'C' and allow restart on next correct catch
resetComboProgress();
combo = 0;
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 even easier to catch glide notes: widen pitch window from 0.20 to 0.25
// --- Make streak much easier while combo balloon is visible ---
var balloonEasier = typeof comboBalloonContainer !== "undefined" && comboBalloonContainer.visible;
var glidePitchWindow = balloonEasier ? 0.55 : 0.25;
if ((balloonEasier || isSliding) && pitchDiff < glidePitchWindow) {
// 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;
// Only advance combo when the glide note ends (on note.hit), not on every correct catch or hold
if (note.glideStarted) {
// --- Show catch feedback for glide note ---
// Logical mapping: perfect (very rare), nice (rare), ok (common), nasty (miss)
var glideType = "ok";
var glideRatio = note.glideTicks ? note.glideScore / note.glideTicks : 0;
var r = Math.random();
if (glideRatio >= 0.98 && r < 0.10) glideType = "perfect";else if (glideRatio >= 0.93 && r < 0.22) glideType = "nice";else if (glideRatio >= 0.40) glideType = "ok";else glideType = "nasty";
showCatchFeedback(glideType);
advanceCombo();
combo = comboStreak;
if (combo > maxCombo) maxCombo = combo;
accuracySum += glideAcc;
accuracyCount++;
// Apply multiplier if combo is complete
var addScore = 0;
if (comboStreak === comboWord.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();
// Show laugh mouth for great catch
if (typeof laughMouthAsset !== "undefined" && typeof mouthAsset !== "undefined") {
laughMouthAsset.visible = true;
mouthAsset.visible = false;
// Set a much longer timeout (3500ms) to revert to mouth asset
game.laughTimeout = Date.now() + 3500;
}
} else {
// Combo broken, reset to 'C' and allow restart on next correct catch
resetComboProgress();
combo = 0;
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;
comboTxt.visible = false;
feedbackTxt.visible = false;
// Hide combo balloon on result overlay
if (typeof comboBalloonContainer !== "undefined") comboBalloonContainer.visible = false;
// Hide all face features on result overlay
faceAsset.visible = false;
hairAsset.visible = false;
leftHairEdge.visible = false;
rightHairEdge.visible = false;
leftEye.visible = false;
rightEye.visible = false;
leftPupil.visible = false;
rightPupil.visible = false;
noseAsset.visible = false;
mouthAsset.visible = false;
laughMouthAsset.visible = false;
leftEyebrow.visible = false;
rightEyebrow.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);
// Add episode complete background image (fills screen, always at back)
var episodeCompleteBg = LK.getAsset('episodeCompleteBg', {
anchorX: 0.5,
anchorY: 0.5
});
episodeCompleteBg.x = gameWidth / 2;
episodeCompleteBg.y = gameHeight / 2;
episodeCompleteBg.scaleX = gameWidth / episodeCompleteBg.width;
episodeCompleteBg.scaleY = gameHeight / episodeCompleteBg.height;
resultOverlay.addChildAt(episodeCompleteBg, 0);
// (avatarFaceHuman removed from result overlay, only visible during the song)
// Title
// Add small background behind 'Episode Complete!' text
var episodeCompleteTextBgSmall = LK.getAsset('episodeCompleteTextBgSmall', {
anchorX: 0.5,
anchorY: 0
});
episodeCompleteTextBgSmall.x = gameWidth / 2;
episodeCompleteTextBgSmall.y = gameHeight / 2 - 700; // moved even more upper, y=666
resultOverlay.addChild(episodeCompleteTextBgSmall);
// Coordinates: x = " + episodeCompleteTextBgSmall.x + ", y = " + episodeCompleteTextBgSmall.y
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
// Removed holdBar image for menu button background in result overlay
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;
// Stop showdown music if it was playing
if (selectedSong && (selectedSong.id === "showdown" || selectedSong.id === "bgmusic")) {
LK.stopMusic();
}
// Hide showdown background when returning to menu
showdownBg.visible = false;
// Hide gameplay UI
pitchBarActive.visible = false;
hitZone.visible = false;
scoreTxt.visible = false;
faceContainer.visible = false;
faceAsset.visible = false;
// comboAnimText.visible = false;//{dJ} // Removed: combo text animation is no longer used
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;
comboTxt.visible = false;
feedbackTxt.visible = false;
// Hide all face features on fallback1 result overlay
faceAsset.visible = false;
hairAsset.visible = false;
leftHairEdge.visible = false;
rightHairEdge.visible = false;
leftEye.visible = false;
rightEye.visible = false;
leftPupil.visible = false;
rightPupil.visible = false;
noseAsset.visible = false;
mouthAsset.visible = false;
laughMouthAsset.visible = false;
leftEyebrow.visible = false;
rightEyebrow.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)
// Add episode complete background image (fills screen, always at back)
var episodeCompleteBg = LK.getAsset('episodeCompleteBg', {
anchorX: 0.5,
anchorY: 0.5
});
episodeCompleteBg.x = gameWidth / 2;
episodeCompleteBg.y = gameHeight / 2;
episodeCompleteBg.scaleX = gameWidth / episodeCompleteBg.width;
episodeCompleteBg.scaleY = gameHeight / episodeCompleteBg.height;
resultOverlay.addChildAt(episodeCompleteBg, 0);
// (avatarFaceHuman removed from fallback1 result overlay, only visible during the song)
// Title
// Add small background behind 'Episode Complete!' text
var episodeCompleteTextBgSmall = LK.getAsset('episodeCompleteTextBgSmall', {
anchorX: 0.5,
anchorY: 0
});
episodeCompleteTextBgSmall.x = gameWidth / 2;
episodeCompleteTextBgSmall.y = gameHeight / 2 - 700; // moved even more upper, y=666
resultOverlay.addChild(episodeCompleteTextBgSmall);
// Coordinates: x = " + episodeCompleteTextBgSmall.x + ", y = " + episodeCompleteTextBgSmall.y
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
// Removed holdBar image for stats background in result overlay
resultOverlay.addChild(resultStats);
// Menu button
// Removed holdBar image for menu button background in result overlay (fallback)
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;
// Stop showdown music if it was playing
if (selectedSong && (selectedSong.id === "showdown" || selectedSong.id === "bgmusic")) {
LK.stopMusic();
}
// Hide gameplay UI
pitchBarActive.visible = false;
hitZone.visible = false;
scoreTxt.visible = false;
faceContainer.visible = false;
faceAsset.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;//{gH} // Removed: combo text animation is no longer used
comboTxt.visible = false;
feedbackTxt.visible = false;
// Hide all face features on fallback2 result overlay
faceAsset.visible = false;
hairAsset.visible = false;
leftHairEdge.visible = false;
rightHairEdge.visible = false;
leftEye.visible = false;
rightEye.visible = false;
leftPupil.visible = false;
rightPupil.visible = false;
noseAsset.visible = false;
mouthAsset.visible = false;
laughMouthAsset.visible = false;
leftEyebrow.visible = false;
rightEyebrow.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)
// Add episode complete background image (fills screen, always at back)
var episodeCompleteBg = LK.getAsset('episodeCompleteBg', {
anchorX: 0.5,
anchorY: 0.5
});
episodeCompleteBg.x = gameWidth / 2;
episodeCompleteBg.y = gameHeight / 2;
episodeCompleteBg.scaleX = gameWidth / episodeCompleteBg.width;
episodeCompleteBg.scaleY = gameHeight / episodeCompleteBg.height;
resultOverlay.addChildAt(episodeCompleteBg, 0);
// (avatarFaceHuman removed from fallback2 result overlay, only visible during the song)
// Title
// Add small background behind 'Episode Complete!' text
var episodeCompleteTextBgSmall = LK.getAsset('episodeCompleteTextBgSmall', {
anchorX: 0.5,
anchorY: 0
});
episodeCompleteTextBgSmall.x = gameWidth / 2;
episodeCompleteTextBgSmall.y = gameHeight / 2 - 700; // moved even more upper, y=666
resultOverlay.addChild(episodeCompleteTextBgSmall);
// Coordinates: x = " + episodeCompleteTextBgSmall.x + ", y = " + episodeCompleteTextBgSmall.y
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
// Removed holdBar image for stats background in result overlay (fallback)
resultOverlay.addChild(resultStats);
// Menu button
// Removed holdBar image for menu button background in result overlay (fallback)
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;
// Stop showdown music if it was playing
if (selectedSong && (selectedSong.id === "showdown" || selectedSong.id === "bgmusic")) {
LK.stopMusic();
}
// Hide gameplay UI
pitchBarActive.visible = false;
hitZone.visible = false;
scoreTxt.visible = false;
faceContainer.visible = false;
faceAsset.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
@@ -291,14 +291,14 @@
/****
* Game Code
****/
-// New background for song selection menu
-// new face asset
-// --- UI Elements ---
+// Create a container for all face features
// --- Face asset for right bottom during song ---
// --- Face asset for right bottom during song ---
-// Create a container for all face features
+// --- UI Elements ---
+// new face asset
+// New background for song selection menu
var faceContainer = new Container();
// Add faceContainer to the game as the first child so it is the bottom-most layer for all face features
game.addChildAt(faceContainer, 0);
// Hide faceContainer by default (hidden in menu, only shown during music)
@@ -1005,29 +1005,26 @@
menuTitle.anchor.set(0.5, 0);
menuTitle.x = gameWidth / 2;
menuTitle.y = 10; // Move even closer to the top
menuContainer.addChild(menuTitle);
-// Add 'Showdown' text to the upper left in the select a song menu
-var showdownText = new Text2("Showdown", {
- size: 100,
- fill: "#000",
- font: "Impact, Arial Black, Comic Sans MS, Comic Sans, cursive, sans-serif"
-});
-showdownText.anchor.set(0, 0); // Top left
-showdownText.x = 40; // Padding from left edge
-showdownText.y = 20; // Padding from top edge
-menuContainer.addChild(showdownText);
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;
+ // Move the first song ("Showdown (Default)") to the upper left
+ if (idx === 0) {
+ btn.anchor.set(0, 0);
+ btn.x = 40; // 40px from left edge, avoid top left menu
+ btn.y = 40; // 40px from top edge, avoid top left menu
+ } else {
+ 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;
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