/// <summary> /// Calculates the movement time, effective distance and other details for the movement from objPrev to objCurr. /// </summary> /// <param name="fourthLastObject">Hit object four objects ago, relative to <paramref name="currentObject"/>.</param> /// <param name="secondLastObject">Hit object immediately preceding <paramref name="lastObject"/></param> /// <param name="lastObject">Hit object immediately preceding <paramref name="currentObject"/>.</param> /// <param name="currentObject">The hit object being currently considered.</param> /// <param name="nextObject">Hit object immediately succeeding <paramref name="currentObject"/>.</param> /// <param name="tapStrain">The tap strain of the current object.</param> TODO: does this have to be passed down? maybe store in the object? /// <param name="noteDensity">The visual note density of the current object.</param> TODO: above /// <param name="gameplayRate">The current rate of the gameplay clock.</param> /// <param name="hidden">Whether the hidden mod is active.</param> /// <returns>List of movements performed in attempt to hit the current object.</returns> public static List <OsuMovement> Extract( [CanBeNull] OsuHitObject secondLastObject, OsuHitObject lastObject, OsuHitObject currentObject, [CanBeNull] OsuHitObject nextObject, double tapStrain, double gameplayRate, bool hidden, double noteDensity, [CanBeNull] OsuHitObject fourthLastObject = null) { var movement = new OsuMovement(); var parameters = new MovementExtractionParameters(fourthLastObject, secondLastObject, lastObject, currentObject, nextObject, gameplayRate); movement.RawMovementTime = parameters.LastToCurrent.TimeDelta; movement.StartTime = currentObject.StartTime / 1000.0; if (currentObject is Spinner || lastObject is Spinner) { movement.Throughput = 0; movement.Distance = 0; movement.MovementTime = 1; movement.Cheesablility = 0; movement.CheeseWindow = 0; return(new List <OsuMovement> { movement }); } movement.EndsOnSlider = currentObject is Slider; double movementThroughput = FittsLaw.Throughput(parameters.LastToCurrent.RelativeLength, parameters.LastToCurrent.TimeDelta); movement.Throughput = movementThroughput; movement.Distance = correctMovementDistance(parameters, movementThroughput, tapStrain, hidden, noteDensity); calculateCheeseWindow(parameters, movementThroughput); movement.MovementTime = parameters.LastToCurrent.TimeDelta; movement.Cheesablility = parameters.Cheesability; movement.CheeseWindow = parameters.CheeseWindow; var movementWithNested = new List <OsuMovement> { movement }; // add zero difficulty movements corresponding to slider ticks/slider ends so combo is reflected properly int extraNestedCount = currentObject.NestedHitObjects.Count - 1; for (int i = 0; i < extraNestedCount; i++) { movementWithNested.Add(OsuMovement.Empty(movement.StartTime)); } return(movementWithNested); }
/// <summary> /// Correction #5 - Cheesing /// The player might make the movement from previous object to current easier by hitting former early and latter late. /// Here we estimate the amount of such cheesing to update MovementTime accordingly. /// </summary> private static void calculateCheeseWindow(MovementExtractionParameters p, double movementThroughput) { double timeEarly = 0; double timeLate = 0; double cheesabilityEarly = 0; double cheesabilityLate = 0; if (p.LastToCurrent.RelativeLength > 0) { double secondLastToLastReciprocalMovementLength; double secondLastToLastThroughput; if (p.SecondLastObject != null) { Debug.Assert(p.SecondLastToLast != null); secondLastToLastReciprocalMovementLength = 1 / (p.SecondLastToLast.Value.TimeDelta + 1e-10); secondLastToLastThroughput = FittsLaw.Throughput(p.SecondLastToLast.Value.RelativeLength, p.SecondLastToLast.Value.TimeDelta); } else { secondLastToLastReciprocalMovementLength = 0; secondLastToLastThroughput = 0; } cheesabilityEarly = SpecialFunctions.Logistic((secondLastToLastThroughput / movementThroughput - 0.6) * (-15)) * 0.5; timeEarly = cheesabilityEarly * (1 / (1 / (p.LastToCurrent.TimeDelta + 0.07) + secondLastToLastReciprocalMovementLength)); double currentToNextReciprocalMovementLength; double currentToNextThroughput; if (p.NextObject != null) { Debug.Assert(p.CurrentToNext != null); currentToNextReciprocalMovementLength = 1 / (p.CurrentToNext.Value.TimeDelta + 1e-10); currentToNextThroughput = FittsLaw.Throughput(p.CurrentToNext.Value.RelativeLength, p.CurrentToNext.Value.TimeDelta); } else { currentToNextReciprocalMovementLength = 0; currentToNextThroughput = 0; } cheesabilityLate = SpecialFunctions.Logistic((currentToNextThroughput / movementThroughput - 0.6) * (-15)) * 0.5; timeLate = cheesabilityLate * (1 / (1 / (p.LastToCurrent.TimeDelta + 0.07) + currentToNextReciprocalMovementLength)); } p.Cheesability = cheesabilityEarly + cheesabilityLate; p.CheeseWindow = (timeEarly + timeLate) / (p.LastToCurrent.TimeDelta + 1e-10); }