mirror of
https://github.com/ppy/osu.git
synced 2024-11-13 16:47:46 +08:00
Encapsulate common HP logic from osu and catch HP calculations
This commit is contained in:
parent
55df8e81b9
commit
8314f656a3
@ -1,138 +1,27 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Judgements;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.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)
|
public CatchHealthProcessor(double drainStartTime)
|
||||||
: base(drainStartTime)
|
: base(drainStartTime)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ApplyBeatmap(IBeatmap beatmap)
|
protected override IEnumerable<HitObject> EnumerateTopLevelHitObjects() => EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana);
|
||||||
{
|
|
||||||
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> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Reset(bool storeResults)
|
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||||
{
|
|
||||||
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 double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.Type);
|
|
||||||
|
|
||||||
private double healthIncreaseFor(HitResult result)
|
|
||||||
{
|
{
|
||||||
double increase = 0;
|
double increase = 0;
|
||||||
|
|
||||||
@ -162,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hpMultiplierNormal * increase;
|
return HpMultiplierNormal * increase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,166 +1,43 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Judgements;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.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)
|
public OsuHealthProcessor(double drainStartTime)
|
||||||
: base(drainStartTime)
|
: base(drainStartTime)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ApplyBeatmap(IBeatmap beatmap)
|
protected override IEnumerable<HitObject> EnumerateTopLevelHitObjects() => Beatmap.HitObjects;
|
||||||
|
|
||||||
|
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject)
|
||||||
{
|
{
|
||||||
lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3);
|
switch (hitObject)
|
||||||
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;
|
case Slider slider:
|
||||||
currentHpUncapped = 1;
|
foreach (var nested in slider.NestedHitObjects)
|
||||||
|
yield return nested;
|
||||||
|
|
||||||
double lowestHp = currentHp;
|
break;
|
||||||
double lastTime = DrainStartTime;
|
|
||||||
int currentBreak = 0;
|
|
||||||
bool fail = false;
|
|
||||||
|
|
||||||
for (int i = 0; i < Beatmap.HitObjects.Count; i++)
|
case Spinner spinner:
|
||||||
{
|
foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick))
|
||||||
HitObject h = Beatmap.HitObjects[i];
|
yield return nested;
|
||||||
|
|
||||||
while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime)
|
break;
|
||||||
{
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||||
|
|
||||||
private double healthIncreaseFor(HitObject hitObject, HitResult result)
|
|
||||||
{
|
{
|
||||||
double increase = 0;
|
double increase = 0;
|
||||||
|
|
||||||
@ -206,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hpMultiplierNormal * increase;
|
return HpMultiplierNormal * increase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
158
osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs
Normal file
158
osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Scoring
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="DrainingHealthProcessor"/> that matches legacy drain rate calculations as best as possible.
|
||||||
|
/// </summary>
|
||||||
|
public abstract partial class LegacyDrainingHealthProcessor : DrainingHealthProcessor
|
||||||
|
{
|
||||||
|
public Action<string>? OnIterationFail;
|
||||||
|
public Action<string>? OnIterationSuccess;
|
||||||
|
|
||||||
|
protected double HpMultiplierNormal { get; private set; }
|
||||||
|
|
||||||
|
private double lowestHpEver;
|
||||||
|
private double lowestHpEnd;
|
||||||
|
private double hpRecoveryAvailable;
|
||||||
|
|
||||||
|
protected LegacyDrainingHealthProcessor(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;
|
||||||
|
int topLevelObjectCount = 0;
|
||||||
|
|
||||||
|
foreach (var h in EnumerateTopLevelHitObjects())
|
||||||
|
{
|
||||||
|
topLevelObjectCount++;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
foreach (var nested in EnumerateNestedHitObjects(h))
|
||||||
|
increaseHp(nested);
|
||||||
|
|
||||||
|
// 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) / Math.Max(1, topLevelObjectCount);
|
||||||
|
|
||||||
|
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 = GetHealthIncreaseFor(hitObject, hitObject.CreateJudgement().MaxResult);
|
||||||
|
currentHpUncapped += amount;
|
||||||
|
currentHp = Math.Max(0, Math.Min(1, currentHp + amount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sealed override double GetHealthIncreaseFor(JudgementResult result) => GetHealthIncreaseFor(result.HitObject, result.Type);
|
||||||
|
|
||||||
|
protected abstract IEnumerable<HitObject> EnumerateTopLevelHitObjects();
|
||||||
|
|
||||||
|
protected abstract IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject);
|
||||||
|
|
||||||
|
protected abstract double GetHealthIncreaseFor(HitObject hitObject, HitResult result);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user