import { noteDisplay, CHROMATIC, ENHARMONIC } from './music.js'; import { buildFretboard } from './fretboard.js'; import { genMode1Question, genMode2Question, evaluateMode2 } from './quiz.js'; // ─── Persistent stats ───────────────────────────────────────────────────────── const STORAGE_KEY = 'guitarTrainerStats'; function defaultLifetimeStats() { return { total: 0, correct: 0, bestStreak: 0, mode1: { total: 0, correct: 0 }, mode2: { total: 0, correct: 0 }, notes: {} }; } function loadLifetimeStats() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) return { ...defaultLifetimeStats(), ...JSON.parse(raw) }; } catch {} return defaultLifetimeStats(); } function saveLifetimeStats() { localStorage.setItem(STORAGE_KEY, JSON.stringify(lifetimeStats)); } let lifetimeStats = loadLifetimeStats(); // ─── Note keyboard input ────────────────────────────────────────────────────── const NOTE_LETTERS = new Set(['a','b','c','d','e','f','g']); const FLAT_TO_SHARP = Object.fromEntries( Object.entries(ENHARMONIC).map(([sharp, flat]) => [flat, sharp]) ); let noteBuffer = ''; let noteTimer = null; function resolveNoteInput(raw) { const letter = raw[0].toUpperCase(); const mod = raw[1] || ''; if (mod === '#') { const n = letter + '#'; return CHROMATIC.includes(n) ? n : null; } if (mod === 'b') return FLAT_TO_SHARP[letter + 'b'] || null; return CHROMATIC.includes(letter) ? letter : null; } function submitNoteBuffer() { clearTimeout(noteTimer); noteTimer = null; const raw = noteBuffer; noteBuffer = ''; if (!raw || mode !== 1 || answered) return; const note = resolveNoteInput(raw); if (note) checkMode1(note); } // ─── State ──────────────────────────────────────────────────────────────────── let mode = 1; // 1, 2, or 'stats' let currentQ = null; let answered = false; let mode2Selection = new Set(); let stats = { correct: 0, total: 0, streak: 0 }; // ─── Settings helpers ───────────────────────────────────────────────────────── const getNumStrings = () => parseInt(document.getElementById('stringCount').value); const getSharpsOnly = () => document.getElementById('sharpsOnly').checked; const getNaturalOnly = () => document.getElementById('naturalOnly').checked; // ─── Rendering ──────────────────────────────────────────────────────────────── function renderQuestion() { answered = false; mode2Selection.clear(); clearTimeout(noteTimer); noteBuffer = ''; const fb = document.getElementById('feedback'); fb.textContent = ''; fb.className = 'feedback'; document.getElementById('nextBtn').disabled = true; if (mode === 1) renderMode1(); else renderMode2(); } function renderMode1() { currentQ = genMode1Question(getNumStrings(), getNaturalOnly()); const numStrings = getNumStrings(); const stringNames = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']; const stringNumber = numStrings - currentQ.stringIdx; document.getElementById('promptLabel').textContent = 'What note is this?'; document.getElementById('promptText').innerHTML = `${stringNames[stringNumber - 1]} string, ` + `fret ${currentQ.fret}`; document.getElementById('fretboardContainer').innerHTML = buildFretboard({ numStrings, highlightPos: { stringIdx: currentQ.stringIdx, fret: currentQ.fret }, }); const btns = document.getElementById('noteButtons'); btns.innerHTML = ''; currentQ.choices.forEach((note, i) => { const btn = document.createElement('button'); btn.className = 'note-btn'; btn.textContent = noteDisplay(note, getSharpsOnly()); btn.dataset.note = note; if (i < 9) btn.title = `Press ${i + 1}`; btns.appendChild(btn); }); document.getElementById('noteButtons').style.display = 'flex'; document.getElementById('mode2Controls').style.display = 'none'; } function renderMode2() { currentQ = genMode2Question(getNumStrings(), getNaturalOnly()); document.getElementById('promptLabel').textContent = 'Find all occurrences of:'; document.getElementById('promptText').innerHTML = `${noteDisplay(currentQ.targetNote, getSharpsOnly())}` + `in frets ${currentQ.fretStart}–${currentQ.fretEnd}`; refreshMode2Fretboard(); document.getElementById('noteButtons').style.display = 'none'; document.getElementById('mode2Controls').style.display = 'flex'; } function refreshMode2Fretboard(showCorrect = false) { document.getElementById('fretboardContainer').innerHTML = buildFretboard({ numStrings: getNumStrings(), interactive: true, fretRange: [currentQ.fretStart, currentQ.fretEnd], mode2Sel: mode2Selection, mode2Correct: showCorrect ? currentQ.correctSet : null, showLabels: showCorrect, sharpsOnly: getSharpsOnly(), }); } // ─── Answer checking ────────────────────────────────────────────────────────── function recordAnswer(isCorrect, note) { lifetimeStats.total++; if (isCorrect) lifetimeStats.correct++; const modeKey = mode === 1 ? 'mode1' : 'mode2'; lifetimeStats[modeKey].total++; if (isCorrect) lifetimeStats[modeKey].correct++; if (!lifetimeStats.notes[note]) lifetimeStats.notes[note] = { total: 0, correct: 0 }; lifetimeStats.notes[note].total++; if (isCorrect) lifetimeStats.notes[note].correct++; if (stats.streak > lifetimeStats.bestStreak) lifetimeStats.bestStreak = stats.streak; saveLifetimeStats(); } function checkMode1(note) { if (answered) return; answered = true; stats.total++; const fb = document.getElementById('feedback'); document.querySelectorAll('.note-btn').forEach(b => { if (b.dataset.note === currentQ.note) b.classList.add('correct'); }); const clickedBtn = document.querySelector(`.note-btn[data-note="${note}"]`); const isCorrect = note === currentQ.note; if (isCorrect) { fb.textContent = '✓ Correct!'; fb.className = 'feedback correct'; stats.correct++; stats.streak++; } else { if (clickedBtn) clickedBtn.classList.add('wrong'); fb.textContent = `✗ It's ${currentQ.note}`; fb.className = 'feedback wrong'; stats.streak = 0; } recordAnswer(isCorrect, currentQ.note); updateStats(); document.getElementById('nextBtn').disabled = false; document.getElementById('fretboardContainer').innerHTML = buildFretboard({ numStrings: getNumStrings(), highlightPos: { stringIdx: currentQ.stringIdx, fret: currentQ.fret }, showLabels: true, sharpsOnly: getSharpsOnly(), }); } function checkMode2() { if (answered) return; answered = true; stats.total++; const { allCorrect, missed, extra } = evaluateMode2(mode2Selection, currentQ.correctSet); refreshMode2Fretboard(true); const fb = document.getElementById('feedback'); if (allCorrect) { const n = currentQ.correctSet.size; fb.textContent = `✓ Perfect! Found all ${n} position${n !== 1 ? 's' : ''}`; fb.className = 'feedback correct'; stats.correct++; stats.streak++; } else { fb.textContent = `✗ Missed ${missed}, extra ${extra} — green = correct positions`; fb.className = 'feedback wrong'; stats.streak = 0; } recordAnswer(allCorrect, currentQ.targetNote); updateStats(); document.getElementById('nextBtn').disabled = false; } function updateStats() { document.getElementById('statCorrect').textContent = stats.correct; document.getElementById('statTotal').textContent = stats.total; document.getElementById('statStreak').textContent = stats.streak; } // ─── Stats screen ───────────────────────────────────────────────────────────── function accColor(acc) { if (acc >= 80) return 'var(--green)'; if (acc >= 50) return 'var(--accent2)'; return 'var(--accent)'; } function modeRow(label, mStat) { const acc = mStat.total ? Math.round(mStat.correct / mStat.total * 100) : 0; return `
| Note | Correct | Seen | Accuracy |
|---|---|---|---|
| ${noteDisplay(r.note, false)} | ${r.correct} | ${r.total} |