Files
Aether-Engine/Assets/Scripts/Input/RowingInputManager.cs
2026-02-20 17:53:43 +01:00

234 lines
8.3 KiB
C#

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<RowingSessionData> 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<float> OnVelocityChanged;
public Action<float, bool, float> OnStrokeCompleted;
public Action<int> 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<float> fakeCurve = new List<float> { 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<float>());
}
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<float>());
}
_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<float> 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<float> curve) { return 1f; /* Replace with your actual Impulse math */ }
private bool AnalyzeSmoothness(List<float> curve) { return true; /* Replace with your actual Smoothness math */ }
}