Skip to content

Commit

Permalink
Revert "Replace vibro nerf with a faster (and still accurate) alterna…
Browse files Browse the repository at this point in the history
…tive"

This reverts commit 8826443.
  • Loading branch information
Rian8337 committed Jan 24, 2025
1 parent 8826443 commit 6b9d109
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ class DroidDifficultyAttributes : DifficultyAttributes() {
@JvmField
var visualDifficultStrainCount = 0.0

/**
* The average delta time of speed objects.
*/
@JvmField
var averageSpeedDeltaTime = 0.0

/**
* Possible sections at which the player can use three fingers on.
*/
Expand Down Expand Up @@ -92,4 +98,14 @@ class DroidDifficultyAttributes : DifficultyAttributes() {
*/
@JvmField
var visualSliderFactor = 1.0

/**
* Describes how much of tap difficulty is contributed by notes that are "vibroable".
*
* A value closer to 1 indicates most tap difficulty is contributed by notes that are not "vibroable".
*
* A value closer to 0 indicates most tap difficulty is contributed by notes that are "vibroable".
*/
@JvmField
var vibroFactor = 1.0
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ class DroidDifficultyCalculator : DifficultyCalculator<DroidPlayableBeatmap, Dro
tapDifficulty = calculateRating(it)
tapDifficultStrainCount = it.countDifficultStrains()
speedNoteCount = it.relevantNoteCount()
averageSpeedDeltaTime = it.relevantDeltaTime()

if (tapDifficulty > 0) {
val tapSkillVibro = DroidTap(mods, true, averageSpeedDeltaTime)

objects.forEach { o -> tapSkillVibro.process(o) }

vibroFactor = calculateRating(tapSkillVibro) / tapDifficulty
}
}

var firstObjectIndex = 0
Expand Down Expand Up @@ -253,7 +262,7 @@ class DroidDifficultyCalculator : DifficultyCalculator<DroidPlayableBeatmap, Dro
) = beatmap.createDroidPlayableBeatmap(
parameters?.mods,
parameters?.customSpeedMultiplier ?: 1f,
parameters?.oldStatistics == true,
parameters?.oldStatistics ?: false,
scope
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.rian.osu.beatmap.PreciseDroidHitWindow
import com.rian.osu.difficulty.attributes.DroidDifficultyAttributes
import com.rian.osu.difficulty.attributes.DroidPerformanceAttributes
import com.rian.osu.math.ErrorFunction
import com.rian.osu.math.Interpolation
import com.rian.osu.mods.*
import com.rian.osu.replay.SliderCheesePenalty
import kotlin.math.*
Expand Down Expand Up @@ -142,11 +141,25 @@ class DroidPerformanceCalculator(
// also cap the note count to prevent buffing filler patterns.
tapValue *= calculateDeviationBasedLengthScaling(min(speedNoteCount, totalHits / 1.45))

// Normalize the deviation to 300 BPM.
val normalizedDeviation = tapDeviation * max(1.0, 50 / averageSpeedDeltaTime)

// We expect the player to get 7500/x deviation when doubletapping x BPM.
// Using this expectation, we penalize score with deviation above 25.
val averageBPM = 60000 / 4 / averageSpeedDeltaTime

val adjustedDeviation = normalizedDeviation *
(1 + 1 / (1 + exp(-(normalizedDeviation - 7500 / averageBPM) / (2 * 300 / averageBPM))))

// Scale the tap value with tap deviation.
tapValue *= 1.05 * ErrorFunction.erf(20 / (sqrt(2.0) * tapDeviation)).pow(0.6)
tapValue *= 1.05 * ErrorFunction.erf(20 / (sqrt(2.0) * adjustedDeviation)).pow(0.6)

// Scale the tap value with high deviation nerf.
tapValue *= calculateTapHighDeviationNerf()
// Additional scaling for tap value based on average BPM and how "vibroable" the beatmap is.
// Higher BPMs require more precise tapping. When the deviation is too high,
// it can be assumed that the player taps invariant to rhythm.
// We make the punishment harsher punishment for such scenario.
tapValue *= vibroFactor.pow(6) +
(1 - vibroFactor.pow(6)) / (1 + exp((tapDeviation - 7500 / averageBPM) / (2 * 300 / averageBPM)))

// Scale the tap value with three-fingered penalty.
tapValue /= tapPenalty
Expand Down Expand Up @@ -428,37 +441,6 @@ class DroidPerformanceCalculator(
Double.POSITIVE_INFINITY
}

/**
* Calculates multiplier for tap to account for improper tapping based on the deviation and tap difficulty.
*
* https://www.desmos.com/calculator/dmogdhzofn
*/
private fun calculateTapHighDeviationNerf(): Double {
if (tapDeviation == Double.POSITIVE_INFINITY) {
return 0.0
}

val tapValue = (5 * max(1.0, difficultyAttributes.tapDifficulty / 0.0675) - 4).pow(3) / 100000

// Decide a point where the PP value achieved compared to the tap deviation is assumed to be
// tapped improperly. Any PP above this point is considered "excess" tap difficulty.
// This is used to cause PP above the cutoff to scale logarithmically towards the original
// tap value, thus nerfing the value.
val excessTapDifficultyCutoff = 100 + 220 * (22 / tapDeviation).pow(6.5)

if (tapValue <= excessTapDifficultyCutoff) {
return 1.0
}

val scale = 50
val adjustedTapValue = scale * ln((tapValue - excessTapDifficultyCutoff) / scale + 1) + excessTapDifficultyCutoff / scale

// 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
val lerp = 1 - ((tapDeviation - 20) / (24 - 20)).coerceIn(0.0, 1.0)

return Interpolation.linear(adjustedTapValue, tapValue, lerp) / tapValue
}

private fun getConvertedHitWindow(): HitWindow {
var od = difficultyAttributes.overallDifficulty.toFloat()
var hitWindow = if (isPrecise) PreciseDroidHitWindow(od) else DroidHitWindow(od)
Expand Down
16 changes: 13 additions & 3 deletions src/com/rian/osu/difficulty/evaluators/DroidTapEvaluator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.rian.osu.difficulty.evaluators
import com.rian.osu.beatmap.hitobject.Spinner
import com.rian.osu.difficulty.DroidDifficultyHitObject
import com.rian.osu.math.ErrorFunction
import kotlin.math.max
import kotlin.math.pow

/**
Expand All @@ -16,15 +17,19 @@ object DroidTapEvaluator {
* Evaluates the difficulty of tapping the current object, based on:
*
* * time between pressing the previous and current object,
* * distance between those objects,
* * and how easily they can be cheesed.
*
* @param current The current object.
* @param considerCheesability Whether to consider cheesability.
* @param strainTimeCap The strain time to cap to.
*/
@JvmStatic
@JvmOverloads
fun evaluateDifficultyOf(
current: DroidDifficultyHitObject,
considerCheesability: Boolean
considerCheesability: Boolean,
strainTimeCap: Double? = null
): Double {
if (
current.obj is Spinner ||
Expand All @@ -35,12 +40,17 @@ object DroidTapEvaluator {
}

val doubletapness = if (considerCheesability) 1 - current.doubletapness else 1.0

val strainTime =
if (strainTimeCap != null) max(50.0, max(strainTimeCap, current.strainTime))
else current.strainTime

var speedBonus = 1.0

if (current.strainTime < MIN_SPEED_BONUS) {
speedBonus += 0.75 * ErrorFunction.erf((MIN_SPEED_BONUS - current.strainTime) / 40).pow(2)
speedBonus += 0.75 * ErrorFunction.erf((MIN_SPEED_BONUS - strainTime) / 40).pow(2)
}

return speedBonus * doubletapness.pow(1.5) * 1000 / current.strainTime
return speedBonus * doubletapness.pow(1.5) * 1000 / strainTime
}
}
34 changes: 32 additions & 2 deletions src/com/rian/osu/difficulty/skills/DroidTap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ class DroidTap(
/**
* Whether to consider cheesability.
*/
private val considerCheesability: Boolean
private val considerCheesability: Boolean,

/**
* The strain time to cap to.
*/
private val strainTimeCap: Double? = null
) : DroidStrainSkill(mods) {
override val starsPerDouble = 1.1

Expand All @@ -28,6 +33,8 @@ class DroidTap(
private val skillMultiplier = 1.375
private val strainDecayBase = 0.3

private val objectDeltaTimes = mutableListOf<Double>()

/**
* Gets the amount of notes that are relevant to the difficulty.
*/
Expand All @@ -44,15 +51,38 @@ class DroidTap(
fold(0.0) { acc, d -> acc + 1 / (1 + exp(-(d / maxStrain * 12 - 6))) }
}

/**
* Gets the delta time relevant to the difficulty.
*/
fun relevantDeltaTime() = objectStrains.run {
if (isEmpty()) {
return 0.0
}

val maxStrain = max()
if (maxStrain == 0.0) {
return 0.0
}

objectDeltaTimes.foldIndexed(0.0) { i, acc, d ->
acc + d / (1 + exp(-(this[i] / maxStrain * 25 - 20)))
} / fold(0.0) { acc, d ->
acc + 1 / (1 + exp(-(d / maxStrain * 25 - 20)))
}
}

override fun strainValueAt(current: DroidDifficultyHitObject): Double {
currentStrain *= strainDecay(current.strainTime)
currentStrain += DroidTapEvaluator.evaluateDifficultyOf(current, considerCheesability) * skillMultiplier
currentStrain += DroidTapEvaluator.evaluateDifficultyOf(
current, considerCheesability, strainTimeCap
) * skillMultiplier

currentRhythm = current.rhythmMultiplier

val totalStrain = currentStrain * currentRhythm

objectStrains.add(totalStrain)
objectDeltaTimes.add(current.deltaTime)

return totalStrain
}
Expand Down

0 comments on commit 6b9d109

Please sign in to comment.