/// <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); }
private static string generateGraphText(List <OsuMovement> movements, double tp) { var sw = new StringWriter(); foreach (var movement in movements) { double time = movement.Time; double ipRaw = movement.IP12; double ipCorrected = FittsLaw.CalculateIP(movement.D, movement.MT * (1 + defaultCheeseLevel * movement.CheesableRatio)); double missProb = 1 - calculateCheeseHitProb(movement, tp, defaultCheeseLevel); sw.WriteLine($"{time} {ipRaw} {ipCorrected} {missProb}"); } string graphText = sw.ToString(); sw.Dispose(); return(graphText); }
public static List <OsuMovement> ExtractMovement(OsuHitObject obj0, OsuHitObject obj1, OsuHitObject obj2, OsuHitObject obj3, Vector <double> tapStrain, double clockRate) { var movement = new OsuMovement(); double t12 = (obj2.StartTime - obj1.StartTime) / clockRate / 1000.0; movement.RawMT = t12; movement.Time = obj2.StartTime / 1000.0; if (obj2 is Spinner || obj1 is Spinner) { movement.IP12 = 0; movement.D = 0; movement.MT = 1; movement.Cheesablility = 0; movement.CheesableRatio = 0; return(new List <OsuMovement>() { movement }); } if (obj0 is Spinner) { obj0 = null; } if (obj3 is Spinner) { obj3 = null; } var pos1 = Vector <double> .Build.Dense(new[] { (double)obj1.Position.X, (double)obj1.Position.Y }); var pos2 = Vector <double> .Build.Dense(new[] { (double)obj2.Position.X, (double)obj2.Position.Y }); var s12 = (pos2 - pos1) / (2 * obj2.Radius); double d12 = s12.L2Norm(); double IP12 = FittsLaw.CalculateIP(d12, t12); movement.IP12 = IP12; var s01 = Vector <double> .Build.Dense(2); var s23 = Vector <double> .Build.Dense(2); double d01 = 0; double d23 = 0; double t01 = 0; double t23 = 0; double flowiness012 = 0; double flowiness123 = 0; bool obj1InTheMiddle = false; bool obj2InTheMiddle = false; // Correction #1 - The Previous Object // Estimate how obj0 affects the difficulty of hitting obj2 double correction0 = 0; if (obj0 != null) { var pos0 = Vector <double> .Build.Dense(new[] { (double)obj0.Position.X, (double)obj0.Position.Y }); s01 = (pos1 - pos0) / (2 * obj2.Radius); d01 = s01.L2Norm(); t01 = (obj1.StartTime - obj0.StartTime) / clockRate / 1000.0; if (d12 != 0) { double tRatio0 = t12 / t01; if (tRatio0 > tRatioThreshold) { if (d01 == 0) { correction0 = correction0Still; } else { double cos012 = Math.Min(Math.Max(-s01.DotProduct(s12) / d01 / d12, -1), 1); double correction0_moving = correction0MovingSpline.Interpolate(cos012); double movingness = SpecialFunctions.Logistic(d01 * 2) * 2 - 1; correction0 = (movingness * correction0_moving + (1 - movingness) * correction0Still) * 1.5; } } else if (tRatio0 < 1 / tRatioThreshold) { if (d01 == 0) { correction0 = 0; } else { double cos012 = Math.Min(Math.Max(-s01.DotProduct(s12) / d01 / d12, -1), 1); correction0 = (1 - cos012) * SpecialFunctions.Logistic((d01 * tRatio0 - 1.5) * 4) * 0.3; } } else { obj1InTheMiddle = true; var normalized_pos0 = -s01 / t01 * t12; double x0 = normalized_pos0.DotProduct(s12) / d12; double y0 = (normalized_pos0 - x0 * s12 / d12).L2Norm(); double correction0Flow = calcCorrection0Or3(d12, x0, y0, k0fInterp, scale0fInterp, coeffs0fInterps); double correction0Snap = calcCorrection0Or3(d12, x0, y0, k0sInterp, scale0sInterp, coeffs0sInterps); double correction0Stop = calcCorrection0Stop(d12, x0, y0); flowiness012 = SpecialFunctions.Logistic((correction0Snap - correction0Flow - 0.05) * 20); correction0 = Mean.PowerMean(new double[] { correction0Flow, correction0Snap, correction0Stop }, -10) * 1.3; //Console.Write(obj2.StartTime + " "); //Console.Write(correction0Flow.ToString("N3") + " "); //Console.Write(correction0Snap.ToString("N3") + " "); //Console.Write(correction0Stop.ToString("N3") + " "); //Console.Write(correction0.ToString("N3") + " "); //Console.WriteLine(); } } } // Correction #2 - The Next Object // Estimate how obj3 affects the difficulty of hitting obj2 double correction3 = 0; if (obj3 != null) { var pos3 = Vector <double> .Build.Dense(new[] { (double)obj3.Position.X, (double)obj3.Position.Y }); s23 = (pos3 - pos2) / (2 * obj2.Radius); d23 = s23.L2Norm(); t23 = (obj3.StartTime - obj2.StartTime) / clockRate / 1000.0; if (d12 != 0) { double tRatio3 = t12 / t23; if (tRatio3 > tRatioThreshold) { if (d23 == 0) { correction3 = 0; } else { double cos123 = Math.Min(Math.Max(-s12.DotProduct(s23) / d12 / d23, -1), 1); double correction3_moving = correction0MovingSpline.Interpolate(cos123); double movingness = SpecialFunctions.Logistic(d23 * 6 - 5) - SpecialFunctions.Logistic(-5); correction3 = (movingness * correction3_moving) * 0.5; } } else if (tRatio3 < 1 / tRatioThreshold) { if (d23 == 0) { correction3 = 0; } else { double cos123 = Math.Min(Math.Max(-s12.DotProduct(s23) / d12 / d23, -1), 1); correction3 = (1 - cos123) * SpecialFunctions.Logistic((d23 * tRatio3 - 1.5) * 4) * 0.15; } } else { obj2InTheMiddle = true; var normalizedPos3 = s23 / t23 * t12; double x3 = normalizedPos3.DotProduct(s12) / d12; double y3 = (normalizedPos3 - x3 * s12 / d12).L2Norm(); double correction3Flow = calcCorrection0Or3(d12, x3, y3, k3fInterp, scale3fInterp, coeffs3fInterps); double correction3Snap = calcCorrection0Or3(d12, x3, y3, k3sInterp, scale3sInterp, coeffs3sInterps); flowiness123 = SpecialFunctions.Logistic((correction3Snap - correction3Flow - 0.05) * 20); correction3 = Math.Max(Mean.PowerMean(correction3Flow, correction3Snap, -10) - 0.1, 0) * 0.5; } } } // Correction #3 - 4-object pattern // Estimate how the whole pattern consisting of obj0 to obj3 affects // the difficulty of hitting obj2. This only takes effect when the pattern // is not so spaced (i.e. does not contain jumps) double patternCorrection = 0; if (obj1InTheMiddle && obj2InTheMiddle) { double gap = (s12 - s23 / 2 - s01 / 2).L2Norm() / (d12 + 0.1); patternCorrection = (SpecialFunctions.Logistic((gap - 0.75) * 8) - SpecialFunctions.Logistic(-6)) * SpecialFunctions.Logistic((d01 - 0.7) * 10) * SpecialFunctions.Logistic((d23 - 0.7) * 10) * Mean.PowerMean(flowiness012, flowiness123, 2) * 0.6; //patternCorrection = 0; } // Correction #4 - Tap Strain // Estimate how tap strain affects difficulty double tapCorrection = 0; if (d12 > 0 && tapStrain != null) { tapCorrection = SpecialFunctions.Logistic((Mean.PowerMean(tapStrain, 2) / IP12 - 1.34) / 0.1) * 0.25; } // Correction #5 - Cheesing // The player might make the movement of obj1 -> obj2 easier by // hitting obj1 early and obj2 late. Here we estimate the amount of // cheesing and update MT accordingly. double timeEarly = 0; double timeLate = 0; double cheesabilityEarly = 0; double cheesabilityLate = 0; if (d12 > 0) { double t01Reciprocal; double ip01; if (obj0 != null) { t01Reciprocal = 1 / (t01 + 1e-10); ip01 = FittsLaw.CalculateIP(d01, t01); } else { t01Reciprocal = 0; ip01 = 0; } cheesabilityEarly = SpecialFunctions.Logistic((ip01 / IP12 - 0.6) * (-15)) * 0.5; timeEarly = cheesabilityEarly * (1 / (1 / (t12 + 0.07) + t01Reciprocal)); double t23Reciprocal; double ip23; if (obj3 != null) { t23Reciprocal = 1 / (t23 + 1e-10); ip23 = FittsLaw.CalculateIP(d23, t23); } else { t23Reciprocal = 0; ip23 = 0; } cheesabilityLate = SpecialFunctions.Logistic((ip23 / IP12 - 0.6) * (-15)) * 0.5; timeLate = cheesabilityLate * (1 / (1 / (t12 + 0.07) + t23Reciprocal)); } // Correction #6 - Small circle bonus double smallCircleBonus = SpecialFunctions.Logistic((55 - 2 * obj2.Radius) / 3.0) * 0.3; // Correction #7 - Stacked notes nerf double stackedThreshold = 0.8; double d12StackedNerf; if (d12 < stackedThreshold) { d12StackedNerf = Math.Max(1.4 * (d12 - stackedThreshold) + stackedThreshold, 0); } else { d12StackedNerf = d12; } // Apply the corrections double d12WithCorrection = d12StackedNerf * (1 + smallCircleBonus) * (1 + correction0 + correction3 + patternCorrection) * (1 + tapCorrection); movement.D = d12WithCorrection; movement.MT = t12; movement.Cheesablility = cheesabilityEarly + cheesabilityLate; movement.CheesableRatio = (timeEarly + timeLate) / (t12 + 1e-10); var movementWithNested = new List <OsuMovement>() { movement }; // add zero difficulty movements corresponding to slider ticks/slider ends so combo is reflected properly int extraNestedCount = obj2.NestedHitObjects.Count - 1; for (int i = 0; i < extraNestedCount; i++) { movementWithNested.Add(GetEmptyMovement(movement.Time)); } return(movementWithNested); }
private static double calculateCheeseHitProb(OsuMovement movement, double tp, double cheeseLevel) { double cheeseMT = movement.MT * (1 + cheeseLevel * movement.CheesableRatio); return(FittsLaw.CalculateHitProb(movement.D, cheeseMT, tp)); }
/// <summary> /// Calculates the movement time, effective distance and other details for the movement from objPrev to objCurr. /// </summary> /// <param name="objNeg4">Object that that was three objects before current</param> /// <param name="objNeg2">Prevprev object</param> /// <param name="objPrev">Previous object</param> /// <param name="objCurr">Current object</param> /// <param name="objNext">Next object</param> /// <param name="tapStrain">Current object tap strain</param> /// <param name="noteDensity">Current object visual note density</param> /// <param name="clockRate">Clock rate</param> /// <param name="hidden">Are we calculating hidden mod?</param> /// <returns>List of movements related to current object</returns> public static List <OsuMovement> ExtractMovement(OsuHitObject objNeg2, OsuHitObject objPrev, OsuHitObject objCurr, OsuHitObject objNext, Vector <double> tapStrain, double clockRate, bool hidden = false, double noteDensity = 0, OsuHitObject objNeg4 = null) { var movement = new OsuMovement(); double tPrevCurr = (objCurr.StartTime - objPrev.StartTime) / clockRate / 1000.0; movement.RawMovementTime = tPrevCurr; movement.Time = objCurr.StartTime / 1000.0; if (objCurr is Spinner || objPrev is Spinner) { movement.IndexOfPerformance = 0; movement.Distance = 0; movement.MovementTime = 1; movement.Cheesablility = 0; movement.CheesableRatio = 0; return(new List <OsuMovement> { movement }); } if (objNeg2 is Spinner) { objNeg2 = null; } if (objNext is Spinner) { objNext = null; } if (objCurr is Slider) { movement.EndsOnSlider = true; } // calculate basic info (position, displacement, distance...) // explanation of abbreviations: // posx: position of obj x // sxy : displacement (normalized) from obj x to obj y // txy : time difference of obj x and obj y // dxy : distance (normalized) from obj x to obj y // ipxy: index of performance of the movement from obj x to obj y var posPrev = Vector <double> .Build.Dense(new[] { objPrev.StackedPosition.X, (double)objPrev.StackedPosition.Y }); var posCurr = Vector <double> .Build.Dense(new[] { objCurr.StackedPosition.X, (double)objCurr.StackedPosition.Y }); var sPrevCurr = (posCurr - posPrev) / (2 * objCurr.Radius); double dPrevCurr = sPrevCurr.L2Norm(); double ipPrevCurr = FittsLaw.CalculateIp(dPrevCurr, tPrevCurr); movement.IndexOfPerformance = ipPrevCurr; var posNeg2 = Vector <double> .Build.Dense(2); var posNext = Vector <double> .Build.Dense(2); var sNeg2Prev = Vector <double> .Build.Dense(2); var sCurrNext = Vector <double> .Build.Dense(2); double dNeg2Prev = 0; double dNeg2Curr = 0; double dCurrNext = 0; double tNeg2Prev = 0; double tCurrNext = 0; double flowinessNeg2PrevCurr = 0; double flowinessPrevCurrNext = 0; bool objPrevTemporallyInTheMiddle = false; bool objCurrTemporallyInTheMiddle = false; double dNeg4Curr = 0; if (objNeg4 != null) { var posNeg4 = Vector <double> .Build.Dense(new[] { objNeg4.StackedPosition.X, (double)objNeg4.StackedPosition.Y }); dNeg4Curr = ((posCurr - posNeg4) / (2 * objCurr.Radius)).L2Norm(); } if (objNeg2 != null) { posNeg2 = Vector <double> .Build.Dense(new[] { objNeg2.StackedPosition.X, (double)objNeg2.StackedPosition.Y }); sNeg2Prev = (posPrev - posNeg2) / (2 * objCurr.Radius); dNeg2Prev = sNeg2Prev.L2Norm(); tNeg2Prev = (objPrev.StartTime - objNeg2.StartTime) / clockRate / 1000.0; dNeg2Curr = ((posCurr - posNeg2) / (2 * objCurr.Radius)).L2Norm(); } if (objNext != null) { posNext = Vector <double> .Build.Dense(new[] { objNext.StackedPosition.X, (double)objNext.StackedPosition.Y }); sCurrNext = (posNext - posCurr) / (2 * objCurr.Radius); dCurrNext = sCurrNext.L2Norm(); tCurrNext = (objNext.StartTime - objCurr.StartTime) / clockRate / 1000.0; } // Correction #1 - The Previous Object // Estimate how objNeg2 affects the difficulty of hitting objCurr double correctionNeg2 = 0; if (objNeg2 != null && dPrevCurr != 0) { double tRatioNeg2 = tPrevCurr / tNeg2Prev; double cosNeg2PrevCurr = Math.Min(Math.Max(-sNeg2Prev.DotProduct(sPrevCurr) / dNeg2Prev / dPrevCurr, -1), 1); if (tRatioNeg2 > t_ratio_threshold) { if (dNeg2Prev == 0) { correctionNeg2 = correction_neg2_still; } else { double correctionNeg2Moving = correction_neg2_moving_spline.Interpolate(cosNeg2PrevCurr); double movingness = SpecialFunctions.Logistic(dNeg2Prev * 6 - 5) - SpecialFunctions.Logistic(-5); correctionNeg2 = (movingness * correctionNeg2Moving + (1 - movingness) * correction_neg2_still) * 1.5; } } else if (tRatioNeg2 < 1 / t_ratio_threshold) { if (dNeg2Prev == 0) { correctionNeg2 = 0; } else { correctionNeg2 = (1 - cosNeg2PrevCurr) * SpecialFunctions.Logistic((dNeg2Prev * tRatioNeg2 - 1.5) * 4) * 0.3; } } else { objPrevTemporallyInTheMiddle = true; var normalizedPosNeg2 = -sNeg2Prev / tNeg2Prev * tPrevCurr; double xNeg2 = normalizedPosNeg2.DotProduct(sPrevCurr) / dPrevCurr; double yNeg2 = (normalizedPosNeg2 - xNeg2 * sPrevCurr / dPrevCurr).L2Norm(); double correctionNeg2Flow = AngleCorrection.FLOW_NEG2.Evaluate(dPrevCurr, xNeg2, yNeg2); double correctionNeg2Snap = AngleCorrection.SNAP_NEG2.Evaluate(dPrevCurr, xNeg2, yNeg2); double correctionNeg2Stop = calcCorrection0Stop(dPrevCurr, xNeg2, yNeg2); flowinessNeg2PrevCurr = SpecialFunctions.Logistic((correctionNeg2Snap - correctionNeg2Flow - 0.05) * 20); correctionNeg2 = Mean.PowerMean(new[] { correctionNeg2Flow, correctionNeg2Snap, correctionNeg2Stop }, -10) * 1.3; } } // Correction #2 - The Next Object // Estimate how objNext affects the difficulty of hitting objCurr double correctionNext = 0; if (objNext != null && dPrevCurr != 0) { double tRatioNext = tPrevCurr / tCurrNext; double cosPrevCurrNext = Math.Min(Math.Max(-sPrevCurr.DotProduct(sCurrNext) / dPrevCurr / dCurrNext, -1), 1); if (tRatioNext > t_ratio_threshold) { if (dCurrNext == 0) { correctionNext = 0; } else { double correctionNextMoving = correction_neg2_moving_spline.Interpolate(cosPrevCurrNext); double movingness = SpecialFunctions.Logistic(dCurrNext * 6 - 5) - SpecialFunctions.Logistic(-5); correctionNext = movingness * correctionNextMoving * 0.5; } } else if (tRatioNext < 1 / t_ratio_threshold) { if (dCurrNext == 0) { correctionNext = 0; } else { correctionNext = (1 - cosPrevCurrNext) * SpecialFunctions.Logistic((dCurrNext * tRatioNext - 1.5) * 4) * 0.15; } } else { objCurrTemporallyInTheMiddle = true; var normalizedPosNext = sCurrNext / tCurrNext * tPrevCurr; double xNext = normalizedPosNext.DotProduct(sPrevCurr) / dPrevCurr; double yNext = (normalizedPosNext - xNext * sPrevCurr / dPrevCurr).L2Norm(); double correctionNextFlow = AngleCorrection.FLOW_NEXT.Evaluate(dPrevCurr, xNext, yNext); double correctionNextSnap = AngleCorrection.SNAP_NEXT.Evaluate(dPrevCurr, xNext, yNext); flowinessPrevCurrNext = SpecialFunctions.Logistic((correctionNextSnap - correctionNextFlow - 0.05) * 20); correctionNext = Math.Max(Mean.PowerMean(correctionNextFlow, correctionNextSnap, -10) - 0.1, 0) * 0.5; } } // Correction #3 - 4-object pattern // Estimate how the whole pattern consisting of objNeg2 to objNext affects // the difficulty of hitting objCurr. This only takes effect when the pattern // is not so spaced (i.e. does not contain jumps) double patternCorrection = 0; if (objPrevTemporallyInTheMiddle && objCurrTemporallyInTheMiddle) { double gap = (sPrevCurr - sCurrNext / 2 - sNeg2Prev / 2).L2Norm() / (dPrevCurr + 0.1); patternCorrection = (SpecialFunctions.Logistic((gap - 1) * 8) - SpecialFunctions.Logistic(-6)) * SpecialFunctions.Logistic((dNeg2Prev - 0.7) * 10) * SpecialFunctions.Logistic((dCurrNext - 0.7) * 10) * Mean.PowerMean(flowinessNeg2PrevCurr, flowinessPrevCurrNext, 2) * 0.6; } // Correction #4 - Tap Strain // Estimate how tap strain affects difficulty double tapCorrection = 0; if (dPrevCurr > 0 && tapStrain != null) { tapCorrection = SpecialFunctions.Logistic((Mean.PowerMean(tapStrain, 2) / ipPrevCurr - 1.34) / 0.1) * 0.15; } // Correction #5 - Cheesing // The player might make the movement of objPrev -> objCurr easier by // hitting objPrev early and objCurr late. Here we estimate the amount of // cheesing and update MT accordingly. double timeEarly = 0; double timeLate = 0; double cheesabilityEarly = 0; double cheesabilityLate = 0; if (dPrevCurr > 0) { double tNeg2PrevReciprocal; double ipNeg2Prev; if (objNeg2 != null) { tNeg2PrevReciprocal = 1 / (tNeg2Prev + 1e-10); ipNeg2Prev = FittsLaw.CalculateIp(dNeg2Prev, tNeg2Prev); } else { tNeg2PrevReciprocal = 0; ipNeg2Prev = 0; } cheesabilityEarly = SpecialFunctions.Logistic((ipNeg2Prev / ipPrevCurr - 0.6) * (-15)) * 0.5; timeEarly = cheesabilityEarly * (1 / (1 / (tPrevCurr + 0.07) + tNeg2PrevReciprocal)); double tCurrNextReciprocal; double ipCurrNext; if (objNext != null) { tCurrNextReciprocal = 1 / (tCurrNext + 1e-10); ipCurrNext = FittsLaw.CalculateIp(dCurrNext, tCurrNext); } else { tCurrNextReciprocal = 0; ipCurrNext = 0; } cheesabilityLate = SpecialFunctions.Logistic((ipCurrNext / ipPrevCurr - 0.6) * (-15)) * 0.5; timeLate = cheesabilityLate * (1 / (1 / (tPrevCurr + 0.07) + tCurrNextReciprocal)); } // Correction #6 - High bpm jump buff (alt buff) double effectiveBpm = 30 / (tPrevCurr + 1e-10); double highBpmJumpBuff = SpecialFunctions.Logistic((effectiveBpm - 354) / 16) * SpecialFunctions.Logistic((dPrevCurr - 1.9) / 0.15) * 0.23; // Correction #7 - Small circle bonus double smallCircleBonus = ((SpecialFunctions.Logistic((55 - 2 * objCurr.Radius) / 3.0) * 0.3) + (Math.Pow(24.5 - Math.Min(objCurr.Radius, 24.5), 1.4) * 0.01315)) * Math.Max(SpecialFunctions.Logistic((dPrevCurr - 0.5) / 0.1), 0.25); // Correction #8 - Stacked notes nerf double dPrevCurrStackedNerf = Math.Max(0, Math.Min(dPrevCurr, Math.Min(1.2 * dPrevCurr - 0.185, 1.4 * dPrevCurr - 0.32))); // Correction #9 - Slow small jump nerf double smallJumpNerfFactor = 1 - 0.17 * Math.Exp(-Math.Pow((dPrevCurr - 2.2) / 0.7, 2)) * SpecialFunctions.Logistic((255 - effectiveBpm) / 10); // Correction #10 - Slow big jump buff double bigJumpBuffFactor = 1 + 0.15 * SpecialFunctions.Logistic((dPrevCurr - 6) / 0.5) * SpecialFunctions.Logistic((210 - effectiveBpm) / 8); // Correction #11 - Hidden Mod double correctionHidden = 0; if (hidden) { correctionHidden = 0.05 + 0.008 * noteDensity; } // Correction #12 - Stacked wiggle fix if (objNeg2 != null && objNext != null) { var dPrevNext = ((posNext - posPrev) / (2 * objCurr.Radius)).L2Norm(); var dNeg2Next = ((posNext - posNeg2) / (2 * objCurr.Radius)).L2Norm(); if (dNeg2Prev < 1 && dNeg2Curr < 1 && dNeg2Next < 1 && dPrevCurr < 1 && dPrevNext < 1 && dCurrNext < 1) { correctionNeg2 = 0; correctionNext = 0; patternCorrection = 0; tapCorrection = 0; } } // Correction #13 - Repetitive jump nerf // Nerf big jumps where objNeg2 and objCurr are close or where objNeg4 and objCurr are close double jumpOverlapCorrection = 1 - (Math.Max(0.15 - 0.1 * dNeg2Curr, 0) + Math.Max(0.1125 - 0.075 * dNeg4Curr, 0)) * SpecialFunctions.Logistic((dPrevCurr - 3.3) / 0.25); // Correction #14 - Sudden distance increase buff double distanceIncreaseBuff = 1; if (objNeg2 != null) { double dNeg2PrevOverlapNerf = Math.Min(1, Math.Pow(dNeg2Prev, 3)); double timeDifferenceNerf = Math.Exp(-4 * Math.Pow(1 - Math.Max(tPrevCurr / (tNeg2Prev + 1e-10), tNeg2Prev / (tPrevCurr + 1e-10)), 2)); double distanceRatio = dPrevCurr / Math.Max(1, dNeg2Prev); double bpmScaling = Math.Max(1, -16 * tPrevCurr + 3.4); distanceIncreaseBuff = 1 + 0.225 * bpmScaling * timeDifferenceNerf * dNeg2PrevOverlapNerf * Math.Max(0, distanceRatio - 2); } // Apply the corrections double dPrevCurrWithCorrection = dPrevCurrStackedNerf * (1 + smallCircleBonus) * (1 + correctionNeg2 + correctionNext + patternCorrection) * (1 + highBpmJumpBuff) * (1 + tapCorrection) * smallJumpNerfFactor * bigJumpBuffFactor * (1 + correctionHidden) * jumpOverlapCorrection * distanceIncreaseBuff; movement.Distance = dPrevCurrWithCorrection; movement.MovementTime = tPrevCurr; movement.Cheesablility = cheesabilityEarly + cheesabilityLate; movement.CheesableRatio = (timeEarly + timeLate) / (tPrevCurr + 1e-10); var movementWithNested = new List <OsuMovement> { movement }; // add zero difficulty movements corresponding to slider ticks/slider ends so combo is reflected properly int extraNestedCount = objCurr.NestedHitObjects.Count - 1; for (int i = 0; i < extraNestedCount; i++) { movementWithNested.Add(GetEmptyMovement(movement.Time)); } return(movementWithNested); }