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
Show file tree
Hide file tree
Changes from all 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
80 changes: 59 additions & 21 deletions osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;

namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
Expand All @@ -14,7 +16,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;

Expand All @@ -28,7 +31,7 @@ public static class FlashlightEvaluator
/// <item><description>and whether the hidden mod is enabled.</description></item>
/// </list>
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidden)
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidden, OsuModFlashlight osuModFlashlight)
{
if (current.BaseObject is Spinner)
return 0;
Expand All @@ -39,36 +42,48 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidd
double scalingFactor = 52.0 / osuHitObject.Radius;
double smallDistNerf = 1.0;
double cumulativeStrainTime = 0.0;
double angleRepeatCount = 0.0;

double result = 0.0;

OsuDifficultyHitObject lastObj = osuCurrent;

double angleRepeatCount = 0.0;

// 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);

cumulativeStrainTime += lastObj.StrainTime;

if (!(currentObj.BaseObject is Spinner))
{
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
double pixelDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
double flashlightRadius = getSize(currentObj.CurrentMaxCombo, osuModFlashlight);
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);
}

// 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);

// We want to nerf objects that can be easily seen within the Flashlight circle radius.
// 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 - 35));

// 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);
// Nerf stacks so that only the first object of the stack is accounted for.
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 45.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)
{
Expand Down Expand Up @@ -97,20 +112,43 @@ 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);

// Longer sliders require more memorisation.
sliderBonus *= pixelTravelDistance;
// More cursor movement requires more memorisation.
sliderBonus *= osuSlider.LazyTravelDistance;

// Nerf sliders with repeats, as less memorisation is required.
if (osuSlider.RepeatCount > 0)
sliderBonus /= (osuSlider.RepeatCount + 1);
// Nerf slow slider velocity.
double sliderVelocity = osuSlider.Distance / osuCurrent.TravelTime;
sliderBonus *= Math.Clamp((sliderVelocity - min_velocity) / (max_velocity - min_velocity), 0, 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 float getSize(int combo, OsuModFlashlight osuModFlashlight)
{
float size = osuModFlashlight.DefaultFlashlightSize * osuModFlashlight.SizeMultiplier.Value;

if (osuModFlashlight.ComboBasedSize.Value)
size *= getComboScaleFor(combo);

return size;
}

private static float getComboScaleFor(int combo)
{
if (combo >= 200)
return 0.625f;
if (combo >= 100)
return 0.8125f;

return 1.0f;
}
}
}
38 changes: 32 additions & 6 deletions osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,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;
}

Expand All @@ -298,6 +298,8 @@ private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes a
if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;

var osuModFlashlight = (OsuModFlashlight)score.Mods.Single(m => m is OsuModFlashlight);

double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty);

// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
Expand All @@ -306,9 +308,32 @@ private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes a

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, and being generally more easily retryable.
flashlightValue *= Math.Min(1.2 - Math.Pow(0.997, attributes.MaxCombo), 1);

// Calculate time spent at each flashlight radius to account for scores where the radius increased due to misses.
double maximumSizePercentage = 1.0;
double mediumSizePercentage = 0.0;
double minimumSizePercentage = 0.0;

if (osuModFlashlight.ComboBasedSize.Value)
{
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);

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

double maximumSizeScalingFactor = flashlightRadiusScalingFactor(osuModFlashlight.SizeMultiplier.Value);
double mediumSizeScalingFactor = flashlightRadiusScalingFactor(0.8125f * osuModFlashlight.SizeMultiplier.Value);
double minimumSizeScalingFactor = flashlightRadiusScalingFactor(0.625f * osuModFlashlight.SizeMultiplier.Value);

flashlightValue *= maximumSizeScalingFactor * maximumSizePercentage + mediumSizeScalingFactor * mediumSizePercentage + minimumSizeScalingFactor * minimumSizePercentage;

// Scale the flashlight value with accuracy _slightly_.
flashlightValue *= 0.5 + accuracy / 2.0;
Expand Down Expand Up @@ -425,6 +450,7 @@ private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attribute
// to make it more punishing on maps with lower amount of hard sections.
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private double flashlightRadiusScalingFactor(double sizeMultiplier) => 1.2 / (1 + Math.Exp(8.58367 * (sizeMultiplier - 0.8125)));

private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,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;

Expand All @@ -108,6 +118,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)
Expand Down Expand Up @@ -348,5 +361,23 @@ private Vector2 getEndCursorPosition(OsuHitObject hitObject)

return pos;
}

private int getObjectCombo(HitObject hitObject)
{
int combo = 0;

addCombo(hitObject, ref combo);

return combo;

static void addCombo(HitObject hitObject, ref int combo)
{
if (hitObject.Judgement.MaxResult.AffectsCombo())
combo++;

foreach (var nested in hitObject.NestedHitObjects)
addCombo(nested, ref combo);
}
}
}
}
11 changes: 6 additions & 5 deletions osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public class Flashlight : StrainSkill
{
private readonly bool hasHiddenMod;
private readonly OsuModFlashlight osuModFlashlight;

public Flashlight(Mod[] mods)
: base(mods)
{
hasHiddenMod = mods.Any(m => m is OsuModHidden);
osuModFlashlight = (OsuModFlashlight)mods.Single(m => m is OsuModFlashlight);
}

private double skillMultiplier => 0.05512;
private double skillMultiplier => 0.0727;
private double strainDecayBase => 0.15;
protected override double DecayWeight => 0.99984;

private double currentStrain;

Expand All @@ -36,13 +39,11 @@ public Flashlight(Mod[] mods)
protected override double StrainValueAt(DifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += FlashlightEvaluator.EvaluateDifficultyOf(current, hasHiddenMod) * skillMultiplier;
currentStrain += FlashlightEvaluator.EvaluateDifficultyOf(current, hasHiddenMod, osuModFlashlight) * skillMultiplier;

return currentStrain;
}

public override double DifficultyValue() => GetCurrentStrainPeaks().Sum();

public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
public static double DifficultyToPerformance(double difficulty) => 28.727 * Math.Pow(difficulty, 2);
}
}
Loading