User prompt
in select a song menu reposition the showdown (default) text to little bit down
User prompt
in select a song menu reposition the showdown (default) text to little bit down
User prompt
in select a song menu reposition the showdown (default) text to little bit down
User prompt
in select a song menu reposition the showdown (default) text to little bit down
User prompt
in select a song menu reposition the showdown (default) text to little bit down right
User prompt
in select a song menu reposition the showdown (default) text to little bit down right
User prompt
in select a song menu reposition the showdown (default) text to little bit down right
User prompt
in select a song menu reposition the showdown (default) text to up left
User prompt
in select a song menu reposition the showdown text to up left
User prompt
move select a song text to a little bit upper
User prompt
can u write a diffirent font that select a song text
User prompt
in select a song menu change the color of select a song text to black
User prompt
Add a new background image for the song selection menu and move the 'Select a Song' text to the top of the screen u did this but i wanna new background for it and name it selection for assets folder
User prompt
lets make a good select a song menu first give a new background for it and name it selection and select a song text shoulf be toppest side of the screen
User prompt
when showdown selection got ended stop the showdown son aswell
User prompt
i cannot see the notes fix it and optimize just give it showdown music as a background song
User prompt
i have to hear that showdown music when im playing showdown fix it
User prompt
if i have selected a showdown song play the showdown asset from music folder and that song has to play 3 times
User prompt
as u can see showdown asset from music play that song when i have selected the showdown song and that song has 20 seconds replay it 3 times
User prompt
as u can see showdown asset from music play that song when i have selected the showdown song and that song has 20 seconds replay it 3 times when it got ended
User prompt
make a new song asset and name it showdown
User prompt
make a new sound asset and name it showdown
User prompt
all notes' holdbar images have a gap between each holdbar images make more holdbar image for fill the holdbar gap
User prompt
all notes' holdbar images have a gap between each holdbar images make more holdbar image for fill the holdbar gap
User prompt
can u understand when any slide sound got ended if u understand make replay the sound when it was finished
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Confetti for success var Confetti = Container.expand(function () { var self = Container.call(this); var asset = self.attachAsset('confetti', { anchorX: 0.5, anchorY: 0.5 }); self.vx = (Math.random() - 0.5) * 20; self.vy = -Math.random() * 20 - 10; self.life = 40 + Math.random() * 20; self.update = function () { self.x += self.vx; self.y += self.vy; self.vy += 1.2; self.life--; asset.rotation += 0.2; if (self.life <= 0) { self.destroy(); } }; return self; }); // Fail splash for miss var FailSplash = Container.expand(function () { var self = Container.call(this); var asset = self.attachAsset('failSplash', { anchorX: 0.5, anchorY: 0.5 }); asset.alpha = 0.7; self.life = 20; self.update = function () { asset.scaleX += 0.1; asset.scaleY += 0.1; asset.alpha -= 0.03; self.life--; if (self.life <= 0) { self.destroy(); } }; return self; }); // Note: All note types (tap, hold, glide) are handled by the Note class with type property. var Note = Container.expand(function () { var self = Container.call(this); // Properties: type ('tap', 'hold', 'glide'), pitch (0-1), time, duration (for hold/glide), glideTo (for glide) self.type = 'tap'; self.pitch = 0.5; // 0 (bottom) to 1 (top) self.time = 0; // When the note should be hit (in ms) self.duration = 0; // For hold/glide notes (ms) self.glideTo = null; // For glide notes: target pitch (0-1) // Use only holdBar image for all note visuals (no other bar assets) // Create a circular, fully opaque holdBar asset for hit/miss feedback var noteAsset = self.attachAsset('holdBar', { anchorX: 0.5, anchorY: 0.5 }); // Make the holdBar image a perfect circle and fully opaque noteAsset.width = 100; noteAsset.height = 100; noteAsset.alpha = 1; self.noteAsset = noteAsset; // Add a note head (noteTap) at the start of every note for good catch feedback var noteHead = self.attachAsset('noteTap', { anchorX: 0.5, anchorY: 0.5 }); // Showdown: make note head larger and more distinct for musical section noteHead.width = 100; noteHead.height = 100; noteHead.x = -noteAsset.width / 2; // Start at the left end of the bar noteHead.y = 0; self.noteHead = noteHead; // Curvy bar for duration: approximate with a sequence of small circles to simulate a curve self.curveBars = []; // Increase the number of holdbar images to fill the gap between holdbar images for all notes var curveBarCount = 88; // Increased from 44 to 88 for even denser, gapless holdbar visuals for (var i = 0; i < curveBarCount; i++) { // Use only holdBar image for all curve segments, as circles and fully opaque var seg = self.attachAsset('holdBar', { anchorX: 0.5, anchorY: 0.5 }); // Make each segment a perfect circle and fully opaque seg.width = 36; seg.height = 36; seg.alpha = 1; if (self.type === 'tap') { seg.tint = 0x66ccff; } else if (self.type === 'hold') { seg.tint = 0x44dd88; } else if (self.type === 'glide') { seg.tint = 0xff66aa; } else { seg.tint = 0xffffff; } self.curveBars.push(seg); } // State self.hit = false; // Whether the note has been hit self.missed = false; // Whether the note was missed // For glide: cache start/end pitch self.glideStart = null; self.glideEnd = null; // Update visuals per frame self.update = function () { // All notes are now click-and-hold for a variable duration (not just tap) // The bar is always horizontal and its length reflects the duration // For tap: treat as a short hold, so bar length is based on tapHoldDuration var tapHoldDuration = 400; // ms, how long you must hold for a tap note (difficulty can adjust this) var barLen = 0; var startPitch = self.pitch; var endPitch = self.pitch; if (self.type === 'tap') { barLen = Math.max(80, tapHoldDuration * noteSpeed); self.duration = tapHoldDuration; } else if (self.type === 'hold') { barLen = Math.max(80, (self.duration || 1) * noteSpeed); } else if (self.type === 'glide') { barLen = Math.max(80, (self.duration || 1) * noteSpeed); startPitch = self.glideStart !== null ? self.glideStart : self.pitch; endPitch = self.glideEnd !== null ? self.glideEnd : self.pitch; } // The main bar is hidden, so only show the note head and curveBars as small segments self.noteAsset.x = 0; self.noteAsset.y = 0; // Place the note head at the start of the bar if (self.noteHead) { // Move noteTap position next to holdBar for specific notes to remove gap and make it look like one piece var noteLabelText = self.children && self.children.length > 0 && self.children[self.children.length - 1] && self.children[self.children.length - 1].text; var moveHeadNotes = ["note7", "note8", "note17", "note27", "note28", "note39", "note47", "note57", "note48"]; if (noteLabelText && moveHeadNotes.indexOf(noteLabelText) !== -1) { // Place noteTap and holdBar at the same starting position for seamless look self.noteHead.x = -barLen / 2; self.noteAsset.x = -barLen / 2; // Move note7 and note8's noteTap even more upper and move holdBar down to get closer to holdBar image if (noteLabelText === "note7" || noteLabelText === "note8") { // Move noteTap further up self.noteHead.y = -82; // Move holdBar further down self.noteAsset.y = 24; } else { // Only set noteHead.y = 0 if not already set for note7 or note8 above if (!(noteLabelText === "note7" || noteLabelText === "note8" && moveHeadNotes.indexOf(noteLabelText) !== -1)) { self.noteHead.y = 0; self.noteAsset.y = 0; } } // Also ensure extraHeadBars (if any) are aligned if (self.extraHeadBars) { for (var eb = 0; eb < self.extraHeadBars.length; eb++) { self.extraHeadBars[eb].x = -barLen / 2; } } } else { // For all other notes, ensure noteTap and holdBar start at the same position self.noteHead.x = -barLen / 2; self.noteAsset.x = -barLen / 2; } self.noteHead.y = 0; // For note7, note8, note17, note27, note28, note39, note47, note57, and note48, fill the gap between holdBar and noteTap by extending the holdBar under the noteTap var fillGapNotes = ["note7", "note8", "note17", "note27", "note28", "note39", "note47", "note57", "note48"]; if (noteLabelText && fillGapNotes.indexOf(noteLabelText) !== -1) { // Add multiple extra holdBar segments under the noteTap to fill the gap and make it look like one piece if (!self.extraHeadBars) { self.extraHeadBars = []; var numExtraBars = 8; // Add 8 extra holdBar images for a more solid connection for (var eb = 0; eb < numExtraBars; eb++) { var extraBar = self.attachAsset('holdBar', { anchorX: 0.5, anchorY: 0.5 }); extraBar.width = 100; extraBar.height = 100; extraBar.alpha = 1; self.extraHeadBars.push(extraBar); } } // Position the extra holdBars so they overlap and fill the gap between noteTap and the first curveBar var spacing = 13; // overlap them more for a solid look for (var eb = 0; eb < self.extraHeadBars.length; eb++) { var bar = self.extraHeadBars[eb]; bar.x = self.noteHead.x + eb * spacing - spacing * (self.extraHeadBars.length - 1) / 2; bar.y = self.noteHead.y; bar.visible = true; } } else if (self.extraHeadBars) { for (var eb = 0; eb < self.extraHeadBars.length; eb++) { self.extraHeadBars[eb].visible = false; } } } // Only show curveBars for hold and glide notes, hide for tap // Make holdBar images closer together by reducing the spacing between them var closerSpacing = 0.65; // 0.65 = 35% closer than default (was 1.0) for (var i = 0; i < self.curveBars.length; i++) { var seg = self.curveBars[i]; var t = i / (self.curveBars.length - 1); var x = t * barLen * closerSpacing; var y = 0; var show = false; if (self.type === 'tap') { // Hide curveBars for tap notes seg.visible = false; continue; } else if (self.type === 'hold') { // Use holdShape and holdShapeParams for custom shapes (curvy, zigzag, diagonal, flat) var shape = self.holdShape || (self.holdShapeParams ? 'custom' : null); var params = self.holdShapeParams || {}; if (shape === 'zigzag') { // Zigzag: alternate up/down with frequency and amplitude var amp = typeof params.amplitude === "number" ? params.amplitude : 0.12; var freq = typeof params.frequency === "number" ? params.frequency : 3; var phase = typeof params.phase === "number" ? params.phase : 0; // Zigzag in pitch space, then convert to y var pitchOffset = Math.sin(t * Math.PI * freq + phase) * amp; y = pitchToY(self.pitch + pitchOffset) - pitchToY(self.pitch); } else if (shape === 'curvy') { // Curvy: smooth sine wave var amp = typeof params.amplitude === "number" ? params.amplitude : 0.13; var freq = typeof params.frequency === "number" ? params.frequency : 2.2; var phase = typeof params.phase === "number" ? params.phase : 0; var pitchOffset = Math.sin(t * Math.PI * freq + phase) * amp; y = pitchToY(self.pitch + pitchOffset) - pitchToY(self.pitch); } else if (shape === 'diagonal') { // Diagonal: linear pitch change var slope = typeof params.slope === "number" ? params.slope : 0.1; var pitchOffset = t * slope; y = pitchToY(self.pitch + pitchOffset) - pitchToY(self.pitch); } else if (shape === 'custom') { // fallback: flat y = 0; } else { // fallback: flat y = 0; } show = true; } else if (self.type === 'glide') { // Interpolate pitch for glide var pitch = lerp(startPitch, endPitch, t); var y0 = pitchToY(startPitch); var y1 = pitchToY(endPitch); y = lerp(y0, y1, t) - pitchToY(self.pitch); show = true; } // Only show every other segment for a "dotted" look, or randomize for zigzag if (show) { if (self.type === 'hold' && self.holdShape === 'curvy') { // For curvy, show every other segment for a dotted curve seg.visible = i % 2 === 0; } else if (self.type === 'hold' && self.holdShape === 'zigzag') { // For zigzag, show all segments for a solid zigzag seg.visible = true; } else if (self.type === 'hold' && self.holdShape === 'diagonal') { // For diagonal, show every segment for a dashed line seg.visible = i % 2 === 0; } else if (self.type === 'hold') { // For flat/custom, show all segments seg.visible = true; } else if (self.type === 'glide') { // For glide, show every other segment for a dotted line seg.visible = i % 2 === 0; } else { seg.visible = false; } } else { seg.visible = false; } seg.x = x - barLen / 2 + 60; seg.y = y; seg.rotation = 0; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222244 }); /**** * Game Code ****/ // new face asset // --- UI Elements --- // --- Face asset for right bottom during song --- // --- Face asset for right bottom during song --- // Create a container for all face features var faceContainer = new Container(); // Add faceContainer to the game as the first child so it is the bottom-most layer for all face features game.addChildAt(faceContainer, 0); // Hide faceContainer by default (hidden in menu, only shown during music) faceContainer.visible = false; // --- Logical face layout --- // Face image (should be at the bottom of the faceContainer) var faceAsset = LK.getAsset('face', { anchorX: 1, anchorY: 1 }); faceAsset.x = 1928; faceAsset.y = 2552; faceContainer.addChild(faceAsset); // --- Declare all face feature assets before using them --- var hairAsset = LK.getAsset('hair', { anchorX: 0.5, anchorY: 1 }); var leftHairEdge = LK.getAsset('hairedge', { anchorX: 1, anchorY: 1 }); var rightHairEdge = LK.getAsset('hairedge', { anchorX: 0, anchorY: 1 }); var leftEye = LK.getAsset('2eyes', { anchorX: 0.5, anchorY: 0.5 }); var rightEye = LK.getAsset('2eyes', { anchorX: 0.5, anchorY: 0.5 }); var leftPupil = LK.getAsset('pupils', { anchorX: 0.5, anchorY: 0.5 }); var rightPupil = LK.getAsset('pupils', { anchorX: 0.5, anchorY: 0.5 }); var noseAsset = LK.getAsset('nose', { anchorX: 0.5, anchorY: 0.5 }); var mouthAsset = LK.getAsset('mouth', { anchorX: 0.5, anchorY: 0.5 }); var laughMouthAsset = LK.getAsset('laugh', { anchorX: 0.5, anchorY: 0.5 }); var leftEyebrow = LK.getAsset('eyebrow', { anchorX: 0.5, anchorY: 0.5 }); var rightEyebrow = LK.getAsset('eyebrow', { anchorX: 0.5, anchorY: 0.5 }); // Hide all face features by default (and in select song menu) hairAsset.visible = false; leftHairEdge.visible = false; rightHairEdge.visible = false; leftEye.visible = false; rightEye.visible = false; leftPupil.visible = false; rightPupil.visible = false; noseAsset.visible = false; mouthAsset.visible = false; laughMouthAsset.visible = false; leftEyebrow.visible = false; rightEyebrow.visible = false; faceAsset.visible = false; // Ensure face features remain hidden in select song menu (menuContainer is visible) function hideFaceFeaturesInMenu() { hairAsset.visible = false; leftHairEdge.visible = false; rightHairEdge.visible = false; leftEye.visible = false; rightEye.visible = false; leftPupil.visible = false; rightPupil.visible = false; noseAsset.visible = false; mouthAsset.visible = false; laughMouthAsset.visible = false; leftEyebrow.visible = false; rightEyebrow.visible = false; faceAsset.visible = false; } hideFaceFeaturesInMenu(); if (typeof menuContainer !== "undefined") { menuContainer.visible = true; menuContainer.interactive = true; } // All other features are positioned relative to the faceAsset's anchor (bottom-right at 1928,2552) // Face image is 400x400, so its center is at (1928-200, 2552-200) = (1728, 2352) var faceCenterX = 1928 - 200; var faceCenterY = 2552 - 200; // Hair (above face image) var hairAsset = LK.getAsset('hair', { anchorX: 0.5, anchorY: 1 }); hairAsset.x = faceCenterX; hairAsset.y = faceCenterY - 140; // moved 30px further down for better alignment faceContainer.addChild(hairAsset); // Left edge hair var leftHairEdge = LK.getAsset('hairedge', { anchorX: 1, anchorY: 1 }); leftHairEdge.x = faceCenterX - 100; leftHairEdge.y = faceCenterY - 10; // moved 30px further down for better alignment faceContainer.addChild(leftHairEdge); // Right edge hair var rightHairEdge = LK.getAsset('hairedge', { anchorX: 0, anchorY: 1 }); rightHairEdge.x = faceCenterX + 100; rightHairEdge.y = faceCenterY - 10; // moved 30px further down for better alignment faceContainer.addChild(rightHairEdge); // Left eye (white part) var leftEye = LK.getAsset('2eyes', { anchorX: 0.5, anchorY: 0.5 }); leftEye.x = faceCenterX - 55; leftEye.y = faceCenterY - 45; faceContainer.addChild(leftEye); // Right eye (white part) var rightEye = LK.getAsset('2eyes', { anchorX: 0.5, anchorY: 0.5 }); rightEye.x = faceCenterX + 55; rightEye.y = faceCenterY - 45; faceContainer.addChild(rightEye); // Left pupil var leftPupil = LK.getAsset('pupils', { anchorX: 0.5, anchorY: 0.5 }); leftPupil.x = leftEye.x; leftPupil.y = leftEye.y; faceContainer.addChild(leftPupil); // Right pupil var rightPupil = LK.getAsset('pupils', { anchorX: 0.5, anchorY: 0.5 }); rightPupil.x = rightEye.x; rightPupil.y = rightEye.y; faceContainer.addChild(rightPupil); // Nose var noseAsset = LK.getAsset('nose', { anchorX: 0.5, anchorY: 0.5 }); noseAsset.x = faceCenterX; noseAsset.y = faceCenterY + 20; faceContainer.addChild(noseAsset); // Mouth var mouthAsset = LK.getAsset('mouth', { anchorX: 0.5, anchorY: 0.5 }); mouthAsset.x = faceCenterX; mouthAsset.y = faceCenterY + 80; faceContainer.addChild(mouthAsset); // Laugh mouth (hidden by default, same position as mouth) var laughMouthAsset = LK.getAsset('laugh', { anchorX: 0.5, anchorY: 0.5 }); laughMouthAsset.x = mouthAsset.x; laughMouthAsset.y = mouthAsset.y; laughMouthAsset.visible = false; faceContainer.addChild(laughMouthAsset); // Left eyebrow var leftEyebrow = LK.getAsset('eyebrow', { anchorX: 0.5, anchorY: 0.5 }); leftEyebrow.x = faceCenterX - 60; leftEyebrow.y = faceCenterY - 90; leftEyebrow.rotation = -0.18; faceContainer.addChild(leftEyebrow); // Right eyebrow var rightEyebrow = LK.getAsset('eyebrow', { anchorX: 0.5, anchorY: 0.5 }); rightEyebrow.x = faceCenterX + 60; rightEyebrow.y = faceCenterY - 90; rightEyebrow.rotation = 0.18; faceContainer.addChild(rightEyebrow); // Move the faceContainer to (0,0) (already correct) faceContainer.x = 0; faceContainer.y = 0; // --- Showdown Background Image --- // Make showdown background image fill the entire screen, always as the bottom-most background // New background asset for episode complete // Small background for episode complete text var showdownBg = LK.getAsset('showdown', { anchorX: 0.5, anchorY: 0.5 }); // Center the image to the screen at (1024, 1366) showdownBg.x = 1024; showdownBg.y = 1366; // Always scale to fill the screen exactly, even if asset size changes showdownBg.scaleX = gameWidth / showdownBg.width; showdownBg.scaleY = gameHeight / showdownBg.height; showdownBg.visible = false; // Remove and re-add to ensure it's always at the back if (showdownBg.parent) { showdownBg.parent.removeChild(showdownBg); } game.addChildAt(showdownBg, 0); // Always at the back // --- Combo System State (needed for combo logic) --- // Combo animation removed: no comboStreak or comboAnimTimer to reset var comboStreak = 0; var comboWord = "COMBO"; comboMultiplier = 1; // 10 slide down sounds, evenly split across the sample // 10 different slide up and 10 different slide down assets // Music (background track) // Sounds (placeholders, actual sound assets will be loaded by LK) // Comedic feedback // Hit zone // Hold bar (for hold notes) // Note types // Trombone slide bar (vertical pitch bar) // Note: Asset creation is handled automatically by LK based on usage below. // Trombone Slide Showdown - Asset Initialization // --- Constants --- // Trombone pitch sound assets for different pitch levels function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } var gameWidth = 2048; var gameHeight = 2732; // Pitch bar (vertical, left side) var pitchBarHeight = gameHeight; var pitchBarY = 0; var pitchBarX = 120; // Hit zone (where notes should be hit) var hitZoneX = 340; var hitZoneWidth = 140; // Note scroll area var noteStartX = gameWidth + 1000; // Offscreen right (even further than before) var noteEndX = hitZoneX; // Hit zone // Note speed (pixels per ms) var noteSpeed = 1.2; // px/ms (tune for difficulty) // --- State --- var notes = []; // All notes in the song var activeNotes = []; // Notes currently on screen var currentTime = 0; // ms since song start var songStarted = false; var songEnded = false; var score = 0; var combo = 0; var maxCombo = 0; var accuracySum = 0; var accuracyCount = 0; // --- Song Selection Menu --- // Rebuild hold note data to generate zigzag, curvy, diagonal, flat, and random horizontal hold bars function randomHoldShape(pitch, duration, idx) { // Avoid too vertical, favor horizontal/curvy/zigzag/diagonal/flat var shapes = ['zigzag', 'curvy', 'diagonal', 'flat']; // Weighted: more horizontal/curvy/zigzag var weights = [2, 2, 1, 1]; var total = 0; for (var i = 0; i < weights.length; i++) total += weights[i]; var r = Math.random() * total; var acc = 0; var shape = 'curvy'; for (var i = 0; i < shapes.length; i++) { acc += weights[i]; if (r < acc) { shape = shapes[i]; break; } } // Parameters for each shape if (shape === 'zigzag') { return { holdShape: 'zigzag', holdShapeParams: { amplitude: 0.09 + Math.random() * 0.07, // 0.09-0.16 frequency: 2 + Math.random() * 2.5, // 2-4.5 phase: Math.random() * Math.PI * 2 } }; } else if (shape === 'curvy') { return { holdShape: 'curvy', holdShapeParams: { amplitude: 0.08 + Math.random() * 0.08, // 0.08-0.16 frequency: 1.2 + Math.random() * 1.8, // 1.2-3 phase: Math.random() * Math.PI * 2 } }; } else if (shape === 'diagonal') { // Slope: -0.18 to 0.18, but not too steep var slope = (Math.random() - 0.5) * 0.28; if (Math.abs(slope) < 0.08) slope = slope < 0 ? -0.12 : 0.12; return { holdShape: 'diagonal', holdShapeParams: { slope: slope } }; } else if (shape === 'flat') { return { holdShape: 'diagonal', holdShapeParams: { slope: 0 } }; } // fallback return { holdShape: 'curvy', holdShapeParams: { amplitude: 0.1, frequency: 2, phase: 0 } }; } // Build a new songList with more horizontal and diagonal notes, and ensure the Showdown music is exactly 1 minute (60s) var songList = [{ name: "Showdown (Default)", id: "bgmusic", notes: function () { // --- 1-minute (60s) song, all notes spaced with a visible gap for easier catching, and total song duration is 1 minute --- // All notes are 'hold' for click-hold mechanic var notes = []; var baseTime = 2000; // Calculate totalNotes so that the sum of all note durations and gaps fits 1 minute var songDuration = 60000; // 60s in ms // Estimate average duration and gap to fit as close as possible to 1 minute var avgNoteDuration = 900; // will be overridden by durations array above var avgGapPx = 75; var avgGapMs = Math.round(avgGapPx / noteSpeed); var totalNotes = Math.floor(songDuration / (avgNoteDuration + avgGapMs)); if (totalNotes < 40) totalNotes = 40; if (totalNotes > 60) totalNotes = 60; // Pitches for variety (cycled, extended for more notes) var pitches = [0.25, 0.35, 0.45, 0.60, 0.52, 0.70, 0.80, 0.65, 0.40, 0.30, 0.22, 0.50, 0.60, 0.75, 0.55, 0.38, 0.28, 0.45, 0.60, 0.78, 0.65, 0.50, 0.35, 0.22, 0.30, 0.45, 0.60, 0.75, 0.80, 0.65, 0.50, 0.35, 0.22, 0.30, 0.45, 0.60, 0.55, 0.40, 0.32, 0.48, 0.62, 0.72, 0.58, 0.68, 0.82, 0.67]; // 48 pitches // Shapes: more horizontal and diagonal, less curvy, cycle for 48 notes var shapes = [{ holdShape: "diagonal", holdShapeParams: { slope: 0.0 } }, { holdShape: "diagonal", holdShapeParams: { slope: 0.12 } }, { holdShape: "diagonal", holdShapeParams: { slope: -0.12 } }, { holdShape: "diagonal", holdShapeParams: { slope: 0.18 } }, { holdShape: "diagonal", holdShapeParams: { slope: -0.18 } }, { holdShape: "curvy", holdShapeParams: { amplitude: 0.08, frequency: 1.2, phase: 0.0 } }, { holdShape: "curvy", holdShapeParams: { amplitude: 0.10, frequency: 2.0, phase: 1.0 } }, { holdShape: "curvy", holdShapeParams: { amplitude: 0.09, frequency: 1.5, phase: 2.0 } }, { holdShape: "diagonal", holdShapeParams: { slope: 0.08 } }, { holdShape: "diagonal", holdShapeParams: { slope: -0.08 } }]; //{2X} // Only 10 shapes, will cycle for 48 notes // Duration pattern for musicality (make some notes shorter, but keep total time unchanged, extended for 48 notes) // Shorter notes: every 3rd note is much shorter, others are a mix of short and medium var durations = []; for (var i = 0; i < totalNotes; i++) { if (i % 3 === 0) { durations.push(500); // much shorter } else if (i % 4 === 0) { durations.push(700); // short } else if (i % 5 === 0) { durations.push(850); // short-medium } else { durations.push(1100); // medium } } // Build a pxGap pattern: set all gaps to be either very close (30-50px) or normal close (60-90px) for all notes var pxGapChoices = []; for (var i = 0; i < totalNotes - 1; i++) { var isSlide9 = false; if (pitches[i % pitches.length] >= 0.9) { isSlide9 = true; } // Randomly choose between very close and normal close var useVeryClose = Math.random() < 0.33; // ~33% chance for very close var closeGap; if (useVeryClose) { closeGap = 30 + Math.floor(Math.random() * 21); // 30-50 inclusive } else { closeGap = isSlide9 ? 48 + Math.floor(Math.random() * 13) // 48-60 inclusive : 60 + Math.floor(Math.random() * 31); // 60-90 inclusive } pxGapChoices.push(closeGap); } var time = baseTime; var debugNoteTable = []; for (var i = 0; i < totalNotes; i++) { var pxGap = 0; if (i === 0) { pxGap = 0; } else if (i === 7) { // Add a more visible gap between note7 and note8 pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap } else if (i === 12) { // Add a more visible gap between note12 and note13 pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap } else if (i === 14) { // Add a more visible gap between note14 and note15 pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap } else if (i === 17) { // Add a more visible gap between note17 and note18 pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap } else if (i === 23) { // Add a visible gap between note23 and note24 pxGap = pxGapChoices[i - 1] + 60; // add extra 60px gap } else if (i === 32) { // Add a more visible gap between note32 and note33 pxGap = pxGapChoices[i - 1] + 220; // add extra 220px gap for more visibility } else if (i === 37) { // Add a more visible gap between note37 and note38 pxGap = pxGapChoices[i - 1] + 260; // add extra 260px gap for even more visibility } else if (i === 38) { // Add a more visible gap between note38 and note39 pxGap = pxGapChoices[i - 1] + 260; // add extra 260px gap for even more visibility } else if (i === 44) { // Add a more visible gap between note44 and note45 pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap for more visibility } else if (i === 47) { // Add a more visible gap between note47 and note48 pxGap = pxGapChoices[i - 1] + 420; // add extra 420px gap for even more visibility } else if (i === 48) { // Add a more visible gap between note48 and note49 pxGap = pxGapChoices[i - 1] + 180; // add extra 180px gap for more visibility } else if (i === 54) { // Add a more visible gap between note53 and note54 pxGap = pxGapChoices[i - 1] + 320; // add extra 320px gap for even more visibility } else { pxGap = pxGapChoices[i - 1]; } var gapMs = Math.round(pxGap / noteSpeed); time += gapMs; // Only add notes that will spawn before or at 1 minute (60000ms) if (time <= baseTime + songDuration) { // Custom override for note2 (i==1) if (i === 1) { notes.push({ name: "note2", type: "hold", time: 2040, // Lower the pitch of note2 so it is not too high pitch: 0.65, // slide9 region (top 10%) changed to lower value for less height duration: 700, // shorter note for variety holdShape: shapes[i % shapes.length].holdShape, holdShapeParams: shapes[i % shapes.length].holdShapeParams }); debugNoteTable.push({ noteNumber: "note2", time: 2040, distance: 48 }); } else { // For all other notes, if in slide9 region, force pitch to be in upper side (0.90-0.99) var pitchVal = pitches[i % pitches.length]; var isSlide9Region = false; if (pitchVal >= 0.9) { pitchVal = 0.90 + Math.random() * 0.09; // 0.90-0.99 isSlide9Region = true; } // Make some notes shorter for variety (every 4th note is shorter) var noteDuration = i % 4 === 0 ? 700 : durations[i % durations.length]; // Store candidate slide9 notes for later random selection if (!window._slide9Candidates) window._slide9Candidates = []; if (isSlide9Region) { window._slide9Candidates.push({ i: i, time: time, pitchVal: pitchVal, noteDuration: noteDuration }); } // For now, push a placeholder; will rebuild after loop if (i === 5 || i === 11 || i === 17 || i === 23 || i === 35 || i === 36 || i === 42) { // note6, note12, note18, note24, note36, note37, note43 (0-based indices 5,11,17,23,35,36,42) // Flat slide9 note: flat = holdShape: 'diagonal', slope: 0, slide9 region = pitch 0.92-0.99 var slide9Pitch = 0.92 + Math.random() * 0.07; // 0.92-0.99 notes.push({ name: "note" + (i + 1), type: "hold", time: time, pitch: slide9Pitch, duration: noteDuration, holdShape: "diagonal", holdShapeParams: { slope: 0 }, slideSound: "slide9" }); debugNoteTable.push({ noteNumber: "note" + (i + 1), time: time, distance: i === 0 ? "— (first note)" : pxGap }); } else { notes.push({ name: "note" + (i + 1), type: "hold", time: time, pitch: pitchVal, duration: noteDuration, holdShape: shapes[i % shapes.length].holdShape, holdShapeParams: shapes[i % shapes.length].holdShapeParams }); debugNoteTable.push({ noteNumber: "note" + (i + 1), time: time, distance: i === 0 ? "— (first note)" : pxGap }); } } } } // Rebuild note4, note11, note22, and note24 as diagonal notes between slide0 and slide1 pitchs var diagonalIndices = [3, 10, 21, 23]; // 0-based indices for note4, note11, note22, note24 // Pick pitches between slide0 and slide1 (bottom 20% of pitch bar) var diagonalPitches = [0.02 + Math.random() * 0.18, 0.02 + Math.random() * 0.18, 0.02 + Math.random() * 0.18, 0.02 + Math.random() * 0.18]; // Use a moderate positive slope for diagonal (upwards) var diagonalSlope = 0.16; for (var j = 0; j < diagonalIndices.length; j++) { var idx = diagonalIndices[j]; if (notes[idx]) { notes[idx] = { name: "note" + (idx + 1), type: "hold", time: notes[idx].time, // Pitch between slide0 and slide1 pitch: diagonalPitches[j], // Make these notes a bit shorter: 350ms (shorter than most) duration: 350, holdShape: "diagonal", holdShapeParams: { slope: diagonalSlope } // No slideSound property, as requested }; } } // Clean up window._slide9Candidates = undefined; // Remove any notes after 1 minute (60000ms) to ensure song is 1 minute while (notes.length > 0 && notes[notes.length - 1].time > baseTime + songDuration) { notes.pop(); debugNoteTable.pop(); } // If last note is too early, stretch all notes to fit songDuration var lastTime = notes.length ? notes[notes.length - 1].time : baseTime; if (lastTime < baseTime + songDuration) { var stretch = (baseTime + songDuration - lastTime) / (notes.length - 1); for (var i = 1; i < notes.length; i++) { notes[i].time += Math.round(i * stretch); debugNoteTable[i].time = notes[i].time; // update debug table as well } } // Clamp last note to end exactly at 1 minute (baseTime + 60000) if (notes.length) { var lastIdx = notes.length - 1; notes[lastIdx].time = baseTime + songDuration; debugNoteTable[lastIdx].time = notes[lastIdx].time; // Also clamp last note's duration so it ends at 1 minute var lastNoteStart = notes[lastIdx].time; var lastNoteDuration = Math.max(100, baseTime + songDuration - lastNoteStart); notes[lastIdx].duration = lastNoteDuration; // Make every 5th note even shorter for more variety for (var i = 0; i < notes.length; i++) { if (i % 5 === 0) { notes[i].duration = Math.min(notes[i].duration, 500); } } } // Print debug table to console in markdown format if (typeof console !== "undefined" && typeof console.log === "function") { var table = "| Note Number | Time (ms) | Distance from Previous Note (px) |\n"; table += "|-------------|-----------|-----------------------------------|\n"; for (var i = 0; i < debugNoteTable.length; i++) { var row = debugNoteTable[i]; table += "| " + row.noteNumber + " | " + row.time + " | " + row.distance + " |\n"; } // Print the full table to the console for all notes console.log(table); // Also print each row individually for easier copy-paste if needed for (var i = 0; i < debugNoteTable.length; i++) { var row = debugNoteTable[i]; console.log("| " + row.noteNumber + " | " + row.time + " | " + row.distance + " |"); } } // Expose debugNoteTable globally for inspection if needed if (typeof window !== "undefined") { window.debugNoteTable = debugNoteTable; } return notes; }() }]; var menuContainer = new Container(); game.addChild(menuContainer); // Menu background REMOVED (pitchBarBg) var menuTitle = new Text2("Select a Song", { size: 120, fill: 0xFFE066 }); menuTitle.anchor.set(0.5, 0); menuTitle.x = gameWidth / 2; menuTitle.y = gameHeight / 2 - 350; menuContainer.addChild(menuTitle); var menuButtons = []; for (var i = 0; i < songList.length; i++) { (function (idx) { var song = songList[idx]; var btn = new Text2(song.name, { size: 90, fill: "#fff" }); btn.anchor.set(0.5, 0.5); btn.x = gameWidth / 2; btn.y = gameHeight / 2 - 100 + idx * 180; btn.interactive = true; btn.buttonMode = true; btn.songIndex = idx; btn.alpha = 0.92; menuContainer.addChild(btn); menuButtons.push(btn); btn.down = function (x, y, obj) { selectSong(idx); }; })(i); } function selectSong(idx) { // --- Red Curtain Animation --- // Create curtain container if not already present if (!game.redCurtainContainer) { var curtainContainer = new Container(); curtainContainer.x = 0; curtainContainer.y = 0; curtainContainer.visible = false; game.redCurtainContainer = curtainContainer; game.addChildAt(curtainContainer, game.children.length); // On top } else { var curtainContainer = game.redCurtainContainer; // Remove any old children while (curtainContainer.children.length > 0) curtainContainer.removeChild(curtainContainer.children[0]); } curtainContainer.visible = true; // Create two curtain halves (left and right) var curtainLeft = LK.getAsset('redcurtain', { anchorX: 0, anchorY: 0 }); var curtainRight = LK.getAsset('redcurtain', { anchorX: 1, anchorY: 0 }); // --- Red Curtain Size Multipliers --- // You can easily change these multipliers to adjust the curtain size! var redCurtainWidthMultiplierLeft = 2.7; // Make this larger to make the left curtain wider var redCurtainWidthMultiplierRight = 2.2; // Make this larger to make the right curtain wider var redCurtainHeightMultiplier = 1.2; // Make this larger to make both curtains taller // Scale curtains to be much wider and cover the whole screen (with overlap) // Make curtains extremely wide to ensure full coverage on all devices // Make left curtain even wider and shift further left to guarantee full coverage curtainLeft.width = gameWidth * redCurtainWidthMultiplierLeft; curtainLeft.height = gameHeight * redCurtainHeightMultiplier; curtainLeft.x = -curtainLeft.width * 0.18; curtainLeft.y = -curtainLeft.height * 0.1; // Right curtain remains wide for overlap curtainRight.width = gameWidth * redCurtainWidthMultiplierRight; curtainRight.height = gameHeight * redCurtainHeightMultiplier; curtainRight.x = gameWidth + curtainRight.width * 0.05; curtainRight.y = -curtainRight.height * 0.1; curtainContainer.addChild(curtainLeft); curtainContainer.addChild(curtainRight); // Animate curtains closing (move both curtains to the left) curtainLeft.x = -curtainLeft.width; curtainRight.x = gameWidth + curtainRight.width; // Animate in (close) - 0.5s tween(curtainLeft, { x: -curtainLeft.width * 0.18 // Move left curtain to its original left position }, { duration: 500, easing: tween.cubicInOut }); tween(curtainRight, { x: gameWidth - curtainRight.width - curtainRight.width * 0.18 // Move right curtain to the left as well }, { duration: 500, easing: tween.cubicInOut, onFinish: function onFinish() { // After closed, wait 0.1s, then continue with song setup and animate out LK.setTimeout(function () { // Set up the selected song selectedSong = songList[idx]; notes = []; for (var n = 0; n < selectedSong.notes.length; n++) { // Deep copy to avoid mutation var noteData = {}; for (var k in selectedSong.notes[n]) noteData[k] = selectedSong.notes[n][k]; // Add a little bit of height for all notes (+30px) if (typeof noteData.pitch === "number") { // Convert pitch to Y, add 30 for a little more height, then convert back to pitch var y = pitchToY(noteData.pitch) + 30; // Clamp y to pitchBarY..pitchBarY+pitchBarHeight if (y < pitchBarY) y = pitchBarY; if (y > pitchBarY + pitchBarHeight) y = pitchBarY + pitchBarHeight; noteData.pitch = yToPitch(y); } notes.push(noteData); } // Remove menu menuContainer.visible = false; menuContainer.interactive = false; menuActive = false; // Start game songStarted = false; songEnded = false; currentTime = 0; score = 0; combo = 0; maxCombo = 0; accuracySum = 0; accuracyCount = 0; // Show UI/gameplay elements pitchBarActive.visible = true; hitZone.visible = true; scoreTxt.visible = true; comboTxt.visible = true; feedbackTxt.visible = true; // Hide combo balloon at song start (will only show on catch) if (typeof comboBalloonContainer !== "undefined") { comboBalloonContainer.visible = false; comboBalloonTxt.setText('x1'); } // Show faceContainer and all face features during the song faceContainer.visible = true; faceAsset.visible = true; hairAsset.visible = true; leftHairEdge.visible = true; rightHairEdge.visible = true; leftEye.visible = true; rightEye.visible = true; leftPupil.visible = true; rightPupil.visible = true; noseAsset.visible = true; mouthAsset.visible = true; laughMouthAsset.visible = false; // Always show mouth by default during song, only show laugh on great/correct catch leftEyebrow.visible = true; rightEyebrow.visible = true; // No face features in select song menu; only show during music // Ensure faceContainer and all face features are positioned and anchored exactly as in the select song menu // (All positions and anchors are already set in the initial faceContainer setup, so we do not need to re-apply them here) faceContainer.x = 0; faceContainer.y = 0; faceAsset.x = 1928; faceAsset.y = 2552; faceAsset.anchorX = 1; faceAsset.anchorY = 1; hairAsset.x = 1728; hairAsset.y = 2352 - 140; // moved 30px further down for better alignment hairAsset.anchorX = 0.5; hairAsset.anchorY = 1; leftHairEdge.x = 1728 - 100; leftHairEdge.y = 2352 - 30; // moved 30px further down for better alignment leftHairEdge.anchorX = 1; leftHairEdge.anchorY = 1; rightHairEdge.x = 1728 + 100; rightHairEdge.y = 2352 - 30; // moved 30px further down for better alignment rightHairEdge.anchorX = 0; rightHairEdge.anchorY = 1; leftEye.x = 1728 - 55; leftEye.y = 2352 - 45; leftEye.anchorX = 0.5; leftEye.anchorY = 0.5; rightEye.x = 1728 + 55; rightEye.y = 2352 - 45; rightEye.anchorX = 0.5; rightEye.anchorY = 0.5; leftPupil.x = 1728 - 55; leftPupil.y = 2352 - 45; leftPupil.anchorX = 0.5; leftPupil.anchorY = 0.5; rightPupil.x = 1728 + 55; rightPupil.y = 2352 - 45; rightPupil.anchorX = 0.5; rightPupil.anchorY = 0.5; noseAsset.x = 1728; noseAsset.y = 2352 + 20; noseAsset.anchorX = 0.5; noseAsset.anchorY = 0.5; mouthAsset.x = 1728; mouthAsset.y = 2352 + 80; mouthAsset.anchorX = 0.5; mouthAsset.anchorY = 0.5; laughMouthAsset.x = 1728; laughMouthAsset.y = 2352 + 80; laughMouthAsset.anchorX = 0.5; laughMouthAsset.anchorY = 0.5; leftEyebrow.x = 1728 - 60; leftEyebrow.y = 2352 - 90; leftEyebrow.anchorX = 0.5; leftEyebrow.anchorY = 0.5; leftEyebrow.rotation = -0.18; rightEyebrow.x = 1728 + 60; rightEyebrow.y = 2352 - 90; rightEyebrow.anchorX = 0.5; rightEyebrow.anchorY = 0.5; rightEyebrow.rotation = 0.18; // Show showdown background only for showdown music if (selectedSong && selectedSong.id === "bgmusic") { showdownBg.visible = true; // Always ensure showdownBg is at the back and centered if (showdownBg.parent) { showdownBg.parent.removeChild(showdownBg); } showdownBg.x = 1024; showdownBg.y = 1366; showdownBg.scaleX = gameWidth / showdownBg.width; showdownBg.scaleY = gameHeight / showdownBg.height; game.addChildAt(showdownBg, 0); // For debug: print center coordinates if (typeof console !== "undefined" && typeof console.log === "function") { console.log("Showdown Background Center Coordinates: (x, y) = (1024, 1366)"); } } else { showdownBg.visible = false; } // Animate curtains opening (drag out) - 1s tween(curtainLeft, { x: -curtainLeft.width }, { duration: 1000, easing: tween.cubicInOut }); tween(curtainRight, { x: gameWidth + curtainRight.width }, { duration: 1000, easing: tween.cubicInOut, onFinish: function onFinish() { curtainContainer.visible = false; // Remove curtain children for next time while (curtainContainer.children.length > 0) curtainContainer.removeChild(curtainContainer.children[0]); } }); }, 100); } }); } var selectedSong = null; var menuActive = true; // --- UI Elements --- // --- Catch Feedback Notification (OK/Nice/Perfect/Nasty) --- var catchFeedbackTxt = new Text2('', { size: 180, //{7Z} // Make it much bigger fill: "#fff" }); catchFeedbackTxt.anchor.set(0.5, 0.5); // Move to right side, vertically centered higher up catchFeedbackTxt.x = gameWidth - 420; catchFeedbackTxt.y = gameHeight / 2 - 350; catchFeedbackTxt.visible = false; game.addChild(catchFeedbackTxt); // Helper to show catch feedback function showCatchFeedback(type) { var text = ""; if (type === "perfect") text = "perfect";else if (type === "nice") text = "nice";else if (type === "ok") text = "ok";else if (type === "nasty") text = "nasty";else text = ""; // Only show notification if a valid catch type is provided if (text !== "") { // Helper to run the shake sequence recursively var _runShakeStep = function runShakeStep(idx) { if (idx >= shakeSequence.length) return; tween(catchFeedbackTxt, shakeSequence[idx], { duration: shakeStep, easing: tween.cubicInOut, onFinish: function onFinish() { _runShakeStep(idx + 1); } }); }; catchFeedbackTxt.setText(text); catchFeedbackTxt.visible = true; catchFeedbackTxt.alpha = 1; catchFeedbackTxt.scaleX = 1; catchFeedbackTxt.scaleY = 1; // Always clear any previous timeout to prevent overlap if (typeof game.catchFeedbackTimeout !== "undefined") { LK.clearTimeout(game.catchFeedbackTimeout); } // Shaking animation: quick left-right shake using tween // We'll shake 3 times (left-right-left) over 0.3s var shakeTimes = 3; var shakeDistance = 32; var shakeDuration = 300; var shakeStep = shakeDuration / (shakeTimes * 2); var originalX = catchFeedbackTxt.x; var shakeSequence = []; for (var i = 0; i < shakeTimes; i++) { shakeSequence.push({ x: originalX + shakeDistance }); shakeSequence.push({ x: originalX - shakeDistance }); } shakeSequence.push({ x: originalX }); _runShakeStep(0); // Always hide the notification after 1.5 seconds, regardless of type game.catchFeedbackTimeout = LK.setTimeout(function () { tween(catchFeedbackTxt, { alpha: 0 }, { duration: 400, onFinish: function onFinish() { catchFeedbackTxt.visible = false; catchFeedbackTxt.alpha = 1; catchFeedbackTxt.setText(''); catchFeedbackTxt.x = originalX; // Defensive: reset position } }); // Defensive: also reset text and visibility in case tween is interrupted LK.setTimeout(function () { catchFeedbackTxt.visible = false; catchFeedbackTxt.alpha = 1; catchFeedbackTxt.setText(''); catchFeedbackTxt.x = originalX; }, 450); }, 1500); } else { // If no valid catch, hide notification immediately catchFeedbackTxt.visible = false; catchFeedbackTxt.alpha = 1; } } // --- Combo Balloon (Message Balloon) --- // Create a container for the balloon and text var comboBalloonContainer = new Container(); // The balloon background (ellipse shape, white, semi-transparent) var comboBalloonBg = LK.getAsset('confetti', { anchorX: 0.5, anchorY: 0.5 }); comboBalloonBg.width = 170; comboBalloonBg.height = 110; comboBalloonBg.tint = 0xffffff; comboBalloonBg.alpha = 0.92; comboBalloonContainer.addChild(comboBalloonBg); // The combo text (e.g. "x1", "x2", ...) var comboBalloonTxt = new Text2('x1', { size: 70, fill: "#222" }); comboBalloonTxt.anchor.set(0.5, 0.5); comboBalloonTxt.x = 0; comboBalloonTxt.y = 0; comboBalloonContainer.addChild(comboBalloonTxt); // Position the balloon to the left of the face (relative to faceCenterX, faceCenterY) comboBalloonContainer.x = faceCenterX - 220; comboBalloonContainer.y = faceCenterY - 40; comboBalloonContainer.visible = false; // Hide by default (show only during song) game.addChild(comboBalloonContainer); // Pitch bar background REMOVED // Pitch bar vertical line (full height) var pitchBarLine = LK.getAsset('pitchBarLine', { anchorX: 0.5, anchorY: 0 }); pitchBarLine.x = pitchBarX; pitchBarLine.y = pitchBarY; game.addChild(pitchBarLine); // Pitch bar active marker (shows current player pitch) var pitchBarActive = LK.getAsset('pitchBarActive', { anchorX: 0.5, anchorY: 0.5 }); pitchBarActive.x = pitchBarX; pitchBarActive.y = pitchBarY + pitchBarHeight / 2; game.addChild(pitchBarActive); // Perfect zone indicator (shows the "perfect" pitch window) // Remove perfectZoneBar and pitchDeviationBar holdBar images, only use tint feedback (no visual bar) var perfectZoneBar = { visible: false, y: 0 }; var pitchDeviationBar = { visible: false, y: 0, tint: 0xff3333, height: 0 }; // Avatar reaction (mirrors performance quality) - REMOVED // Hit zone var hitZone = LK.getAsset('hitZone', { anchorX: 0.5, anchorY: 0.5 }); hitZone.x = hitZoneX; hitZone.y = gameHeight / 2; hitZone.alpha = 0.13; game.addChild(hitZone); // Score text var scoreTxt = new Text2('0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); scoreTxt.x = LK.gui.top.width / 2; // Center score horizontally in GUI scoreTxt.y = 0; LK.gui.top.addChild(scoreTxt); // Combo text animation removed: no comboAnimText UI element. // Time counter text (rightmost upper edge) var timeCounterTxt = new Text2('0:00', { size: 110, fill: "#fff" }); // Place at the rightmost upper edge, with a small margin from the edge timeCounterTxt.anchor.set(1, 0); // right aligned, top timeCounterTxt.x = 700; // set to 700 instead of LK.gui.top.width timeCounterTxt.y = 0; LK.gui.top.addChild(timeCounterTxt); // Dummy comboTxt for legacy compatibility (prevents ReferenceError) var comboTxt = { setText: function setText() {}, visible: false }; // Dummy feedbackTxt for legacy compatibility (prevents ReferenceError) var feedbackTxt = { setText: function setText() {}, visible: false }; // Hide gameplay UI until song is selected pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; // comboAnimText.visible = false;//{4A} // Removed: combo text animation is no longer used // --- Player Pitch State --- var playerPitch = 0.5; // 0 (bottom) to 1 (top) var isSliding = false; // --- PitchBarActive Sound State --- var pitchBarSound = null; var pitchBarSoundId = null; var pitchBarSoundPlaying = false; var lastPitchBarPitch = null; var pitchBarSoundType = null; // "up" or "down" // Array for 10 slide sounds (slide0 to slide9) var slideSounds = []; for (var i = 0; i < 10; i++) { var snd = LK.getSound('slide' + i); // Attach an onEnd handler to each slide sound to replay if needed (function (idx, sndRef) { if (sndRef && typeof sndRef.onEnd === "function") { sndRef.onEnd(function (id) { // Only replay if this is the currently playing sound and user is still sliding in this region if (typeof pitchBarSoundId !== "undefined" && pitchBarSoundId === id && typeof isSliding !== "undefined" && isSliding && typeof game !== "undefined" && typeof game.lastSlideSoundIdx !== "undefined" && game.lastSlideSoundIdx === idx) { // Defensive: stop all slide sounds before replaying for (var j = 0; j < slideSounds.length; j++) { if (slideSounds[j] && typeof slideSounds[j].stop === "function") { slideSounds[j].stop(); } } // Replay the sound with the same options as before var minRate = 0.7; var maxRate = 1.3; var rate = minRate + (maxRate - minRate) * playerPitch; var playOptions = { loop: false, volume: 1.0, rate: rate }; pitchBarSoundId = sndRef.play(playOptions); pitchBarSoundPlaying = true; // Set rate again in case playOptions is not respected if (sndRef && typeof sndRef.setRate === "function") { sndRef.setRate(pitchBarSoundId, rate); } } }); } })(i, snd); slideSounds.push(snd); } // --- Song Data (set by menu) --- // --- Helper Functions --- function clamp(val, min, max) { return val < min ? min : val > max ? max : val; } function lerp(a, b, t) { return a + (b - a) * t; } // Returns a list of all note durations (ms) for the current song function getAllNoteDurations() { var durations = []; for (var i = 0; i < notes.length; i++) { if (typeof notes[i].duration !== "undefined") { durations.push(notes[i].duration); } } return durations; } // Returns a list of all notes with their duration (note number and duration) function getAllNotesWithDuration() { var noteList = []; for (var i = 0; i < notes.length; i++) { noteList.push({ noteNumber: "note" + (i + 1), duration: notes[i].duration }); } return noteList; } // Print all notes with their duration to the console in a readable list if (typeof console !== "undefined" && typeof console.log === "function") { var allNotesWithDuration = getAllNotesWithDuration(); var durationTable = "| Note Number | Duration (ms) |\n"; durationTable += "|-------------|---------------|\n"; for (var i = 0; i < allNotesWithDuration.length; i++) { var row = allNotesWithDuration[i]; durationTable += "| " + row.noteNumber + " | " + row.duration + " |\n"; } console.log(durationTable); // Also print as array of objects for easy copy-paste console.log(allNotesWithDuration); } // Converts pitch (0-1) to y position on pitch bar function pitchToY(pitch) { return pitchBarY + (1 - pitch) * pitchBarHeight; } // Converts y position to pitch (0-1) function yToPitch(y) { var rel = (y - pitchBarY) / pitchBarHeight; return clamp(1 - rel, 0, 1); } // Spawns a note object and adds to game function spawnNote(noteData) { // All notes are now click-and-hold, and cannot cross (no overlap in time) var note = new Note(); // Force all notes to be 'hold' type for click-hold mechanic note.type = 'hold'; note.pitch = noteData.pitch; note.time = noteData.time; // For tap notes, treat as short hold if (noteData.type === 'tap') { note.duration = 400; } else { note.duration = noteData.duration || 0; } note.glideTo = null; note.hit = false; note.missed = false; // Always spawn notes fully offscreen right, never in the middle note.x = noteStartX; note.y = pitchToY(note.pitch) + 2; // Defensive: for noteTap with holdBar, force x to noteStartX as well if (note.type === 'tap' || note.type === 'hold' || note.type === 'glide') { note.x = noteStartX; } // Assign hold shape if present if (noteData.holdShape) { note.holdShape = noteData.holdShape; note.holdShapeParams = noteData.holdShapeParams; } // --- Enforce minimum X distance between note head (noteTap) and holdBar for each note --- // The note head is at -barLen/2, the holdBar is at +barLen/2 (centered at note.x) // We want the distance between the right edge of noteTap and the left edge of holdBar to be at least 15px // Get the barLen for this note (should match Note.update logic) var tapHoldDuration = 400; // ms, how long you must hold for a tap note (difficulty can adjust this) var barLen = 0; if (note.type === 'tap') { barLen = Math.max(80, tapHoldDuration * noteSpeed); note.duration = tapHoldDuration; } else if (note.type === 'hold') { barLen = Math.max(80, (note.duration || 1) * noteSpeed); } else if (note.type === 'glide') { barLen = Math.max(80, (note.duration || 1) * noteSpeed); } // Get note head and holdBar asset widths var noteHeadWidth = note.noteHead ? note.noteHead.width : 0; var holdBarWidth = note.noteAsset ? note.noteAsset.width : 0; // Calculate the right edge of note head and left edge of holdBar var noteHeadRight = note.x - barLen / 2 + noteHeadWidth / 2; var holdBarLeft = note.x + barLen / 2 - holdBarWidth / 2; // If the distance is less than 15, increase barLen to enforce minimum distance var minXDist = 15; var actualDist = holdBarLeft - noteHeadRight; if (actualDist < minXDist) { var needed = minXDist - actualDist; barLen += needed; // Update duration to match new barLen if (note.type === 'tap') { note.duration = Math.round(barLen / noteSpeed); } else { note.duration = Math.round(barLen / noteSpeed); } } // Store the barLen for use in Note.update (optional, for consistency) note.barLen = barLen; // --- Enforce minimum X distance between consecutive notes (head to head) --- // Find the last active note (if any) and check its head position if (activeNotes.length > 0) { var lastNote = activeNotes[activeNotes.length - 1]; var lastBarLen = lastNote.barLen || 0; var lastNoteHeadWidth = lastNote.noteHead ? lastNote.noteHead.width : 0; var lastNoteHeadX = lastNote.x - lastBarLen / 2; var thisNoteHeadX = note.x - barLen / 2; if (Math.abs(thisNoteHeadX - lastNoteHeadX) < minXDist) { // Shift this note further right to enforce minimum distance var shift = minXDist - Math.abs(thisNoteHeadX - lastNoteHeadX); note.x += shift; } } // --- Add a visible label to each note with its name (note1, note2, ...) --- var noteLabel = null; var noteIndex = notes.indexOf(noteData) + 1; noteLabel = new Text2("note" + noteIndex, { size: 48, fill: "#fff" }); noteLabel.anchor.set(0.5, 1); noteLabel.x = 0; noteLabel.y = -70; note.addChild(noteLabel); activeNotes.push(note); game.addChild(note); } // Combo logic removed: combo text animation and related functions are no longer used. var comboMultiplier = 1; // 1x by default, 2x when full combo function advanceCombo() { // Prevent advancing combo if lockout is active if (game.comboBalloonLockout) return; // No combo animation, just set multiplier if needed comboMultiplier = 1; // Show a happy face (smile) when combo increases if (typeof laughMouthAsset !== "undefined" && typeof mouthAsset !== "undefined") { laughMouthAsset.visible = true; mouthAsset.visible = false; } // Update combo balloon text and show only on catch if (typeof comboBalloonContainer !== "undefined" && typeof comboBalloonTxt !== "undefined") { // Always increment streak and show x1, x2, x3, ... for every consecutive catch (no cap) comboStreak = (typeof comboStreak === "number" ? comboStreak : 0) + 1; comboBalloonTxt.setText('x' + comboStreak); // Show the balloon immediately on every catch, no delay comboBalloonContainer.visible = true; // Hide the balloon after 2 seconds (2000ms) for more visible streaks if (typeof game.comboBalloonTimeout !== "undefined") { LK.clearTimeout(game.comboBalloonTimeout); } game.comboBalloonTimeout = LK.setTimeout(function () { comboBalloonContainer.visible = false; }, 2000); // Add a lockout to prevent next catch for a bit longer after showing the balloon (easier to get higher streaks) if (typeof game.comboBalloonLockoutTimeout !== "undefined") { LK.clearTimeout(game.comboBalloonLockoutTimeout); } game.comboBalloonLockout = true; game.comboBalloonLockoutTimeout = LK.setTimeout(function () { game.comboBalloonLockout = false; }, 900); // 900ms lockout before next catch can be registered } } function resetComboProgress() { comboMultiplier = 1; // Show a worried face (normal mouth) when combo breaks if (typeof laughMouthAsset !== "undefined" && typeof mouthAsset !== "undefined") { laughMouthAsset.visible = false; mouthAsset.visible = true; } // On miss, reset comboStreak and set comboBalloonTxt to x1, and hide the balloon if (typeof comboBalloonContainer !== "undefined" && typeof comboBalloonTxt !== "undefined") { comboStreak = 0; comboBalloonTxt.setText('x1'); comboBalloonContainer.visible = false; if (typeof game.comboBalloonTimeout !== "undefined") { LK.clearTimeout(game.comboBalloonTimeout); game.comboBalloonTimeout = undefined; } } } // Spawns confetti at (x, y) function spawnConfetti(x, y) { for (var i = 0; i < 22; i++) { var c = new Confetti(); c.x = x; c.y = y; game.addChild(c); } } // Spawns fail splash at (x, y) function spawnFailSplash(x, y) { // Fail splash animation removed: do nothing } // --- Input Handling --- // The player can press/hold anywhere on the vertical pitch field to set pitch. // The vertical position of the cursor/finger determines the pitch (0-1). // The player must press/hold as the note enters the hit zone, and track pitch for glides/holds. var pitchFieldActive = false; // True if player is pressing/holding var pitchFieldY = pitchBarY + pitchBarHeight / 2; // Last y position of input game.down = function (x, y, obj) { if (menuActive) return; // Allow input anywhere on the screen to control pitch pitchFieldActive = true; isSliding = true; // Defensive: combo animation should never block pitchBarActive movement pitchFieldY = y; playerPitch = yToPitch(y); updatePitchBar(); // --- Slide region sound logic: play slide0-slide9 sound only on region change or new press --- // Calculate which slide region we're in var idx = 0; if (pitchBarActive && typeof pitchBarActive.y === "number") { var rel = (pitchBarActive.y - pitchBarY) / pitchBarHeight; rel = Math.max(0, Math.min(1, rel)); idx = Math.floor((1 - rel) * 10); // 0 = bottom, 9 = top if (idx < 0) idx = 0; if (idx > 9) idx = 9; } if (typeof game.lastSlideSoundIdx === "undefined") { game.lastSlideSoundIdx = -1; } if (typeof game.lastSlideSoundType === "undefined") { game.lastSlideSoundType = null; } // Always play the slide sound on down, even if the region or type has not changed // Always stop all slide sounds before playing the new one to prevent overlap for (var i = 0; i < slideSounds.length; i++) { if (slideSounds[i] && typeof slideSounds[i].stop === "function") { slideSounds[i].stop(); } } pitchBarSound = slideSounds[idx]; pitchBarSoundType = "down"; // Default to down on click var minRate = 0.7; var maxRate = 1.3; var rate = minRate + (maxRate - minRate) * playerPitch; var playOptions = { loop: false, volume: 1.0, rate: rate }; pitchBarSoundId = pitchBarSound.play(playOptions); pitchBarSoundPlaying = true; game.lastSlideSoundIdx = idx; game.lastSlideSoundType = "down"; if (pitchBarSound && typeof pitchBarSound.setRate === "function") { pitchBarSound.setRate(pitchBarSoundId, rate); } }; game.move = function (x, y, obj) { if (menuActive) return; // Always allow pitchBarActive to be moved while holding, regardless of note state // Defensive: always set isSliding true if move is called and pitchFieldActive or isSliding if (pitchFieldActive || isSliding) { isSliding = true; pitchFieldY = y; playerPitch = yToPitch(y); updatePitchBar(); // --- Slide region sound logic: play slide0-slide9 sound only on region change, and only once per region until finger is lifted --- var idx = 0; if (pitchBarActive && typeof pitchBarActive.y === "number") { var rel = (pitchBarActive.y - pitchBarY) / pitchBarHeight; rel = Math.max(0, Math.min(1, rel)); idx = Math.floor((1 - rel) * 10); // 0 = bottom, 9 = top if (idx < 0) idx = 0; if (idx > 9) idx = 9; } if (typeof game.lastSlideSoundIdx === "undefined") { game.lastSlideSoundIdx = -1; } if (typeof game.lastSlideSoundType === "undefined") { game.lastSlideSoundType = null; } // Only play the slide sound if the region has changed since last time if (game.lastSlideSoundIdx !== idx) { // Always stop all slide sounds before playing the new one to prevent overlap for (var i = 0; i < slideSounds.length; i++) { if (slideSounds[i] && typeof slideSounds[i].stop === "function") { slideSounds[i].stop(); } } pitchBarSound = slideSounds[idx]; pitchBarSoundType = "down"; // Default to down on move var minRate = 0.7; var maxRate = 1.3; var rate = minRate + (maxRate - minRate) * playerPitch; var playOptions = { loop: false, volume: 1.0, rate: rate }; pitchBarSoundId = pitchBarSound.play(playOptions); pitchBarSoundPlaying = true; game.lastSlideSoundIdx = idx; game.lastSlideSoundType = "down"; if (pitchBarSound && typeof pitchBarSound.setRate === "function") { pitchBarSound.setRate(pitchBarSoundId, rate); } } // Update slide sound pitch (rate) in real time, but do not replay sound if (pitchBarSound && typeof pitchBarSound.setRate === "function" && pitchBarSoundId !== null) { var minRate = 0.7; var maxRate = 1.3; var rate = minRate + (maxRate - minRate) * playerPitch; pitchBarSound.setRate(pitchBarSoundId, rate); } } // Defensive: combo animation should never block pitchBarActive movement // If comboAnimActive is true, still allow pitchBarActive to move as above }; game.up = function (x, y, obj) { if (menuActive) return; pitchFieldActive = false; isSliding = false; // Defensive: always allow pitchBarActive to move again on next down // Stop all slide region sounds to prevent stuck sounds for (var i = 0; i < slideSounds.length; i++) { if (slideSounds[i] && typeof slideSounds[i].stop === "function") { slideSounds[i].stop(); } } pitchBarSoundPlaying = false; pitchBarSoundId = null; }; // Update pitch bar marker position function updatePitchBar() { pitchBarActive.y = pitchToY(playerPitch); } // --- Game Loop --- game.update = function () { if (menuActive) { // Hide combo balloon in menu if (typeof comboBalloonContainer !== "undefined") comboBalloonContainer.visible = false; if (typeof game.comboBalloonTimeout !== "undefined") { LK.clearTimeout(game.comboBalloonTimeout); game.comboBalloonTimeout = undefined; } // Hide time counter in menu if (typeof timeCounterTxt !== "undefined") timeCounterTxt.visible = false; // Stop pitchBarActive sound if playing // Stop all slide region sounds to prevent stuck sounds when menu is active for (var i = 0; i < slideSounds.length; i++) { if (slideSounds[i] && typeof slideSounds[i].stop === "function") { slideSounds[i].stop(); } } pitchBarSoundPlaying = false; pitchBarSoundId = null; // Hide combo and time counter in menu if (typeof comboAnimText !== "undefined") comboAnimText.visible = false; if (typeof timeCounterTxt !== "undefined") timeCounterTxt.visible = false; // Block game update until song is selected return; } // Show and update time counter if (typeof timeCounterTxt !== "undefined") { timeCounterTxt.visible = true; // Clamp to 60s (1 minute) for showdown var totalMs = Math.max(0, Math.min(currentTime, 60000)); var min = Math.floor(totalMs / 60000); var sec = Math.floor(totalMs % 60000 / 1000); var secStr = sec < 10 ? "0" + sec : "" + sec; timeCounterTxt.setText(min + ":" + secStr); // If we reach exactly 1:01 (61000ms), force the episode complete menu after 2s // Defensive: Only trigger after 1:01, not before if (!songEnded && currentTime >= 61000) { if (typeof game.lastShowdownEndTime === "undefined" || game.lastShowdownEndTime === null) { game.lastShowdownEndTime = currentTime; } } } // --- Ensure all face features are always above the background and visible during the song --- if (faceContainer && faceContainer.parent) { // Always keep faceContainer above showdownBg (background) if (showdownBg && showdownBg.parent) { // Remove and re-add faceContainer after showdownBg to ensure correct order if (faceContainer.parent.children.indexOf(faceContainer) < faceContainer.parent.children.indexOf(showdownBg)) { faceContainer.parent.removeChild(faceContainer); faceContainer.parent.addChild(faceContainer); } } // Ensure all face features are visible and above the face image var faceFeatures = [hairAsset, leftHairEdge, rightHairEdge, leftEye, rightEye, leftPupil, rightPupil, noseAsset, mouthAsset, laughMouthAsset, leftEyebrow, rightEyebrow]; for (var i = 0; i < faceFeatures.length; i++) { var feature = faceFeatures[i]; if (feature && feature.parent !== faceContainer) { faceContainer.addChild(feature); } if (feature) feature.visible = true; } // Always keep faceAsset as the bottom-most child in faceContainer if (faceAsset && faceAsset.parent === faceContainer && faceContainer.children[0] !== faceAsset) { faceContainer.removeChild(faceAsset); faceContainer.addChildAt(faceAsset, 0); } // Always show mouth asset by default unless laugh is being shown for a great/correct catch if (typeof game.laughTimeout !== "undefined" && laughMouthAsset.visible) { // If the timeout expired, hide laugh and show mouth if (Date.now() > game.laughTimeout) { laughMouthAsset.visible = false; mouthAsset.visible = true; game.laughTimeout = undefined; } else { laughMouthAsset.visible = true; mouthAsset.visible = false; } } else { laughMouthAsset.visible = false; mouthAsset.visible = true; } } // --- PitchBarActive Sound Logic --- // Only play slide sound while holding (isSliding) and pitchBarActive is visible if (pitchBarActive.visible && isSliding) { // Track last Y for pitchBarActive to determine direction if (typeof pitchBarActive.lastY === "undefined") { pitchBarActive.lastY = pitchBarActive.y; } var movingDown = false; var movingUp = false; if (pitchBarActive.y > pitchBarActive.lastY + 1) { movingDown = true; } else if (pitchBarActive.y < pitchBarActive.lastY - 1) { movingUp = true; } // Choose sound type and volume var slideVolume = 0.7; var newSoundType = null; if (movingDown) { slideVolume = 1.0; newSoundType = "down"; } else if (movingUp) { slideVolume = 0.35; newSoundType = "up"; } else { // Not moving, keep last type or default to down newSoundType = pitchBarSoundType || "down"; } // Divide pitch bar into 10 equal regions var idx = 0; if (pitchBarActive && typeof pitchBarActive.y === "number") { var rel = (pitchBarActive.y - pitchBarY) / pitchBarHeight; rel = Math.max(-0.1, Math.min(1.1, rel)); idx = Math.floor((1 - rel) * 10); // 0 = bottom, 9 = top if (idx < 0) idx = 0; if (idx > 9) idx = 9; // Label the region for debug/UX if (!game.slideSoundTxt) { game.slideSoundTxt = new Text2('', { size: 60, fill: "#fff" }); game.slideSoundTxt.anchor.set(0.5, 0.5); game.slideSoundTxt.x = pitchBarActive.x + 220; game.slideSoundTxt.y = pitchBarActive.y; game.addChild(game.slideSoundTxt); } var soundLabel = "slide" + idx; game.slideSoundTxt.setText(soundLabel); game.slideSoundTxt.x = pitchBarActive.x + 220; game.slideSoundTxt.y = pitchBarActive.y; game.slideSoundTxt.visible = true; } if (typeof game.lastSlideSoundIdx === "undefined") { game.lastSlideSoundIdx = -1; } if (typeof game.lastSlideSoundType === "undefined") { game.lastSlideSoundType = null; } // Only play the slide sound if the region has changed since last time (like Trombone Champ) if (game.lastSlideSoundIdx !== idx) { // Stop all slide sounds before playing the new one for (var i = 0; i < slideSounds.length; i++) { if (slideSounds[i] && typeof slideSounds[i].stop === "function") { slideSounds[i].stop(); } } pitchBarSound = slideSounds[idx]; pitchBarSoundType = newSoundType; var minRate = 0.7; var maxRate = 1.3; var rate = minRate + (maxRate - minRate) * playerPitch; var playOptions = { loop: false, volume: slideVolume, rate: rate }; pitchBarSoundId = pitchBarSound.play(playOptions); pitchBarSoundPlaying = true; game.lastSlideSoundIdx = idx; game.lastSlideSoundType = newSoundType; if (pitchBarSound && typeof pitchBarSound.setRate === "function") { pitchBarSound.setRate(pitchBarSoundId, rate); } if (pitchBarSound && typeof pitchBarSound.setVolume === "function") { pitchBarSound.setVolume(pitchBarSoundId, slideVolume); } } else { // If still in the same region, only update pitch/volume for smoothness, do not replay sound if (pitchBarSound && typeof pitchBarSound.setRate === "function" && pitchBarSoundId !== null) { var minRate = 0.7; var maxRate = 1.3; var rate = minRate + (maxRate - minRate) * playerPitch; pitchBarSound.setRate(pitchBarSoundId, rate); } if (pitchBarSound && typeof pitchBarSound.setVolume === "function" && pitchBarSoundId !== null) { pitchBarSound.setVolume(pitchBarSoundId, slideVolume); } } pitchBarActive.lastY = pitchBarActive.y; } else { // Stop all slide region sounds immediately if not holding or not visible (no delay) for (var i = 0; i < slideSounds.length; i++) { if (slideSounds[i] && typeof slideSounds[i].stop === "function") { slideSounds[i].stop(); } } pitchBarSoundPlaying = false; pitchBarSoundId = null; // Hide slide sound label if present if (game.slideSoundTxt) { game.slideSoundTxt.visible = false; } } if (!songStarted) { // Delay the start of the showdown song by 2 seconds if (typeof game.showdownSongDelayTimer === "undefined") { game.showdownSongDelayTimer = 120; // 2 seconds at 60 FPS game.showdownSongStarted = false; } if (!game.showdownSongStarted) { game.showdownSongDelayTimer--; if (game.showdownSongDelayTimer <= 0) { // If showdown song is selected, play 'showdown' music asset 3 times if (selectedSong && selectedSong.id === 'showdown') { // Helper to play showdown music 3 times in a row var _playShowdownMusicNTimes = function playShowdownMusicNTimes(n) { if (game.showdownPlayCount >= n) { // After 3 plays, start the game 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); } return; } // Play showdown music, do not loop LK.playMusic('showdown', { loop: false }); game.showdownPlayCount++; // Wait for the showdown music to end, then play again // Defensive: estimate duration as 60s (or use asset duration if available) var showdownDurationMs = 60000; LK.setTimeout(function () { _playShowdownMusicNTimes(n); }, showdownDurationMs); }; if (typeof game.showdownPlayCount === "undefined") { game.showdownPlayCount = 0; } game.showdownPlayCount = 0; _playShowdownMusicNTimes(3); } else { LK.playMusic(selectedSong ? selectedSong.id : 'bgmusic'); game.showdownSongStarted = true; songStarted = true; currentTime = 0; score = 0; combo = 0; maxCombo = 0; accuracySum = 0; accuracyCount = 0; scoreTxt.setText('0'); comboTxt.setText(''); feedbackTxt.setText(''); // Remove any old notes for (var i = activeNotes.length - 1; i >= 0; i--) { activeNotes[i].destroy(); activeNotes.splice(i, 1); } } } } // Block further update until song actually starts if (!songStarted) return; } // Advance time currentTime += 1000 / 60; // 60 FPS // --- Visual pitch deviation indicator and avatar reaction --- var minPitchDiff = 1.0; var bestType = null; var bestPitch = null; for (var i = 0; i < activeNotes.length; i++) { var note = activeNotes[i]; if (!note.hit && !note.missed) { // Only consider notes in the hit zone var inHitZone = Math.abs(note.x - hitZoneX) < hitZoneWidth / 2; if (inHitZone) { var targetPitch = note.pitch; if (note.type === 'glide') { var glideT = clamp((currentTime - note.time) / note.duration, 0, 1); targetPitch = lerp(note.glideStart, note.glideEnd, glideT); } var diff = Math.abs(playerPitch - targetPitch); if (diff < minPitchDiff) { minPitchDiff = diff; bestType = note.type; bestPitch = targetPitch; } } } } // Show deviation bar only if a note is in the hit zone if (minPitchDiff < 1.0) { pitchDeviationBar.visible = true; pitchDeviationBar.y = pitchBarActive.y; // Color: green if close, yellow if moderate, red if far // Match easier catch: widen perfect/ok windows for feedback if (minPitchDiff < 0.10) { pitchDeviationBar.tint = 0x44dd88; } else if (minPitchDiff < 0.20) { pitchDeviationBar.tint = 0xffe066; } else { pitchDeviationBar.tint = 0xff3333; } // Height: larger if more off pitchDeviationBar.height = 120 + minPitchDiff * 400; // Perfect zone indicator follows the current note's target pitch if (bestPitch !== null) { perfectZoneBar.visible = true; perfectZoneBar.y = pitchToY(bestPitch); } else { perfectZoneBar.visible = false; } } else { pitchDeviationBar.visible = false; perfectZoneBar.visible = false; } // Avatar reaction: happy if close, worried if off, shocked if very off // (avatarFace logic removed) // Spawn notes as they come into view, only when their time is reached (like Trombone Champ) var lastSpawnedNoteX = null; for (var i = 0; i < notes.length; i++) { var noteData = notes[i]; // Remove any notes that would spawn after 1 minute (60000ms) if (noteData.time > 60000 + 3400) { if (noteData.spawned) { for (var j = activeNotes.length - 1; j >= 0; j--) { if (activeNotes[j].time === noteData.time) { activeNotes[j].destroy(); activeNotes.splice(j, 1); } } } continue; } // Defensive: also remove any notes with time > 60000, even if they somehow exist if (noteData.time > 60000) { if (noteData.spawned) { for (var j = activeNotes.length - 1; j >= 0; j--) { if (activeNotes[j].time === noteData.time) { activeNotes[j].destroy(); activeNotes.splice(j, 1); } } } continue; } // Only spawn if not already spawned and the note's scheduled time minus travel time is reached // This ensures notes start offscreen right and scroll in, not appear in the middle var noteTravelTime = (noteStartX - hitZoneX) / noteSpeed; // Calculate the X position where this note would spawn var spawnX = noteStartX; // Always spawn notes as soon as they are eligible, never skip if (!noteData.spawned && currentTime >= noteData.time - noteTravelTime) { // Enforce minimum X distance between notes var canSpawn = true; // Check last spawned note's X position if (lastSpawnedNoteX !== null && Math.abs(spawnX - lastSpawnedNoteX) < 15) { canSpawn = false; } // Prevent spawning in the middle of the screen (only allow if at noteStartX) if (spawnX !== noteStartX) { canSpawn = false; } if (canSpawn) { spawnNote(noteData); noteData.spawned = true; lastSpawnedNoteX = spawnX; } } } // Update notes for (var i = activeNotes.length - 1; i >= 0; i--) { var note = activeNotes[i]; // Calculate note's current x based on time var noteTravelTime = (noteStartX - hitZoneX) / noteSpeed; var t = currentTime - (note.time - noteTravelTime); // Notes start at the right and move left toward the hit zone if (t < 0) { // Not yet on screen, keep fully offscreen right note.x = noteStartX; } else { note.x = noteStartX - t * noteSpeed; // Clamp so note never appears in the middle before scrolling in if (note.x > noteStartX) note.x = noteStartX; // Defensive: never allow any note to appear left of noteStartX before scrolling in if (t < 0 && note.x !== noteStartX) note.x = noteStartX; } // Clamp to hitZoneX and continue left after if (note.x < hitZoneX) note.x = hitZoneX + (note.x - hitZoneX); // For all notes: y position is fixed to their initial pitch, even for glides (no vertical movement) if (!note.hit && !note.missed) { note.y = pitchToY(note.pitch); } // If hit or missed, do not update y (freeze at last value) // Remove notes that have gone far enough offscreen left if (note.x < -650) { note.destroy(); activeNotes.splice(i, 1); continue; } // Check for hit/miss if (!note.hit && !note.missed) { // Tap note: now must be click-and-hold in hit zone for a short time (like a mini-hold) if (note.type === 'tap') { var tapHoldDuration = 400; // ms, must match Note class var hitWindow = tapHoldDuration; // ms, tap is now a short hold var timeDiff = Math.abs(currentTime - note.time); var inHitZone = Math.abs(note.x - hitZoneX) < hitZoneWidth / 2; var pitchDiff = Math.abs(playerPitch - note.pitch); // --- Tap notes do not give points, only visual feedback for holding --- if (inHitZone && timeDiff < hitWindow) { if (!note.holdStarted) { note.holdStarted = true; note.holdReleased = false; } note.noteAsset.scaleX = 1.2; note.noteAsset.scaleY = 1.2; } else if (note.holdStarted) { note.noteAsset.scaleX = 1; note.noteAsset.scaleY = 1; } // Track if player released before end of tap hold if (note.holdStarted && !isSliding && !note.holdReleased) { note.holdReleased = true; } // End of tap "hold" (after hitWindow) if (currentTime > note.time + hitWindow && !note.hit) { note.hit = true; // No points for tap notes, just combo/accuracy for holding and releasing var tapAcc = 1; // Always 1 for holding if (note.holdStarted) { // --- Show catch feedback for tap note --- // Logical mapping: perfect (very rare), nice (rare), ok (common), nasty (miss) var tapType = "ok"; var tapDiff = Math.abs(playerPitch - note.pitch); var r = Math.random(); if (tapDiff < 0.025 && r < 0.12) tapType = "perfect";else if (tapDiff < 0.06 && r < 0.25) tapType = "nice";else if (tapDiff < 0.18) tapType = "ok";else tapType = "nasty"; showCatchFeedback(tapType); // Only advance combo when the tap note ends (on note.hit), not on every correct catch or hold advanceCombo(); combo = comboStreak; if (combo > maxCombo) maxCombo = combo; accuracySum += tapAcc; accuracyCount++; // Apply multiplier if combo is complete var addScore = 0; if (comboStreak === comboWord.length && comboMultiplier > 1) { addScore = Math.round(100 * comboMultiplier); score += addScore; } else { addScore = 100; score += addScore; } score = Math.round(score); scoreTxt.setText(score + ''); spawnConfetti(note.x, note.y); LK.getSound('hit').play(); // Show laugh mouth for great catch if (typeof laughMouthAsset !== "undefined" && typeof mouthAsset !== "undefined") { laughMouthAsset.visible = true; mouthAsset.visible = false; // Set a much longer timeout (3500ms) to revert to mouth asset game.laughTimeout = Date.now() + 3500; } } else { // Combo broken, reset to 'C' and allow restart on next correct catch resetComboProgress(); combo = 0; spawnFailSplash(note.x, note.y); LK.getSound('miss').play(); } } // Missed if passed hit window and not started if (currentTime > note.time + hitWindow + 200 && !note.hit) { note.missed = true; resetComboProgress(); combo = 0; spawnFailSplash(note.x, note.y); LK.getSound('miss').play(); } } // Hold note: must hold correct pitch during duration (horizontal, click-and-hold) else if (note.type === 'hold') { var holdStart = note.time; var holdEnd = note.time + note.duration; var inHoldZone = currentTime >= holdStart - 180 && currentTime <= holdEnd + 180; var inHitZone = Math.abs(note.x - hitZoneX) < hitZoneWidth / 2; // --- HEAD CATCH LOGIC --- // Only allow hold scoring if the head of the note is caught in the hit zone at the correct time and pitch // Only allow one hold bar to be caught at a time (like Trombone Champ) if (!note.headCaught) { // Check if any other hold note is currently being held/caught var anotherHoldCaught = false; for (var j = 0; j < activeNotes.length; j++) { var other = activeNotes[j]; if (other !== note && other.type === 'hold' && other.headCaught && !other.hit && !other.missed) { anotherHoldCaught = true; break; } } // Only allow catching this hold bar if no other is currently caught if (!anotherHoldCaught) { // Check if the head of the note (the first frame it enters the hit zone) is caught var headInZone = Math.abs(note.x - hitZoneX) < hitZoneWidth / 2; var headTimeOk = Math.abs(currentTime - holdStart) < 180; // --- Make combo balloon streak much easier: much wider pitch window and allow streak even if not sliding when balloon is visible --- var balloonEasier = typeof comboBalloonContainer !== "undefined" && comboBalloonContainer.visible; // --- Make combo balloon streak much easier: much wider pitch window and allow streak even if not sliding when balloon is visible, but only if player is actually sliding --- // Make 'perfect' catch much harder: use a much tighter window for head catch // Only allow catch if player is actually sliding (isSliding) at the moment of catch, regardless of balloon var headPitchWindow = balloonEasier ? 0.65 : 0.13; // was 0.28, now 0.13 for normal, still wide for balloon var headPitchOk = Math.abs(playerPitch - note.pitch) < headPitchWindow && isSliding; if (headInZone && headTimeOk && headPitchOk) { // Before catching, forcibly release all other hold notes' headCaught state for (var j = 0; j < activeNotes.length; j++) { var other = activeNotes[j]; if (other !== note && other.type === 'hold' && other.headCaught && !other.hit && !other.missed) { other.headCaught = false; other.holdStarted = false; other.holdReleased = true; } } note.headCaught = true; note.holdStarted = true; note.holdScore = 0; note.holdTicks = 0; // Play slide9 sound for flat slide9 notes if (!note.slideSoundPlayed) { note.slideSoundPlayed = true; if (note.slideSound === "slide9") { var slide9Sound = LK.getSound("slide9"); if (slide9Sound) slide9Sound.play(); } } note.headCatchTime = currentTime; note.noteAsset.scaleX = 1.2; note.noteAsset.scaleY = 1.2; } } } // Only allow hold scoring if head was caught if (note.headCaught && inHoldZone && inHitZone) { // --- Begin: Like Trombone Champ, update points in real time while holding note --- if (typeof note.lastScoreFrame === "undefined" || note.lastScoreFrame !== Math.floor(currentTime * 1000)) { // Make it even easier to keep holding: widen pitch window from 0.18 to 0.22 // --- Make streak much easier while combo balloon is visible --- var balloonEasier = typeof comboBalloonContainer !== "undefined" && comboBalloonContainer.visible; var sustainPitchWindow = balloonEasier ? 0.55 : 0.22; // Only allow sustain animation/score if player is actually sliding (isSliding) if (isSliding && Math.abs(playerPitch - note.pitch) < sustainPitchWindow) { // Give more points for correct catch (e.g. 6 per frame, easier scoring) if (typeof note.lastScoreIncrementFrame === "undefined" || note.lastScoreIncrementFrame !== Math.floor(currentTime * 1000)) { note.holdScore += 6; score += 6; note.lastScoreIncrementFrame = Math.floor(currentTime * 1000); } // Removed showFeedback('Good!', "#66ccff"); } else { // Give a smaller penalty for fault (e.g. -0.1 per frame, less punishing) note.holdScore += 0; score -= 0.1; if (score < 0) score = 0; // Removed showFeedback('Bad!', "#ffcc66"); } // Always round score to nearest integer to avoid floating point artifacts score = Math.round(score); scoreTxt.setText(score + ''); note.lastScoreFrame = Math.floor(currentTime * 1000); } note.holdTicks += 1; // Match the easier catch window for visual feedback note.noteAsset.scaleX = isSliding && Math.abs(playerPitch - note.pitch) < 0.18 ? 1.2 : 1; note.noteAsset.scaleY = isSliding && Math.abs(playerPitch - note.pitch) < 0.18 ? 1.2 : 1; // --- End: Like Trombone Champ, update points in real time while holding note --- // Track if player released before end of hold if (!isSliding && !note.holdReleased) { // Allow a short release (up to 120ms) without breaking combo if (!note.releaseGraceStart) note.releaseGraceStart = currentTime; if (currentTime - note.releaseGraceStart > 120) { note.holdReleased = true; } } else if (isSliding) { note.releaseGraceStart = null; } } else if (note.holdStarted) { note.noteAsset.scaleX = 1; note.noteAsset.scaleY = 1; // --- Decrease score if player is holding the holdbar at the wrong time (not in correct window) --- if (isSliding) { // Make it even easier to get a fault: trigger combo fault on every frame of wrong hold, and only allow combo if holding at the right time score -= 0.4; // Stronger penalty if (score < 0) score = 0; // Always round score to nearest integer to avoid floating point artifacts score = Math.round(score); scoreTxt.setText(score + ''); // --- Combo system: reset combo if player is holding at wrong time (fault) --- // Only break combo if not in grace period // Reset combo text animation to 'C' immediately on every frame of wrong time hold resetComboProgress(); combo = 0; // Combo text animation removed: no comboAnimText update. // No need to throttle with lastWrongHoldFrame, always fault on every frame of wrong hold } } // End of hold: only if head was caught if (currentTime > holdEnd && !note.hit) { note.hit = true; // Do NOT force player to release hold or stop isSliding when note ends // (fix: allow pitchBarActive to keep moving and playing slide sounds after a note ends) // Only stop slide region sounds if player is not holding anymore (handled in game.up) // (no pitchFieldActive = false, no isSliding = false, no forced sound stop here) var holdAcc = note.holdTicks ? note.holdScore / note.holdTicks : 0; // Only advance combo when the note ends (on note.hit), not on every correct catch or hold if (note.headCaught && note.holdStarted) { // --- Show catch feedback for hold note --- // Only show feedback if player actually started sliding (holdStarted) var holdType = "ok"; // Logical mapping: perfect (very rare), nice (rare), ok (common), nasty (miss) var holdRatio = note.holdTicks ? note.holdScore / note.holdTicks : 0; var r = Math.random(); if (holdRatio >= 0.98 && r < 0.10) holdType = "perfect";else if (holdRatio >= 0.93 && r < 0.22) holdType = "nice";else if (holdRatio >= 0.40) holdType = "ok";else holdType = "nasty"; showCatchFeedback(holdType); advanceCombo(); combo = comboStreak; if (combo > maxCombo) maxCombo = combo; accuracySum += holdAcc; accuracyCount++; // Apply multiplier if combo is complete var addScore = 0; if (comboStreak === comboWord.length && comboMultiplier > 1) { addScore = Math.round(note.holdScore * comboMultiplier); score += addScore; } else { addScore = Math.round(note.holdScore); score += addScore; } score = Math.round(score); scoreTxt.setText(score + ''); spawnConfetti(note.x, note.y); LK.getSound('tromboneGood').play(); // Show laugh mouth for great catch if (typeof laughMouthAsset !== "undefined" && typeof mouthAsset !== "undefined") { laughMouthAsset.visible = true; mouthAsset.visible = false; // Set a much longer timeout (3500ms) to revert to mouth asset game.laughTimeout = Date.now() + 3500; } } else { // Combo broken, reset to 'C' and allow restart on next correct catch resetComboProgress(); combo = 0; spawnFailSplash(note.x, note.y); LK.getSound('miss').play(); } } // Missed if passed hold window and not started or head not caught if (currentTime > holdEnd + 200 && !note.hit) { note.missed = true; // Reset combo text animation to 'C' immediately on miss notes resetComboProgress(); combo = 0; spawnFailSplash(note.x, note.y); LK.getSound('miss').play(); } } // Glide note: must follow pitch from start to end (horizontal, click-and-hold) else if (note.type === 'glide') { var glideStart = note.time; var glideEnd = note.time + note.duration; var inGlideZone = currentTime >= glideStart - 180 && currentTime <= glideEnd + 180; var inHitZone = Math.abs(note.x - hitZoneX) < hitZoneWidth / 2; if (inGlideZone && inHitZone) { var glideT = clamp((currentTime - glideStart) / note.duration, 0, 1); var targetPitch = lerp(note.glideStart, note.glideEnd, glideT); var pitchDiff = Math.abs(playerPitch - targetPitch); // Make it even easier to catch glide notes: widen pitch window from 0.20 to 0.25 // --- Make streak much easier while combo balloon is visible --- var balloonEasier = typeof comboBalloonContainer !== "undefined" && comboBalloonContainer.visible; var glidePitchWindow = balloonEasier ? 0.55 : 0.25; if ((balloonEasier || isSliding) && pitchDiff < glidePitchWindow) { // Good glide if (!note.glideStarted) { note.glideStarted = true; note.glideScore = 0; note.glideTicks = 0; note.glideReleased = false; // Track if player released at end // Do not play slide sound when catching glide note start; only focus on click/release for sound if (!note.slideSoundPlayed) { note.slideSoundPlayed = true; } } note.glideScore += 1; note.glideTicks += 1; // Match the easier catch window for visual feedback note.noteAsset.scaleX = 1.2; note.noteAsset.scaleY = 1.2; // Track if player released before end of glide if (note.glideStarted && !isSliding && !note.glideReleased) { note.glideReleased = true; } } else if (note.glideStarted) { note.noteAsset.scaleX = 1; note.noteAsset.scaleY = 1; } } // End of glide if (currentTime > glideEnd && !note.hit) { note.hit = true; var glideAcc = note.glideTicks ? note.glideScore / note.glideTicks : 0; // Only advance combo when the glide note ends (on note.hit), not on every correct catch or hold if (note.glideStarted) { // --- Show catch feedback for glide note --- // Logical mapping: perfect (very rare), nice (rare), ok (common), nasty (miss) var glideType = "ok"; var glideRatio = note.glideTicks ? note.glideScore / note.glideTicks : 0; var r = Math.random(); if (glideRatio >= 0.98 && r < 0.10) glideType = "perfect";else if (glideRatio >= 0.93 && r < 0.22) glideType = "nice";else if (glideRatio >= 0.40) glideType = "ok";else glideType = "nasty"; showCatchFeedback(glideType); advanceCombo(); combo = comboStreak; if (combo > maxCombo) maxCombo = combo; accuracySum += glideAcc; accuracyCount++; // Apply multiplier if combo is complete var addScore = 0; if (comboStreak === comboWord.length && comboMultiplier > 1) { addScore = Math.round(note.glideScore * comboMultiplier); score += addScore; } else { addScore = Math.round(note.glideScore); score += addScore; } score = Math.round(score); scoreTxt.setText(score + ''); spawnConfetti(note.x, note.y); LK.getSound('tromboneGood').play(); // Show laugh mouth for great catch if (typeof laughMouthAsset !== "undefined" && typeof mouthAsset !== "undefined") { laughMouthAsset.visible = true; mouthAsset.visible = false; // Set a much longer timeout (3500ms) to revert to mouth asset game.laughTimeout = Date.now() + 3500; } } else { // Combo broken, reset to 'C' and allow restart on next correct catch resetComboProgress(); combo = 0; spawnFailSplash(note.x, note.y); LK.getSound('miss').play(); } } // Missed if passed glide window and not started if (currentTime > glideEnd + 200 && !note.hit) { note.missed = true; resetComboProgress(); combo = 0; spawnFailSplash(note.x, note.y); LK.getSound('miss').play(); } } } } // Remove hit/missed notes after a delay for (var i = activeNotes.length - 1; i >= 0; i--) { var note = activeNotes[i]; if ((note.hit || note.missed) && currentTime - note.time > 800) { // Reset slideSoundPlayed so it can be triggered again for new notes note.slideSoundPlayed = false; note.destroy(); activeNotes.splice(i, 1); } } // --- Showdown End: Wait 2 seconds after last note is hit or missed, then show 'Episode Complete!' --- // Track the time when the last note is hit or missed if (typeof game.lastShowdownEndTime === "undefined") { game.lastShowdownEndTime = null; } // --- NEW: End episode exactly at 1 minute (60000ms) in showdown music --- if (!songEnded && currentTime >= 60000) { songEnded = true; // Calculate accuracy and rank var acc = accuracyCount ? Math.round(Math.min(accuracySum / accuracyCount, 1) * 100) : 0; var rank = "D"; // Optimize rank thresholds for new scoring (much easier to get higher rank) if (acc >= 90 && score > 800 && maxCombo >= notes.length * 0.7) { rank = "S"; } else if (acc >= 80 && score > 600 && maxCombo >= notes.length * 0.5) { rank = "A"; } else if (acc >= 65 && score > 400 && maxCombo >= notes.length * 0.3) { rank = "B"; } else if (acc >= 40 && score > 150 && maxCombo >= notes.length * 0.1) { rank = "C"; } // Hide gameplay UI pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; comboTxt.visible = false; feedbackTxt.visible = false; // Hide combo balloon on result overlay if (typeof comboBalloonContainer !== "undefined") comboBalloonContainer.visible = false; // Hide all face features on result overlay faceAsset.visible = false; hairAsset.visible = false; leftHairEdge.visible = false; rightHairEdge.visible = false; leftEye.visible = false; rightEye.visible = false; leftPupil.visible = false; rightPupil.visible = false; noseAsset.visible = false; mouthAsset.visible = false; laughMouthAsset.visible = false; leftEyebrow.visible = false; rightEyebrow.visible = false; // Hide time counter on result overlay if (typeof timeCounterTxt !== "undefined") timeCounterTxt.visible = false; // Show result overlay if (typeof resultOverlay !== "undefined" && resultOverlay) { resultOverlay.destroy(); } var resultOverlay = new Container(); game.addChild(resultOverlay); // Add episode complete background image (fills screen, always at back) var episodeCompleteBg = LK.getAsset('episodeCompleteBg', { anchorX: 0.5, anchorY: 0.5 }); episodeCompleteBg.x = gameWidth / 2; episodeCompleteBg.y = gameHeight / 2; episodeCompleteBg.scaleX = gameWidth / episodeCompleteBg.width; episodeCompleteBg.scaleY = gameHeight / episodeCompleteBg.height; resultOverlay.addChildAt(episodeCompleteBg, 0); // (avatarFaceHuman removed from result overlay, only visible during the song) // Title // Add small background behind 'Episode Complete!' text var episodeCompleteTextBgSmall = LK.getAsset('episodeCompleteTextBgSmall', { anchorX: 0.5, anchorY: 0 }); episodeCompleteTextBgSmall.x = gameWidth / 2; episodeCompleteTextBgSmall.y = gameHeight / 2 - 700; // moved even more upper, y=666 resultOverlay.addChild(episodeCompleteTextBgSmall); // Coordinates: x = " + episodeCompleteTextBgSmall.x + ", y = " + episodeCompleteTextBgSmall.y var resultTitle = new Text2("Episode Complete!", { size: 120, fill: 0xFFE066 }); resultTitle.anchor.set(0.5, 0); resultTitle.x = gameWidth / 2; resultTitle.y = gameHeight / 2 - 480; resultOverlay.addChild(resultTitle); // Rank var rankText = new Text2("Rank: " + rank, { size: 200, fill: rank === "S" ? "#ffd700" : rank === "A" ? "#aaffaa" : rank === "B" ? "#66ccff" : rank === "C" ? "#ffcc66" : "#ff6666" }); rankText.anchor.set(0.5, 0); rankText.x = gameWidth / 2; rankText.y = gameHeight / 2 - 260; resultOverlay.addChild(rankText); // Score, Combo, Accuracy var resultStats = new Text2("Score: " + score + "\n\nMax Combo: " + maxCombo + "\n\nAccuracy: " + acc + "%", { size: 90, fill: "#fff" }); resultStats.anchor.set(0.5, 0); resultStats.x = gameWidth / 2; resultStats.y = gameHeight / 2 - 10; resultOverlay.addChild(resultStats); // Menu button // Removed holdBar image for menu button background in result overlay var menuBtn = new Text2("Back to Menu", { size: 90, fill: 0xFFE066 }); menuBtn.anchor.set(0.5, 0.5); menuBtn.x = gameWidth / 2; menuBtn.y = gameHeight / 2 + 350; menuBtn.interactive = true; menuBtn.buttonMode = true; resultOverlay.addChild(menuBtn); menuBtn.down = function (x, y, obj) { // Remove overlay, show menu, reset state resultOverlay.destroy(); menuContainer.visible = true; menuContainer.interactive = true; menuActive = true; // Hide showdown background when returning to menu showdownBg.visible = false; // Hide gameplay UI pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; faceContainer.visible = false; faceAsset.visible = false; // comboAnimText.visible = false;//{dJ} // Removed: combo text animation is no longer used comboTxt.visible = false; feedbackTxt.visible = false; // Remove all notes for (var i = activeNotes.length - 1; i >= 0; i--) { activeNotes[i].destroy(); activeNotes.splice(i, 1); } // Reset song state songStarted = false; songEnded = false; currentTime = 0; score = 0; combo = 0; maxCombo = 0; accuracySum = 0; accuracyCount = 0; game.lastShowdownEndTime = null; }; // Block further game update until menu is shown again return; } // --- (Legacy fallback: if for some reason the above doesn't trigger, keep the original fallback for > 3s after last note time, but not before 1:01) --- if (!songEnded) { // Find the last note in the song var lastNoteData = notes.length ? notes[notes.length - 1] : null; var lastNoteObj = null; // Find the corresponding active note object (if still present) for (var i = 0; i < activeNotes.length; i++) { if (activeNotes[i].time === (lastNoteData ? lastNoteData.time : -1)) { lastNoteObj = activeNotes[i]; break; } } // If the last note exists and is hit or missed, record the time if (lastNoteObj && (lastNoteObj.hit || lastNoteObj.missed)) { if (game.lastShowdownEndTime === null) { game.lastShowdownEndTime = currentTime; } } // If the last note is no longer in activeNotes (already removed), but all notes are hit/missed, also record the time if (!lastNoteObj && notes.length && notes[notes.length - 1].spawned) { // Check if all notes are hit or missed var allDone = true; for (var i = 0; i < notes.length; i++) { var found = false; for (var j = 0; j < activeNotes.length; j++) { if (activeNotes[j].time === notes[i].time) { found = true; break; } } if (found) { // Still active, not done allDone = false; break; } } if (allDone && game.lastShowdownEndTime === null) { game.lastShowdownEndTime = currentTime; } } // If 2 seconds have passed since last note was hit/missed, show result if (game.lastShowdownEndTime !== null && currentTime - game.lastShowdownEndTime > 2000) { songEnded = true; // Calculate accuracy and rank var acc = accuracyCount ? Math.round(Math.min(accuracySum / accuracyCount, 1) * 100) : 0; var rank = "D"; // Optimize rank thresholds for new scoring (much easier to get higher rank) if (acc >= 90 && score > 800 && maxCombo >= notes.length * 0.7) { rank = "S"; } else if (acc >= 80 && score > 600 && maxCombo >= notes.length * 0.5) { rank = "A"; } else if (acc >= 65 && score > 400 && maxCombo >= notes.length * 0.3) { rank = "B"; } else if (acc >= 40 && score > 150 && maxCombo >= notes.length * 0.1) { rank = "C"; } // --- LIKE POINT END SECTION --- // This is the "end section of like point" where you can finalize points, score, and show results. // (You can add any additional logic here for 'like point' if needed.) // Hide gameplay UI pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; comboTxt.visible = false; feedbackTxt.visible = false; // Hide all face features on fallback1 result overlay faceAsset.visible = false; hairAsset.visible = false; leftHairEdge.visible = false; rightHairEdge.visible = false; leftEye.visible = false; rightEye.visible = false; leftPupil.visible = false; rightPupil.visible = false; noseAsset.visible = false; mouthAsset.visible = false; laughMouthAsset.visible = false; leftEyebrow.visible = false; rightEyebrow.visible = false; // Hide time counter on result overlay if (typeof timeCounterTxt !== "undefined") timeCounterTxt.visible = false; // Show result overlay if (typeof resultOverlay !== "undefined" && resultOverlay) { resultOverlay.destroy(); } var resultOverlay = new Container(); game.addChild(resultOverlay); // Dim background REMOVED (pitchBarBg) // Add episode complete background image (fills screen, always at back) var episodeCompleteBg = LK.getAsset('episodeCompleteBg', { anchorX: 0.5, anchorY: 0.5 }); episodeCompleteBg.x = gameWidth / 2; episodeCompleteBg.y = gameHeight / 2; episodeCompleteBg.scaleX = gameWidth / episodeCompleteBg.width; episodeCompleteBg.scaleY = gameHeight / episodeCompleteBg.height; resultOverlay.addChildAt(episodeCompleteBg, 0); // (avatarFaceHuman removed from fallback1 result overlay, only visible during the song) // Title // Add small background behind 'Episode Complete!' text var episodeCompleteTextBgSmall = LK.getAsset('episodeCompleteTextBgSmall', { anchorX: 0.5, anchorY: 0 }); episodeCompleteTextBgSmall.x = gameWidth / 2; episodeCompleteTextBgSmall.y = gameHeight / 2 - 700; // moved even more upper, y=666 resultOverlay.addChild(episodeCompleteTextBgSmall); // Coordinates: x = " + episodeCompleteTextBgSmall.x + ", y = " + episodeCompleteTextBgSmall.y var resultTitle = new Text2("Episode Complete!", { size: 120, fill: 0xFFE066 }); resultTitle.anchor.set(0.5, 0); resultTitle.x = gameWidth / 2; resultTitle.y = gameHeight / 2 - 480; resultOverlay.addChild(resultTitle); // Rank var rankText = new Text2("Rank: " + rank, { size: 200, fill: rank === "S" ? "#ffd700" : rank === "A" ? "#aaffaa" : rank === "B" ? "#66ccff" : rank === "C" ? "#ffcc66" : "#ff6666" }); rankText.anchor.set(0.5, 0); rankText.x = gameWidth / 2; rankText.y = gameHeight / 2 - 260; resultOverlay.addChild(rankText); // Score, Combo, Accuracy var resultStats = new Text2("Score: " + score + "\n\nMax Combo: " + maxCombo + "\n\nAccuracy: " + acc + "%", { size: 90, fill: "#fff" }); resultStats.anchor.set(0.5, 0); resultStats.x = gameWidth / 2; resultStats.y = gameHeight / 2 - 10; // Add a semi-transparent background for stats for readability // Removed holdBar image for stats background in result overlay resultOverlay.addChild(resultStats); // Menu button // Removed holdBar image for menu button background in result overlay (fallback) var menuBtn = new Text2("Back to Menu", { size: 90, fill: 0xFFE066 }); menuBtn.anchor.set(0.5, 0.5); menuBtn.x = gameWidth / 2; menuBtn.y = gameHeight / 2 + 350; menuBtn.interactive = true; menuBtn.buttonMode = true; resultOverlay.addChild(menuBtn); menuBtn.down = function (x, y, obj) { // Remove overlay, show menu, reset state resultOverlay.destroy(); menuContainer.visible = true; menuContainer.interactive = true; menuActive = true; // Hide gameplay UI pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; faceContainer.visible = false; faceAsset.visible = false; comboTxt.visible = false; feedbackTxt.visible = false; // Remove all notes for (var i = activeNotes.length - 1; i >= 0; i--) { activeNotes[i].destroy(); activeNotes.splice(i, 1); } // Reset song state songStarted = false; songEnded = false; currentTime = 0; score = 0; combo = 0; maxCombo = 0; accuracySum = 0; accuracyCount = 0; game.lastShowdownEndTime = null; }; // Block further game update until menu is shown again return; } } // (Legacy fallback: if for some reason the above doesn't trigger, keep the original fallback for > 3s after last note time, but not before 1:01) if (!songEnded && currentTime > notes[notes.length - 1].time + 3000 && currentTime >= 61000) { // Only allow fallback after 1:01 (61000ms) songEnded = true; // Calculate accuracy and rank var acc = accuracyCount ? Math.round(Math.min(accuracySum / accuracyCount, 1) * 100) : 0; var rank = "D"; // Optimize rank thresholds for new scoring (much easier to get higher rank) if (acc >= 90 && score > 800 && maxCombo >= notes.length * 0.7) { rank = "S"; } else if (acc >= 80 && score > 600 && maxCombo >= notes.length * 0.5) { rank = "A"; } else if (acc >= 65 && score > 400 && maxCombo >= notes.length * 0.3) { rank = "B"; } else if (acc >= 40 && score > 150 && maxCombo >= notes.length * 0.1) { rank = "C"; } // --- LIKE POINT END SECTION --- // This is the "end section of like point" where you can finalize points, score, and show results. // (You can add any additional logic here for 'like point' if needed.) // Hide gameplay UI pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; // comboAnimText.visible = false;//{gH} // Removed: combo text animation is no longer used comboTxt.visible = false; feedbackTxt.visible = false; // Hide all face features on fallback2 result overlay faceAsset.visible = false; hairAsset.visible = false; leftHairEdge.visible = false; rightHairEdge.visible = false; leftEye.visible = false; rightEye.visible = false; leftPupil.visible = false; rightPupil.visible = false; noseAsset.visible = false; mouthAsset.visible = false; laughMouthAsset.visible = false; leftEyebrow.visible = false; rightEyebrow.visible = false; // Hide time counter on result overlay if (typeof timeCounterTxt !== "undefined") timeCounterTxt.visible = false; // Show result overlay if (typeof resultOverlay !== "undefined" && resultOverlay) { resultOverlay.destroy(); } var resultOverlay = new Container(); game.addChild(resultOverlay); // Dim background REMOVED (pitchBarBg) // Add episode complete background image (fills screen, always at back) var episodeCompleteBg = LK.getAsset('episodeCompleteBg', { anchorX: 0.5, anchorY: 0.5 }); episodeCompleteBg.x = gameWidth / 2; episodeCompleteBg.y = gameHeight / 2; episodeCompleteBg.scaleX = gameWidth / episodeCompleteBg.width; episodeCompleteBg.scaleY = gameHeight / episodeCompleteBg.height; resultOverlay.addChildAt(episodeCompleteBg, 0); // (avatarFaceHuman removed from fallback2 result overlay, only visible during the song) // Title // Add small background behind 'Episode Complete!' text var episodeCompleteTextBgSmall = LK.getAsset('episodeCompleteTextBgSmall', { anchorX: 0.5, anchorY: 0 }); episodeCompleteTextBgSmall.x = gameWidth / 2; episodeCompleteTextBgSmall.y = gameHeight / 2 - 700; // moved even more upper, y=666 resultOverlay.addChild(episodeCompleteTextBgSmall); // Coordinates: x = " + episodeCompleteTextBgSmall.x + ", y = " + episodeCompleteTextBgSmall.y var resultTitle = new Text2("Episode Complete!", { size: 120, fill: 0xFFE066 }); resultTitle.anchor.set(0.5, 0); resultTitle.x = gameWidth / 2; resultTitle.y = gameHeight / 2 - 480; resultOverlay.addChild(resultTitle); // Rank var rankText = new Text2("Rank: " + rank, { size: 200, fill: rank === "S" ? "#ffd700" : rank === "A" ? "#aaffaa" : rank === "B" ? "#66ccff" : rank === "C" ? "#ffcc66" : "#ff6666" }); rankText.anchor.set(0.5, 0); rankText.x = gameWidth / 2; rankText.y = gameHeight / 2 - 260; resultOverlay.addChild(rankText); // Score, Combo, Accuracy var resultStats = new Text2("Score: " + score + "\n\nMax Combo: " + maxCombo + "\n\nAccuracy: " + acc + "%", { size: 90, fill: "#fff" }); resultStats.anchor.set(0.5, 0); resultStats.x = gameWidth / 2; resultStats.y = gameHeight / 2 - 10; // Add a semi-transparent background for stats for readability // Removed holdBar image for stats background in result overlay (fallback) resultOverlay.addChild(resultStats); // Menu button // Removed holdBar image for menu button background in result overlay (fallback) var menuBtn = new Text2("Back to Menu", { size: 90, fill: 0xFFE066 }); menuBtn.anchor.set(0.5, 0.5); menuBtn.x = gameWidth / 2; menuBtn.y = gameHeight / 2 + 350; menuBtn.interactive = true; menuBtn.buttonMode = true; resultOverlay.addChild(menuBtn); menuBtn.down = function (x, y, obj) { // Remove overlay, show menu, reset state resultOverlay.destroy(); menuContainer.visible = true; menuContainer.interactive = true; menuActive = true; // Hide gameplay UI pitchBarActive.visible = false; hitZone.visible = false; scoreTxt.visible = false; faceContainer.visible = false; faceAsset.visible = false; comboTxt.visible = false; feedbackTxt.visible = false; // Remove all notes for (var i = activeNotes.length - 1; i >= 0; i--) { activeNotes[i].destroy(); activeNotes.splice(i, 1); } // Reset song state songStarted = false; songEnded = false; currentTime = 0; score = 0; combo = 0; maxCombo = 0; accuracySum = 0; accuracyCount = 0; game.lastShowdownEndTime = null; }; // Block further game update until menu is shown again return; } }; // --- Start music --- // Now handled in game.update after song selection
===================================================================
--- original.js
+++ change.js
@@ -2020,50 +2020,68 @@
}
if (!game.showdownSongStarted) {
game.showdownSongDelayTimer--;
if (game.showdownSongDelayTimer <= 0) {
- // If showdown song is selected, play it for 20s, 3 times in a row
- if (selectedSong && selectedSong.id === "showdown") {
- game.showdownPlayCount = 0;
- game.showdownMaxPlays = 3;
- // Helper to play showdown segment
- game.playShowdownSegment = function () {
- if (game.showdownPlayCount < game.showdownMaxPlays) {
- LK.playMusic('showdown', {
- loop: false,
- start: 0,
- end: 20 / 60
- }); // play first 20s
- game.showdownPlayCount++;
- // Schedule next segment after 20s (20,000ms)
- if (typeof game.showdownTimeout !== "undefined") {
- LK.clearTimeout(game.showdownTimeout);
+ // If showdown song is selected, play 'showdown' music asset 3 times
+ if (selectedSong && selectedSong.id === 'showdown') {
+ // Helper to play showdown music 3 times in a row
+ var _playShowdownMusicNTimes = function playShowdownMusicNTimes(n) {
+ if (game.showdownPlayCount >= n) {
+ // After 3 plays, start the game
+ 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);
}
- game.showdownTimeout = LK.setTimeout(function () {
- game.playShowdownSegment();
- }, 20000);
+ return;
}
+ // Play showdown music, do not loop
+ LK.playMusic('showdown', {
+ loop: false
+ });
+ game.showdownPlayCount++;
+ // Wait for the showdown music to end, then play again
+ // Defensive: estimate duration as 60s (or use asset duration if available)
+ var showdownDurationMs = 60000;
+ LK.setTimeout(function () {
+ _playShowdownMusicNTimes(n);
+ }, showdownDurationMs);
};
- game.playShowdownSegment();
+ if (typeof game.showdownPlayCount === "undefined") {
+ game.showdownPlayCount = 0;
+ }
+ game.showdownPlayCount = 0;
+ _playShowdownMusicNTimes(3);
} 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);
+ }
}
- 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;
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