using UnityEngine; using TMPro; using System; using UnityEngine.UI; // Runs before default-order scripts so the per-track held-state table (trackHeld) is // updated from keyboard/touch this frame *before* hold notes read it. This preserves the // frame-accurate timing the old direct Input.GetKey polling had in HoldNote. [DefaultExecutionOrder(-100)] public class InputManager : MonoBehaviour { public static InputManager Instance { get; private set; } // Legacy KeyCode-based events (maintained for backward compatibility with existing Note/HoldNote code). // These are now fired by OnTrackInput handlers after receiving dspTime from GameplayInputActions. public static event Action OnKeyPressed; public static event Action OnKeyReleased; // New dspTime-carrying events: track index + dspTime at the moment Input System captured the event. // Allows sub-frame-accurate judgement by converting dspTime → song time via GameplayClock.SongTimeFromDsp. public static event Action OnTrackPressedWithDspTime; public static event Action OnTrackReleasedWithDspTime; private static readonly string[] TrackColors = { "red", "green", "yellow", "purple", "blue" }; [Header("Inspector")] public TextMeshProUGUI[] trackJudgeTexts = new TextMeshProUGUI[5]; [Header("Inspector")] public Text[] trackKeyTexts = new Text[5]; [Header("Lane Background Settings")] [Tooltip("The sprites for the 5 lanes (Red, Green, Yellow, Purple, Blue)")] public SpriteRenderer[] laneSprites = new SpriteRenderer[5]; [Tooltip("Alpha value when pressed (0-255)")] public float pressedAlpha = 30f; [Header("Inspector")] public bool showJudgeText = true; // Documentation text normalized. [Header("Inspector")] [Tooltip("Documentation text normalized.")] public Color keyActiveColor = Color.yellow; [Tooltip("Documentation text normalized.")] public Color keyInactiveColor = Color.black; // Documentation text normalized. public Color perfectColor = Color.yellow; public Color greatColor = Color.green; public Color goodColor = Color.cyan; public Color missColor = Color.red; private KeyCode[] cachedKeys = new KeyCode[5]; private KeyCode cachedSecondaryTrack3Key = KeyCode.V; private bool pauseBlockedLastFrame = false; // Per-track held count. Written by both keyboard (Update) and touch (PressTrack/ // ReleaseTrack) so hold-note logic can query one platform-agnostic source instead // of polling Input.GetKey directly (which cannot be driven by touch on Android). // Count-based tracking fixes multi-touch on the same lane: one finger lifting no // longer releases the lane while another finger is still holding it. private int[] trackHeldCount = new int[5]; /// /// True while the given track (0-4) is currently held, regardless of whether the /// source is keyboard or touch. Replaces direct Input.GetKey polling in hold notes. /// public bool IsTrackHeld(int trackIndex) { if (trackIndex < 0 || trackIndex >= trackHeldCount.Length) return false; return trackHeldCount[trackIndex] > 0; } private void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); // Documentation text normalized. RefreshKeyCache(); // Documentation text normalized. if (trackKeyTexts != null) { foreach (var t in trackKeyTexts) { if (t != null) t.color = keyInactiveColor; } } // Ensure GameplayInputActions exists (auto-create if missing). if (GameplayInputActions.Instance == null) { GameObject inputActionsGo = new GameObject("GameplayInputActions"); inputActionsGo.AddComponent(); DontDestroyOnLoad(inputActionsGo); } // Subscribe to Input System events from GameplayInputActions (dspTime-carrying press/release). GameplayInputActions.OnTrackPressed += OnTrackInput_Press; GameplayInputActions.OnTrackReleased += OnTrackInput_Release; } private void OnDestroy() { if (GameplayInputActions.Instance != null) { GameplayInputActions.OnTrackPressed -= OnTrackInput_Press; GameplayInputActions.OnTrackReleased -= OnTrackInput_Release; } } /// /// Handle track press from GameplayInputActions (Input System). Updates held state, /// fires both legacy KeyCode event and new dspTime event, and updates visuals. /// private void OnTrackInput_Press(int trackIndex, double pressDspTime) { if (IsBlockedByPause()) return; KeyCode sourceKey = trackIndex >= 0 && trackIndex < cachedKeys.Length ? cachedKeys[trackIndex] : KeyCode.None; PressTrack(trackIndex, sourceKey, pressDspTime); } /// /// Handle track release from GameplayInputActions (Input System). Updates held state, /// fires both legacy KeyCode event and new dspTime event, and updates visuals. /// private void OnTrackInput_Release(int trackIndex, double releaseDspTime) { if (IsBlockedByPause()) return; ReleaseTrack(trackIndex, releaseDspTime); } private void RefreshKeyCache() { for (int i = 0; i < TrackColors.Length; i++) { cachedKeys[i] = KeyBindingManager.GetKeyForColor(TrackColors[i]); } cachedSecondaryTrack3Key = KeyBindingManager.GetSecondaryKeyForTrack3(); } private void Start() { // Ensure runtime input and UI labels both use current bindings. RefreshKeyCache(); RefreshKeyLabels(); // Initialize lane sprites to 0 alpha if (laneSprites != null) { foreach (var sprite in laneSprites) { if (sprite != null) SetSpriteAlpha(sprite, 0f); } } } private void SetSpriteAlpha(SpriteRenderer sprite, float alpha255) { if (sprite == null) return; Color c = sprite.color; c.a = Mathf.Clamp01(alpha255 / 255f); sprite.color = c; } private void Update() { bool pauseBlocked = IsBlockedByPause(); if (pauseBlocked) { if (!pauseBlockedLastFrame) ForceReleaseAllKeys(); pauseBlockedLastFrame = true; return; } pauseBlockedLastFrame = false; // Legacy Input.GetKeyDown polling removed: keyboard input now comes from // GameplayInputActions (Input System) via OnTrackInput_Press/Release handlers. // Touch input still drives PressTrack/ReleaseTrack directly via TrackTouchInput. } /// /// Begin holding a track (0-4). Called by keyboard Update on key-down and by touch /// regions on pointer-down. Sets the held-state table, raises OnKeyPressed with the /// track's bound key so event-driven tap notes keep working, and updates lane visuals. /// Always fires OnKeyPressed to allow rapid same-track taps even when held. /// public void PressTrack(int index) { KeyCode sourceKey = index >= 0 && index < cachedKeys.Length ? cachedKeys[index] : KeyCode.None; // Touch/other callers without an event timestamp use the current dspTime. PressTrack(index, sourceKey, AudioSettings.dspTime); } public void PressTrack(int index, KeyCode sourceKey) { PressTrack(index, sourceKey, AudioSettings.dspTime); } public void PressTrack(int index, KeyCode sourceKey, double pressDspTime) { if (index < 0 || index >= TrackColors.Length) return; bool wasHeld = trackHeldCount[index] > 0; trackHeldCount[index]++; // Always fire tap judgment pulse, even if already held (fixes rapid same-track taps). // Fire both the legacy KeyCode event and the new dspTime event so both consumers work. KeyCode key = cachedKeys[index]; if (key != KeyCode.None) OnKeyPressed?.Invoke(key); OnTrackPressedWithDspTime?.Invoke(index, pressDspTime); if (trackKeyTexts != null && index < trackKeyTexts.Length) { var txt = trackKeyTexts[index]; if (txt != null) { if (sourceKey != KeyCode.None) txt.text = KeyBindingManager.GetDisplayName(sourceKey); txt.color = keyActiveColor; } } // Only update visuals on the first press (0→1 transition) if (wasHeld) return; if (laneSprites != null && index < laneSprites.Length) SetSpriteAlpha(laneSprites[index], pressedAlpha); if (trackKeyTexts != null && index < trackKeyTexts.Length) { var txt = trackKeyTexts[index]; if (txt != null) txt.color = keyActiveColor; } } /// /// Release a track (0-4). Called by keyboard Update on key-up and by touch regions /// on pointer-up. Clears the held-state table, raises OnKeyReleased, and resets visuals. /// public void ReleaseTrack(int index) { ReleaseTrack(index, AudioSettings.dspTime); } public void ReleaseTrack(int index, double releaseDspTime) { if (index < 0 || index >= TrackColors.Length) return; if (trackHeldCount[index] <= 0) return; // not held; nothing to release trackHeldCount[index]--; if (trackHeldCount[index] > 0) return; // another finger/key is still holding this lane KeyCode key = cachedKeys[index]; if (key != KeyCode.None) OnKeyReleased?.Invoke(key); OnTrackReleasedWithDspTime?.Invoke(index, releaseDspTime); if (laneSprites != null && index < laneSprites.Length) SetSpriteAlpha(laneSprites[index], 0f); if (trackKeyTexts != null && index < trackKeyTexts.Length) { var txt = trackKeyTexts[index]; if (txt != null) txt.color = keyInactiveColor; } } private bool IsBlockedByPause() { var pm = PauseManager.Instance; return pm != null && pm.IsPaused; } /// /// Force a release event for all bound keys and reset UI indicators. /// Useful for external "clear input" operations where the system should treat /// all keys as released regardless of physical state. /// public void ForceReleaseAllKeys() { for (int i = 0; i < TrackColors.Length; i++) { string color = TrackColors[i]; bool wasHeld = trackHeldCount[i] > 0; trackHeldCount[i] = 0; // clear held state so hold notes don't see a stale press after pause/clear KeyCode key = KeyBindingManager.GetKeyForColor(color); if (key == KeyCode.None) continue; if (wasHeld) { try { OnKeyReleased?.Invoke(key); } catch { } } int index = GetIndexForColor(color); if (laneSprites != null && index >= 0 && index < laneSprites.Length) { SetSpriteAlpha(laneSprites[index], 0f); } if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length) { var txt = trackKeyTexts[index]; if (txt != null) txt.color = keyInactiveColor; } } } // Refresh displayed labels for key bindings (called after rebind) public void RefreshKeyLabels() { if (trackKeyTexts != null) { for (int i = 0; i < TrackColors.Length && i < trackKeyTexts.Length; i++) { var txt = trackKeyTexts[i]; if (txt == null) continue; KeyCode k = KeyBindingManager.GetKeyForColor(TrackColors[i]); txt.text = KeyBindingManager.GetDisplayName(k); } } // Keep runtime input cache in sync with latest bindings even when label UI is missing. RefreshKeyCache(); } private int GetIndexForColor(string color) { // Documentation text normalized. switch (color) { case "red": return 0; case "green": return 1; case "yellow": return 2; case "purple": return 3; case "blue": return 4; default: return -1; } } /// /// Documentation text normalized. public void ShowJudgeResult(int trackIndex, string result) { if (!showJudgeText) return; if (trackJudgeTexts == null || trackIndex < 0 || trackIndex >= trackJudgeTexts.Length) return; var textObj = trackJudgeTexts[trackIndex]; if (textObj == null) return; textObj.text = result; switch (result) { case "Perfect": textObj.color = perfectColor; break; case "Great": textObj.color = greatColor; break; case "Good": textObj.color = goodColor; break; case "Miss": textObj.color = missColor; break; default: textObj.color = Color.white; break; } } }