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