osu/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

221 lines
8.2 KiB
C#
Raw Normal View History

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
2023-11-13 04:46:47 +00:00
public partial class OsuHealthProcessor : DrainingHealthProcessor
{
2023-11-13 04:46:47 +00:00
public Action<string>? OnIterationFail;
public Action<string>? OnIterationSuccess;
private double lowestHpEver;
private double lowestHpEnd;
private double hpRecoveryAvailable;
private double hpMultiplierNormal;
public OsuHealthProcessor(double drainStartTime)
: base(drainStartTime)
{
}
public override void ApplyBeatmap(IBeatmap beatmap)
{
lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3);
lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4);
hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0);
base.ApplyBeatmap(beatmap);
}
protected override void Reset(bool storeResults)
{
hpMultiplierNormal = 1;
base.Reset(storeResults);
}
protected override double ComputeDrainRate()
{
double testDrop = 0.00025;
double currentHp;
double currentHpUncapped;
while (true)
{
currentHp = 1;
currentHpUncapped = 1;
double lowestHp = currentHp;
double lastTime = DrainStartTime;
int currentBreak = 0;
bool fail = false;
for (int i = 0; i < Beatmap.HitObjects.Count; i++)
{
HitObject h = Beatmap.HitObjects[i];
// Find active break (between current and lastTime)
double localLastTime = lastTime;
double breakTime = 0;
2023-11-10 10:09:09 +00:00
// TODO: This doesn't handle overlapping/sequential breaks correctly (/b/614).
// Subtract any break time from the duration since the last object
if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count)
{
BreakPeriod e = Beatmap.Breaks[currentBreak];
if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime)
{
// consider break start equal to object end time for version 8+ since drain stops during this time
breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime;
currentBreak++;
}
}
reduceHp(testDrop * (h.StartTime - lastTime - breakTime));
lastTime = h.GetEndTime();
if (currentHp < lowestHp)
lowestHp = currentHp;
if (currentHp <= lowestHpEver)
{
fail = true;
testDrop *= 0.96;
OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})");
break;
}
double hpReduction = testDrop * (h.GetEndTime() - h.StartTime);
double hpOverkill = Math.Max(0, hpReduction - currentHp);
reduceHp(hpReduction);
if (h is Slider slider)
{
foreach (var nested in slider.NestedHitObjects)
increaseHp(nested);
}
else if (h is Spinner spinner)
{
foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick))
increaseHp(nested);
}
2023-11-10 10:09:09 +00:00
// Note: Because HP is capped during the above increases, long sliders (with many ticks) or spinners
// will appear to overkill at lower drain levels than they should. However, it is also not correct to simply use the uncapped version.
if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver)
{
fail = true;
testDrop *= 0.96;
OnIterationFail?.Invoke($"FAILED drop {testDrop}: overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})");
break;
}
increaseHp(h);
}
if (!fail && currentHp < lowestHpEnd)
{
fail = true;
testDrop *= 0.94;
hpMultiplierNormal *= 1.01;
OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})");
}
double recovery = (currentHpUncapped - 1) / Beatmap.HitObjects.Count;
if (!fail && recovery < hpRecoveryAvailable)
{
fail = true;
testDrop *= 0.96;
hpMultiplierNormal *= 1.01;
OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})");
}
if (!fail)
{
OnIterationSuccess?.Invoke($"PASSED drop {testDrop}");
return testDrop;
}
}
void reduceHp(double amount)
{
currentHpUncapped = Math.Max(0, currentHpUncapped - amount);
currentHp = Math.Max(0, currentHp - amount);
}
void increaseHp(HitObject hitObject)
{
double amount = healthIncreaseFor(hitObject, hitObject.CreateJudgement().MaxResult);
currentHpUncapped += amount;
currentHp = Math.Max(0, Math.Min(1, currentHp + amount));
}
}
protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.HitObject, result.Type);
private double healthIncreaseFor(HitObject hitObject, HitResult result)
{
double increase;
switch (result)
{
case HitResult.SmallTickMiss:
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
case HitResult.LargeTickMiss:
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
case HitResult.Miss:
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2);
case HitResult.SmallTickHit:
// This result always comes from the slider tail, which is judged the same as a repeat.
increase = 0.02;
break;
case HitResult.LargeTickHit:
// This result comes from either a slider tick or repeat.
increase = hitObject is SliderTick ? 0.015 : 0.02;
break;
case HitResult.Meh:
increase = 0.002;
break;
case HitResult.Ok:
increase = 0.011;
break;
case HitResult.Great:
increase = 0.03;
break;
case HitResult.SmallBonus:
increase = 0.0085;
break;
case HitResult.LargeBonus:
increase = 0.01;
break;
default:
increase = 0;
break;
}
return hpMultiplierNormal * increase;
}
}
}