Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flashlight difficulty rework #28278

Open
wants to merge 24 commits into
base: pp-dev
Choose a base branch
from
Open
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5409017
add PreviousMaxCombo and CurrentMaxCombo to OsuDifficultyHitObject
molneya Apr 21, 2024
6f110a8
consider jump from lazy end position for sliders
molneya Apr 25, 2024
cd1bebd
adjust accuracy value for hd and hdfl
molneya Apr 25, 2024
d89f558
fix spinners not increasing cumulative strain time
molneya Apr 25, 2024
6fd3d58
new flashlight slider difficulty calculation
molneya Apr 25, 2024
bc96fd2
account for visible radius in flashlight evaluator
molneya May 14, 2024
5bfc6c0
account for misses causing increased flashlight radius in performance
molneya May 20, 2024
3d038ce
new flashlight length bonus based on max combo
molneya May 20, 2024
baacb9b
dont assume worst case scenario for averageMissingComboLength
molneya May 20, 2024
23f3732
convert into ?: expression
molneya May 20, 2024
db68fd7
new flashlight distance nerfs
molneya May 21, 2024
d5d295f
flashlight cleanups
molneya May 21, 2024
d5910dd
Merge branches 'fl-acc', 'fl-spinners', 'fl-slider-rework', 'fl-combo…
molneya May 21, 2024
3aa0576
buff flashlight difficulty constants
molneya May 21, 2024
8314857
tweak flashlight constants some more
molneya May 21, 2024
e06e57c
better variable names
molneya May 28, 2024
a0c45bb
support custom flashlight settings
tsunyoku Nov 7, 2024
005a5de
use better logic to calculate an object's combo
tsunyoku Nov 7, 2024
6e424f6
Merge branch 'master' into fl-main-2
tsunyoku Nov 7, 2024
eb58407
fix flashlight difficulty to performance formula
tsunyoku Nov 7, 2024
de9b130
general balancing
molneya Jan 12, 2025
b8740ea
simplify flashlight max combo scaling
molneya Jan 12, 2025
6f3676e
Merge branch 'pp-dev' into fl-main-2
stanriders Jan 14, 2025
f92696b
account for size multiplier setting in flashlight pp
molneya Jan 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 46 additions & 17 deletions osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;

namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
@@ -14,7 +15,8 @@ public static class FlashlightEvaluator
private const double hidden_bonus = 0.2;

private const double min_velocity = 0.5;
private const double slider_multiplier = 1.3;
private const double max_velocity = 1.5;
private const double slider_multiplier = 0.3;

private const double min_angle_multiplier = 0.2;

@@ -52,23 +54,36 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidd
var currentObj = (OsuDifficultyHitObject)current.Previous(i);
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);

cumulativeStrainTime += lastObj.StrainTime;

if (!(currentObj.BaseObject is Spinner))
{
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
double pixelDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
double flashlightRadius = getComboScaleFor(currentObj.CurrentMaxCombo);
double objectOpacity = osuCurrent.OpacityAt(currentHitObject.StartTime, hidden);

cumulativeStrainTime += lastObj.StrainTime;
// Consider the jump from the lazy end position for sliders.
if (currentHitObject is Slider currentSlider)
{
Vector2 lazyEndPosition = currentSlider.LazyEndPosition ?? currentSlider.StackedPosition;
pixelDistance = Math.Min(pixelDistance, (osuHitObject.StackedPosition - lazyEndPosition).Length);
}

// We want to nerf objects that can be easily seen within the Flashlight circle radius.
// Apply a nerf based on the visibility from the current object.
double radiusVisibility = Math.Min(1.0, pixelDistance / (flashlightRadius + osuHitObject.Radius - 80));
double visibilityNerf = 1.0 - objectOpacity * (1.0 - radiusVisibility);

// Small jumps within the visible Flashlight radius should be nerfed.
if (i == 0)
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
smallDistNerf = Math.Min(1.0, pixelDistance / (flashlightRadius - 45));

// We also want to nerf stacks so that only the first object of the stack is accounted for.
// 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));
// Bonus based on object opacity.
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - objectOpacity);

result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
result += visibilityNerf * stackNerf * opacityBonus * scalingFactor * pixelDistance / cumulativeStrainTime;

if (currentObj.Angle != null && osuCurrent.Angle != null)
{
@@ -97,20 +112,34 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidd
// Invert the scaling factor to determine the true travel distance independent of circle size.
double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor;

// Reward sliders based on velocity.
sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
// Reward sliders based on cursor velocity.
sliderBonus = Math.Log(pixelTravelDistance / osuCurrent.TravelTime + 1);

// More cursor movement requires more memorisation.
sliderBonus *= osuSlider.LazyTravelDistance;

// Longer sliders require more memorisation.
sliderBonus *= pixelTravelDistance;
// Nerf slow slider velocity.
double sliderVelocity = osuSlider.Distance / osuCurrent.TravelTime;
sliderBonus *= Math.Clamp((sliderVelocity - min_velocity) / (max_velocity - min_velocity), 0, 1);

// Nerf sliders with repeats, as less memorisation is required.
if (osuSlider.RepeatCount > 0)
sliderBonus /= (osuSlider.RepeatCount + 1);
// Nerf sliders the more repeats they have, as less memorisation is required.
sliderBonus /= 0.75 * osuSlider.RepeatCount + 1;
}

result += sliderBonus * slider_multiplier;
result += Math.Pow(sliderBonus, 1.2) * slider_multiplier;

return result;
}

private static double getComboScaleFor(int combo)
{
// Taken from ModFlashlight.
if (combo >= 200)
return 125.0;
if (combo >= 100)
return 162.5;

return 200.0;
}
molneya marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
double baseFlashlightPerformance = 0.0;

if (mods.Any(h => h is OsuModFlashlight))
baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0;
baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 28.727;

double basePerformance =
Math.Pow(
26 changes: 19 additions & 7 deletions osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
Original file line number Diff line number Diff line change
@@ -212,12 +212,12 @@ private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes att
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
if (score.Mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14;
// Use different multiplier when adding hidden or traceable to flashlight.
else if (score.Mods.Any(m => m is OsuModFlashlight))
accuracyValue *= score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable) ? 1.12 : 1.08;
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
accuracyValue *= 1.08;

if (score.Mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02;

return accuracyValue;
}

@@ -226,17 +226,29 @@ private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes a
if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;

double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 28.727;

// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));

flashlightValue *= getComboScalingFactor(attributes);

// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
(totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
// Account for shorter maps having more time played at a larger flashlight radius.
flashlightValue *= Math.Min(1, 0.7 + 0.1 * Math.Max(0, Math.Log(attributes.MaxCombo / 100.0)) + 0.1 * Math.Max(0, Math.Log(attributes.MaxCombo / 200.0)));

// Account for scores where the flashlight radius is increased due to misses.
int missingCombo = attributes.MaxCombo - scoreMaxCombo;
double missingComboPercentage = (double)missingCombo / attributes.MaxCombo;

// For balancing purposes, assume the player made 3 misses for every memorisation mistake.
double averageMissingComboLength = Math.Max(missingCombo, 1) / Math.Max(effectiveMissCount / 3, 1);

double maximumSizePercentage = Math.Clamp(averageMissingComboLength, 0, 100) / averageMissingComboLength * missingComboPercentage;
double mediumSizePercentage = Math.Clamp(averageMissingComboLength - 100, 0, 100) / averageMissingComboLength * missingComboPercentage;
double minimumSizePercentage = 1.0 - mediumSizePercentage - maximumSizePercentage;

flashlightValue *= minimumSizePercentage + 0.6 * mediumSizePercentage + 0.2 * maximumSizePercentage;

// Scale the flashlight value with accuracy _slightly_.
flashlightValue *= 0.5 + accuracy / 2.0;
Original file line number Diff line number Diff line change
@@ -83,6 +83,16 @@ public class OsuDifficultyHitObject : DifficultyHitObject
/// </summary>
public double HitWindowGreat { get; private set; }

/// <summary>
/// The maximum combo played before this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public int PreviousMaxCombo { get; private set; }

/// <summary>
/// The maximum combo played after this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public int CurrentMaxCombo { get; private set; }

private readonly OsuHitObject? lastLastObject;
private readonly OsuHitObject lastObject;

@@ -105,6 +115,9 @@ public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObje
}

setDistances(clockRate);

PreviousMaxCombo = index > 0 ? ((OsuDifficultyHitObject)Previous(0)).CurrentMaxCombo : getObjectCombo(lastObject);
CurrentMaxCombo = PreviousMaxCombo + getObjectCombo(hitObject);
molneya marked this conversation as resolved.
Show resolved Hide resolved
}

public double OpacityAt(double time, bool hidden)
@@ -327,5 +340,18 @@ private Vector2 getEndCursorPosition(OsuHitObject hitObject)

return pos;
}

private int getObjectCombo(HitObject hitObject)
{
if (hitObject is Slider slider)
{
if (slider.NestedHitObjects[1] is SliderRepeat)
return slider.RepeatCount + 2;
else
return slider.NestedHitObjects.Count;
}
molneya marked this conversation as resolved.
Show resolved Hide resolved

return 1;
}
}
}
Loading