User prompt
in groove song notes should be flat or more horizontal diagonal or curvy or zigzag and 1notes per 0.8 seconds
User prompt
when im clicked the groove song the game getting slowe for frame per seconds so optimize that to 30fps
User prompt
in groove song there has to be more flat long notes
User prompt
groove song's notes should generation to ensure proper 40-second coverage with 0.6s/0.8s spacing in and notes duration time should be 0.6s/0.9s in groove song notes and notes can be flat and more horizontal diagonal and curvy and zigzag but notes should be more wider and more longer
User prompt
groove song's notes should generation to ensure proper 40-second coverage with 0.6s/0.8s spacing in and notes duration time should be 0.6s/0.9s in groove song notes and notes can be flat and more horizontal diagonal and curvy and zigzag
User prompt
remove the all notes for groove song
User prompt
in groove song notes should spawn per 0.8 seconds to 1.1 seconds randomly
User prompt
in groove song some notes should be little or shorter
User prompt
seperate equaly the notes to every pitch in groove song
User prompt
in groove song seperate the all notes to every pitch equaly and when u gonna but the edge pitches like slide9 and slide 0 make sure they have to be flat or more horizontal diagonal note
User prompt
in showdown song the first notes spawning on middle of the screen ensure that first note should be like other notes
User prompt
when i started the game i dont need to see pitchbarline şn select a song menu hide it
User prompt
add a one more note for showdown song and that should be short note
User prompt
Add one more final note to complete showdown song with 54 total notes u said that but i have to see that one more note at last seconds but i didnt be ensure
User prompt
make a visible one more notes to end of the showdown
User prompt
make a one more notes to end of the showdown
User prompt
make a one more notes end of the showdown and dont forget this song must be 1 minute
User prompt
in showdown just for last notes should spawn 2 seconds later before the last notes
User prompt
remove the notes text for all notes
User prompt
add a last 1 note for showdown song
User prompt
in showdown convert some zigzag notes to more horizontal curvy notes
User prompt
make some note be longer in showdown song
User prompt
in showndown there is 2 notes spawn before 3 seconds remove these 2 notes
User prompt
make a notes for showdown until 1 minutes and star with 2. seconds andthe notes must be flat and diagonal but more horizontal and more horizontal curvy and horizontal zigzag and the notes should be equalty for every pitch slide0 to slide9
User prompt
make a notes for showdown until 1 minutes and star with 2. seconds andthe notes must be flat and diagonal but more horizontal and more horizontal curvy and horizontal zigzag and the notes should be equalty for every pitch slide0 to slide9 and they have to spawn 0.5 to 1 seconds randomly
/**** * 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 = []; // Massively increase the number of holdbar images to completely eliminate all gaps between holdbar images var curveBarCount = 200; // Increased from 150 to 200 for absolutely seamless 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 larger and fully opaque for maximum overlap and gapless appearance seg.width = 42; seg.height = 42; 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", "note13", "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, note13, 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", "note13", "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 extremely close together to completely eliminate all gaps between them var closerSpacing = 0.15; // 0.15 = extremely close spacing for completely seamless bars (was 0.25) 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; } // Note labels removed - no positioning or visibility updates needed }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222244 }); /**** * Game Code ****/ // Music for new song (40s, id: randomgroove) // New assets for the new random song // 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 // --- Trombone image for showdown song --- var tromboneImg = LK.getAsset('trombone', { anchorX: 0.5, anchorY: 0.5 }); // Place trombone above the face animation // Face center is at (faceCenterX, faceCenterY), face is 400x400, so above face is faceCenterY - 200 - offset tromboneImg.x = faceCenterX; tromboneImg.y = faceCenterY - 370; // moved up by 20px more (total 170px above the top of the face) tromboneImg.visible = false; game.addChild(tromboneImg); // --- 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 - Use much larger starting position to ensure notes never appear in middle var noteStartX = gameWidth + 25000; // Much further offscreen right to prevent middle spawning 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", id: "bgmusic", notes: function () { var notes = []; var baseTime = 3000; var songDuration = 60000; // 60s in ms // Create notes starting at 3 seconds until 1 minute // Distribute equally across all pitch slides (slide0 to slide9) var pitches = [0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95]; // slide0 to slide9 var totalNotes = 50; // Good number for 57 seconds of gameplay var noteInterval = (songDuration - baseTime) / totalNotes; // Evenly spaced // Define horizontal patterns - more horizontal than vertical var patterns = [ // Flat patterns { holdShape: 'diagonal', holdShapeParams: { slope: 0 } }, { holdShape: 'diagonal', holdShapeParams: { slope: 0 } }, // Slight diagonal patterns (more horizontal) { holdShape: 'diagonal', holdShapeParams: { slope: 0.08 } }, { holdShape: 'diagonal', holdShapeParams: { slope: -0.08 } }, { holdShape: 'diagonal', holdShapeParams: { slope: 0.12 } }, { holdShape: 'diagonal', holdShapeParams: { slope: -0.12 } }, // Horizontal curvy patterns { holdShape: 'curvy', holdShapeParams: { amplitude: 0.06, frequency: 1.8, phase: 0 } }, { holdShape: 'curvy', holdShapeParams: { amplitude: 0.08, frequency: 2.2, phase: 1.5 } }, { holdShape: 'curvy', holdShapeParams: { amplitude: 0.05, frequency: 1.5, phase: 3.0 } }, // More horizontal curvy patterns (converted from zigzag) { holdShape: 'curvy', holdShapeParams: { amplitude: 0.04, frequency: 1.8, phase: 0 } }, { holdShape: 'curvy', holdShapeParams: { amplitude: 0.06, frequency: 2.0, phase: 2.0 } }, { holdShape: 'curvy', holdShapeParams: { amplitude: 0.05, frequency: 1.6, phase: 1.0 } }]; // Create notes with equal distribution across pitches for (var i = 0; i < totalNotes; i++) { var noteTime = baseTime + i * noteInterval; // Ensure we don't exceed 1 minute if (noteTime >= baseTime + songDuration) break; // Cycle through pitches to ensure equal distribution var pitchIndex = i % pitches.length; var pitch = pitches[pitchIndex]; // Cycle through patterns for variety var patternIndex = i % patterns.length; var pattern = patterns[patternIndex]; // Duration varies with some notes being much longer for variety var duration; if (i % 7 === 0) { duration = 3200; // Every 7th note is much longer } else if (i % 5 === 0) { duration = 2800; // Every 5th note is longer } else if (i % 3 === 0) { duration = 2400; // Every 3rd note is moderately longer } else { duration = 1800 + i % 3 * 400; // Base duration with some variation (1800ms to 2600ms) } var note = { name: "note" + (i + 1), type: "hold", time: Math.round(noteTime), pitch: pitch, duration: duration, holdShape: pattern.holdShape, holdShapeParams: pattern.holdShapeParams }; notes.push(note); } // Add one final note to complete the showdown song var finalNoteTime = baseTime + songDuration - 2000; // 2 seconds before the end (was 4000) var finalNote = { name: "note" + (totalNotes + 1), type: "hold", time: Math.round(finalNoteTime), pitch: 0.50, // Center pitch for a strong finish duration: 3000, // Long duration for a dramatic ending holdShape: 'curvy', holdShapeParams: { amplitude: 0.06, frequency: 1.5, phase: 0 } }; notes.push(finalNote); // Add one more final note at the very end var veryFinalNoteTime = baseTime + songDuration - 1000; // 1 second before the end var veryFinalNote = { name: "note" + (totalNotes + 2), type: "hold", time: Math.round(veryFinalNoteTime), pitch: 0.75, // Higher pitch for triumphant ending duration: 2000, // 2 second duration ending right at 1 minute holdShape: 'diagonal', holdShapeParams: { slope: 0.08 // Slight upward slope for climactic finish } }; notes.push(veryFinalNote); // Add one more ultimate final note to complete showdown var ultimateFinalNoteTime = baseTime + songDuration - 500; // 0.5 seconds before the end var ultimateFinalNote = { name: "note" + (totalNotes + 3), type: "hold", time: Math.round(ultimateFinalNoteTime), pitch: 0.85, // Even higher pitch for grand finale duration: 1500, // 1.5 second duration for epic conclusion holdShape: 'curvy', holdShapeParams: { amplitude: 0.07, frequency: 1.8, phase: 0 } }; notes.push(ultimateFinalNote); // Add one more visible final note to complete showdown var visibleFinalNoteTime = baseTime + songDuration - 250; // 0.25 seconds before the end var visibleFinalNote = { name: "note" + (totalNotes + 4), type: "hold", time: Math.round(visibleFinalNoteTime), pitch: 0.90, // Highest pitch for triumphant ending duration: 1000, // 1 second duration for visible finale holdShape: 'diagonal', holdShapeParams: { slope: 0.10 // Upward slope for climactic finish } }; notes.push(visibleFinalNote); // Add one more absolutely final note to ensure visibility at the very end var absoluteFinalNoteTime = baseTime + songDuration - 100; // 0.1 seconds before the end var absoluteFinalNote = { name: "note" + (totalNotes + 5), type: "hold", time: Math.round(absoluteFinalNoteTime), pitch: 0.95, // Maximum pitch for grand finale duration: 800, // 0.8 second duration to end right at 1 minute holdShape: 'curvy', holdShapeParams: { amplitude: 0.08, frequency: 2.0, phase: 0 } }; notes.push(absoluteFinalNote); // Add one more short note to complete showdown song var shortFinalNoteTime = baseTime + songDuration - 50; // 0.05 seconds before the end var shortFinalNote = { name: "note" + (totalNotes + 6), type: "hold", time: Math.round(shortFinalNoteTime), pitch: 0.85, // High pitch for final flourish duration: 400, // Short 0.4 second duration holdShape: 'diagonal', holdShapeParams: { slope: 0.05 // Slight upward slope for final accent } }; notes.push(shortFinalNote); // Print debug info to console if (typeof console !== "undefined" && typeof console.log === "function") { console.log("=== SHOWDOWN SONG WITH HORIZONTAL PATTERNS - 1 MINUTE DURATION ==="); console.log("Total notes: " + notes.length); console.log("Song duration: " + songDuration + "ms (exactly 60 seconds)"); console.log("Notes start at: " + baseTime + "ms (2 seconds)"); console.log("Pitch distribution: Equal across slide0-slide9"); console.log("Patterns: Flat, diagonal, horizontal curvy, and horizontal zigzag"); console.log("Final note added at: " + finalNoteTime + "ms with duration: 3000ms"); console.log("Very final note added at: " + veryFinalNoteTime + "ms with duration: 2000ms"); console.log("Ultimate final note added at: " + ultimateFinalNoteTime + "ms with duration: 1500ms"); console.log("Visible final note added at: " + visibleFinalNoteTime + "ms with duration: 1000ms"); console.log("Absolute final note added at: " + absoluteFinalNoteTime + "ms with duration: 800ms"); console.log("Short final note added at: " + shortFinalNoteTime + "ms with duration: 400ms"); console.log("Song ends exactly at 1 minute (60000ms) with " + notes.length + " total notes"); } return notes; }() }, { name: "Groove", id: "randomgroove", notes: function () { // --- Groove song: 3-star difficulty with moderate complexity --- var notes = []; var baseTime = 2000; var songDuration = 40000; // 40s in ms for Groove song var totalNotes = 40; // Only 40 notes for Groove song if (totalNotes < 40) { totalNotes = 40; } if (totalNotes > 40) { totalNotes = 40; } // 3-star difficulty pitches - moderate range with balanced patterns var pitches = [0.30, 0.70, 0.40, 0.60, 0.35, 0.65, 0.45, 0.55, 0.32, 0.68, 0.42, 0.58, 0.38, 0.62, 0.48, 0.52, 0.34, 0.66, 0.44, 0.56, 0.36, 0.64, 0.46, 0.54, 0.33, 0.67, 0.43, 0.57, 0.39, 0.61, 0.49, 0.51, 0.37, 0.63, 0.47, 0.53, 0.31, 0.69, 0.41, 0.59, 0.50, 0.28, 0.72, 0.26, 0.74, 0.29]; // 46 moderate pitches // 3-star difficulty shapes - balanced mix of diagonal and curvy patterns var shapes = [{ holdShape: "diagonal", holdShapeParams: { slope: 0.0 } }, { holdShape: "diagonal", holdShapeParams: { slope: 0.20 } }, { holdShape: "diagonal", holdShapeParams: { slope: -0.20 } }, { holdShape: "curvy", holdShapeParams: { amplitude: 0.15, frequency: 2.5, phase: 0.0 } }, { holdShape: "diagonal", holdShapeParams: { slope: 0.25 } }, { holdShape: "curvy", holdShapeParams: { amplitude: 0.12, frequency: 2.0, phase: 1.5 } }, { holdShape: "diagonal", holdShapeParams: { slope: -0.25 } }, { holdShape: "curvy", holdShapeParams: { amplitude: 0.18, frequency: 3.0, phase: 2.0 } }, { holdShape: "diagonal", holdShapeParams: { slope: 0.15 } }, { holdShape: "curvy", holdShapeParams: { amplitude: 0.14, frequency: 2.2, phase: 0.8 } }]; // 10 moderate shapes for 3-star difficulty // 3-star difficulty durations - much longer notes for easier gameplay var durations = []; for (var i = 0; i < totalNotes; i++) { if (i % 3 === 0) { durations.push(3200); // much longer (was 2200) } else if (i % 4 === 0) { durations.push(3600); // much longer (was 2600) } else if (i % 5 === 0) { durations.push(4000); // very long (was 3000) } else { durations.push(3400); // much longer standard (was 2400) } } // 3-star difficulty spacing: 0.8s (800ms) to 1.3s (1300ms) random spacing for moderate timing var time = baseTime; var debugNoteTable = []; for (var i = 0; i < totalNotes; i++) { // 3-star difficulty: 0.8s to 1.3s random spacing var gapMs = 800 + Math.floor(Math.random() * 501); // Random between 800ms and 1300ms if (i === 0) { gapMs = 0; } time += gapMs; // Remove note37, note38, and note39 (i == 36, 37, 38) if (i === 36 || i === 37 || i === 38) { continue; } if (time <= baseTime + songDuration) { var pitchVal = pitches[i % pitches.length]; var noteDuration = durations[i % durations.length]; var noteObj = { name: "note" + (i + 1), type: "hold", time: time, pitch: pitchVal, duration: noteDuration, holdShape: shapes[i % shapes.length].holdShape, holdShapeParams: shapes[i % shapes.length].holdShapeParams }; notes.push(noteObj); debugNoteTable.push({ noteNumber: "note" + (i + 1), time: time, distance: i === 0 ? "— (first note)" : gapMs + "ms" }); } } // Clean up window._slide9Candidates = undefined; // Remove any notes after 40 seconds (40000ms) to ensure song is 40s while (notes.length > 0 && notes[notes.length - 1].time > baseTime + songDuration) { notes.pop(); debugNoteTable.pop(); } // Clamp last note to end exactly at 40 seconds (baseTime + 40000) 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 40s var lastNoteStart = notes[lastIdx].time; var lastNoteDuration = Math.max(100, baseTime + songDuration - lastNoteStart); notes[lastIdx].duration = lastNoteDuration; } // Print debug table to console in markdown format if (typeof console !== "undefined" && typeof console.log === "function") { var table = "| Note Number | Time (ms) | Distance from Previous Note (ms) |\n"; table += "|-------------|-----------|-----------------------------------|\n"; for (var i = 0; i < debugNoteTable.length; i++) { var row = debugNoteTable[i]; table += "| " + row.noteNumber + " | " + row.time + " | " + row.distance + " |\n"; } console.log("=== GROOVE SONG (3-STAR DIFFICULTY) ==="); console.log(table); 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; }() }, { name: "Space", id: "space", notes: function () { // --- Space song: EXACTLY 80 seconds, with notes spaced exactly 0.8s or 1.1s apart --- var notes = []; var baseTime = 2000; var songDuration = 80000; // Exactly 80 seconds (80000ms) // Enhanced cosmic pitches with full range coverage - expanded for 80 seconds var pitches = [0.12, 0.18, 0.25, 0.32, 0.38, 0.45, 0.52, 0.58, 0.65, 0.72, 0.78, 0.85, 0.92, 0.15, 0.22, 0.28, 0.35, 0.42, 0.48, 0.55, 0.62, 0.68, 0.75, 0.82, 0.88, 0.95, 0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90, 0.16, 0.24, 0.33, 0.41, 0.49, 0.57, 0.66, 0.74, 0.83, 0.91, 0.13, 0.21, 0.29, 0.37, 0.46, 0.54, 0.63, 0.71, 0.79, 0.87, 0.17, 0.26, 0.34, 0.43, 0.51, 0.59, 0.67, 0.76, 0.84, 0.93, 0.14, 0.23, 0.31, 0.39, 0.47, 0.56, 0.64, 0.73, 0.81, 0.89, 0.19, 0.27, 0.36, 0.44, 0.53, 0.61, 0.69, 0.77, 0.86, 0.94, 0.11, 0.35, 0.59, 0.73, 0.87, 0.41, 0.65, 0.29, 0.83, 0.47, 0.33, 0.67, 0.52, 0.76, 0.24, 0.58, 0.82, 0.46, 0.71, 0.95]; // 100 unique pitch values for 80 seconds // Enhanced space-themed shapes with more cosmic variety var shapes = [{ holdShape: "diagonal", holdShapeParams: { slope: 0.0 } }, { holdShape: "curvy", holdShapeParams: { amplitude: 0.20, frequency: 1.8, phase: 0.0 } }, { holdShape: "zigzag", holdShapeParams: { amplitude: 0.03, frequency: 3.2, phase: 1.5 } }, { holdShape: "diagonal", holdShapeParams: { slope: 0.04 } }, { holdShape: "diagonal", holdShapeParams: { slope: -0.04 } }, { holdShape: "curvy", holdShapeParams: { amplitude: 0.04, frequency: 2.6, phase: 3.14 } }, { holdShape: "zigzag", holdShapeParams: { amplitude: 0.025, frequency: 4.0, phase: 0.9 } }, { holdShape: "diagonal", holdShapeParams: { slope: 0.03 } }, { holdShape: "diagonal", holdShapeParams: { slope: -0.03 } }, { holdShape: "curvy", holdShapeParams: { amplitude: 0.035, frequency: 2.1, phase: 2.8 } }, { holdShape: "zigzag", holdShapeParams: { amplitude: 0.02, frequency: 3.8, phase: 2.2 } }, { holdShape: "diagonal", holdShapeParams: { slope: 0.05 } }]; // Optimized durations for 80-second space journey var durations = []; // Generate notes with exactly 0.8s (800ms) or 1.1s (1100ms) spacing var spacingPattern = [800, 1100]; // Alternating pattern: 0.8s, 1.1s, 0.8s, 1.1s... var time = baseTime; var debugNoteTable = []; var noteIndex = 0; // Generate notes until we reach exactly 80 seconds while (time < baseTime + songDuration) { // Calculate note duration based on pattern - much longer durations for easier gameplay var noteDuration; if (noteIndex % 10 === 0) { noteDuration = 2400; // much longer cosmic bursts (was 800) } else if (noteIndex % 7 === 0) { noteDuration = 2800; // much longer stellar notes (was 1200) } else if (noteIndex % 5 === 0) { noteDuration = 3200; // very long galactic holds (was 1600) } else if (noteIndex % 3 === 0) { noteDuration = 3600; // extremely long space drifts (was 2000) } else { noteDuration = 3000; // much longer standard cosmic duration (was 1800) } var pitchVal = pitches[noteIndex % pitches.length]; // Ensure note doesn't extend past song end var maxDuration = baseTime + songDuration - time; if (noteDuration > maxDuration) { noteDuration = Math.max(200, maxDuration); } var noteObj = { name: "spaceNote" + (noteIndex + 1), type: "hold", time: Math.round(time), pitch: pitchVal, duration: noteDuration, holdShape: shapes[noteIndex % shapes.length].holdShape, holdShapeParams: shapes[noteIndex % shapes.length].holdShapeParams }; notes.push(noteObj); // Calculate spacing for next note var spacing = spacingPattern[noteIndex % spacingPattern.length]; debugNoteTable.push({ noteNumber: "spaceNote" + (noteIndex + 1), time: Math.round(time), distance: noteIndex === 0 ? "— (first note)" : spacing + "ms", pitch: pitchVal.toFixed(2), duration: noteDuration }); // Move to next note time time += spacing; noteIndex++; // Safety check to prevent infinite loop if (noteIndex > 200) break; } // Remove the last 3 notes from the space song if (notes.length >= 3) { notes.pop(); // Remove last note notes.pop(); // Remove second to last note notes.pop(); // Remove third to last note debugNoteTable.pop(); // Remove last debug entry debugNoteTable.pop(); // Remove second to last debug entry debugNoteTable.pop(); // Remove third to last debug entry } // Ensure exactly 80 seconds: adjust last note to end precisely at 82000ms if (notes.length > 0) { var lastNote = notes[notes.length - 1]; var targetEndTime = baseTime + songDuration; // Exactly 82000ms (2000 + 80000) // Ensure the last note starts before or at the target end time if (lastNote.time > targetEndTime) { lastNote.time = targetEndTime - 500; // Move it back to ensure it fits } // If the last note would go past 80 seconds, adjust its duration if (lastNote.time + lastNote.duration > targetEndTime) { lastNote.duration = targetEndTime - lastNote.time; if (lastNote.duration < 200) { lastNote.duration = 200; // Minimum duration } } // Update debug table for last note debugNoteTable[debugNoteTable.length - 1].duration = lastNote.duration; debugNoteTable[debugNoteTable.length - 1].time = lastNote.time; } // Print comprehensive debug information if (typeof console !== "undefined" && typeof console.log === "function") { console.log("=== SPACE SONG 80 SECONDS WITH 0.8s/1.1s SPACING - COMPLETE DEBUG ==="); var table = "| Note Number | Time (ms) | Distance (ms) | Pitch | Duration (ms) |\n"; table += "|-------------|-----------|---------------|-------|---------------|\n"; for (var i = 0; i < debugNoteTable.length; i++) { var row = debugNoteTable[i]; table += "| " + row.noteNumber + " | " + row.time + " | " + row.distance + " | " + row.pitch + " | " + row.duration + " |\n"; } console.log(table); console.log("=== SPACE SONG SUMMARY ==="); console.log("Total notes generated: " + notes.length); console.log("Song duration: " + songDuration + "ms (exactly 80 seconds)"); console.log("Note spacing pattern: 800ms (0.8s) and 1100ms (1.1s) alternating"); if (notes.length > 0) { console.log("First note starts at: " + notes[0].time + "ms"); console.log("Last note starts at: " + notes[notes.length - 1].time + "ms"); console.log("Last note ends at: " + (notes[notes.length - 1].time + notes[notes.length - 1].duration) + "ms"); console.log("Song ends exactly at: " + (baseTime + songDuration) + "ms"); console.log("Time coverage: " + (notes[notes.length - 1].time + notes[notes.length - 1].duration - baseTime) / 1000 + " seconds"); } console.log("=== VERIFICATION ==="); console.log("Expected end time: " + (baseTime + songDuration) + "ms"); console.log("Actual end time: " + (notes[notes.length - 1].time + notes[notes.length - 1].duration) + "ms"); console.log("Duration match: " + (notes[notes.length - 1].time + notes[notes.length - 1].duration <= baseTime + songDuration ? "PERFECT" : "NEEDS ADJUSTMENT")); } // Expose debugNoteTable globally for inspection if (typeof window !== "undefined") { window.debugNoteTableSpaceRemade = debugNoteTable; window.spaceNotesVerification = { totalNotes: notes.length, songDuration: songDuration, actualEndTime: notes.length > 0 ? notes[notes.length - 1].time + notes[notes.length - 1].duration : 0, expectedEndTime: baseTime + songDuration, isPerfect: notes.length > 0 ? notes[notes.length - 1].time + notes[notes.length - 1].duration <= baseTime + songDuration : false }; } 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: "#fff", font: "Comic Sans MS, Comic Sans, cursive, Arial Black, Impact, sans-serif" }); menuTitle.anchor.set(0.5, 0); menuTitle.x = gameWidth / 2; menuTitle.y = 90; // Move a little bit down from 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: idx === 0 ? "#ff2222" : idx === 1 ? "#3399ff" : idx === 2 ? "#A020F0" : "#fff" }); // Add "More coming soon" text to the menu, positioned at the middle bottom if (i === 0) { if (typeof moreComingSoonTxt === "undefined") { var moreComingSoonTxt = new Text2("More Coming Soon!!!", { size: 90, fill: "#222", font: "Impact, Arial Black, Comic Sans MS, Comic Sans, cursive, sans-serif, Arial" }); moreComingSoonTxt.anchor.set(0.5, 1); moreComingSoonTxt.x = gameWidth / 2; moreComingSoonTxt.y = gameHeight - 150; // moved down to accommodate third song menuContainer.addChild(moreComingSoonTxt); } } // Move the first song ("Showdown (Default)") a little bit up and more to the left if (idx === 0) { btn.anchor.set(0, 0); btn.x = 270; btn.y = 500; // --- Add 5 stars and 'Difficult:' label next to ShowDown using text-based stars --- var starRow = new Container(); var diffLabel = new Text2("Difficult:", { size: 74, fill: "#000" }); diffLabel.anchor.set(0, 0.5); diffLabel.x = 0; diffLabel.y = 60; starRow.addChild(diffLabel); var starsText = new Text2("★ ★ ★ ★ ☆", { size: 74, fill: 0xFF2222, font: "Arial Black, Impact, sans-serif" }); starsText.anchor.set(0, 0.5); starsText.x = diffLabel.x + diffLabel.width + 32; starsText.y = 60; starRow.addChild(starsText); starRow.x = btn.x + btn.width + 258; starRow.y = btn.y - 18; menuContainer.addChild(starRow); } else if (idx === 1) { // Groove song button btn.anchor.set(0, 0); btn.x = 270; btn.y = 700; // --- Add 5 stars and 'Difficult:' label next to Groove using text-based stars --- var starRow = new Container(); var diffLabel = new Text2("Difficult:", { size: 74, fill: "#000" }); diffLabel.anchor.set(0, 0.5); // Move 'Difficult:' a little to the left (e.g. +70px) diffLabel.x = 28; diffLabel.y = 60; starRow.addChild(diffLabel); // Text-based stars: 3 filled, 2 empty, all blue color var starsText = new Text2("★ ★ ★ ☆ ☆", { size: 74, fill: 0x3399FF, font: "Arial Black, Impact, sans-serif" }); starsText.anchor.set(0, 0.5); starsText.x = diffLabel.x + diffLabel.width + 32; starsText.y = 60; starRow.addChild(starsText); starRow.x = btn.x + btn.width + 258 + 110; starRow.y = btn.y - 18; menuContainer.addChild(starRow); } else if (idx === 2) { // Space song button btn.anchor.set(0, 0); btn.x = 270; btn.y = 900; // --- Add 5 stars and 'Difficult:' label next to Space using text-based stars --- var starRow = new Container(); var diffLabel = new Text2("Difficult:", { size: 74, fill: "#000" }); diffLabel.anchor.set(0, 0.5); diffLabel.x = 58; diffLabel.y = 60; starRow.addChild(diffLabel); // Text-based stars: 5 filled, all purple color var starsText = new Text2("★ ★ ★ ★ ★", { size: 74, fill: 0xA020F0, font: "Arial Black, Impact, sans-serif" }); starsText.anchor.set(0, 0.5); starsText.x = diffLabel.x + diffLabel.width + 32; starsText.y = 60; starRow.addChild(starsText); starRow.x = btn.x + btn.width + 258 + 140; starRow.y = btn.y - 18; menuContainer.addChild(starRow); } 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); } var grooveSongPlayed = false; var showdownSongPlayed = false; function selectSong(idx) { // Prevent playing a new song if one is already playing if (songStarted && !songEnded) { // Defensive: do not start a new song if one is already playing return; } // Prevent playing ShowDown if Groove has already been played if (idx === 0 && grooveSongPlayed) { // Optionally show a message or just return return; } // Track if Groove or ShowDown is being played if (idx === 0) { showdownSongPlayed = true; } if (idx === 1) { grooveSongPlayed = true; } // --- 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; // Show trombone image only for showdown song, but always start hidden (will show on press) tromboneImg.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; 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); if (typeof console !== "undefined" && typeof console.log === "function") { console.log("Showdown Background Center Coordinates: (x, y) = (1024, 1366)"); } // Hide randomgroove background if present if (typeof randomGrooveBg !== "undefined") { randomGrooveBg.visible = false; } } else if (selectedSong && selectedSong.id === "randomgroove") { // Show new randomgroove background if (typeof randomGrooveBg === "undefined") { randomGrooveBg = LK.getAsset('randomgroove_bg', { anchorX: 0.5, anchorY: 0.5 }); randomGrooveBg.x = 1024; randomGrooveBg.y = 1366; randomGrooveBg.scaleX = gameWidth / randomGrooveBg.width; randomGrooveBg.scaleY = gameHeight / randomGrooveBg.height; game.addChildAt(randomGrooveBg, 0); } randomGrooveBg.visible = true; // Hide other backgrounds showdownBg.visible = false; if (typeof spaceBg !== "undefined") { spaceBg.visible = false; } } else if (selectedSong && selectedSong.id === "space") { // Show new space background if (typeof spaceBg === "undefined") { spaceBg = LK.getAsset('space_bg', { anchorX: 0.5, anchorY: 0.5 }); spaceBg.x = 1024; spaceBg.y = 1366; spaceBg.scaleX = gameWidth / spaceBg.width; spaceBg.scaleY = gameHeight / spaceBg.height; game.addChildAt(spaceBg, 0); } spaceBg.visible = true; // Hide other backgrounds showdownBg.visible = false; if (typeof randomGrooveBg !== "undefined") { randomGrooveBg.visible = false; } } else { showdownBg.visible = false; if (typeof randomGrooveBg !== "undefined") { randomGrooveBg.visible = false; } if (typeof spaceBg !== "undefined") { spaceBg.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 }); // Remove any name label from pitchBarActive (slide) element if present if (pitchBarActive.children && pitchBarActive.children.length > 0) { for (var i = pitchBarActive.children.length - 1; i >= 0; i--) { var child = pitchBarActive.children[i]; if (child && typeof child.text === "string" && child.text.indexOf("slide") === 0) { pitchBarActive.removeChild(child); } } } 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); } // Global note counter for sequential numbering if (typeof game.noteCounter === 'undefined') { game.noteCounter = 0; } // 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; // Increment counter and assign note name game.noteCounter++; note.noteName = 'note' + game.noteCounter; // Set the note label text if (note.noteLabel) { note.noteLabel.setText(note.noteName); } // 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 ALL notes (including showdown first 2 notes) fully offscreen right // Force consistent spawning position for all notes regardless of song or note number note.x = noteStartX + 1000; // Extra margin to ensure complete offscreen spawning note.y = pitchToY(note.pitch) + 2; // Ensure first 2 showdown notes also spawn offscreen right like all other notes // No special positioning logic - all notes use the same spawn location // Assign hold shape if present if (noteData.holdShape) { note.holdShape = noteData.holdShape; note.holdShapeParams = noteData.holdShapeParams; } // Note text labels removed - no longer creating or displaying note labels // --- 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; } } 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; } // Increase streak and multiplier comboStreak = (typeof comboStreak === "number" ? comboStreak : 0) + 1; comboMultiplier = Math.min(Math.max(comboStreak, 1), 50); // Always update the message balloon text to show the current streak number (x1, x2, x3, ...) for every streak if (typeof comboBalloonContainer !== "undefined" && typeof comboBalloonTxt !== "undefined") { comboBalloonTxt.setText('x' + comboStreak); comboBalloonContainer.visible = true; // Always reset the 3s timer on every streak, so the balloon stays visible for 3s after the last streak if (typeof game.comboBalloonTimeout !== "undefined") { LK.clearTimeout(game.comboBalloonTimeout); } game.comboBalloonTimeout = LK.setTimeout(function () { // After 3s, hide the balloon if no new catch has occurred comboBalloonContainer.visible = false; comboStreak = 1; comboMultiplier = 1; }, 3000); // Make lockout extremely short (much 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; }, 5); // 5ms lockout before next catch can be registered (was 20ms) } } 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 to 0 and set comboBalloonTxt to x1, hide the balloon if streak is 0 if (typeof comboBalloonContainer !== "undefined" && typeof comboBalloonTxt !== "undefined") { comboStreak = 0; comboMultiplier = 1; comboBalloonTxt.setText('x1'); comboBalloonContainer.visible = false; // Also clear the combo balloon timeout so it doesn't reappear 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; } // Show trombone image if showdown song, groove song, or space song is active if (tromboneImg && selectedSong && (selectedSong.id === "bgmusic" || selectedSong.id === "showdown" || selectedSong.id === "randomgroove" || selectedSong.id === "space")) { tromboneImg.visible = true; // Shake trombone image if visible (showdown or groove song) // ms per step var _runTromboneShakeStep = function runTromboneShakeStep(idx) { if (idx >= shakeSteps.length) { return; } tween(tromboneImg, shakeSteps[idx], { duration: shakeDuration, easing: tween.cubicInOut, onFinish: function onFinish() { _runTromboneShakeStep(idx + 1); } }); }; // Defensive: stop any previous shake by resetting scale/rotation tromboneImg.scaleX = 1; tromboneImg.scaleY = 1; tromboneImg.rotation = 0; // More intense and longer shake: increase rotation, scale, and repeat more times var shakeSteps = [{ rotation: 0.22, scaleX: 1.18, scaleY: 0.85 }, { rotation: -0.22, scaleX: 0.85, scaleY: 1.18 }, { rotation: 0.18, scaleX: 1.12, scaleY: 0.90 }, { rotation: -0.18, scaleX: 0.90, scaleY: 1.12 }, { rotation: 0.12, scaleX: 1.08, scaleY: 0.92 }, { rotation: -0.12, scaleX: 0.92, scaleY: 1.08 }, { rotation: 0, scaleX: 1, scaleY: 1 }]; var shakeDuration = 60; _runTromboneShakeStep(0); } // 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; // Hide trombone image when releasing anywhere if (tromboneImg) { tromboneImg.visible = 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, 40s for Groove, 80s for Space var clampMs = 60000; if (selectedSong && selectedSong.id === "randomgroove") { clampMs = 40000; } else if (selectedSong && selectedSong.id === "space") { clampMs = 80000; } var totalMs = Math.max(0, Math.min(currentTime, clampMs)); 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:21 (81000ms), force the episode complete menu after 2s // Defensive: Only trigger after 1:21, not before, but exclude Space song if (!songEnded && currentTime >= 81000 && (!selectedSong || selectedSong.id !== "randomgroove" && selectedSong.id !== "space")) { if (typeof game.lastShowdownEndTime === "undefined" || game.lastShowdownEndTime === null) { game.lastShowdownEndTime = currentTime; } } // For Groove song, stop the counter at 40s (do not update further) if (selectedSong && selectedSong.id === "randomgroove" && songEnded) { // Freeze the counter at 40s timeCounterTxt.setText("0:40"); } // For Space song, stop the counter at 80s (1:20) else if (selectedSong && selectedSong.id === "space" && songEnded) { // Freeze the counter at 80s (which is 1:20) timeCounterTxt.setText("1:20"); } } // --- 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 // Separate the duration logic for the message balloon (comboBalloonContainer) and the laugh mouth animation if (typeof game.laughTimeout !== "undefined") { // Only control laughMouthAsset/mouthAsset visibility based on laughTimeout 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; } // The comboBalloonContainer visibility and its timer are now fully independent and handled in advanceCombo/resetComboProgress } // Remove all name label text from pitchBarActive (slide0 to slide9) every frame robustly for both showdown and groove songs if (pitchBarActive && pitchBarActive.children && pitchBarActive.children.length > 0) { for (var i = pitchBarActive.children.length - 1; i >= 0; i--) { var child = pitchBarActive.children[i]; if (child && typeof child.text === "string" && (/^slide[0-9]$/.test(child.text) || child.text === "slide0" || child.text === "slide1" || child.text === "slide2" || child.text === "slide3" || child.text === "slide4" || child.text === "slide5" || child.text === "slide6" || child.text === "slide7" || child.text === "slide8" || child.text === "slide9")) { if (typeof child.setText === "function") { child.setText(""); } child.text = ""; child.visible = false; child.alpha = 0; if (typeof pitchBarActive.removeChild === "function") { pitchBarActive.removeChild(child); } } } } // Completely hide the slide text label next to pitchBarActive (never create or show slide text) if (game.slideSoundTxt) { game.slideSoundTxt.visible = false; game.slideSoundTxt.setText(""); game.slideSoundTxt.alpha = 0; } // 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; } // (slide text label next to pitchBarActive is now always hidden and never created or updated) } 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; // (slide text label next to pitchBarActive is now always hidden and never created or updated) } 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 if (selectedSong && selectedSong.id === 'randomgroove') { LK.playMusic('randomgroove', { 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(''); for (var i = activeNotes.length - 1; i >= 0; i--) { activeNotes[i].destroy(); activeNotes.splice(i, 1); } } else if (selectedSong && selectedSong.id === 'space') { LK.playMusic('space', { loop: true }); // Set a timer to stop the space music after exactly 80 seconds LK.setTimeout(function () { if (selectedSong && selectedSong.id === 'space' && songStarted && !songEnded) { LK.stopMusic(); } }, 80000); game.showdownSongStarted = true; songStarted = true; currentTime = 0; score = 0; combo = 0; maxCombo = 0; accuracySum = 0; accuracyCount = 0; scoreTxt.setText('0'); comboTxt.setText(''); feedbackTxt.setText(''); 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) - but allow Space song to go to 80s if (selectedSong && selectedSong.id === "space") { // For Space song, allow notes up to 80 seconds (80000ms + travel time) if (noteData.time > 80000 + 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; } } else { // For other songs, keep 60 second limit 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 for non-Space songs 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 + 1000 - hitZoneX) / noteSpeed; // Calculate the X position where this note would spawn var spawnX = noteStartX + 1000; // Always spawn notes as soon as they are eligible, never skip if (!noteData.spawned && currentTime >= noteData.time - noteTravelTime) { // Defensive: always spawn notes at noteStartX + extra margin 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 - ALL notes use consistent scroll behavior var noteTravelTime = (noteStartX + 1000 - 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 time to appear, keep fully offscreen right with extra margin note.x = noteStartX + 1000; } else { // Calculate position as note scrolls from right to left note.x = noteStartX + 1000 - t * noteSpeed; // Ensure note never jumps to middle - it must scroll in from right if (note.x > noteStartX + 1000) { note.x = noteStartX + 1000; } } // 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) { // Give a little bit more score for good catch! var tapBonus = 0; if (tapDiff < 0.025) { tapBonus = 8; } else if (tapDiff < 0.06) { tapBonus = 4; } else if (tapDiff < 0.12) { tapBonus = 2; } addScore = Math.round(100 * comboMultiplier) + tapBonus; score += addScore; } else { var tapBonus = 0; if (tapDiff < 0.025) { tapBonus = 8; } else if (tapDiff < 0.06) { tapBonus = 4; } else if (tapDiff < 0.12) { tapBonus = 2; } addScore = 100 + tapBonus; 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 // Make headPitchWindow even wider for balloon streaking, and easier for normal as well var headPitchWindow = balloonEasier ? 0.95 : 0.22; // was 0.65/0.13, now 0.95/0.22 for much easier streaks 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.95 : 0.33; // was 0.55/0.22, now 0.95/0.33 for much easier streaks // 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)) { // Give a little bit more score for good catch! var extraScore = 0; // If player is very close to perfect pitch, give a bonus if (Math.abs(playerPitch - note.pitch) < 0.08) { extraScore = 2; // bonus for very accurate hold } else if (Math.abs(playerPitch - note.pitch) < 0.16) { extraScore = 1; // small bonus for good catch } note.holdScore += 6 + extraScore; score += 6 + extraScore; 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) { // Give a little bit more score for good catch! var holdBonus = 0; if (holdRatio >= 0.98) { holdBonus = 12; } else if (holdRatio >= 0.93) { holdBonus = 6; } else if (holdRatio >= 0.80) { holdBonus = 2; } addScore = Math.round(note.holdScore * comboMultiplier) + holdBonus; score += addScore; } else { var holdBonus = 0; if (holdRatio >= 0.98) { holdBonus = 12; } else if (holdRatio >= 0.93) { holdBonus = 6; } else if (holdRatio >= 0.80) { holdBonus = 2; } addScore = Math.round(note.holdScore) + holdBonus; 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) { // Give a little bit more score for good catch! var glideBonus = 0; if (glideRatio >= 0.98) { glideBonus = 12; } else if (glideRatio >= 0.93) { glideBonus = 6; } else if (glideRatio >= 0.80) { glideBonus = 2; } addScore = Math.round(note.glideScore * comboMultiplier) + glideBonus; score += addScore; } else { var glideBonus = 0; if (glideRatio >= 0.98) { glideBonus = 12; } else if (glideRatio >= 0.93) { glideBonus = 6; } else if (glideRatio >= 0.80) { glideBonus = 2; } addScore = Math.round(note.glideScore) + glideBonus; 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; } // --- Stop space music after 80 seconds (80000ms) --- if (selectedSong && selectedSong.id === "space" && !songEnded && currentTime >= 80000) { LK.stopMusic(); // Show episode complete menu for Space song after 80s songEnded = true; // Calculate logical accuracy based on notes hit vs notes missed var acc = "0.00"; var totalNotes = notes.length; var notesHit = 0; var notesMissed = 0; // Count hit and missed notes from active notes that were processed for (var i = 0; i < activeNotes.length; i++) { if (activeNotes[i].hit) { notesHit++; } else if (activeNotes[i].missed) { notesMissed++; } } // For notes that are no longer active, count them as hit if they were spawned var processedNotes = notesHit + notesMissed; var remainingNotes = Math.max(0, totalNotes - processedNotes); notesHit += remainingNotes; // Assume remaining notes were hit if processed if (totalNotes > 0) { var logicalAccuracy = notesHit / totalNotes * 100; // Cap accuracy at 100% and ensure it's not negative if (logicalAccuracy > 100) { logicalAccuracy = 100; } if (logicalAccuracy < 0) { logicalAccuracy = 0; } acc = logicalAccuracy.toFixed(2); } else { acc = "0.00"; } var rank = "F"; // Space song ranking system - logical separation with 8000+ for S rank if (score >= 8000) { rank = "S"; } else if (score >= 6500) { rank = "A"; } else if (score >= 5000) { rank = "B"; } else if (score >= 3500) { rank = "C"; } else if (score >= 2000) { rank = "D"; } else { rank = "F"; } // 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); // 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 - 780; resultOverlay.addChild(episodeCompleteTextBgSmall); var resultTitle = new Text2("Episode Complete!", { size: 120, fill: 0xA020F0 // purple }); resultTitle.anchor.set(0.5, 0); resultTitle.x = gameWidth / 2; resultTitle.y = gameHeight / 2 - 480; resultOverlay.addChild(resultTitle); var rankText = new Text2("Rank: " + rank, { size: 200, fill: 0xA020F0 }); rankText.anchor.set(0.5, 0); rankText.x = gameWidth / 2; rankText.y = gameHeight / 2 - 260; resultOverlay.addChild(rankText); 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); var menuBtn = new Text2("Back to Menu", { size: 90, fill: 0xA020F0 }); 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) { resultOverlay.destroy(); menuContainer.visible = true; menuContainer.interactive = true; menuActive = true; // Reset song played flags when returning to menu grooveSongPlayed = false; showdownSongPlayed = false; // Hide gameplay UI pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; faceContainer.visible = false; faceAsset.visible = false; tromboneImg.visible = false; comboTxt.visible = false; feedbackTxt.visible = false; // Hide spaceBg if present to prevent background mix bug if (typeof spaceBg !== "undefined") { spaceBg.visible = false; } // Remove all notes for (var i = activeNotes.length - 1; i >= 0; i--) { activeNotes[i].destroy(); activeNotes.splice(i, 1); } // Reset spawned flag for all notes so they can be respawned when a song is played again if (typeof notes !== "undefined" && notes.length) { for (var i = 0; i < notes.length; i++) { notes[i].spawned = false; } } // 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; } // --- Stop randomgroove music after 40 seconds (40000ms) --- else if (selectedSong && selectedSong.id === "randomgroove" && !songEnded && currentTime >= 40000) { LK.stopMusic(); // Show episode complete menu for Groove song after 40s songEnded = true; // Calculate logical accuracy based on notes hit vs notes missed var acc = "0.00"; var totalNotes = notes.length; var notesHit = 0; var notesMissed = 0; // Count hit and missed notes from active notes that were processed for (var i = 0; i < activeNotes.length; i++) { if (activeNotes[i].hit) { notesHit++; } else if (activeNotes[i].missed) { notesMissed++; } } // For notes that are no longer active, count them as hit if they were spawned var processedNotes = notesHit + notesMissed; var remainingNotes = Math.max(0, totalNotes - processedNotes); notesHit += remainingNotes; // Assume remaining notes were hit if processed if (totalNotes > 0) { var logicalAccuracy = notesHit / totalNotes * 100; // Cap accuracy at 100% and ensure it's not negative if (logicalAccuracy > 100) { logicalAccuracy = 100; } if (logicalAccuracy < 0) { logicalAccuracy = 0; } acc = logicalAccuracy.toFixed(2); } else { acc = "0.00"; } var rank = "F"; if (selectedSong && selectedSong.id === "randomgroove") { if (score >= 3500) { rank = "S"; } else if (score >= 3000) { rank = "A"; } else if (score >= 2500) { rank = "B"; } else if (score >= 2000) { rank = "C"; } else if (score >= 1500) { rank = "D"; } else { rank = "F"; } } else { if (score >= 4000) { rank = "S"; } else if (score >= 3500) { rank = "A"; } else if (score >= 3000) { rank = "B"; } else if (score >= 2500) { rank = "C"; } else if (score >= 2000) { rank = "D"; } else { rank = "F"; } } // 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); // 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 - 780; resultOverlay.addChild(episodeCompleteTextBgSmall); var resultTitle = new Text2("Episode Complete!", { size: 120, fill: 0xA020F0 //{o9} // purple }); resultTitle.anchor.set(0.5, 0); resultTitle.x = gameWidth / 2; resultTitle.y = gameHeight / 2 - 480; resultOverlay.addChild(resultTitle); var rankText = new Text2("Rank: " + rank, { size: 200, fill: 0x3399FF }); rankText.anchor.set(0.5, 0); rankText.x = gameWidth / 2; rankText.y = gameHeight / 2 - 260; resultOverlay.addChild(rankText); 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); var menuBtn = new Text2("Back to Menu", { size: 90, fill: 0x3399FF }); 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) { resultOverlay.destroy(); menuContainer.visible = true; menuContainer.interactive = true; menuActive = true; // Reset Groove/ShowDown played flags when returning to menu grooveSongPlayed = false; showdownSongPlayed = false; // Hide gameplay UI pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; faceContainer.visible = false; faceAsset.visible = false; tromboneImg.visible = false; comboTxt.visible = false; feedbackTxt.visible = false; // Hide randomGrooveBg if present to prevent background mix bug if (typeof randomGrooveBg !== "undefined") { randomGrooveBg.visible = false; } // Hide spaceBg if present to prevent background mix bug if (typeof spaceBg !== "undefined") { spaceBg.visible = false; } // Remove all notes for (var i = activeNotes.length - 1; i >= 0; i--) { activeNotes[i].destroy(); activeNotes.splice(i, 1); } // Reset spawned flag for all notes so they can be respawned when a song is played again if (typeof notes !== "undefined" && notes.length) { for (var i = 0; i < notes.length; i++) { notes[i].spawned = false; } } // 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; } // --- NEW: End episode exactly at 1 minute (60000ms) in showdown music --- if (!songEnded && currentTime >= 60000 && (!selectedSong || selectedSong.id !== "space" && selectedSong.id !== "randomgroove")) { songEnded = true; // Calculate logical accuracy based on notes hit vs notes missed var acc = "0.00"; var totalNotes = notes.length; var notesHit = 0; var notesMissed = 0; // Count hit and missed notes from active notes that were processed for (var i = 0; i < activeNotes.length; i++) { if (activeNotes[i].hit) { notesHit++; } else if (activeNotes[i].missed) { notesMissed++; } } // For notes that are no longer active, count them as hit if they were spawned var processedNotes = notesHit + notesMissed; var remainingNotes = Math.max(0, totalNotes - processedNotes); notesHit += remainingNotes; // Assume remaining notes were hit if processed if (totalNotes > 0) { var logicalAccuracy = notesHit / totalNotes * 100; // Cap accuracy at 100% and ensure it's not negative if (logicalAccuracy > 100) { logicalAccuracy = 100; } if (logicalAccuracy < 0) { logicalAccuracy = 0; } acc = logicalAccuracy.toFixed(2); } else { acc = "0.00"; } var rank = "F"; if (selectedSong && selectedSong.id === "randomgroove") { if (score >= 3500) { rank = "S"; } else if (score >= 3000) { rank = "A"; } else if (score >= 2500) { rank = "B"; } else if (score >= 2000) { rank = "C"; } else if (score >= 1500) { rank = "D"; } else { rank = "F"; } } else { if (score >= 4000) { rank = "S"; } else if (score >= 3500) { rank = "A"; } else if (score >= 3000) { rank = "B"; } else if (score >= 2500) { rank = "C"; } else if (score >= 2000) { rank = "D"; } else { rank = "F"; } } // Hide gameplay UI pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; comboTxt.visible = false; feedbackTxt.visible = false; // Stop showdown music if it was playing (when showing episode complete menu) if (selectedSong && (selectedSong.id === "showdown" || selectedSong.id === "bgmusic")) { LK.stopMusic(); } // 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 - 780; // moved further up by 80px, y=586 resultOverlay.addChild(episodeCompleteTextBgSmall); // Coordinates: x = " + episodeCompleteTextBgSmall.x + ", y = " + episodeCompleteTextBgSmall.y var resultTitle = new Text2("Episode Complete!", { size: 120, fill: 0xA020F0 //{qF} // purple }); 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: 0x3399FF }); 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: 0x3399FF }); 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; // Reset Groove/ShowDown played flags when returning to menu grooveSongPlayed = false; showdownSongPlayed = false; // 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 randomGrooveBg if present to prevent background mix bug if (typeof randomGrooveBg !== "undefined") { randomGrooveBg.visible = false; } // Hide gameplay UI pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; faceContainer.visible = false; faceAsset.visible = false; tromboneImg.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; // Reset note counter for consistent numbering game.noteCounter = 0; }; // 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 logical accuracy based on notes hit vs notes missed (fallback1) var acc = "0.00"; var totalNotes = notes.length; var notesHit = 0; var notesMissed = 0; // Count hit and missed notes from active notes that were processed for (var i = 0; i < activeNotes.length; i++) { if (activeNotes[i].hit) { notesHit++; } else if (activeNotes[i].missed) { notesMissed++; } } // For notes that are no longer active, count them as hit if they were spawned var processedNotes = notesHit + notesMissed; var remainingNotes = Math.max(0, totalNotes - processedNotes); notesHit += remainingNotes; // Assume remaining notes were hit if processed if (totalNotes > 0) { var logicalAccuracy = notesHit / totalNotes * 100; // Cap accuracy at 100% and ensure it's not negative if (logicalAccuracy > 100) { logicalAccuracy = 100; } if (logicalAccuracy < 0) { logicalAccuracy = 0; } acc = logicalAccuracy.toFixed(2); } else { acc = "0.00"; } var rank = "F"; if (selectedSong && selectedSong.id === "randomgroove") { if (score >= 3500) { rank = "S"; } else if (score >= 3000) { rank = "A"; } else if (score >= 2500) { rank = "B"; } else if (score >= 2000) { rank = "C"; } else if (score >= 1500) { rank = "D"; } else { rank = "F"; } } else { if (score >= 4000) { rank = "S"; } else if (score >= 3500) { rank = "A"; } else if (score >= 3000) { rank = "B"; } else if (score >= 2500) { rank = "C"; } else if (score >= 2000) { rank = "D"; } else { rank = "F"; } } // --- 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; // Stop showdown music if it was playing (when showing episode complete menu - fallback1) if (selectedSong && (selectedSong.id === "showdown" || selectedSong.id === "bgmusic")) { LK.stopMusic(); } // 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 - 780; // moved further up by 80px, y=586 resultOverlay.addChild(episodeCompleteTextBgSmall); // Coordinates: x = " + episodeCompleteTextBgSmall.x + ", y = " + episodeCompleteTextBgSmall.y var resultTitle = new Text2("Episode Complete!", { size: 120, fill: 0xA020F0 //{tw} // purple }); 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: 0x3399FF }); 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: 0x3399FF }); 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; // Reset Groove/ShowDown played flags when returning to menu grooveSongPlayed = false; showdownSongPlayed = false; // Stop showdown music if it was playing if (selectedSong && (selectedSong.id === "showdown" || selectedSong.id === "bgmusic")) { LK.stopMusic(); } // Hide randomGrooveBg if present to prevent background mix bug if (typeof randomGrooveBg !== "undefined") { randomGrooveBg.visible = false; } // 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 spawned flag for all notes so they can be respawned when a song is played again if (typeof notes !== "undefined" && notes.length) { for (var i = 0; i < notes.length; i++) { notes[i].spawned = false; } } // 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 && notes.length > 0 && currentTime > notes[notes.length - 1].time + 3000 && currentTime >= 81000 && (!selectedSong || selectedSong.id !== "space")) { // Only allow fallback after 1:21 (81000ms), but exclude Space song songEnded = true; // Calculate logical accuracy based on notes hit vs notes missed (fallback2) var acc = "0.00"; var totalNotes = notes.length; var notesHit = 0; var notesMissed = 0; // Count hit and missed notes from active notes that were processed for (var i = 0; i < activeNotes.length; i++) { if (activeNotes[i].hit) { notesHit++; } else if (activeNotes[i].missed) { notesMissed++; } } // For notes that are no longer active, count them as hit if they were spawned var processedNotes = notesHit + notesMissed; var remainingNotes = Math.max(0, totalNotes - processedNotes); notesHit += remainingNotes; // Assume remaining notes were hit if processed if (totalNotes > 0) { var logicalAccuracy = notesHit / totalNotes * 100; // Cap accuracy at 100% and ensure it's not negative if (logicalAccuracy > 100) { logicalAccuracy = 100; } if (logicalAccuracy < 0) { logicalAccuracy = 0; } acc = logicalAccuracy.toFixed(2); } else { acc = "0.00"; } var rank = "F"; if (selectedSong && selectedSong.id === "randomgroove") { if (score >= 3500) { rank = "S"; } else if (score >= 3000) { rank = "A"; } else if (score >= 2500) { rank = "B"; } else if (score >= 2000) { rank = "C"; } else if (score >= 1500) { rank = "D"; } else { rank = "F"; } } else { if (score >= 4000) { rank = "S"; } else if (score >= 3500) { rank = "A"; } else if (score >= 3000) { rank = "B"; } else if (score >= 2500) { rank = "C"; } else if (score >= 2000) { rank = "D"; } else { rank = "F"; } } // --- 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; // Stop showdown music if it was playing (when showing episode complete menu - fallback2) if (selectedSong && (selectedSong.id === "showdown" || selectedSong.id === "bgmusic")) { LK.stopMusic(); } // 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 - 780; // moved further up by 80px, y=586 resultOverlay.addChild(episodeCompleteTextBgSmall); // Coordinates: x = " + episodeCompleteTextBgSmall.x + ", y = " + episodeCompleteTextBgSmall.y var resultTitle = new Text2("Episode Complete!", { size: 120, fill: 0xA020F0 //{wc} // purple }); 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: 0x3399FF }); 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: 0x3399FF }); 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; // Reset Groove/ShowDown played flags when returning to menu grooveSongPlayed = false; showdownSongPlayed = false; // Stop showdown music if it was playing if (selectedSong && (selectedSong.id === "showdown" || selectedSong.id === "bgmusic")) { LK.stopMusic(); } // Hide randomGrooveBg if present to prevent background mix bug if (typeof randomGrooveBg !== "undefined") { randomGrooveBg.visible = false; } // 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 spawned flag for all notes so they can be respawned when a song is played again if (typeof notes !== "undefined" && notes.length) { for (var i = 0; i < notes.length; i++) { notes[i].spawned = false; } } // 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
@@ -292,16 +292,16 @@
/****
* Game Code
****/
-// New background for song selection menu
-// new face asset
-// --- UI Elements ---
+// Music for new song (40s, id: randomgroove)
+// New assets for the new random song
+// 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
-// New assets for the new random song
-// Music for new song (40s, id: randomgroove)
+// --- 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)
@@ -900,8 +900,24 @@
phase: 0
}
};
notes.push(absoluteFinalNote);
+ // Add one more short note to complete showdown song
+ var shortFinalNoteTime = baseTime + songDuration - 50; // 0.05 seconds before the end
+ var shortFinalNote = {
+ name: "note" + (totalNotes + 6),
+ type: "hold",
+ time: Math.round(shortFinalNoteTime),
+ pitch: 0.85,
+ // High pitch for final flourish
+ duration: 400,
+ // Short 0.4 second duration
+ holdShape: 'diagonal',
+ holdShapeParams: {
+ slope: 0.05 // Slight upward slope for final accent
+ }
+ };
+ notes.push(shortFinalNote);
// Print debug info to console
if (typeof console !== "undefined" && typeof console.log === "function") {
console.log("=== SHOWDOWN SONG WITH HORIZONTAL PATTERNS - 1 MINUTE DURATION ===");
console.log("Total notes: " + notes.length);
@@ -913,8 +929,9 @@
console.log("Very final note added at: " + veryFinalNoteTime + "ms with duration: 2000ms");
console.log("Ultimate final note added at: " + ultimateFinalNoteTime + "ms with duration: 1500ms");
console.log("Visible final note added at: " + visibleFinalNoteTime + "ms with duration: 1000ms");
console.log("Absolute final note added at: " + absoluteFinalNoteTime + "ms with duration: 800ms");
+ console.log("Short final note added at: " + shortFinalNoteTime + "ms with duration: 400ms");
console.log("Song ends exactly at 1 minute (60000ms) with " + notes.length + " total notes");
}
return notes;
}()
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