@@ -67,6 +67,7 @@ public sealed class GameplayHighwayController : MonoBehaviour
6767 private Button resultRestartButton ;
6868 private Button resultMenuButton ;
6969 private Button resultApplyCalibrationButton ;
70+ private Button resultPracticeWeakestButton ;
7071 private Button practicePreviousSectionButton ;
7172 private Button practiceNextSectionButton ;
7273 private Button practiceLoopSectionButton ;
@@ -84,6 +85,7 @@ public sealed class GameplayHighwayController : MonoBehaviour
8485 private Label resultBreakdownLabel ;
8586 private Label resultPracticeLabel ;
8687 private Label resultCalibrationLabel ;
88+ private Label resultErrorMapLabel ;
8789 private Label practiceSectionLabel ;
8890 private Label practiceStatusLabel ;
8991 private VisualElement resultPerformancePanel ;
@@ -121,6 +123,8 @@ public sealed class GameplayHighwayController : MonoBehaviour
121123 private readonly TimingCalibrationAdvisor keyboardCalibration = new TimingCalibrationAdvisor ( ) ;
122124 private readonly TimingCalibrationAdvisor midiCalibration = new TimingCalibrationAdvisor ( ) ;
123125 private readonly PracticePerformanceAnalyzer performanceAnalyzer = new PracticePerformanceAnalyzer ( ) ;
126+ private PracticeErrorMapAnalyzer errorMapAnalyzer ;
127+ private PracticeErrorCell weakestPracticeError ;
124128 private DrumInputSource lastCalibrationSource = DrumInputSource . Keyboard ;
125129 private DrumPad ? latestPulsePad ;
126130 private bool invalidConfigurationLogged ;
@@ -164,6 +168,7 @@ public sealed class GameplayHighwayController : MonoBehaviour
164168 public string LastChartExportPath { get ; private set ; }
165169 public string LastChartPackagePath { get ; private set ; }
166170 public int EditableChartNoteCount => chartDraftEditor ? . Notes . Count ?? 0 ;
171+ public PracticeErrorCell WeakestPracticeError => weakestPracticeError ;
167172
168173 private void Awake ( )
169174 {
@@ -307,6 +312,7 @@ private void BindView()
307312 resultRestartButton = root . Q < Button > ( "result-restart-button" ) ;
308313 resultMenuButton = root . Q < Button > ( "result-menu-button" ) ;
309314 resultApplyCalibrationButton = root . Q < Button > ( "result-apply-calibration" ) ;
315+ resultPracticeWeakestButton = root . Q < Button > ( "result-practice-weakest" ) ;
310316 practicePreviousSectionButton = root . Q < Button > ( "practice-previous-section" ) ;
311317 practiceNextSectionButton = root . Q < Button > ( "practice-next-section" ) ;
312318 practiceLoopSectionButton = root . Q < Button > ( "practice-loop-section" ) ;
@@ -324,6 +330,7 @@ private void BindView()
324330 resultBreakdownLabel = root . Q < Label > ( "result-breakdown" ) ;
325331 resultPracticeLabel = root . Q < Label > ( "result-practice" ) ;
326332 resultCalibrationLabel = root . Q < Label > ( "result-calibration" ) ;
333+ resultErrorMapLabel = root . Q < Label > ( "result-error-map" ) ;
327334 practiceSectionLabel = root . Q < Label > ( "practice-section-label" ) ;
328335 practiceStatusLabel = root . Q < Label > ( "practice-status" ) ;
329336 resultPerformancePanel = root . Q < VisualElement > ( "result-performance-panel" ) ;
@@ -523,6 +530,7 @@ private void HandleHitResolved(HitResult result)
523530 {
524531 if ( result == null ) return ;
525532 performanceAnalyzer . Record ( result . Note . Pad , result . Grade ) ;
533+ errorMapAnalyzer ? . Record ( result ) ;
526534 scoreTracker . Apply ( result ) ;
527535 if ( result . Grade == HitGrade . Miss )
528536 {
@@ -608,6 +616,8 @@ public void RestartRun()
608616 keyboardCalibration . Reset ( ) ;
609617 midiCalibration . Reset ( ) ;
610618 performanceAnalyzer . Reset ( ) ;
619+ errorMapAnalyzer ? . Reset ( ) ;
620+ weakestPracticeError = null ;
611621 if ( metronomeSource != null ) metronomeSource . Stop ( ) ;
612622 metronomeScheduled = false ;
613623 pulseDeadlines . Clear ( ) ;
@@ -686,6 +696,17 @@ private void InitializePracticeLab()
686696 {
687697 GameplaySessionDefinition session = CurrentSession ;
688698 practiceSections = GameplayPracticeSections . Create ( session . Bars , session . BeatsPerBar , session . Bpm ) ;
699+ var analysisSections = new PracticeSectionDefinition [ practiceSections . Count ] ;
700+ for ( int index = 0 ; index < practiceSections . Count ; index ++ )
701+ {
702+ GameplayPracticeRange section = practiceSections [ index ] ;
703+ analysisSections [ index ] = new PracticeSectionDefinition (
704+ index ,
705+ section . Label ,
706+ section . StartSeconds ,
707+ section . EndSeconds ) ;
708+ }
709+ errorMapAnalyzer = new PracticeErrorMapAnalyzer ( analysisSections ) ;
689710 selectedPracticeSectionIndex = 0 ;
690711 RefreshPracticeStatus ( ) ;
691712 }
@@ -709,6 +730,8 @@ private void RestartPracticePass()
709730 keyboardCalibration . Reset ( ) ;
710731 midiCalibration . Reset ( ) ;
711732 performanceAnalyzer . Reset ( ) ;
733+ errorMapAnalyzer ? . Reset ( ) ;
734+ weakestPracticeError = null ;
712735 pulseDeadlines . Clear ( ) ;
713736 latestPulsePad = null ;
714737 matching . RestartSession ( range . StartSeconds , range . EndSeconds ) ;
@@ -842,6 +865,7 @@ private void ShowResults(HitMatchingSnapshot matchingSnapshot)
842865 $ "PERFECT { matchingSnapshot . PerfectCount } GOOD { matchingSnapshot . GoodCount } " +
843866 $ "EARLY/LATE { matchingSnapshot . EarlyCount + matchingSnapshot . LateCount } MISS { matchingSnapshot . MissCount } ";
844867 RenderPracticeRecommendation ( ) ;
868+ RenderErrorMap ( ) ;
845869 RenderCalibrationRecommendation ( ) ;
846870 SetDisplayed ( chartCreatorResults , false ) ;
847871 }
@@ -1066,6 +1090,7 @@ private void BindRunControls()
10661090 if ( resultRestartButton != null ) resultRestartButton . clicked += RestartRun ;
10671091 if ( resultMenuButton != null ) resultMenuButton . clicked += ReturnToMainMenu ;
10681092 if ( resultApplyCalibrationButton != null ) resultApplyCalibrationButton . clicked += ApplyCalibrationRecommendation ;
1093+ if ( resultPracticeWeakestButton != null ) resultPracticeWeakestButton . clicked += PracticeWeakestArea ;
10691094 if ( practicePreviousSectionButton != null ) practicePreviousSectionButton . clicked += SelectPreviousPracticeSection ;
10701095 if ( practiceNextSectionButton != null ) practiceNextSectionButton . clicked += SelectNextPracticeSection ;
10711096 if ( practiceLoopSectionButton != null ) practiceLoopSectionButton . clicked += LoopSelectedPracticeSection ;
@@ -1093,6 +1118,7 @@ private void UnbindRunControls()
10931118 if ( resultRestartButton != null ) resultRestartButton . clicked -= RestartRun ;
10941119 if ( resultMenuButton != null ) resultMenuButton . clicked -= ReturnToMainMenu ;
10951120 if ( resultApplyCalibrationButton != null ) resultApplyCalibrationButton . clicked -= ApplyCalibrationRecommendation ;
1121+ if ( resultPracticeWeakestButton != null ) resultPracticeWeakestButton . clicked -= PracticeWeakestArea ;
10961122 if ( practicePreviousSectionButton != null ) practicePreviousSectionButton . clicked -= SelectPreviousPracticeSection ;
10971123 if ( practiceNextSectionButton != null ) practiceNextSectionButton . clicked -= SelectNextPracticeSection ;
10981124 if ( practiceLoopSectionButton != null ) practiceLoopSectionButton . clicked -= LoopSelectedPracticeSection ;
@@ -1448,6 +1474,45 @@ private void RenderPracticeRecommendation()
14481474 $ "FOCUS PROSSIMO: { PadLabel ( weakest . Pad ) } · { weakest . Accuracy : 0.0} % · { tendency } ";
14491475 }
14501476
1477+ private void RenderErrorMap ( )
1478+ {
1479+ if ( resultErrorMapLabel == null || resultPracticeWeakestButton == null ) return ;
1480+ weakestPracticeError = errorMapAnalyzer ? . Weakest ( ) ;
1481+ if ( weakestPracticeError == null )
1482+ {
1483+ resultErrorMapLabel . text = "NESSUN RISULTATO DISPONIBILE" ;
1484+ resultPracticeWeakestButton . SetEnabled ( false ) ;
1485+ return ;
1486+ }
1487+
1488+ var cells = new List < PracticeErrorCell > ( errorMapAnalyzer . Snapshot ( ) ) ;
1489+ cells . Sort ( ( left , right ) =>
1490+ {
1491+ int byAccuracy = left . Accuracy . CompareTo ( right . Accuracy ) ;
1492+ if ( byAccuracy != 0 ) return byAccuracy ;
1493+ int bySection = left . Section . Index . CompareTo ( right . Section . Index ) ;
1494+ return bySection != 0 ? bySection : left . Pad . CompareTo ( right . Pad ) ;
1495+ } ) ;
1496+ int count = Math . Min ( 4 , cells . Count ) ;
1497+ var entries = new string [ count ] ;
1498+ for ( int index = 0 ; index < count ; index ++ )
1499+ {
1500+ PracticeErrorCell cell = cells [ index ] ;
1501+ entries [ index ] = $ "{ cell . Section . Label } : { PadLabel ( cell . Pad ) } { cell . Accuracy : 0} %";
1502+ }
1503+ resultErrorMapLabel . text = string . Join ( " · " , entries ) ;
1504+ resultPracticeWeakestButton . SetEnabled ( true ) ;
1505+ resultPracticeWeakestButton . text =
1506+ $ "ALLENA { weakestPracticeError . Section . Label } · { PadLabel ( weakestPracticeError . Pad ) } ";
1507+ }
1508+
1509+ public void PracticeWeakestArea ( )
1510+ {
1511+ if ( weakestPracticeError == null || weakestPracticeError . Section . Index >= practiceSections . Count ) return ;
1512+ selectedPracticeSectionIndex = weakestPracticeError . Section . Index ;
1513+ LoopSelectedPracticeSection ( ) ;
1514+ }
1515+
14511516 private static string PadLabel ( DrumPad pad )
14521517 {
14531518 switch ( pad )
0 commit comments