示例#1
0
        /// <summary>
        /// Distance is main measure of difficulty in Fitt's Law so we correct it when we need to adjust difficulty of certain aspects/patterns.
        /// </summary>
        private static double correctMovementDistance(MovementExtractionParameters parameters, double movementThroughput, double tapStrain, bool hidden, double noteDensity)
        {
            double previousObjectPlacementCorrection = calculatePreviousObjectPlacementCorrection(parameters);
            double nextObjectPlacementCorrection     = calculateNextObjectPlacementCorrection(parameters);
            double fourObjectPatternCorrection       = calculateFourObjectPatternCorrection(parameters);
            double placementCorrection = 1.0 + previousObjectPlacementCorrection + nextObjectPlacementCorrection + fourObjectPatternCorrection;

            double tapCorrection = calculateTapStrainBuff(tapStrain, parameters.LastToCurrent, movementThroughput);

            if (isStackedWiggle(parameters))
            {
                placementCorrection = 1.0;
                tapCorrection       = 1.0;
            }

            return(placementCorrection *
                   calculateStackedNoteNerf(parameters.LastToCurrent) *
                   tapCorrection *
                   calculateSmallCircleBuff(parameters) *
                   calculateHighBPMJumpBuff(parameters) *
                   calculateSmallJumpNerf(parameters) *
                   calculateBigJumpBuff(parameters) *
                   calculateHiddenCorrection(hidden, noteDensity) *
                   calculateJumpOverlapCorrection(parameters));
        }
示例#2
0
        /// <summary>
        /// Correction #6 - High bpm jump buff (alt buff)
        /// High speed (300 bpm+) jumps are underweighted by fitt's law so we're correting for it here.
        /// </summary>
        private static double calculateHighBPMJumpBuff(MovementExtractionParameters p)
        {
            var bpmCutoff = SpecialFunctions.Logistic((p.EffectiveBPM - 354) / 16.0);

            var distanceBuff = SpecialFunctions.Logistic((p.LastToCurrent.RelativeLength - 1.9) / 0.15);

            return(1.0 + bpmCutoff * distanceBuff * 0.23);
        }
示例#3
0
        /// <summary>
        /// Correction #13 - Repetitive jump nerf
        /// We apply a nerf to big jumps where second-last or fourth-last and current objects are close.
        /// This mainly targets repeating jumps such as
        /// 1  3
        ///  \/
        ///  /\
        /// 4  2
        /// </summary>
        private static double calculateJumpOverlapCorrection(MovementExtractionParameters p)
        {
            var secondLastToCurrentNerf = Math.Max(0.15 - 0.1 * p.SecondLastToCurrent?.RelativeLength ?? 0.0, 0.0);
            var fourthLastToCurrentNerf = Math.Max(0.1125 - 0.075 * p.FourthLastToCurrent?.RelativeLength ?? 0.0, 0.0);

            var distanceCutoff = SpecialFunctions.Logistic((p.LastToCurrent.RelativeLength - 3.3) / 0.25);

            return(1.0 - (secondLastToCurrentNerf + fourthLastToCurrentNerf) * distanceCutoff);
        }
示例#4
0
        /// <summary>
        /// Correction #10 - Slow big jump buff
        /// We apply buff to jumps with distance starting from ~4 on low BPMs.
        /// Graphs: https://www.desmos.com/calculator/fmewz0foql
        /// </summary>
        private static double calculateBigJumpBuff(MovementExtractionParameters p)
        {
            // this applies buff up until ~250 bpm
            var bpmCutoff = SpecialFunctions.Logistic((210 - p.EffectiveBPM) / 8.0);

            var distanceBuff = SpecialFunctions.Logistic((p.LastToCurrent.RelativeLength - 6.0) / 0.5);

            return(1.0 + distanceBuff * bpmCutoff * 0.15);
        }
示例#5
0
        /// <summary>
        /// Correction #9 - Slow small jump nerf
        /// We apply nerf to jumps within ~1-3.5 distance (with peak at 2.2) depending on BPM.
        /// Graphs: https://www.desmos.com/calculator/lbwtkv1qom
        /// </summary>
        private static double calculateSmallJumpNerf(MovementExtractionParameters p)
        {
            // this applies nerf up to 300 bpm and starts deminishing it at ~200 bpm
            var bpmCutoff = SpecialFunctions.Logistic((255 - p.EffectiveBPM) / 10.0);

            var distanceNerf = Math.Exp(-Math.Pow((p.LastToCurrent.RelativeLength - 2.2) / 0.7, 2.0));

            return(1.0 - distanceNerf * bpmCutoff * 0.17);
        }
示例#6
0
        /// <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);
        }
示例#7
0
        /// <summary>
        /// Correction #7 - Small circle bonus
        /// Small circles (CS 6.5+) are underweighted by fitt's law so we're correting for it here.
        /// Graphs: https://www.desmos.com/calculator/u6rjndtklb
        /// </summary>
        private static double calculateSmallCircleBuff(MovementExtractionParameters p)
        {
            // we only want to buff radiuses starting from about 35 (CS 4 radius is 36.48)
            var radiusCutoff = SpecialFunctions.Logistic((55 - 2 * p.CurrentObject.Radius) / 2.9) * 0.275;

            // we want to reduce bonus for small distances
            var distanceCutoff = Math.Max(SpecialFunctions.Logistic((p.LastToCurrent.RelativeLength - 0.5) / 0.1), 0.25);

            var bonusCurve = Math.Min(SpecialFunctions.Logistic((-p.CurrentObject.Radius + 10.0) / 4.0) * 0.8, 24.5);

            return(1.0 + (radiusCutoff + bonusCurve) * distanceCutoff);
        }
示例#8
0
        /// <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);
        }
示例#9
0
        /// <summary>
        /// Correction #2 - The Next Object
        /// Estimate how next object placement affects the difficulty of hitting current object.
        /// </summary>
        private static double calculateNextObjectPlacementCorrection(MovementExtractionParameters p)
        {
            if (p.NextObject == null || p.LastToCurrent.RelativeLength == 0)
            {
                return(0);
            }

            Debug.Assert(p.CurrentToNext != null);

            double movementLengthRatio = p.LastToCurrent.TimeDelta / p.CurrentToNext.Value.TimeDelta;
            double movementAngleCosine = cosineOfAngleBetweenPairs(p.LastToCurrent, p.CurrentToNext.Value);

            if (movementLengthRatio > movement_length_ratio_threshold)
            {
                if (p.CurrentToNext.Value.RelativeLength == 0)
                {
                    return(0);
                }

                double correctionNextMoving = correction_moving_spline.Interpolate(movementAngleCosine);

                double movingness = SpecialFunctions.Logistic(p.CurrentToNext.Value.RelativeLength * 6 - 5) - SpecialFunctions.Logistic(-5);
                return(movingness * correctionNextMoving * 0.5);
            }

            if (movementLengthRatio < 1 / movement_length_ratio_threshold)
            {
                if (p.CurrentToNext.Value.RelativeLength == 0)
                {
                    return(0);
                }

                return((1 - movementAngleCosine) * SpecialFunctions.Logistic((p.CurrentToNext.Value.RelativeLength * movementLengthRatio - 1.5) * 4) * 0.15);
            }

            p.CurrentObjectTemporallyCenteredBetweenNeighbours = true;

            // rescale CurrentToNext so that it's comparable timescale-wise to LastToCurrent
            var timeNormalizedNext = p.CurrentToNext.Value.RelativeVector / p.CurrentToNext.Value.TimeDelta * p.LastToCurrent.TimeDelta;

            // transform nextObject to coordinates anchored in lastObject
            double nextTransformedX = timeNormalizedNext.DotProduct(p.LastToCurrent.RelativeVector) / p.LastToCurrent.RelativeLength;
            double nextTransformedY = (timeNormalizedNext - nextTransformedX * p.LastToCurrent.RelativeVector / p.LastToCurrent.RelativeLength).L2Norm();

            double correctionNextFlow = AngleCorrection.FLOW_NEXT.Evaluate(p.LastToCurrent.RelativeLength, nextTransformedX, nextTransformedY);
            double correctionNextSnap = AngleCorrection.SNAP_NEXT.Evaluate(p.LastToCurrent.RelativeLength, nextTransformedX, nextTransformedY);

            p.LastToNextFlowiness = SpecialFunctions.Logistic((correctionNextSnap - correctionNextFlow - 0.05) * 20);

            return(Math.Max(PowerMean.Of(correctionNextFlow, correctionNextSnap, -10) - 0.1, 0) * 0.5);
        }
示例#10
0
        /// <summary>
        /// Correction #1 - The Previous Object
        /// Estimate how second-last object placement affects the difficulty of hitting current object.
        /// </summary>
        private static double calculatePreviousObjectPlacementCorrection(MovementExtractionParameters p)
        {
            if (p.SecondLastObject == null || p.LastToCurrent.RelativeLength == 0)
            {
                return(0);
            }

            Debug.Assert(p.SecondLastToLast != null);

            double movementLengthRatio = p.LastToCurrent.TimeDelta / p.SecondLastToLast.Value.TimeDelta;
            double movementAngleCosine = cosineOfAngleBetweenPairs(p.SecondLastToLast.Value, p.LastToCurrent);

            if (movementLengthRatio > movement_length_ratio_threshold)
            {
                if (p.SecondLastToLast.Value.RelativeLength == 0)
                {
                    return(0);
                }

                double angleCorrection = correction_moving_spline.Interpolate(movementAngleCosine);

                double movingness = SpecialFunctions.Logistic(p.SecondLastToLast.Value.RelativeLength * 6 - 5) - SpecialFunctions.Logistic(-5);
                return(movingness * angleCorrection * 1.5);
            }

            if (movementLengthRatio < 1 / movement_length_ratio_threshold)
            {
                if (p.SecondLastToLast.Value.RelativeLength == 0)
                {
                    return(0);
                }

                return((1 - movementAngleCosine) * SpecialFunctions.Logistic((p.SecondLastToLast.Value.RelativeLength * movementLengthRatio - 1.5) * 4) * 0.3);
            }

            p.LastObjectTemporallyCenteredBetweenNeighbours = true;

            // rescale SecondLastToLast so that it's comparable timescale-wise to LastToCurrent
            var timeNormalisedSecondLastToLast = -p.SecondLastToLast.Value.RelativeVector / p.SecondLastToLast.Value.TimeDelta * p.LastToCurrent.TimeDelta;
            // transform secondLastObject to coordinates anchored in lastObject
            double secondLastTransformedX = timeNormalisedSecondLastToLast.DotProduct(p.LastToCurrent.RelativeVector) / p.LastToCurrent.RelativeLength;
            double secondLastTransformedY = (timeNormalisedSecondLastToLast - secondLastTransformedX * p.LastToCurrent.RelativeVector / p.LastToCurrent.RelativeLength).L2Norm();

            double correctionSecondLastFlow = AngleCorrection.FLOW_SECONDLAST.Evaluate(p.LastToCurrent.RelativeLength, secondLastTransformedX, secondLastTransformedY);
            double correctionSecondLastSnap = AngleCorrection.SNAP_SECONDLAST.Evaluate(p.LastToCurrent.RelativeLength, secondLastTransformedX, secondLastTransformedY);
            double correctionSecondLastStop = SpecialFunctions.Logistic(10 * Math.Sqrt(secondLastTransformedX * secondLastTransformedX + secondLastTransformedY * secondLastTransformedY + 1) - 12);

            p.SecondLastToCurrentFlowiness = SpecialFunctions.Logistic((correctionSecondLastSnap - correctionSecondLastFlow - 0.05) * 20);

            return(PowerMean.Of(new[] { correctionSecondLastFlow, correctionSecondLastSnap, correctionSecondLastStop }, -10) * 1.3);
        }
示例#11
0
        /// <summary>
        /// Correction #12 - Stacked wiggle fix
        /// If distance between each 4 objects is less than 1 (meaning they overlap) reset all angle corrections as well as tap correction.
        /// This fixes "wiggles" (usually a stream of objects that are placed in a zig-zag pattern that can be aimed in a straight line by going through overlapped places)
        /// </summary>
        private static bool isStackedWiggle(MovementExtractionParameters p)
        {
            if (p.SecondLastObject == null || p.NextObject == null)
            {
                return(false);
            }

            return(p.SecondLastToLast?.RelativeLength < 1 &&
                   p.SecondLastToCurrent?.RelativeLength < 1 &&
                   p.SecondLastToNext?.RelativeLength < 1 &&
                   p.LastToCurrent.RelativeLength < 1 &&
                   p.LastToNext?.RelativeLength < 1 &&
                   p.CurrentToNext?.RelativeLength < 1);
        }
示例#12
0
        /// <summary>
        /// Correction #3 - 4-object pattern
        /// Estimate how the whole pattern consisting of second-last to next objects affects the difficulty of hitting current object.
        /// This only takes effect when the pattern is not so spaced (i.e. does not contain jumps)
        /// </summary>
        private static double calculateFourObjectPatternCorrection(MovementExtractionParameters p)
        {
            if (!p.LastObjectTemporallyCenteredBetweenNeighbours ||
                !p.CurrentObjectTemporallyCenteredBetweenNeighbours)
            {
                return(0);
            }

            Debug.Assert(p.CurrentToNext != null);
            Debug.Assert(p.SecondLastToLast != null);

            double gap = (p.LastToCurrent.RelativeVector - p.CurrentToNext.Value.RelativeVector / 2 - p.SecondLastToLast.Value.RelativeVector / 2).L2Norm() / (p.LastToCurrent.RelativeLength + 0.1);

            return((SpecialFunctions.Logistic((gap - 1) * 8) - SpecialFunctions.Logistic(-6)) *
                   SpecialFunctions.Logistic((p.SecondLastToLast.Value.RelativeLength - 0.7) * 10) * SpecialFunctions.Logistic((p.CurrentToNext.Value.RelativeLength - 0.7) * 10) *
                   PowerMean.Of(p.SecondLastToCurrentFlowiness, p.LastToNextFlowiness, 2) * 0.6);
        }