From 81ef11d8556d0e687562e4f0b00eb9573edbd461 Mon Sep 17 00:00:00 2001 From: OmikronApex Date: Mon, 13 Jul 2026 00:35:09 +0200 Subject: [PATCH] fix(v3): floor accuracy percentages so 100% means all notes hit Math.round let 431/433 (99.54%) display as 100%. Floor at every accuracy display site (HUD, library badges, dashboard, lessons, profile, playlists, calibration overlay); stored fractions and mastery thresholds unchanged. Co-Authored-By: Claude Fable 5 --- static/v3/dashboard.js | 3 ++- static/v3/lessons.js | 2 +- static/v3/live-performance-hud.js | 3 ++- static/v3/playlists.js | 2 +- static/v3/profile.js | 2 +- static/v3/progress.js | 2 +- static/v3/songs.js | 5 +++-- tests/js/live_performance_hud.test.js | 3 ++- 8 files changed, 13 insertions(+), 9 deletions(-) diff --git a/static/v3/dashboard.js b/static/v3/dashboard.js index 5123535..59e5e7e 100644 --- a/static/v3/dashboard.js +++ b/static/v3/dashboard.js @@ -33,7 +33,8 @@ // Accuracy badge ramp (design/04-badges.md §C): ≥90% good, 50–89% mid, <50% low. function accuracyBadge(acc) { if (acc == null) return ''; - const pct = Math.round(acc * 100); + // Floor, never round: 100% must mean every note hit. + const pct = Math.floor(acc * 100); const color = acc >= 0.9 ? 'bg-fb-good' : (acc >= 0.5 ? 'bg-fb-mid' : 'bg-fb-low'); const text = acc >= 0.5 && acc < 0.9 ? 'text-black' : 'text-white'; return '✓ Passed'; if (st && st.best_accuracy != null && st.best_accuracy > 0) { const acc = st.best_accuracy; - const pct = Math.round(acc * 100); + const pct = Math.floor(acc * 100); const color = acc >= 0.9 ? 'text-fb-good' : (acc >= 0.5 ? 'text-fb-mid' : 'text-fb-low'); return '' + pct + '%'; } diff --git a/static/v3/live-performance-hud.js b/static/v3/live-performance-hud.js index 32b47ed..37d8512 100644 --- a/static/v3/live-performance-hud.js +++ b/static/v3/live-performance-hud.js @@ -21,7 +21,8 @@ function accuracyPct(hits, misses) { const judged = hits + misses; if (judged <= 0) return null; - return Math.round((hits / Math.max(1, judged)) * 100); + // Floor, never round: 100% must mean every judged note was hit. + return Math.floor((hits / Math.max(1, judged)) * 100); } function calculateLivePerformanceState({ hits = 0, misses = 0, streak = 0, bestStreak = 0 } = {}) { diff --git a/static/v3/playlists.js b/static/v3/playlists.js index 2efff61..7684344 100644 --- a/static/v3/playlists.js +++ b/static/v3/playlists.js @@ -73,7 +73,7 @@ ? ' data-play-fn="' + esc(playFn) + '"' + (arrIdx != null ? ' data-play-arr="' + arrIdx + '"' : '') : ''; const acc = (isAlbum && typeof opts.acc === 'number') - ? '' + Math.round(opts.acc * 100) + '%' + ? '' + Math.floor(opts.acc * 100) + '%' : ''; const pin = (isAlbum && s.arrangement) ? '' + esc(s.arrangement) + '' : ''; diff --git a/static/v3/profile.js b/static/v3/profile.js index 71f7cb9..ce79818 100644 --- a/static/v3/profile.js +++ b/static/v3/profile.js @@ -232,7 +232,7 @@ host.innerHTML = '
    ' + rows.map((s, i) => { const acc = Number(s.best_accuracy) || 0; - const pct = Math.round(acc * 100); + const pct = Math.floor(acc * 100); const score = Number(s.best_score) || 0; return '
  1. ' + '' + (i + 1) + '' + diff --git a/static/v3/progress.js b/static/v3/progress.js index 5867449..018775c 100644 --- a/static/v3/progress.js +++ b/static/v3/progress.js @@ -265,7 +265,7 @@ const onboarding = st.onboarding || {}; if (onboarding.calibration_status === 'completed') return; // raced a 100% run const pending = onboarding.calibration_status === 'pending'; - const pct = Math.max(0, Math.min(100, Math.round((detail.accuracy || 0) * 100))); + const pct = Math.max(0, Math.min(100, Math.floor((detail.accuracy || 0) * 100))); const overlay = document.createElement('div'); overlay.id = 'v3-calibration-retry'; diff --git a/static/v3/songs.js b/static/v3/songs.js index a05536c..255e814 100644 --- a/static/v3/songs.js +++ b/static/v3/songs.js @@ -482,7 +482,8 @@ function accuracyBadge(filename, variant) { const acc = state.accuracy[filename]; if (acc == null) return ''; - const pct = Math.round(acc * 100); + // Floor, never round: 100% must mean every note hit. + const pct = Math.floor(acc * 100); if (variant === 'tree') { const color = acc >= MASTERY_ACCURACY ? 'text-fb-good' : acc >= 0.5 ? 'text-fb-mid' : 'text-fb-low'; return '' + pct + '%'; @@ -1312,7 +1313,7 @@ c.year ? String(c.year) : ''] .filter(Boolean).join(' · '); const acc = (typeof c.best_accuracy === 'number') - ? '' + Math.round(c.best_accuracy * 100) + '%' + ? '' + Math.floor(c.best_accuracy * 100) + '%' : 'not played'; return '
    { runtime.onHit(); runtime.onMiss(); - assert.equal(els.percent.textContent, '67%'); + // Floored, not rounded: 100% must mean every judged note was hit. + assert.equal(els.percent.textContent, '66%'); assert.equal(els.hits.textContent, 'Hits 2 / 3'); assert.equal(els.streak.textContent, 'Streak 0'); assert.match(els.state.textContent, /Recovering/);