From 15b870590757fdba85076dd2ae32914357585aeb Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Mon, 14 Sep 2026 07:16:29 +0000 Subject: [PATCH] Unified target-note controls: key filter + fret window across all modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New shared PracticeFilter settings (keyTonic/keyMode/fretWindow), persisted via AppSettings.practice, defaults off (behavior unchanged). - music/targetPool.ts: single source for the effective target pool — base playable range ∩ fret window ∩ key/mode pitch classes (9 modes incl. pentatonic, same scale machinery as Phrase Mode). - useGameLogic now builds validNotes via computeTargetPool. - TargetNoteControls component (key on/off + tonic/mode, fret window on/off + min/max) rendered in Controls for all game modes; key is independent of the displayed key signature. - Phrase Mode binds its key/fret-window setup to the same shared filter (read/write mirror) and drops the duplicate controls in its setup panel. - Tests: targetPool.test.ts (7 cases); suite 156/156 green, build ok. --- src/components/Controls.tsx | 4 + src/components/PhraseTrainer.tsx | 69 ++++++--------- src/components/TargetNoteControls.tsx | 119 ++++++++++++++++++++++++++ src/hooks/useGameLogic.ts | 82 ++++-------------- src/music/targetPool.test.ts | 78 +++++++++++++++++ src/music/targetPool.ts | 63 ++++++++++++++ src/types/SettingsTypes.ts | 28 ++++++ 7 files changed, 339 insertions(+), 104 deletions(-) create mode 100644 src/components/TargetNoteControls.tsx create mode 100644 src/music/targetPool.test.ts create mode 100644 src/music/targetPool.ts diff --git a/src/components/Controls.tsx b/src/components/Controls.tsx index 6c1b99e..d29d369 100644 --- a/src/components/Controls.tsx +++ b/src/components/Controls.tsx @@ -3,6 +3,7 @@ import { TUNINGS, INSTRUMENT_TUNINGS } from '../music/Tunings'; import { INSTRUMENT_DEFINITIONS } from '../music/InstrumentConfigs'; import { getTempoMarking } from '../music/TempoMarkings'; import { TuningMeter } from './TuningMeter'; +import { TargetNoteControls } from './TargetNoteControls'; import { useSettings } from '../context/useSettings'; import { type Difficulty, type RhythmSettings } from '../types/SettingsTypes'; @@ -193,6 +194,9 @@ export const Controls: React.FC = ({ currentPitch }) => { )} + {/* Target-note filter: unified controls, all modes */} + + {/* Bottom Section: Tools Grid (2 Columns now) */}
{/* Tool 1: Tuner */} diff --git a/src/components/PhraseTrainer.tsx b/src/components/PhraseTrainer.tsx index 9d9a0d5..6cf7a5d 100644 --- a/src/components/PhraseTrainer.tsx +++ b/src/components/PhraseTrainer.tsx @@ -13,13 +13,13 @@ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRe import { useSettings } from '../context/useSettings'; import { useAudioPlayer } from '../hooks/useAudioPlayer'; import { usePhraseTrainer, type PhraseTrainerApi, type PhrasePhase } from '../hooks/usePhraseTrainer'; -import { getPhraseSettings, type PhraseSettings } from '../types/SettingsTypes'; +import { getPhraseSettings, getPracticeFilter, type PhraseSettings, type PracticeFilter } from '../types/SettingsTypes'; import { rawFrameCount } from '../hooks/rawFrameBus'; import { audioEngine } from '../audio/AudioEngine'; import { computePlayableNotes, isFrettedInstrument, positionsWithinWindow, fretWindowNotes } from '../music/playableRange'; import { generateMelody } from '../music/melodyGenerator'; import { generateScaleDrill } from '../music/scaleDrills'; -import { isMode, MODE_LABELS, TONICS, type ModeId } from '../music/scales'; +import { isMode, type ModeId } from '../music/scales'; import { scoreEvents, type Result, type Score, type ScoreEvent } from '../score/model'; import { getNoteDetails } from '../music/NoteUtils'; import { EXERCISES, loadExercise } from '../exercises/library'; @@ -68,8 +68,27 @@ export const PhraseTrainer = forwardRef( const setSettings = updateSettings; const { playNote } = useAudioPlayer(); const phrase = getPhraseSettings(settings); + // Unified target-note controls: Phrase Mode reads/writes the shared + // practice filter so all modes expose identical key/fret-window state. + const pf = getPracticeFilter(settings); + phrase.keyTonic = pf.keyTonic; + phrase.keyMode = pf.keyMode; + phrase.fretWindowEnabled = pf.fretWindowEnabled; + phrase.fretMin = pf.fretMin; + phrase.fretMax = pf.fretMax; const setPhrase = useCallback((updates: Partial) => { - updateSettings(s => ({ ...s, phrase: { ...getPhraseSettings(s), ...updates } })); + updateSettings(s => { + const next = { ...s, phrase: { ...getPhraseSettings(s), ...updates } }; + // Mirror shared filter fields into the unified practice filter. + const shared: Partial = {}; + if ('keyTonic' in updates) shared.keyTonic = updates.keyTonic; + if ('keyMode' in updates) shared.keyMode = updates.keyMode; + if ('fretWindowEnabled' in updates) shared.fretWindowEnabled = updates.fretWindowEnabled; + if ('fretMin' in updates) shared.fretMin = updates.fretMin; + if ('fretMax' in updates) shared.fretMax = updates.fretMax; + if (Object.keys(shared).length > 0) next.practice = { ...getPracticeFilter(s), ...shared }; + return next; + }); }, [updateSettings]); const currentInstrumentDef = INSTRUMENT_DEFINITIONS[settings.instrument]; @@ -489,22 +508,9 @@ export const PhraseTrainer = forwardRef( {(phrase.material === 'melody' || phrase.material === 'scale') && ( - <> - - - + + Key/mode: use the "Target key" controls below the sheet — shared across modes. + )} {phrase.material === 'melody' && ( @@ -604,28 +610,9 @@ export const PhraseTrainer = forwardRef( )} {fretted && ( - <> - - {phrase.fretWindowEnabled && ( - <> - - - - )} - + + Fret window: use the "Fret window" controls below the sheet — shared across modes. + )}