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

Add difficulty skill for Flashlight mod #14217

Merged
merged 20 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class OsuDifficultyAttributes : DifficultyAttributes
{
public double AimStrain { get; set; }
public double SpeedStrain { get; set; }
public double FlashlightStrain { get; set; }
public double ApproachRate { get; set; }
public double OverallDifficulty { get; set; }
public int HitCircleCount { get; set; }
Expand Down
5 changes: 4 additions & 1 deletion osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat

double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2;

HitWindows hitWindows = new OsuHitWindows();
Expand All @@ -56,6 +57,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
Mods = mods,
AimStrain = aimRating,
SpeedStrain = speedRating,
FlashlightStrain = flashlightRating,
MBmasher marked this conversation as resolved.
Show resolved Hide resolved
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
MaxCombo = maxCombo,
Expand All @@ -82,7 +84,8 @@ protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(I
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
{
new Aim(mods),
new Speed(mods)
new Speed(mods),
new Flashlight(mods)
};

protected override Mod[] DifficultyAdjustmentMods => new Mod[]
MBmasher marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
79 changes: 53 additions & 26 deletions osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public override double Calculate(Dictionary<string, double> categoryRatings = nu
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);

// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.

if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
Expand All @@ -52,18 +52,21 @@ public override double Calculate(Dictionary<string, double> categoryRatings = nu
double aimValue = computeAimValue();
double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue();
double flashlightValue = computeFlashlightValue();
double totalValue =
Math.Pow(
Math.Pow(aimValue, 1.1) +
Math.Pow(speedValue, 1.1) +
Math.Pow(accuracyValue, 1.1), 1.0 / 1.1
Math.Pow(accuracyValue, 1.1) +
Math.Pow(flashlightValue, 1.1), 1.0 / 1.1
) * multiplier;

if (categoryRatings != null)
{
categoryRatings.Add("Aim", aimValue);
categoryRatings.Add("Speed", speedValue);
categoryRatings.Add("Accuracy", accuracyValue);
categoryRatings.Add("Flashlight", flashlightValue);
categoryRatings.Add("OD", Attributes.OverallDifficulty);
categoryRatings.Add("AR", Attributes.ApproachRate);
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
Expand All @@ -81,7 +84,7 @@ private double computeAimValue()

double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;

// Longer maps are worth more
// Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);

Expand All @@ -91,7 +94,7 @@ private double computeAimValue()
if (countMiss > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);

// Combo scaling
// Combo scaling.
if (Attributes.MaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);

Expand All @@ -109,23 +112,11 @@ private double computeAimValue()
if (mods.Any(h => h is OsuModHidden))
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);

double flashlightBonus = 1.0;
aimValue *= approachRateBonus;

if (mods.Any(h => h is OsuModFlashlight))
{
// Apply object-based bonus for flashlight.
flashlightBonus = 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
(totalHits > 200
? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
(totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
: 0.0);
}

aimValue *= Math.Max(flashlightBonus, approachRateBonus);

// Scale the aim value with accuracy _slightly_
// Scale the aim value with accuracy _slightly_.
aimValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that
// It is important to also consider accuracy difficulty when doing that.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;

return aimValue;
Expand All @@ -135,7 +126,7 @@ private double computeSpeedValue()
{
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;

// Longer maps are worth more
// Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
speedValue *= lengthBonus;
Expand All @@ -144,7 +135,7 @@ private double computeSpeedValue()
if (countMiss > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));

// Combo scaling
// Combo scaling.
if (Attributes.MaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);

Expand All @@ -159,7 +150,7 @@ private double computeSpeedValue()
if (mods.Any(m => m is OsuModHidden))
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);

// Scale the speed value with accuracy and OD
// Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
Expand All @@ -169,7 +160,7 @@ private double computeSpeedValue()

private double computeAccuracyValue()
{
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;

Expand All @@ -178,15 +169,15 @@ private double computeAccuracyValue()
else
betterAccuracyPercentage = 0;

// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
if (betterAccuracyPercentage < 0)
betterAccuracyPercentage = 0;

// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;

// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));

if (mods.Any(m => m is OsuModHidden))
Expand All @@ -197,6 +188,42 @@ private double computeAccuracyValue()
return accuracyValue;
}

private double computeFlashlightValue()
{
if (!mods.Any(h => h is OsuModFlashlight))
return 0.0;

double rawFlashlight = Attributes.FlashlightStrain;

if (mods.Any(m => m is OsuModTouchDevice))
rawFlashlight = Math.Pow(rawFlashlight, 0.8);

double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;

// Add an additional bonus for HDFL.
if (mods.Any(h => h is OsuModHidden))
flashlightValue *= 1.3;

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

// Combo scaling.
if (Attributes.MaxCombo > 0)
flashlightValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);

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

// Scale the flashlight value with accuracy _slightly_.
flashlightValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that.
flashlightValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;

return flashlightValue;
}

private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
Expand Down
70 changes: 70 additions & 0 deletions osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;

namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
/// <summary>
/// Represents the skill required to memorise and hit every object in a map with the Flashlight mod enabled.
/// </summary>
public class Flashlight : OsuStrainSkill
{
public Flashlight(Mod[] mods)
: base(mods)
{
}

protected override double SkillMultiplier => 0.15;
protected override double StrainDecayBase => 0.15;
protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.

protected override 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 result = 0.0;

if (Previous.Count > 0)
MBmasher marked this conversation as resolved.
Show resolved Hide resolved
{
double cumulativeStrainTime = 0.0;

for (int i = 0; i < Previous.Count; i++)
{
var osuPrevious = (OsuDifficultyHitObject)Previous[i];
var osuPreviousHitObject = (OsuHitObject)(osuPrevious.BaseObject);

if (!(osuPrevious.BaseObject is Spinner))
{
double jumpDistance = (osuHitObject.StackedPosition - osuPreviousHitObject.EndPosition).Length;

cumulativeStrainTime += osuPrevious.StrainTime;

// We want to nerf objects that can be easily seen within the Flashlight circle radius.
MBmasher marked this conversation as resolved.
Show resolved Hide resolved
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, (osuPrevious.JumpDistance / scalingFactor) / 25.0);

result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
}
}
}

return Math.Pow(smallDistNerf * result, 2.0);
}
}
}