using System; using System.Collections.Generic; using UnityEngine; public class RowingInputManager : MonoBehaviour { public static RowingInputManager Instance; public enum InputSource { LivePM5, Recorded, Simulated } [Header("Data Source")] public InputSource currentInputSource = InputSource.LivePM5; [Header("Recorded Mode Controls")] public List recordedSessions; public int currentSessionIndex = 0; [Range(0.1f, 5f)] public float playbackSpeed = 1f; public bool loopSimulation = true; [Header("Simulated Mode (Button Mashing)")] public KeyCode simulateStrokeKey = KeyCode.Space; public float simulateWattJump = 50f; // Lowered slightly so you have to work for it public float simulateWattDecayRate = 85f; // Increased so the "flywheel" slows down faster public float simulateMaxWatts = 300f; // The new hard cap! private float _currentSimulatedWatts = 0f; private float _lastSimStrokeTime = 0f; [Header("Game Physics Settings")] public float maxGameSpeed = 50f; public float wattsToSpeedMultiplier = 0.1f; public float speedSmoothTime = 0.75f; public int userMaxHR = 190; [Header("Live Output (Read Only)")] public float CurrentSmoothedVelocity; public float LastStrokeImpulse; public bool IsLastStrokeSmooth; public int MaxPowerOutput; public int CurrentHRZone; public float LastStrokeRatio; public Action OnVelocityChanged; public Action OnStrokeCompleted; public Action OnZoneChanged; private float _targetVelocity; private float _velocityVelocity; // Recorded State private float _playbackTimer = 0f; private int _playbackFrameIndex = 0; // Stroke Tracking State Machine private enum StrokeState { Idle, Drive, Recovery } private StrokeState _currentState = StrokeState.Idle; private float _driveTimer = 0f; private float _recoveryTimer = 0f; private void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); } private void Start() { if (PerformanceMonitorManager.Instance != null) { PerformanceMonitorManager.Instance.OnStatsUpdated += (stats) => { if (currentInputSource == InputSource.LivePM5) ProcessStats(stats.Watts, stats.SPM, stats.HeartRate); }; PerformanceMonitorManager.Instance.OnForceCurveUpdated += (curve) => { if (currentInputSource == InputSource.LivePM5) ProcessForceCurve(curve); }; } } private void Update() { HandleKeyboardControls(); if (currentInputSource == InputSource.Recorded) { RunRecordedSession(); } else if (currentInputSource == InputSource.Simulated) { RunManualSimulation(); } // Apply Smoothing CurrentSmoothedVelocity = Mathf.SmoothDamp(CurrentSmoothedVelocity, _targetVelocity, ref _velocityVelocity, speedSmoothTime); OnVelocityChanged?.Invoke(CurrentSmoothedVelocity); // Timers if (_currentState == StrokeState.Drive) _driveTimer += Time.deltaTime; else if (_currentState == StrokeState.Recovery) _recoveryTimer += Time.deltaTime; } private void RunManualSimulation() { // 1. Decay the watts over time (Simulating the flywheel slowing down) _currentSimulatedWatts = Mathf.Max(0f, _currentSimulatedWatts - (simulateWattDecayRate * Time.deltaTime)); // 2. Handle the "Pull" if (Input.GetKeyDown(simulateStrokeKey)) { // Add power, but firmly cap it so we don't break the sound barrier _currentSimulatedWatts = Mathf.Min(_currentSimulatedWatts + simulateWattJump, simulateMaxWatts); // Calculate a fake SPM based on how fast you are mashing float timeSinceLast = Time.time - _lastSimStrokeTime; int fakeSPM = timeSinceLast > 0 ? Mathf.RoundToInt(60f / timeSinceLast) : 0; _lastSimStrokeTime = Time.time; // Fake HR that scales up gently with your power output int fakeHR = Mathf.Clamp(100 + Mathf.RoundToInt(_currentSimulatedWatts / 4f), 70, 200); ProcessStats(Mathf.RoundToInt(_currentSimulatedWatts), fakeSPM, fakeHR); // Generate a fake, perfectly smooth force curve (a bell shape) List fakeCurve = new List { 20f, 60f, 120f, 150f, 120f, 60f, 20f }; ProcessForceCurve(fakeCurve); // Immediately send an empty list to signify the drive ended and trigger recovery ProcessForceCurve(new List()); } else { // If not pulling, just update the decaying watts ProcessStats(Mathf.RoundToInt(_currentSimulatedWatts), 0, 100); } } private void RunRecordedSession() { if (recordedSessions == null || recordedSessions.Count == 0 || recordedSessions[currentSessionIndex] == null) return; var session = recordedSessions[currentSessionIndex]; _playbackTimer += Time.deltaTime * playbackSpeed; while (_playbackFrameIndex < session.frames.Count && _playbackTimer >= session.frames[_playbackFrameIndex].timestamp) { RowingFrame frame = session.frames[_playbackFrameIndex]; ProcessStats(frame.watts, frame.spm, frame.heartRate); if (frame.forceCurve != null && frame.forceCurve.Count > 0) { ProcessForceCurve(frame.forceCurve); } else if (frame.watts == 0 && frame.spm == 0) { ProcessForceCurve(new List()); } _playbackFrameIndex++; } if (_playbackFrameIndex >= session.frames.Count) { if (loopSimulation) { _playbackTimer = 0f; _playbackFrameIndex = 0; } } } private void HandleKeyboardControls() { if (currentInputSource == InputSource.Recorded) { if (Input.GetKeyDown(KeyCode.UpArrow)) playbackSpeed += 0.25f; if (Input.GetKeyDown(KeyCode.DownArrow)) playbackSpeed = Mathf.Max(0.25f, playbackSpeed - 0.25f); for (int i = 0; i < recordedSessions.Count; i++) { if (Input.GetKeyDown(KeyCode.Alpha1 + i)) { currentSessionIndex = i; _playbackTimer = 0f; _playbackFrameIndex = 0; Debug.Log($"Switched to Session: {recordedSessions[i].sessionName}"); } } } } // --- Core Logic (Shared by Live, Recorded, and Simulated) --- private void ProcessStats(int watts, int spm, int hr) { _targetVelocity = Mathf.Clamp(watts * wattsToSpeedMultiplier, 0, maxGameSpeed); if (watts > MaxPowerOutput) MaxPowerOutput = watts; int newZone = CalculateHRZone(hr); if (newZone != CurrentHRZone) { CurrentHRZone = newZone; OnZoneChanged?.Invoke(CurrentHRZone); } } private void ProcessForceCurve(List points) { if (points == null || points.Count == 0) { if (_currentState == StrokeState.Drive) { _currentState = StrokeState.Recovery; } return; } if (_currentState == StrokeState.Recovery || _currentState == StrokeState.Idle) { if (_recoveryTimer > 0) { LastStrokeRatio = _driveTimer / _recoveryTimer; LastStrokeImpulse = CalculateImpulse(points); IsLastStrokeSmooth = AnalyzeSmoothness(points); OnStrokeCompleted?.Invoke(LastStrokeImpulse, IsLastStrokeSmooth, LastStrokeRatio); } _driveTimer = 0f; _recoveryTimer = 0f; _currentState = StrokeState.Drive; } } private int CalculateHRZone(int currentHR) { return 1; /* Replace with your actual HR math from previous step */ } private float CalculateImpulse(List curve) { return 1f; /* Replace with your actual Impulse math */ } private bool AnalyzeSmoothness(List curve) { return true; /* Replace with your actual Smoothness math */ } }