// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Diagnostics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { /// /// Stores the spinning history of a single spinner.
/// Instants of movement deltas may be added or removed from this in order to calculate the total rotation for the spinner. ///
/// /// A single, full rotation of the spinner is defined as a 360-degree rotation of the spinner, starting from 0, going in a single direction.
///
/// /// If the player spins 90-degrees clockwise, then changes direction, they need to spin 90-degrees counter-clockwise to return to 0 /// and then continue rotating the spinner for another 360-degrees in the same direction. /// public class SpinnerSpinHistory { /// /// The sum of all complete spins and any current partial spin, in degrees. /// /// /// This is the final scoring value. /// public float TotalRotation => 360 * completedSpins.Count + currentSpinMaxRotation; private readonly Stack completedSpins = new Stack(); /// /// The total accumulated (absolute) rotation. /// private float totalAccumulatedRotation; private float totalAccumulatedRotationAtLastCompletion; /// /// For the current spin, represents the maximum absolute rotation (from 0..360) achieved by the user. /// /// /// This is used to report in the case a user spins backwards. /// Basically it allows us to not reduce the total rotation in such a case. /// /// This also stops spinner "cheese" where a user may rapidly change directions and cause an increase /// in rotations. /// private float currentSpinMaxRotation; /// /// The current spin, from -360..360. /// private float currentSpinRotation => totalAccumulatedRotation - totalAccumulatedRotationAtLastCompletion; private double lastReportTime = double.NegativeInfinity; /// /// Report a delta update based on user input. /// /// The current time. /// The delta of the angle moved through since the last report. public void ReportDelta(double currentTime, float delta) { if (delta == 0) return; // Importantly, outside of tests the max delta entering here is 180 degrees. // If it wasn't for tests, we could add this line: // // Debug.Assert(Math.Abs(delta) < 180); // // For this to be 101% correct, we need to add the ability for important frames to be // created based on gameplay intrinsics (ie. there should be one frame for any spinner delta 90 < n < 180 degrees). // // But this can come later. totalAccumulatedRotation += delta; if (currentTime >= lastReportTime) { currentSpinMaxRotation = Math.Max(currentSpinMaxRotation, Math.Abs(currentSpinRotation)); // Handle the case where the user has completed another spin. // Note that this does could be an `if` rather than `while` if the above assertion held true. // It is a `while` loop to handle tests which throw larger values at this method. while (currentSpinMaxRotation >= 360) { int direction = Math.Sign(currentSpinRotation); completedSpins.Push(new CompletedSpin(currentTime, direction)); // Incrementing the last completion point will cause `currentSpinRotation` to // hold the remaining spin that needs to be considered. totalAccumulatedRotationAtLastCompletion += direction * 360; // Reset the current max as we are entering a new spin. // Importantly, carry over the remainder (which is now stored in `currentSpinRotation`). currentSpinMaxRotation = Math.Abs(currentSpinRotation); } } else { // When rewinding, the main thing we care about is getting `totalAbsoluteRotationsAtLastCompletion` // to the correct value. We can used the stored history for this. while (completedSpins.TryPeek(out var segment) && segment.CompletionTime > currentTime) { completedSpins.Pop(); totalAccumulatedRotationAtLastCompletion -= segment.Direction * 360; } // This is a best effort. We may not have enough data to match this 1:1, but that's okay. // We know that the player is somewhere in a spin. // In the worst case, this will be lower than expected, and recover in forward playback. currentSpinMaxRotation = Math.Abs(currentSpinRotation); } lastReportTime = currentTime; } /// /// Represents a single completed spin. /// private class CompletedSpin { /// /// The time at which this spin completion occurred. /// public readonly double CompletionTime; /// /// The direction this spin completed in. /// public readonly int Direction; public CompletedSpin(double completionTime, int direction) { Debug.Assert(direction == -1 || direction == 1); CompletionTime = completionTime; Direction = direction; } } } }