Skip to content

Commit

Permalink
Merge pull request #25569 from smoogipoo/legacy-hp-abstraction
Browse files Browse the repository at this point in the history
Encapsulate common HP logic from osu and catch HP calculations
  • Loading branch information
bdach authored Nov 27, 2023
2 parents 6eebf63 + 8314f65 commit e2d5197
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 255 deletions.
121 changes: 5 additions & 116 deletions osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,138 +1,27 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;

namespace osu.Game.Rulesets.Catch.Scoring
{
public partial class CatchHealthProcessor : DrainingHealthProcessor
public partial class CatchHealthProcessor : LegacyDrainingHealthProcessor
{
public Action<string>? OnIterationFail;
public Action<string>? OnIterationSuccess;

private double lowestHpEver;
private double lowestHpEnd;
private double hpRecoveryAvailable;
private double hpMultiplierNormal;

public CatchHealthProcessor(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;

List<HitObject> allObjects = EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana).ToList();

for (int i = 0; i < allObjects.Count; i++)
{
HitObject h = allObjects[i];

while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime)
{
// If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects.
// This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered,
// but this shouldn't have a noticeable impact in practice.
lastTime = h.StartTime;
currentBreak++;
}

reduceHp(testDrop * (h.StartTime - lastTime));

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;
}

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) / allObjects.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.CreateJudgement().MaxResult);
currentHpUncapped += amount;
currentHp = Math.Max(0, Math.Min(1, currentHp + amount));
}
}
protected override IEnumerable<HitObject> EnumerateTopLevelHitObjects() => EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana);

protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.Type);
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();

private double healthIncreaseFor(HitResult result)
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
{
double increase = 0;

Expand Down Expand Up @@ -162,7 +51,7 @@ private double healthIncreaseFor(HitResult result)
break;
}

return hpMultiplierNormal * increase;
return HpMultiplierNormal * increase;
}
}
}
155 changes: 16 additions & 139 deletions osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,166 +1,43 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
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
{
public partial class OsuHealthProcessor : DrainingHealthProcessor
public partial class OsuHealthProcessor : LegacyDrainingHealthProcessor
{
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 IEnumerable<HitObject> EnumerateTopLevelHitObjects() => Beatmap.HitObjects;

protected override void Reset(bool storeResults)
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject)
{
hpMultiplierNormal = 1;
base.Reset(storeResults);
}

protected override double ComputeDrainRate()
{
double testDrop = 0.00025;
double currentHp;
double currentHpUncapped;

while (true)
switch (hitObject)
{
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];

while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime)
{
// If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects.
// This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered,
// but this shouldn't have a noticeable impact in practice.
lastTime = h.StartTime;
currentBreak++;
}

reduceHp(testDrop * (h.StartTime - lastTime));

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

switch (h)
{
case Slider slider:
{
foreach (var nested in slider.NestedHitObjects)
increaseHp(nested);
break;
}

case Spinner spinner:
{
foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick))
increaseHp(nested);
break;
}
}

// 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;
}
}
case Slider slider:
foreach (var nested in slider.NestedHitObjects)
yield return nested;

void reduceHp(double amount)
{
currentHpUncapped = Math.Max(0, currentHpUncapped - amount);
currentHp = Math.Max(0, currentHp - amount);
}
break;

void increaseHp(HitObject hitObject)
{
double amount = healthIncreaseFor(hitObject, hitObject.CreateJudgement().MaxResult);
currentHpUncapped += amount;
currentHp = Math.Max(0, Math.Min(1, currentHp + amount));
case Spinner spinner:
foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick))
yield return nested;

break;
}
}

protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.HitObject, result.Type);

private double healthIncreaseFor(HitObject hitObject, HitResult result)
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
{
double increase = 0;

Expand Down Expand Up @@ -206,7 +83,7 @@ private double healthIncreaseFor(HitObject hitObject, HitResult result)
break;
}

return hpMultiplierNormal * increase;
return HpMultiplierNormal * increase;
}
}
}
Loading

0 comments on commit e2d5197

Please sign in to comment.