diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a145b9a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,41 @@ +# Section Map — AI Agent Guide + +Draws a minimap bar of the current song's structure over the player, with +click/wheel seeking and a per-section "glass fill" difficulty indicator. +Frontend-only — there's no `routes.py`; all logic lives in `screen.js`, +reading state straight off the Host's `window.highway` object. + +## Plugin-spec compliance (see got-feedBack/feedBack-plugin-spec) + +- **Folder name must equal `plugin.json`'s `id` exactly** (case-sensitive: + `section_map`) — a mismatch is a silent skip at plugin discovery. +- **Idempotent script guard, already in place:** the `playSong`/`showScreen` + wrapping and the `setInterval` poller are all installed inside a single + `window.__slopsmithSectionMapHooksInstalled` guard at the bottom of + `screen.js`. The Host may re-execute `screen.js` on plugin reload — any + new top-level listener/timer needs to go inside that same guard, not a + bare call alongside it. +- **No bespoke API with `feedback-plugin-dynamic-difficulty`.** Both plugins + independently read the same Host surface (`highway.getPhrases()` / + `hasPhraseData()` / `getMastery()`) for section-difficulty data — this + plugin works whether or not dynamic_difficulty is installed, as long as + *something* populates that phrase data. Don't reach into + dynamic_difficulty's own globals/localStorage directly; go through + `window.highway`. +- **Seeking must go through the Host's canonical funnel** + (`window.feedBack.seek` / `window.slopsmith.seek`, wrapped by `_smSeek`), + not by poking `audio.currentTime` directly — see the comment on `_smSeek` + for why that breaks under native/streaming playback backends. The direct + `audio.currentTime` path only exists as a last-resort fallback for a Host + old enough to lack the seek API. +- **No `MutationObserver`, no DOM polling for section data** — `_smUpdate` + reads `highway.getSections()`/`getSongInfo()`/`getTime()` directly and + only re-renders the bar when the sections reference actually changes. + +## Versioning + +Bump `version` in `plugin.json` whenever a change is user-visible — a +rendering fix, a new interaction (click/wheel/hover), a changed setting +(best-practices rule 4: bump on every release; the plugin manager uses +this to detect updates). Patch (`1.x.y`) for fixes, minor (`1.x.0`) for +new features, matching normal semver conventions. diff --git a/plugin.json b/plugin.json index f9fb6a7..c8dc30f 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "id": "section_map", "name": "Section Map", - "version": "1.2.0", + "version": "1.2.1", "private": false, "script": "screen.js", "category": "practice", diff --git a/screen.js b/screen.js index c9060c9..00f18ff 100644 --- a/screen.js +++ b/screen.js @@ -5,17 +5,20 @@ let _smBar = null; let _smSections = []; let _smDuration = 0; +// Most-specific-first: 'pre' must come before 'chorus'/'verse' so a +// "pre-chorus"/"pre-verse" section name (which contains both substrings) +// matches its own color instead of falling through to the base section's. const SM_COLORS = { + 'pre': '#84cc16', + 'noguitar': '#374151', + 'breakdown': '#f97316', + 'riff': '#06b6d4', 'intro': '#3b82f6', 'verse': '#22c55e', 'chorus': '#eab308', 'bridge': '#a855f7', 'solo': '#ef4444', 'outro': '#6b7280', - 'breakdown': '#f97316', - 'riff': '#06b6d4', - 'pre': '#84cc16', - 'noguitar': '#374151', 'default': '#4b5563', }; @@ -186,11 +189,16 @@ function _smRender() { const color = _smGetColor(sec.name); // Clean up section name for display + const ordinalMatch = sec.name.match(/(\d+)$/); let label = sec.name.replace(/\d+$/, '').trim(); label = label.charAt(0).toUpperCase() + label.slice(1); + // The on-bar label drops the trailing digit (no room for "Verse 2" in + // a ~9px-tall strip), but the hover title keeps it — otherwise two + // "Verse" blocks are indistinguishable on mouseover. + const titleLabel = ordinalMatch ? `${label} ${ordinalMatch[1]}` : label; html += `
+ title="${titleLabel} (${_smFmt(sec.time)})"> ${label}
`; @@ -325,7 +333,18 @@ if (typeof module !== 'undefined' && module.exports) { if (window[HOOK_KEY]) return; window[HOOK_KEY] = true; - // Poll for updates + // Poll for updates. sectionmap#3 asked to replace this with an event + // listener once highway emits a sections-changed event — highway has no + // such event (sections arrive over the WS with no dedicated signal), but + // it does emit 'song:ready' once section/phrase/etc. data has fully + // landed, so listen for that to build the bar immediately instead of + // waiting up to 200ms after a song loads. The interval still has to stay: + // the playback-position marker/highlight/glass-fill refresh every tick + // need continuous updates tied to the audio clock, which isn't something + // an event can replace without just re-inventing polling. + if (window.feedBack && typeof window.feedBack.on === 'function') { + window.feedBack.on('song:ready', _smUpdate); + } setInterval(_smUpdate, 200); // Hook into playSong