public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidden) { if (current.BaseObject is Spinner) { return(0); } var osuCurrent = (OsuDifficultyHitObject)current; var osuHitObject = (OsuHitObject)(osuCurrent.BaseObject); double scalingFactor = 52.0 / osuHitObject.Radius; double smallDistNerf = 1.0; double cumulativeStrainTime = 0.0; double result = 0.0; OsuDifficultyHitObject lastObj = osuCurrent; // This is iterating backwards in time from the current object. for (int i = 0; i < Math.Min(current.Index, 10); i++) { var currentObj = (OsuDifficultyHitObject)current.Previous(i); var currentHitObject = (OsuHitObject)(currentObj.BaseObject); if (!(currentObj.BaseObject is Spinner)) { double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length; cumulativeStrainTime += lastObj.StrainTime; // We want to nerf objects that can be easily seen within the Flashlight circle radius. if (i == 0) { smallDistNerf = Math.Min(1.0, jumpDistance / 75.0); } // We also want to nerf stacks so that only the first object of the stack is accounted for. double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0); // Bonus based on how visible the object is. double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden)); result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime; } lastObj = currentObj; } result = Math.Pow(smallDistNerf * result, 2.0); // Additional bonus for Hidden due to there being no approach circles. if (hidden) { result *= 1.0 + hidden_bonus; } return(result); }
public static double EvaluateDifficultyOf(DifficultyHitObject current, double greatWindow) { if (current.BaseObject is Spinner) { return(0); } // derive strainTime for calculation var osuCurrObj = (OsuDifficultyHitObject)current; var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null; var osuNextObj = (OsuDifficultyHitObject)current.Next(0); double strainTime = osuCurrObj.StrainTime; double greatWindowFull = greatWindow * 2; double doubletapness = 1; // Nerf doubletappable doubles. if (osuNextObj != null) { double currDeltaTime = Math.Max(1, osuCurrObj.DeltaTime); double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime); double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime); double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference); double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / greatWindowFull), 2); doubletapness = Math.Pow(speedRatio, 1 - windowRatio); } // Cap deltatime to the OD 300 hitwindow. // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1); // derive speedBonus for calculation double speedBonus = 1.0; if (strainTime < min_speed_bonus) { speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); } double travelDistance = osuPrevObj?.TravelDistance ?? 0; double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance); return((speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) * doubletapness / strainTime); }
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => CurrentStrain *strainDecay(time - current.Previous(0).StartTime);
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) => applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base) + applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
public static double EvaluateDifficultyOf(DifficultyHitObject current, double greatWindow) { if (current.BaseObject is Spinner) { return(0); } int previousIslandSize = 0; double rhythmComplexitySum = 0; int islandSize = 1; double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms bool firstDeltaSwitch = false; int historicalNoteCount = Math.Min(current.Index, 32); int rhythmStart = 0; while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max) { rhythmStart++; } for (int i = rhythmStart; i > 0; i--) { OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1); OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(i); OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(i + 1); double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now currHistoricalDecay = Math.Min((double)(historicalNoteCount - i) / historicalNoteCount, currHistoricalDecay); // either we're limited by time or limited by object count. double currDelta = currObj.StrainTime; double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6)); windowPenalty = Math.Min(1, windowPenalty); double effectiveRatio = windowPenalty * currRatio; if (firstDeltaSwitch) { if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta)) { if (islandSize < 7) { islandSize++; // island is still progressing, count size. } } else { if (current.Previous(i - 1).BaseObject is Slider) // bpm change is into slider, this is easy acc window { effectiveRatio *= 0.125; } if (current.Previous(i).BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle { effectiveRatio *= 0.25; } if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) { effectiveRatio *= 0.25; } if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5) { effectiveRatio *= 0.50; } if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. { effectiveRatio *= 0.125; } rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2; startRatio = effectiveRatio; previousIslandSize = islandSize; // log the last island size. if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting { firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. } islandSize = 1; } } else if (prevDelta > 1.25 * currDelta) // we want to be speeding up. { // Begin counting island until we change speed again. firstDeltaSwitch = true; startRatio = effectiveRatio; islandSize = 1; } } return(Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2); //produces multiplier that can be applied to strain. range [1, infinity) (not really though) }
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliders) { if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner) { return(0); } var osuCurrObj = (OsuDifficultyHitObject)current; var osuLastObj = (OsuDifficultyHitObject)current.Previous(0); var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1); // Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle. double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime; // But if the last object is a slider, then we extend the travel velocity through the slider into the current object. if (osuLastObj.BaseObject is Slider && withSliders) { double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end. double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity. } // As above, do the same for the previous hitobject. double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime; if (osuLastLastObj.BaseObject is Slider && withSliders) { double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime; double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime; prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity); } double wideAngleBonus = 0; double acuteAngleBonus = 0; double sliderBonus = 0; double velocityChangeBonus = 0; double aimStrain = currVelocity; // Start strain with regular velocity. if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same. { if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null) { double currAngle = osuCurrObj.Angle.Value; double lastAngle = osuLastObj.Angle.Value; double lastLastAngle = osuLastLastObj.Angle.Value; // Rewarding angles, take the smaller velocity as base. double angleBonus = Math.Min(currVelocity, prevVelocity); wideAngleBonus = calcWideAngleBonus(currAngle); acuteAngleBonus = calcAcuteAngleBonus(currAngle); if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2. { acuteAngleBonus = 0; } else { acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. * Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4 * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter). } // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse. acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); } } if (Math.Max(prevVelocity, currVelocity) != 0) { // We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities. prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime; currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime; // Scale with ratio of difference compared to 0.5 * max dist. double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2); // Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity)); // Reward for % distance slowed down compared to previous, paying attention to not award overlap double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity) // do not award overlap * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2); // Choose the largest bonus, multiplied by ratio. velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio; // Penalize for rhythm changes. velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); } if (osuLastObj.TravelTime != 0) { // Reward sliders based on velocity. sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; } // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger. aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier); // Add in additional slider velocity bonus. if (withSliders) { aimStrain += sliderBonus * slider_multiplier; } return(aimStrain); }