User prompt
in combo text animation im seeing always c letter fix it letter by letter to make combo word
User prompt
✅ Reset combo text animation to 'C' immediately on wrong time hold or miss notes (hold note penalty). ✅ Reset combo text animation to 'C' immediately on miss notes (hold note miss penalty). u said this but when i do wrong time hold for a second example reset the combo text animation from c letter
User prompt
Reset combo text animation to 'C' immediately on wrong time hold, miss click, or miss notes u said this but wrong time hold and miss notes pennalty doesnt work fix it
User prompt
Reset combo text animation to 'C' immediately on wrong time hold, miss click, or miss notes u said this but when i missed a note there is still combo text animation u have to reset it and start it from c letter and make it that penalt when i got wrong time holding aswell
User prompt
Reset combo text animation to 'C' immediately on wrong time hold or miss click u said this but do Reset combo text animation to 'C' immediately on wrong time hold or miss click or miss notes
User prompt
Reset combo text animation to nothing immediately on wrong time hold or miss click u said that but combo text animation is not resetting from nothing it has to be start with c later when i have got miss a note or miss click or wrong time holding
User prompt
when i do wrong time hold or miss click for notes restart the combo text animation from nothing
User prompt
combo text animation is getting letter c then co then com then comb and then combo if condition has done but streak is going like always get a letter by letter even i didint have streak the notes but if we had a wrong time hold or missclick reset the streaking combo animation but still i can ge combo text animation even if i had a wrong holdor miss click
User prompt
if i have getting streak of holding bar catching the letter should be stay what i have got like if i got co streak dont remove from the screen i mean that letters should stay in screen till i got wrong hold or missclick and reset he combo text animation to starting
User prompt
if i have getting streak of holding bar catching the letter should be stay what i have got like if i got co streak dont remove from the screen i mean that letters should stay in screen till i got wrong hold or missclick
User prompt
combo text animation is getting letter c then co then com then comb and then combo if condition has done but streak is going like always get a letter by letter even i didint have streak the notes but if we had a wrong time hold or missclick reset the streaking combo animation
User prompt
combo text animation is getting letter c then co then com then comb and then combo if condition has done but streak is going like always get a letter by letter do be a combo but if we had a wrong hold or missclick reset the streaking combo animation
User prompt
the combo text animation reseting during when i hold a mouse click dont reseting in that way difficult
User prompt
erase the accuracy point text in complete menu and make a logic accuracy because accuracy peint is always %100
User prompt
the combo letter animation should be step by step but i cannot go on when i get just c letter fix it and make it easier
User prompt
i cannot write the combo animation when im doing streak make it easier to write combo step by step c if i have good caught then if i have good caught co then if i have good caught com then if i have good caught comb if i have good caught then combo and combo do multiply to our score
User prompt
i cannot write the combo animation when im doing streak make it easier to write combo step by step c then co then com then comb then combo and combo do multiply to our score
User prompt
i cannot write the combo animation when im doing streak make it easier to write combo
User prompt
make the easier way to write combo because i can only do c make easier write to combo nd do multiply aswell
User prompt
i cannot still streak the combo text make it easier to do combo but reset the text
User prompt
make all notes should get closer space between each notes do it for difficult game
User prompt
reposition for all not +50 in Pitch (Y) axiss coordinate
User prompt
| Note # | Start Time (ms) | Duration (ms) | Pitch (Y) | Length in X (px) | Gap to Next Note (px) | |--------|-----------------|---------------|-----------|------------------|-----------------------|| note2 | 2040 | 550 | 1772 | 600 | 30 make it
User prompt
write a list of all info of all notes like how long they are in coordinate and write duration on screen and whats gap between next notes
User prompt
give me list of all info of all notes like how long they are in coordinate and write duration on screen and whats gap between next notes
/**** * 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 comboProgress = 0; // How many correct in a row (0 to comboWord.length) var comboMultiplier = 1; // Multiplier for score var comboActive = false; // True if player is in a combo streak var comboMissCount = 0; // Count consecutive misses to make combo easier // Reveal steps: ["C", "CO", "COM", "COMB", "COMBO"] var comboSteps = ["C", "CO", "COM", "COMB", "COMBO"]; function startComboAnim() { comboAnimActive = true; // Show the current step of the combo word if (comboProgress > 0 && comboProgress <= comboSteps.length) { comboAnimText.setText(comboSteps[comboProgress - 1]); comboAnimText.visible = true; } else { comboAnimText.setText(''); comboAnimText.visible = false; } // If full combo, keep text and set multiplier if (comboProgress === comboSteps.length) { comboMultiplier = 2; comboAnimText.setText(comboSteps[comboSteps.length - 1]); comboAnimText.visible = true; } else { comboMultiplier = 1; } // No timer animation, just update instantly for each streak if (comboAnimTimer) { LK.clearTimeout(comboAnimTimer); comboAnimTimer = null; } } // Call this when a combo is successfully continued (good catch) function advanceCombo() { comboMissCount = 0; // Reset miss count on success if (comboProgress < comboSteps.length) { comboProgress++; comboActive = true; startComboAnim(); } // If full combo, multiplier is already set in startComboAnim } // Call this when combo is broken (miss) function resetComboProgress() { comboMissCount = (typeof comboMissCount === "number" ? comboMissCount : 0) + 1; if (comboMissCount < 2) { // Show combo text as "C" and keep multiplier if only one miss comboProgress = 1; comboMultiplier = 1; comboActive = false; comboAnimText.setText(comboSteps[0]); comboAnimText.visible = true; if (comboAnimTimer) { LK.clearTimeout(comboAnimTimer); comboAnimTimer = null; } return; } // Reset combo and hide text after two consecutive misses comboProgress = 0; comboMultiplier = 1; comboActive = false; comboAnimText.setText(''); comboAnimText.visible = false; if (comboAnimTimer) { LK.clearTimeout(comboAnimTimer); comboAnimTimer = null; } comboMissCount = 0; } // 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 = comboProgress; if (combo > maxCombo) maxCombo = combo; accuracySum += tapAcc; accuracyCount++; // Apply multiplier if combo is complete var addScore = 0; if (comboProgress === comboWord.length && comboMultiplier > 1) { addScore = Math.round(100 * comboMultiplier); score += addScore; // Do NOT reset multiplier; keep it as long as streak continues } else { addScore = 100 * comboMultiplier; 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; comboTxt.setText(''); 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 if (comboProgress > 0 && (!note.releaseGraceStart || currentTime - note.releaseGraceStart > 120)) { resetComboProgress(); combo = 0; comboAnimText.setText(''); comboAnimText.visible = false; } // 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 = comboProgress; if (combo > maxCombo) maxCombo = combo; accuracySum += holdAcc; accuracyCount++; // Apply multiplier if combo is complete var addScore = 0; if (comboProgress === comboWord.length && comboMultiplier > 1) { addScore = Math.round(note.holdScore * comboMultiplier); score += addScore; // Do NOT reset multiplier; keep it as long as streak continues } else { addScore = note.holdScore * comboMultiplier; 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; resetComboProgress(); combo = 0; comboTxt.setText(''); 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 = comboProgress; if (combo > maxCombo) maxCombo = combo; accuracySum += glideAcc; accuracyCount++; // Apply multiplier if combo is complete var addScore = 0; if (comboProgress === comboWord.length && comboMultiplier > 1) { addScore = Math.round(note.glideScore * comboMultiplier); score += addScore; // Do NOT reset multiplier; keep it as long as streak continues } else { addScore = note.glideScore * comboMultiplier; 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; comboTxt.setText(''); 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, Accuracy Point var accuracyPoint = Math.round(acc * score / 100); var resultStats = new Text2("Score: " + score + "\n\nMax Combo: " + maxCombo + "\n\nAccuracy: " + acc + "%" + "\n\nAccuracy Point: " + accuracyPoint, { 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, Accuracy Point var accuracyPoint = Math.round(acc * score / 100); var resultStats = new Text2("Score: " + score + "\n\nMax Combo: " + maxCombo + "\n\nAccuracy: " + acc + "%" + "\n\nAccuracy Point: " + accuracyPoint, { 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, Accuracy Point var accuracyPoint = Math.round(acc * score / 100); var resultStats = new Text2("Score: " + score + "\n\nMax Combo: " + maxCombo + "\n\nAccuracy: " + acc + "%" + "\n\nAccuracy Point: " + accuracyPoint, { 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
@@ -902,76 +902,51 @@
note.addChild(noteLabel);
activeNotes.push(note);
game.addChild(note);
}
-// Combo letter-by-letter animation and multiplier logic
+// Combo letter-by-letter animation and multiplier logic (step by step: C, CO, COM, COMB, COMBO)
var comboProgress = 0; // How many correct in a row (0 to comboWord.length)
var comboMultiplier = 1; // Multiplier for score
var comboActive = false; // True if player is in a combo streak
var comboMissCount = 0; // Count consecutive misses to make combo easier
// Reveal steps: ["C", "CO", "COM", "COMB", "COMBO"]
var comboSteps = ["C", "CO", "COM", "COMB", "COMBO"];
function startComboAnim() {
comboAnimActive = true;
- // Show the current combo step
+ // Show the current step of the combo word
if (comboProgress > 0 && comboProgress <= comboSteps.length) {
comboAnimText.setText(comboSteps[comboProgress - 1]);
comboAnimText.visible = true;
} else {
comboAnimText.setText('');
comboAnimText.visible = false;
}
- if (comboAnimTimer) {
- LK.clearTimeout(comboAnimTimer);
- comboAnimTimer = null;
- }
- // Animate next letter if possible
- function nextLetter() {
- if (comboProgress > 0 && comboProgress < comboSteps.length) {
- comboAnimText.setText(comboSteps[comboProgress]);
- comboAnimText.visible = true;
- comboAnimTimer = LK.setTimeout(function () {
- comboProgress++;
- if (comboProgress < comboSteps.length) {
- nextLetter();
- } else {
- // Combo word complete, keep text visible and apply multiplier
- comboMultiplier = 2;
- comboAnimText.setText(comboSteps[comboSteps.length - 1]);
- comboAnimText.visible = true;
- }
- }, 80);
- } else if (comboProgress === comboSteps.length) {
- comboMultiplier = 2;
- comboAnimText.setText(comboSteps[comboSteps.length - 1]);
- comboAnimText.visible = true;
- }
- }
- // Only animate if not already at full combo
- if (comboProgress > 0 && comboProgress < comboSteps.length) {
- nextLetter();
- } else if (comboProgress === comboSteps.length) {
+ // If full combo, keep text and set multiplier
+ if (comboProgress === comboSteps.length) {
comboMultiplier = 2;
comboAnimText.setText(comboSteps[comboSteps.length - 1]);
comboAnimText.visible = true;
+ } else {
+ comboMultiplier = 1;
}
+ // No timer animation, just update instantly for each streak
+ if (comboAnimTimer) {
+ LK.clearTimeout(comboAnimTimer);
+ comboAnimTimer = null;
+ }
}
// Call this when a combo is successfully continued (good catch)
function advanceCombo() {
- // Make combo easier: allow streaking as long as player doesn't miss twice in a row
comboMissCount = 0; // Reset miss count on success
if (comboProgress < comboSteps.length) {
comboProgress++;
comboActive = true;
startComboAnim();
- if (comboProgress === comboSteps.length) {
- comboMultiplier = 2;
- }
}
+ // If full combo, multiplier is already set in startComboAnim
}
// Call this when combo is broken (miss)
function resetComboProgress() {
- // Make combo easier: only reset combo if player misses twice in a row
comboMissCount = (typeof comboMissCount === "number" ? comboMissCount : 0) + 1;
if (comboMissCount < 2) {
// Show combo text as "C" and keep multiplier if only one miss
comboProgress = 1;
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