Add legacy reference health processor

This commit is contained in:
Dan Balasescu 2023-10-23 16:37:26 +09:00
parent 1a8636638d
commit fecc6f580b
No known key found for this signature in database
3 changed files with 287 additions and 1 deletions

View File

@ -0,0 +1,197 @@
// 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.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
// Reference implementation for osu!stable's HP drain.
public partial class LegacyOsuHealthProcessor : LegacyDrainingHealthProcessor
{
private const double hp_bar_maximum = 200;
private const double hp_combo_geki = 14;
private const double hp_hit_300 = 6;
private const double hp_slider_repeat = 4;
private const double hp_slider_tick = 3;
private double lowestHpEver;
private double lowestHpEnd;
private double lowestHpComboEnd;
private double hpRecoveryAvailable;
private double hpMultiplierNormal;
private double hpMultiplierComboEnd;
public LegacyOsuHealthProcessor(double drainStartTime)
: base(drainStartTime)
{
}
public override void ApplyBeatmap(IBeatmap beatmap)
{
lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 195, 160, 60);
lowestHpComboEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 170, 80);
lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 180, 80);
hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 8, 4, 0);
base.ApplyBeatmap(beatmap);
}
protected override void Reset(bool storeResults)
{
hpMultiplierNormal = 1;
hpMultiplierComboEnd = 1;
base.Reset(storeResults);
}
protected override double ComputeDrainRate()
{
double testDrop = 0.05;
double currentHp;
double currentHpUncapped;
do
{
currentHp = hp_bar_maximum;
currentHpUncapped = hp_bar_maximum;
double lowestHp = currentHp;
double lastTime = DrainStartTime;
int currentBreak = 0;
bool fail = false;
int comboTooLowCount = 0;
string failReason = string.Empty;
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;
// 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;
failReason = $"hp too low ({currentHp / hp_bar_maximum} < {lowestHpEver / hp_bar_maximum})";
break;
}
double hpReduction = testDrop * (h.GetEndTime() - h.StartTime);
double hpOverkill = Math.Max(0, hpReduction - currentHp);
reduceHp(hpReduction);
if (h is Slider slider)
{
for (int j = 0; j < slider.RepeatCount + 2; j++)
increaseHp(hpMultiplierNormal * hp_slider_repeat);
foreach (var _ in slider.NestedHitObjects.OfType<SliderTick>())
increaseHp(hpMultiplierNormal * hp_slider_tick);
}
else if (h is Spinner spinner)
{
foreach (var _ in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick))
increaseHp(hpMultiplierNormal * 1.7);
}
if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver)
{
fail = true;
testDrop *= 0.96;
failReason = $"overkill ({currentHp / hp_bar_maximum} - {hpOverkill / hp_bar_maximum} <= {lowestHpEver / hp_bar_maximum})";
break;
}
if (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo)
{
increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300);
if (currentHp < lowestHpComboEnd)
{
if (++comboTooLowCount > 2)
{
hpMultiplierComboEnd *= 1.07;
hpMultiplierNormal *= 1.03;
fail = true;
failReason = $"combo end hp too low ({currentHp / hp_bar_maximum} < {lowestHpComboEnd / hp_bar_maximum})";
break;
}
}
}
else
increaseHp(hpMultiplierNormal * hp_hit_300);
}
if (!fail && currentHp < lowestHpEnd)
{
fail = true;
testDrop *= 0.94;
hpMultiplierComboEnd *= 1.01;
hpMultiplierNormal *= 1.01;
failReason = $"end hp too low ({currentHp / hp_bar_maximum} < {lowestHpEnd / hp_bar_maximum})";
}
double recovery = (currentHpUncapped - hp_bar_maximum) / Beatmap.HitObjects.Count;
if (!fail && recovery < hpRecoveryAvailable)
{
fail = true;
testDrop *= 0.96;
hpMultiplierComboEnd *= 1.02;
hpMultiplierNormal *= 1.01;
failReason = $"recovery too low ({recovery / hp_bar_maximum} < {hpRecoveryAvailable / hp_bar_maximum})";
}
if (fail)
{
if (Log)
Console.WriteLine($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}");
continue;
}
if (Log)
Console.WriteLine($"PASSED drop {testDrop / hp_bar_maximum}");
return testDrop / hp_bar_maximum;
} while (true);
void reduceHp(double amount)
{
currentHpUncapped = Math.Max(0, currentHpUncapped - amount);
currentHp = Math.Max(0, currentHp - amount);
}
void increaseHp(double amount)
{
currentHpUncapped += amount;
currentHp = Math.Max(0, Math.Min(hp_bar_maximum, currentHp + amount));
}
}
}
}

View File

@ -42,7 +42,6 @@ public partial class DrainingHealthProcessor : HealthProcessor
private const double max_health_target = 0.4;
private IBeatmap beatmap;
private double gameplayEndTime;
private readonly double drainStartTime;

View File

@ -0,0 +1,90 @@
// 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.
#nullable disable
using System;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Scoring
{
/// <summary>
/// A <see cref="HealthProcessor"/> which continuously drains health.<br />
/// At HP=0, the minimum health reached for a perfect play is 95%.<br />
/// At HP=5, the minimum health reached for a perfect play is 70%.<br />
/// At HP=10, the minimum health reached for a perfect play is 30%.
/// </summary>
public abstract partial class LegacyDrainingHealthProcessor : HealthProcessor
{
protected double DrainStartTime { get; }
protected double GameplayEndTime { get; private set; }
protected IBeatmap Beatmap { get; private set; }
protected PeriodTracker NoDrainPeriodTracker { get; private set; }
public bool Log { get; set; }
public double DrainRate { get; private set; }
/// <summary>
/// Creates a new <see cref="DrainingHealthProcessor"/>.
/// </summary>
/// <param name="drainStartTime">The time after which draining should begin.</param>
protected LegacyDrainingHealthProcessor(double drainStartTime)
{
DrainStartTime = drainStartTime;
}
protected override void Update()
{
base.Update();
if (NoDrainPeriodTracker?.IsInAny(Time.Current) == true)
return;
// When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time
double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, DrainStartTime, GameplayEndTime);
double currentGameplayTime = Math.Clamp(Time.Current, DrainStartTime, GameplayEndTime);
Health.Value -= DrainRate * (currentGameplayTime - lastGameplayTime);
}
public override void ApplyBeatmap(IBeatmap beatmap)
{
Beatmap = beatmap;
if (beatmap.HitObjects.Count > 0)
GameplayEndTime = beatmap.HitObjects[^1].GetEndTime();
NoDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period(
beatmap.HitObjects
.Select(hitObject => hitObject.GetEndTime())
.Where(endTime => endTime <= breakPeriod.StartTime)
.DefaultIfEmpty(double.MinValue)
.Last(),
beatmap.HitObjects
.Select(hitObject => hitObject.StartTime)
.Where(startTime => startTime >= breakPeriod.EndTime)
.DefaultIfEmpty(double.MaxValue)
.First()
)));
base.ApplyBeatmap(beatmap);
}
protected override void Reset(bool storeResults)
{
base.Reset(storeResults);
DrainRate = 1;
if (storeResults)
DrainRate = ComputeDrainRate();
}
protected abstract double ComputeDrainRate();
}
}