protected override double StrainValueOf(OsuDifficultyHitObject current) { double distance = current.Distance; double speedValue; if (distance > single_spacing_threshold) { speedValue = 1.25; } else if (distance > stream_spacing_threshold) { speedValue = 1.07 + 0.18 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold); } else if (distance > almost_diameter) { speedValue = 0.99 + 0.08 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter); } else if (distance > almost_diameter / 2) { speedValue = 0.95 + 0.04 * (distance - almost_diameter / 2) / (almost_diameter / 2); } else { speedValue = 0.95; } return(speedValue / current.DeltaTime); }
protected override double StrainValueOf(OsuDifficultyHitObject current) { double result = 0; const double scale = 90; double applyDiminishingExp(double val) => Math.Pow(val, 0.99); if (Previous.Count > 0) { if (current.Angle != null && current.Angle.Value > angle_bonus_begin) { var angleBonus = Math.Sqrt( Math.Max(Previous[0].JumpDistance - scale, 0) * Math.Pow(Math.Sin(current.Angle.Value - angle_bonus_begin), 2) * Math.Max(current.JumpDistance - scale, 0)); result = 1.5 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, Previous[0].StrainTime); } } double jumpDistanceExp = applyDiminishingExp(current.JumpDistance); double travelDistanceExp = applyDiminishingExp(current.TravelDistance); return(Math.Max( result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(current.StrainTime, timing_threshold), (Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / current.StrainTime )); }
protected override double StrainValueOf(OsuDifficultyHitObject current) { double distance = Math.Min(SINGLE_SPACING_THRESHOLD, current.TravelDistance + current.JumpDistance); double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime); double speedBonus = 1.0; if (deltaTime < min_speed_bonus) { speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2); } double angleBonus = 1.0; if (current.Angle != null && current.Angle.Value < angle_bonus_begin) { angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - current.Angle.Value)), 2) / 3.57; if (current.Angle.Value < pi_over_2) { angleBonus = 1.28; if (distance < 90 && current.Angle.Value < pi_over_4) { angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1); } else if (distance < 90) { angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - current.Angle.Value) / pi_over_4); } } } return((1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / SINGLE_SPACING_THRESHOLD, 3.5)) / current.StrainTime); }
protected override double StrainValueOf(OsuDifficultyHitObject current) { double distance = current.TravelDistance + current.JumpDistance; double speedValue; if (distance > single_spacing_threshold) { speedValue = 2.5; } else if (distance > stream_spacing_threshold) { speedValue = 1.6 + 0.9 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold); } else if (distance > almost_diameter) { speedValue = 1.2 + 0.4 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter); } else if (distance > almost_diameter / 2) { speedValue = 0.95 + 0.25 * (distance - almost_diameter / 2) / (almost_diameter / 2); } else { speedValue = 0.95; } return(speedValue / current.StrainTime); }
private double strainValueOf(DifficultyHitObject current) { 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 < Previous.Count; i++) { var currentObj = (OsuDifficultyHitObject)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); }
private double calculateControlBonus(OsuDifficultyHitObject osuCurrent, OsuDifficultyHitObject osuPrevious) { var prevCursSpeed = Math.Max(1, osuPrevious.JumpDistance / osuPrevious.StrainTime * control_spacing_scale); var curCursSpeed = Math.Max(1, osuCurrent.JumpDistance / osuCurrent.StrainTime * control_spacing_scale); var speedRatio = Math.Max(prevCursSpeed / curCursSpeed, curCursSpeed / prevCursSpeed); var speedChange = Math.Pow(speedRatio, 2.5) / (osuCurrent.StrainTime + osuPrevious.StrainTime); return(speedChange * osuCurrent.JumpDistance / 1600); }
/// <summary> /// Process an <see cref="OsuDifficultyHitObject"/> and update current strain values accordingly. /// </summary> public void Process(OsuDifficultyHitObject current) { currentStrain *= strainDecay(current.DeltaTime); if (!(current.BaseObject is Spinner)) { currentStrain += StrainValueOf(current) * SkillMultiplier; } currentSectionPeak = Math.Max(currentStrain, currentSectionPeak); Previous.Push(current); }
private double strainValueOf(DifficultyHitObject current) { 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 < Previous.Count; i++) { var currentObj = (OsuDifficultyHitObject)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); result += stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime; } lastObj = currentObj; } return(Math.Pow(smallDistNerf * result, 2.0)); }
protected override double StrainValueOf(OsuDifficultyHitObject current) { /* double distance = current.Distance; * * double speedValue; * if (distance > single_spacing_threshold) * speedValue = 2.5; * else if (distance > stream_spacing_threshold) * speedValue = 1.6 + 0.9 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold); * else if (distance > almost_diameter) * speedValue = 1.2 + 0.4 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter); * else if (distance > almost_diameter / 2) * speedValue = 0.95 + 0.25 * (distance - almost_diameter / 2) / (almost_diameter / 2); * else * speedValue = 0.95; */ return(1 / current.DeltaTime); }
protected override IEnumerable <DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { // The first jump is formed by the first two hitobjects of the map. // If the map has less than two OsuHitObjects, the enumerator will not return anything. difficultyHitObjects.Clear(); for (int i = 1; i < beatmap.HitObjects.Count; i++) { var lastLast = i > 1 ? beatmap.HitObjects[i - 2] : null; var last = beatmap.HitObjects[i - 1]; var current = beatmap.HitObjects[i]; var difficultyHitObject = new OsuDifficultyHitObject(current, lastLast, last, difficultyHitObjects, clockRate); yield return(difficultyHitObject); difficultyHitObjects.Add(difficultyHitObject); } }
protected override double StrainValueOf(OsuDifficultyHitObject current) { double distance = Math.Pow(current.Distance, 0.99); double time = current.DeltaTime; // Any 1/4 note above 150 BPM will receive a buff if the angle is 90 degrees or below if (current.JumpAngle <= 90 && current.JumpAngle >= 0 && distance > 39) { time *= 0.67 + Math.Min(time, 100) / 300; } // Any jump with an angle of above 60 degrees will scale harder with distance if (current.JumpAngle > 60) { distance += Math.Pow(Math.Max(current.Distance - (current.BaseObject.Radius / 52) * current.BaseObject.Radius, 0), 1.3) * ((current.JumpAngle - 60) / 1200); } double aimValue = distance / time; return(aimValue); }
private double calculateRepeatJumpPenalty(OsuDifficultyHitObject osuCurrent) { if (osuCurrent.LastDistance == null) { return(0); } if (osuCurrent.JumpDistance < repeatjump_min_spacing) { return(0); } const double spacing_range = repeatjump_max_spacing - repeatjump_min_spacing; double spacingScale = Math.Min(osuCurrent.JumpDistance - repeatjump_min_spacing, spacing_range) / spacing_range; const double start_distance = 2.5 * 52; double jumpScale = osuCurrent.JumpDistance / (6 * 52); double scaledStartDistance = jumpScale * start_distance; double innerDistance = Math.Max(scaledStartDistance - (double)osuCurrent.LastDistance, 0) / scaledStartDistance; return(innerDistance * spacingScale * 0.065); }
/// <summary> /// Process an <see cref="OsuDifficultyHitObject"/> and update current strain values accordingly. /// </summary> public void Process(OsuDifficultyHitObject current) { //At roughly the equivalent of 50 stacked 220 bpm 1/4 notes, we start to decay less to account for stamina. //The threshold at which we do this goes up every time this happens. if (SkillMultiplier == 1400 && currentStrain >= staminaThreshold && decayMultiplier < 1.67 && current.DeltaTime < 100) { decayMultiplier *= 1.03; staminaThreshold *= Math.Pow(1.03, 2); } //Strain decay will very rapidly approach the normal value once the streaming stops. else if (SkillMultiplier == 1400 && current.DeltaTime >= 100) { double staminaRecovery = 1 / Math.Pow(1.03, 5); decayMultiplier = Math.Max(1, decayMultiplier * staminaRecovery); staminaThreshold = Math.Max(1, staminaThreshold * Math.Pow(staminaRecovery, 2)); double time = current.DeltaTime - 100; //Any extended break during gameplay will most likely force decay to go back to its original value. for (int i = 0; time > 0; i++) { time -= 75; decayMultiplier = Math.Max(1, decayMultiplier * Math.Pow(staminaRecovery, 1 + i)); staminaThreshold = Math.Max(234.63, staminaThreshold * Math.Pow(Math.Pow(staminaRecovery, 2), 1 + i)); if (decayMultiplier == 1) { break; } } } currentStrain *= strainDecay(current.DeltaTime); if (!(current.BaseObject is Spinner)) { currentStrain += StrainValueOf(current) * SkillMultiplier; } currentSectionPeak = Math.Max(currentStrain, currentSectionPeak); Previous.Push(current); }
protected override double StrainValueOf(OsuDifficultyHitObject current) => (Math.Pow(current.TravelDistance, 0.99) + Math.Pow(current.JumpDistance, 0.99)) / current.StrainTime;
/// <summary> /// Calculates the strain value of an <see cref="OsuDifficultyHitObject"/>. This value is affected by previously processed objects. /// </summary> protected abstract double StrainValueOf(OsuDifficultyHitObject current);
/// <summary> /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>. /// </summary> private double calculateRhythmBonus(DifficultyHitObject current) { 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 rhythmStart = 0; while (rhythmStart < Previous.Count - 2 && current.StartTime - Previous[rhythmStart].StartTime < history_time_max) { rhythmStart++; } for (int i = rhythmStart; i > 0; i--) { OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1]; OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i]; OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)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)(Previous.Count - i) / Previous.Count, 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 (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window { effectiveRatio *= 0.125; } if (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) }
protected override double StrainValueOf(OsuDifficultyHitObject current) => Math.Pow(current.JumpDistance, 0.99) / current.DeltaTime;