1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-06 07:42:55 +08:00

Merge pull request #25420 from smoogipoo/hp-drain-fix-breaks

Fix osu! and base HP processor break time simulation
This commit is contained in:
Dean Herbert 2023-11-24 15:18:04 +09:00 committed by GitHub
commit 2f3c05154d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 202 additions and 41 deletions

View File

@ -0,0 +1,93 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestSceneOsuHealthProcessor
{
[Test]
public void TestNoBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 2000 }
}
});
Assert.That(hp.DrainRate, Is.EqualTo(1.4E-5).Within(0.1E-5));
}
[Test]
public void TestSingleBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1500)
}
});
Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5));
}
[Test]
public void TestOverlappingBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1400),
new BreakPeriod(750, 1500),
}
});
Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5));
}
[Test]
public void TestSequentialBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1000),
new BreakPeriod(1000, 1500),
}
});
Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5));
}
}
}

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Judgements; 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;
@ -62,26 +61,16 @@ namespace osu.Game.Rulesets.Osu.Scoring
{ {
HitObject h = Beatmap.HitObjects[i]; HitObject h = Beatmap.HitObjects[i];
// Find active break (between current and lastTime) while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime)
double localLastTime = lastTime;
double breakTime = 0;
// TODO: This doesn't handle overlapping/sequential breaks correctly (/b/614).
// Subtract any break time from the duration since the last object
// Note that this method is a bit convoluted, but matches stable code for compatibility.
if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count)
{ {
BreakPeriod e = Beatmap.Breaks[currentBreak]; // 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,
if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) // but this shouldn't have a noticeable impact in practice.
{ lastTime = h.StartTime;
// consider break start equal to object end time for version 8+ since drain stops during this time currentBreak++;
breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime;
currentBreak++;
}
} }
reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); reduceHp(testDrop * (h.StartTime - lastTime));
lastTime = h.GetEndTime(); lastTime = h.GetEndTime();

View File

@ -192,7 +192,8 @@ namespace osu.Game.Tests.Gameplay
AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
AddAssert("not failed", () => !processor.HasFailed); AddAssert("not failed", () => !processor.HasFailed);
AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result",
() => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied }));
AddAssert("failed", () => processor.HasFailed); AddAssert("failed", () => processor.HasFailed);
} }
@ -232,6 +233,84 @@ namespace osu.Game.Tests.Gameplay
assertHealthEqualTo(1); assertHealthEqualTo(1);
} }
[Test]
public void TestNoBreakDrainRate()
{
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
{
HitObjects =
{
new JudgeableHitObject { StartTime = 0 },
new JudgeableHitObject { StartTime = 2000 }
}
});
Assert.That(hp.DrainRate, Is.EqualTo(4.5E-5).Within(0.1E-5));
}
[Test]
public void TestSingleBreakDrainRate()
{
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
{
HitObjects =
{
new JudgeableHitObject { StartTime = 0 },
new JudgeableHitObject { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1500)
}
});
Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5));
}
[Test]
public void TestOverlappingBreakDrainRate()
{
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
{
HitObjects =
{
new JudgeableHitObject { StartTime = 0 },
new JudgeableHitObject { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1400),
new BreakPeriod(750, 1500),
}
});
Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5));
}
[Test]
public void TestSequentialBreakDrainRate()
{
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
{
HitObjects =
{
new JudgeableHitObject { StartTime = 0 },
new JudgeableHitObject { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1000),
new BreakPeriod(1000, 1500),
}
});
Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5));
}
private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks) private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks)
{ {
var beatmap = new Beatmap var beatmap = new Beatmap

View File

@ -103,18 +103,20 @@ namespace osu.Game.Rulesets.Scoring
if (beatmap.HitObjects.Count > 0) if (beatmap.HitObjects.Count > 0)
gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); gameplayEndTime = beatmap.HitObjects[^1].GetEndTime();
noDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period( noDrainPeriodTracker = new PeriodTracker(
beatmap.HitObjects beatmap.Breaks.Select(breakPeriod =>
.Select(hitObject => hitObject.GetEndTime()) new Period(
.Where(endTime => endTime <= breakPeriod.StartTime) beatmap.HitObjects
.DefaultIfEmpty(double.MinValue) .Select(hitObject => hitObject.GetEndTime())
.Last(), .Where(endTime => endTime <= breakPeriod.StartTime)
beatmap.HitObjects .DefaultIfEmpty(double.MinValue)
.Select(hitObject => hitObject.StartTime) .Last(),
.Where(startTime => startTime >= breakPeriod.EndTime) beatmap.HitObjects
.DefaultIfEmpty(double.MaxValue) .Select(hitObject => hitObject.StartTime)
.First() .Where(startTime => startTime >= breakPeriod.EndTime)
))); .DefaultIfEmpty(double.MaxValue)
.First()
)));
targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, min_health_target, mid_health_target, max_health_target); targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, min_health_target, mid_health_target, max_health_target);
@ -159,26 +161,24 @@ namespace osu.Game.Rulesets.Scoring
{ {
double currentHealth = 1; double currentHealth = 1;
double lowestHealth = 1; double lowestHealth = 1;
int currentBreak = -1; int currentBreak = 0;
for (int i = 0; i < healthIncreases.Count; i++) for (int i = 0; i < healthIncreases.Count; i++)
{ {
double currentTime = healthIncreases[i].time; double currentTime = healthIncreases[i].time;
double lastTime = i > 0 ? healthIncreases[i - 1].time : DrainStartTime; double lastTime = i > 0 ? healthIncreases[i - 1].time : DrainStartTime;
// Subtract any break time from the duration since the last object while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= currentTime)
if (Beatmap.Breaks.Count > 0)
{ {
// Advance the last break occuring before the current time // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects.
while (currentBreak + 1 < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak + 1].EndTime < currentTime) // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered,
currentBreak++; // but this shouldn't have a noticeable impact in practice.
lastTime = currentTime;
if (currentBreak >= 0) currentBreak++;
lastTime = Math.Max(lastTime, Beatmap.Breaks[currentBreak].EndTime);
} }
// Apply health adjustments // Apply health adjustments
currentHealth -= (healthIncreases[i].time - lastTime) * result; currentHealth -= (currentTime - lastTime) * result;
lowestHealth = Math.Min(lowestHealth, currentHealth); lowestHealth = Math.Min(lowestHealth, currentHealth);
currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health);