From b4948cfb0547b0779804fbe9d1d2efccefbb4566 Mon Sep 17 00:00:00 2001 From: Baron Luca Date: Wed, 19 Aug 2026 18:09:44 +0200 Subject: [PATCH] feat(gameplay): add accessible reactive stage --- docs/README.md | 1 + docs/development/reactive-stage.md | 21 ++ .../Gameplay/GameplayHighwayController.cs | 40 ++- .../Runtime/Gameplay/GameplayReactiveStage.cs | 243 ++++++++++++++++++ .../Gameplay/GameplayReactiveStage.cs.meta | 2 + .../Tests/EditMode/GameplayHighwayTests.cs | 52 ++++ .../PlayMode/GameplayHighwayPlayModeTests.cs | 26 ++ .../HitTheKit/UI/Gameplay/GameplayHighway.uss | 24 ++ .../UI/Gameplay/GameplayHighway.uxml | 2 + 9 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 docs/development/reactive-stage.md create mode 100644 src/HitTheKit.Unity/Assets/HitTheKit/Runtime/Gameplay/GameplayReactiveStage.cs create mode 100644 src/HitTheKit.Unity/Assets/HitTheKit/Runtime/Gameplay/GameplayReactiveStage.cs.meta diff --git a/docs/README.md b/docs/README.md index 6c399dd..694819f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -50,6 +50,7 @@ without creating an unreviewed second source of truth. - [Auto Tempo Coach](development/auto-tempo-coach.md) - [Audio and latency sound check](development/audio-latency-sound-check.md) - [Ghost Replay](development/ghost-replay.md) +- [Accessible reactive stage](development/reactive-stage.md) - [Demo-song vertical slice](development/demo-song-vertical-slice.md) - [Keyboard hit matching](development/keyboard-hit-matching.md) - [Pad visuals](development/pad-visuals.md) diff --git a/docs/development/reactive-stage.md b/docs/development/reactive-stage.md new file mode 100644 index 0000000..066e5a0 --- /dev/null +++ b/docs/development/reactive-stage.md @@ -0,0 +1,21 @@ +# Accessible reactive stage + +The gameplay stage reacts to real runtime signals already produced by the hit matcher and score tracker. It does not own timing, scoring, input, or song state. + +## Signals + +- Combo selects a bounded energy band: calm, building, live, or peak. +- The latest hit grade provides a short accent. +- The latest drum pad selects the accent color already used by that lane. +- Misses and unmatched inputs enter a visible recovery state. + +The stage uses text and geometric patterns as well as color. A miss therefore remains distinguishable in high-contrast and color-impaired viewing conditions. + +## Motion and intensity safety + +- Full visual intensity is capped at `0.78`. +- `Reduce motion` replaces moving patterns with a steady composition and caps intensity at `0.22`. +- Pulses last at least `0.18` seconds and the stage never alternates the full screen on/off. +- `High contrast` increases outlines and preserves the textual stage state. + +All visuals are procedural UI Toolkit drawing. No commercial assets, per-frame material creation, telemetry, or additional gameplay systems are introduced. diff --git a/src/HitTheKit.Unity/Assets/HitTheKit/Runtime/Gameplay/GameplayHighwayController.cs b/src/HitTheKit.Unity/Assets/HitTheKit/Runtime/Gameplay/GameplayHighwayController.cs index fcea386..3b98c37 100644 --- a/src/HitTheKit.Unity/Assets/HitTheKit/Runtime/Gameplay/GameplayHighwayController.cs +++ b/src/HitTheKit.Unity/Assets/HitTheKit/Runtime/Gameplay/GameplayHighwayController.cs @@ -61,6 +61,7 @@ public sealed class GameplayHighwayController : MonoBehaviour private Label keyGuideSnareHiHatLabel; private Label keyGuideFloorKickLabel; private Label impactCueLabel; + private Label reactiveStageStatusLabel; private Button menuButton; private Button pauseButton; private Button resumeButton; @@ -120,6 +121,7 @@ public sealed class GameplayHighwayController : MonoBehaviour private readonly List chartArticulationChoices = new List(); private GameplayHighwaySurface surface; private GameplayKitSurface kitSurface; + private GameplayReactiveStageSurface reactiveStageSurface; private bool showInstructionalKit; private HitMatchingPrototype subscribedMatching; private readonly GameplayScoreTracker scoreTracker = new GameplayScoreTracker(); @@ -140,6 +142,9 @@ public sealed class GameplayHighwayController : MonoBehaviour private bool metronomeScheduled; private bool metronomeSeekedWhilePaused; private bool resultRecorded; + private HitGrade? latestStageGrade; + private bool latestStageWrongInput; + private float stagePulseDeadline; private GameplayAutoTempoRecommendation autoTempoRecommendation; private bool isChangingTempo; private readonly GameplayPracticeLoop practiceLoop = new GameplayPracticeLoop(); @@ -154,6 +159,9 @@ public sealed class GameplayHighwayController : MonoBehaviour public GameplayPresentationTheme Theme { get; private set; } public GameplayHighwaySurface Surface => surface; public GameplayKitSurface KitSurface => kitSurface; + public GameplayReactiveStageSurface ReactiveStageSurface => reactiveStageSurface; + public GameplayReactiveStageState ReactiveStageState => reactiveStageSurface?.State ?? + GameplayReactiveStageCalculator.Calculate(0, null, null, 0, false, false, false); public bool IsInstructionalKitVisible => showInstructionalKit; public Texture2D ActiveBackground => BackgroundFor(Theme); public string EnvironmentTitle => GameplayEnvironmentProfile.For(Theme).Title; @@ -273,6 +281,7 @@ private void SetTheme(GameplayPresentationTheme theme) } surface?.SetTheme(theme); + reactiveStageSurface?.SetTheme(theme); if (impactCueLabel != null) impactCueLabel.style.top = Length.Percent(environment.StrikeRatio * 100f); if (kitSurface != null) @@ -306,6 +315,7 @@ private void BindView() positionLabel = root.Q