@@ -61,6 +61,7 @@ public sealed class GameplayHighwayController : MonoBehaviour
6161 private Label keyGuideSnareHiHatLabel ;
6262 private Label keyGuideFloorKickLabel ;
6363 private Label impactCueLabel ;
64+ private Label reactiveStageStatusLabel ;
6465 private Button menuButton ;
6566 private Button pauseButton ;
6667 private Button resumeButton ;
@@ -120,6 +121,7 @@ public sealed class GameplayHighwayController : MonoBehaviour
120121 private readonly List < DrumArticulation > chartArticulationChoices = new List < DrumArticulation > ( ) ;
121122 private GameplayHighwaySurface surface ;
122123 private GameplayKitSurface kitSurface ;
124+ private GameplayReactiveStageSurface reactiveStageSurface ;
123125 private bool showInstructionalKit ;
124126 private HitMatchingPrototype subscribedMatching ;
125127 private readonly GameplayScoreTracker scoreTracker = new GameplayScoreTracker ( ) ;
@@ -140,6 +142,9 @@ public sealed class GameplayHighwayController : MonoBehaviour
140142 private bool metronomeScheduled ;
141143 private bool metronomeSeekedWhilePaused ;
142144 private bool resultRecorded ;
145+ private HitGrade ? latestStageGrade ;
146+ private bool latestStageWrongInput ;
147+ private float stagePulseDeadline ;
143148 private GameplayAutoTempoRecommendation autoTempoRecommendation ;
144149 private bool isChangingTempo ;
145150 private readonly GameplayPracticeLoop practiceLoop = new GameplayPracticeLoop ( ) ;
@@ -154,6 +159,9 @@ public sealed class GameplayHighwayController : MonoBehaviour
154159 public GameplayPresentationTheme Theme { get ; private set ; }
155160 public GameplayHighwaySurface Surface => surface ;
156161 public GameplayKitSurface KitSurface => kitSurface ;
162+ public GameplayReactiveStageSurface ReactiveStageSurface => reactiveStageSurface ;
163+ public GameplayReactiveStageState ReactiveStageState => reactiveStageSurface ? . State ??
164+ GameplayReactiveStageCalculator . Calculate ( 0 , null , null , 0 , false , false , false ) ;
157165 public bool IsInstructionalKitVisible => showInstructionalKit ;
158166 public Texture2D ActiveBackground => BackgroundFor ( Theme ) ;
159167 public string EnvironmentTitle => GameplayEnvironmentProfile . For ( Theme ) . Title ;
@@ -273,6 +281,7 @@ private void SetTheme(GameplayPresentationTheme theme)
273281 }
274282
275283 surface ? . SetTheme ( theme ) ;
284+ reactiveStageSurface ? . SetTheme ( theme ) ;
276285 if ( impactCueLabel != null )
277286 impactCueLabel . style . top = Length . Percent ( environment . StrikeRatio * 100f ) ;
278287 if ( kitSurface != null )
@@ -306,6 +315,7 @@ private void BindView()
306315 positionLabel = root . Q < Label > ( "song-position" ) ;
307316 judgmentLabel = root . Q < Label > ( "judgment-label" ) ;
308317 kitGuidanceLabel = root . Q < Label > ( "kit-guidance-label" ) ;
318+ reactiveStageStatusLabel = root . Q < Label > ( "reactive-stage-status" ) ;
309319 environmentTitleLabel = root . Q < Label > ( "environment-title" ) ;
310320 environmentSubtitleLabel = root . Q < Label > ( "environment-subtitle" ) ;
311321 currentInputLabel = root . Q < Label > ( "current-input" ) ;
@@ -381,17 +391,23 @@ private void BindView()
381391 if ( keyGuideFloorKickLabel != null ) keyGuideFloorKickLabel . text = $ "{ preferences . FloorTomKey } /{ preferences . KickKey } TIMPANO / GRANCASSA";
382392
383393 VisualElement highwayHost = root . Q < VisualElement > ( "highway-host" ) ;
394+ VisualElement reactiveStageHost = root . Q < VisualElement > ( "reactive-stage-host" ) ;
384395 VisualElement kitVisualHost = root . Q < VisualElement > ( "kit-visual-host" ) ;
385396 VisualElement targetsHost = root . Q < VisualElement > ( "targets-host" ) ;
386397 VisualElement kickHost = root . Q < VisualElement > ( "kick-target-host" ) ;
387- if ( highwayHost == null || kitVisualHost == null || targetsHost == null || kickHost == null )
398+ if ( highwayHost == null || reactiveStageHost == null || kitVisualHost == null ||
399+ targetsHost == null || kickHost == null )
388400 {
389401 Debug . LogError ( "Gameplay highway UXML is missing a required host element." , this ) ;
390402 enabled = false ;
391403 return ;
392404 }
393405
394406 highwayHost . Clear ( ) ;
407+ reactiveStageHost . Clear ( ) ;
408+ reactiveStageSurface = new GameplayReactiveStageSurface ( ) ;
409+ reactiveStageSurface . AddToClassList ( "reactive-stage-surface" ) ;
410+ reactiveStageHost . Add ( reactiveStageSurface ) ;
395411 surface = new GameplayHighwaySurface { name = "gameplay-highway-surface" } ;
396412 surface . AddToClassList ( "highway-surface" ) ;
397413 highwayHost . Add ( surface ) ;
@@ -474,6 +490,19 @@ private void RefreshPresentation()
474490 ? Mathf . Clamp01 ( ( deadline - Time . unscaledTime ) / PulseDurationSeconds )
475491 : 0 ;
476492 surface . SetFrame ( upcoming , position , HighwayLookAheadSeconds , latestPulsePad , pulse , ghostReplay . Ghost ) ;
493+ PlayerPreferencesSnapshot preferences = PlayerPreferencesRuntime . Current . Snapshot ;
494+ float stagePulse = Mathf . Clamp01 ( ( stagePulseDeadline - Time . unscaledTime ) /
495+ GameplayReactiveStageCalculator . MinimumPulseDurationSeconds ) ;
496+ GameplayReactiveStageState stageState = GameplayReactiveStageCalculator . Calculate (
497+ scoreTracker . Snapshot . Combo ,
498+ latestStageGrade ,
499+ latestPulsePad ,
500+ stagePulse ,
501+ latestStageWrongInput ,
502+ preferences . ReducedMotion ,
503+ preferences . HighContrast ) ;
504+ reactiveStageSurface ? . SetState ( stageState ) ;
505+ if ( reactiveStageStatusLabel != null ) reactiveStageStatusLabel . text = stageState . Label ;
477506 if ( ghostStatusLabel != null )
478507 ghostStatusLabel . text = ghostReplay . HasGhost
479508 ? $ "GHOST ATTIVO · { ghostReplay . Ghost . Count } COLPI"
@@ -508,6 +537,9 @@ private void HandleInputProcessed(DrumInputEvent input, HitResult result)
508537 else CaptureGhostHit ( input , result ) ;
509538 latestPulsePad = input . Pad ;
510539 pulseDeadlines [ input . Pad ] = Time . unscaledTime + PulseDurationSeconds ;
540+ stagePulseDeadline = Time . unscaledTime + GameplayReactiveStageCalculator . MinimumPulseDurationSeconds ;
541+ latestStageGrade = result ? . Grade ;
542+ latestStageWrongInput = result == null ;
511543 lastCalibrationSource = input . Source == DrumInputSource . Midi
512544 ? DrumInputSource . Midi
513545 : DrumInputSource . Keyboard ;
@@ -550,6 +582,9 @@ public bool CaptureGhostHit(DrumInputEvent input, HitResult result) =>
550582 private void HandleHitResolved ( HitResult result )
551583 {
552584 if ( result == null ) return ;
585+ latestStageGrade = result . Grade ;
586+ latestStageWrongInput = false ;
587+ stagePulseDeadline = Time . unscaledTime + GameplayReactiveStageCalculator . MinimumPulseDurationSeconds ;
553588 performanceAnalyzer . Record ( result . Note . Pad , result . Grade ) ;
554589 errorMapAnalyzer ? . Record ( result ) ;
555590 scoreTracker . Apply ( result ) ;
@@ -644,6 +679,9 @@ public void RestartRun()
644679 metronomeScheduled = false ;
645680 pulseDeadlines . Clear ( ) ;
646681 latestPulsePad = null ;
682+ latestStageGrade = null ;
683+ latestStageWrongInput = false ;
684+ stagePulseDeadline = 0 ;
647685 matching . RestartSession ( ) ;
648686 songClock . RestartPlayback ( ) ;
649687 RunState = GameplayRunState . Countdown ;
0 commit comments