1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-21 08:52:54 +08:00

Merge branch 'ppy:master' into adjust_beatmap_length_according_to_rate

This commit is contained in:
Simon G 2023-12-22 18:08:35 +01:00 committed by GitHub
commit 3487b5865e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 453 additions and 33 deletions

View File

@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Mods namespace osu.Game.Rulesets.Catch.Tests.Mods
{ {
public partial class TestSceneCatchModPerfect : ModPerfectTestScene public partial class TestSceneCatchModPerfect : ModFailConditionTestScene
{ {
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();

View File

@ -1,14 +1,19 @@
// 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.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
public partial class TestSceneManiaModPerfect : ModPerfectTestScene public partial class TestSceneManiaModPerfect : ModFailConditionTestScene
{ {
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
@ -24,5 +29,52 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
[TestCase(false)] [TestCase(false)]
[TestCase(true)] [TestCase(true)]
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
[Test]
public void TestGreatHit() => CreateModTest(new ModTestData
{
Mod = new ManiaModPerfect(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Note
{
StartTime = 1000,
}
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1020, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
[Test]
public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
{
Mod = new ManiaModPerfect(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HoldNote
{
StartTime = 1000,
EndTime = 3000,
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1000, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
} }
} }

View File

@ -0,0 +1,72 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModSuddenDeath : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
public TestSceneManiaModSuddenDeath()
: base(new ManiaModSuddenDeath())
{
}
[Test]
public void TestGreatHit() => CreateModTest(new ModTestData
{
Mod = new ManiaModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Note
{
StartTime = 1000,
}
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1020, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
[Test]
public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
{
Mod = new ManiaModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HoldNote
{
StartTime = 1000,
EndTime = 3000,
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1000, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
}
}

View File

@ -1,11 +1,26 @@
// 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 osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModPerfect : ModPerfect public class ManiaModPerfect : ModPerfect
{ {
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
{
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
return false;
// Mania allows imperfect "Great" hits without failing.
if (result.Judgement.MaxResult == HitResult.Perfect)
return result.Type < HitResult.Great;
return result.Type != result.Judgement.MaxResult;
}
private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo();
} }
} }

View File

@ -1,17 +1,21 @@
// 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.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
public partial class TestSceneOsuModPerfect : ModPerfectTestScene public partial class TestSceneOsuModPerfect : ModFailConditionTestScene
{ {
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
@ -50,5 +54,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss); CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss);
} }
[Test]
public void TestMissSliderTail() => CreateModTest(new ModTestData
{
Mod = new OsuModPerfect(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
Position = new Vector2(256, 192),
StartTime = 1000,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
new OsuReplayFrame(1001, new Vector2(256, 192)),
}
});
} }
} }

View File

@ -0,0 +1,53 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModStrictTracking : OsuModTestScene
{
[Test]
public void TestSliderInput() => CreateModTest(new ModTestData
{
Mod = new OsuModStrictTracking(),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 1000,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(0, 100))
}
}
}
}
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
new OsuReplayFrame(500, new Vector2(200, 0), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200, 0)),
new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
new OsuReplayFrame(1750, new Vector2(0, 100), OsuAction.LeftButton),
new OsuReplayFrame(1751, new Vector2(0, 100)),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
});
}
}

View File

@ -0,0 +1,77 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModSuddenDeath : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
public TestSceneOsuModSuddenDeath()
: base(new OsuModSuddenDeath())
{
}
[Test]
public void TestMissTail() => CreateModTest(new ModTestData
{
Mod = new OsuModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
Position = new Vector2(256, 192),
StartTime = 1000,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
new OsuReplayFrame(1001, new Vector2(256, 192)),
}
});
[Test]
public void TestMissTick() => CreateModTest(new ModTestData
{
Mod = new OsuModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
Position = new Vector2(256, 192),
StartTime = 1000,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), })
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
new OsuReplayFrame(1001, new Vector2(256, 192)),
}
});
}
}

View File

@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods
if (!slider.HeadCircle.IsHit) if (!slider.HeadCircle.IsHit)
handleHitCircle(slider.HeadCircle); handleHitCircle(slider.HeadCircle);
requiresHold |= slider.Ball.IsHovered || h.IsHovered; requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(true);
break; break;
case DrawableSpinner spinner: case DrawableSpinner spinner:

View File

@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
if (e.NewValue || slider.Judged) return; if (e.NewValue || slider.Judged) return;
if (slider.Time.Current < slider.HitObject.StartTime)
return;
var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First(); var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First();
if (!tail.Judged) if (!tail.Judged)

View File

@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
updateTracking(isMouseInFollowArea(Tracking)); updateTracking(IsMouseInFollowArea(Tracking));
} }
public void PostProcessHeadJudgement(DrawableSliderHead head) public void PostProcessHeadJudgement(DrawableSliderHead head)
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!head.Judged || !head.Result.IsHit) if (!head.Judged || !head.Result.IsHit)
return; return;
if (!isMouseInFollowArea(true)) if (!IsMouseInFollowArea(true))
return; return;
Debug.Assert(screenSpaceMousePosition != null); Debug.Assert(screenSpaceMousePosition != null);
@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// If all ticks were hit so far, enable tracking the full extent. // If all ticks were hit so far, enable tracking the full extent.
// If any ticks were missed, assume tracking would've broken at some point, and should only activate if the cursor is within the slider ball. // If any ticks were missed, assume tracking would've broken at some point, and should only activate if the cursor is within the slider ball.
// For the second case, this may be the last chance we have to enable tracking before other objects get judged, otherwise the same would normally happen via Update(). // For the second case, this may be the last chance we have to enable tracking before other objects get judged, otherwise the same would normally happen via Update().
updateTracking(allTicksInRange || isMouseInFollowArea(false)); updateTracking(allTicksInRange || IsMouseInFollowArea(false));
} }
public void TryJudgeNestedObject(DrawableOsuHitObject nestedObject, double timeOffset) public void TryJudgeNestedObject(DrawableOsuHitObject nestedObject, double timeOffset)
@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// Whether the mouse is currently in the follow area. /// Whether the mouse is currently in the follow area.
/// </summary> /// </summary>
/// <param name="expanded">Whether to test against the maximum area of the follow circle.</param> /// <param name="expanded">Whether to test against the maximum area of the follow circle.</param>
private bool isMouseInFollowArea(bool expanded) public bool IsMouseInFollowArea(bool expanded)
{ {
if (screenSpaceMousePosition is not Vector2 pos) if (screenSpaceMousePosition is not Vector2 pos)
return false; return false;

View File

@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Osu
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new OsuHealthProcessor(drainStartTime);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this);
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap);

View File

@ -0,0 +1,72 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public partial class OsuHealthProcessor : DrainingHealthProcessor
{
public OsuHealthProcessor(double drainStartTime, double drainLenience = 0)
: base(drainStartTime, drainLenience)
{
}
protected override int? GetDensityGroup(HitObject hitObject) => (hitObject as IHasComboInformation)?.ComboIndex;
protected override double GetHealthIncreaseFor(JudgementResult result)
{
switch (result.Type)
{
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:
// When classic slider mechanics are enabled, this result comes from the tail.
return 0.02;
case HitResult.LargeTickHit:
switch (result.HitObject)
{
case SliderTick:
return 0.015;
case SliderHeadCircle:
case SliderTailCircle:
case SliderRepeat:
return 0.02;
}
break;
case HitResult.Meh:
return 0.002;
case HitResult.Ok:
return 0.011;
case HitResult.Great:
return 0.03;
case HitResult.SmallBonus:
return 0.0085;
case HitResult.LargeBonus:
return 0.01;
}
return base.GetHealthIncreaseFor(result);
}
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Mods namespace osu.Game.Rulesets.Taiko.Tests.Mods
{ {
public partial class TestSceneTaikoModPerfect : ModPerfectTestScene public partial class TestSceneTaikoModPerfect : ModFailConditionTestScene
{ {
protected override Ruleset CreatePlayerRuleset() => new TestTaikoRuleset(); protected override Ruleset CreatePlayerRuleset() => new TestTaikoRuleset();

View File

@ -28,7 +28,9 @@ namespace osu.Game.Rulesets.Mods
} }
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
=> result.Type.AffectsAccuracy() => (isRelevantResult(result.Judgement.MinResult) || isRelevantResult(result.Judgement.MaxResult) || isRelevantResult(result.Type))
&& result.Type != result.Judgement.MaxResult; && result.Type != result.Judgement.MaxResult;
private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo();
} }
} }

View File

@ -61,7 +61,9 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
protected readonly double DrainLenience; protected readonly double DrainLenience;
private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>(); private readonly List<HealthIncrease> healthIncreases = new List<HealthIncrease>();
private readonly Dictionary<int, double> densityMultiplierByGroup = new Dictionary<int, double>();
private double gameplayEndTime; private double gameplayEndTime;
private double targetMinimumHealth; private double targetMinimumHealth;
@ -133,14 +135,33 @@ namespace osu.Game.Rulesets.Scoring
{ {
base.ApplyResultInternal(result); base.ApplyResultInternal(result);
if (!result.Type.IsBonus()) if (IsSimulating && !result.Type.IsBonus())
healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); {
healthIncreases.Add(new HealthIncrease(
result.HitObject.GetEndTime() + result.TimeOffset,
GetHealthIncreaseFor(result),
GetDensityGroup(result.HitObject)));
}
} }
protected override double GetHealthIncreaseFor(JudgementResult result) => base.GetHealthIncreaseFor(result) * getDensityMultiplier(GetDensityGroup(result.HitObject));
private double getDensityMultiplier(int? group)
{
if (group == null)
return 1;
return densityMultiplierByGroup.TryGetValue(group.Value, out double multiplier) ? multiplier : 1;
}
protected virtual int? GetDensityGroup(HitObject hitObject) => null;
protected override void Reset(bool storeResults) protected override void Reset(bool storeResults)
{ {
base.Reset(storeResults); base.Reset(storeResults);
densityMultiplierByGroup.Clear();
if (storeResults) if (storeResults)
DrainRate = ComputeDrainRate(); DrainRate = ComputeDrainRate();
@ -152,6 +173,24 @@ namespace osu.Game.Rulesets.Scoring
if (healthIncreases.Count <= 1) if (healthIncreases.Count <= 1)
return 0; return 0;
// Normalise the health gain during sections with higher densities.
(int group, double avgIncrease)[] avgIncreasesByGroup = healthIncreases
.Where(i => i.Group != null)
.GroupBy(i => i.Group)
.Select(g => ((int)g.Key!, g.Sum(i => i.Amount) / (g.Max(i => i.Time) - g.Min(i => i.Time) + 1)))
.ToArray();
if (avgIncreasesByGroup.Length > 1)
{
double overallAverageIncrease = avgIncreasesByGroup.Average(g => g.avgIncrease);
foreach ((int group, double avgIncrease) in avgIncreasesByGroup)
{
// Reduce the health increase for groups that return more health than average.
densityMultiplierByGroup[group] = Math.Min(1, overallAverageIncrease / avgIncrease);
}
}
int adjustment = 1; int adjustment = 1;
double result = 1; double result = 1;
@ -165,8 +204,8 @@ namespace osu.Game.Rulesets.Scoring
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;
while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= currentTime) while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= currentTime)
{ {
@ -177,10 +216,12 @@ namespace osu.Game.Rulesets.Scoring
currentBreak++; currentBreak++;
} }
double multiplier = getDensityMultiplier(healthIncreases[i].Group);
// Apply health adjustments // Apply health adjustments
currentHealth -= (currentTime - 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].Amount * multiplier);
// Common scenario for when the drain rate is definitely too harsh // Common scenario for when the drain rate is definitely too harsh
if (lowestHealth < 0) if (lowestHealth < 0)
@ -198,5 +239,7 @@ namespace osu.Game.Rulesets.Scoring
return result; return result;
} }
private record struct HealthIncrease(double Time, double Amount, int? Group);
} }
} }

View File

@ -8,11 +8,11 @@ using osu.Game.Rulesets.Objects;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
{ {
public abstract partial class ModPerfectTestScene : ModTestScene public abstract partial class ModFailConditionTestScene : ModTestScene
{ {
private readonly ModPerfect mod; private readonly ModFailCondition mod;
protected ModPerfectTestScene(ModPerfect mod) protected ModFailConditionTestScene(ModFailCondition mod)
{ {
this.mod = mod; this.mod = mod;
} }
@ -26,15 +26,15 @@ namespace osu.Game.Tests.Visual
HitObjects = { testData.HitObject } HitObjects = { testData.HitObject }
}, },
Autoplay = !shouldMiss, Autoplay = !shouldMiss,
PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss) PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss)
}); });
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new PerfectModTestPlayer(); protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModFailConditionTestPlayer(CurrentTestData, AllowFail);
private partial class PerfectModTestPlayer : TestPlayer protected partial class ModFailConditionTestPlayer : ModTestPlayer
{ {
public PerfectModTestPlayer() public ModFailConditionTestPlayer(ModTestData data, bool allowFail)
: base(showResults: false) : base(data, allowFail)
{ {
} }

View File

@ -20,35 +20,35 @@ namespace osu.Game.Tests.Visual
{ {
protected sealed override bool HasCustomSteps => true; protected sealed override bool HasCustomSteps => true;
private ModTestData currentTestData; protected ModTestData CurrentTestData { get; private set; }
protected void CreateModTest(ModTestData testData) => CreateTest(() => protected void CreateModTest(ModTestData testData) => CreateTest(() =>
{ {
AddStep("set test data", () => currentTestData = testData); AddStep("set test data", () => CurrentTestData = testData);
}); });
public override void TearDownSteps() public override void TearDownSteps()
{ {
AddUntilStep("test passed", () => AddUntilStep("test passed", () =>
{ {
if (currentTestData == null) if (CurrentTestData == null)
return true; return true;
return currentTestData.PassCondition?.Invoke() ?? false; return CurrentTestData.PassCondition?.Invoke() ?? false;
}); });
base.TearDownSteps(); base.TearDownSteps();
} }
protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => CurrentTestData?.Beatmap ?? base.CreateBeatmap(ruleset);
protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) protected sealed override TestPlayer CreatePlayer(Ruleset ruleset)
{ {
var mods = new List<Mod>(SelectedMods.Value); var mods = new List<Mod>(SelectedMods.Value);
if (currentTestData.Mods != null) if (CurrentTestData.Mods != null)
mods.AddRange(currentTestData.Mods); mods.AddRange(CurrentTestData.Mods);
if (currentTestData.Autoplay) if (CurrentTestData.Autoplay)
mods.Add(ruleset.GetAutoplayMod()); mods.Add(ruleset.GetAutoplayMod());
SelectedMods.Value = mods; SelectedMods.Value = mods;
@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual
return CreateModPlayer(ruleset); return CreateModPlayer(ruleset);
} }
protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(currentTestData, AllowFail); protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(CurrentTestData, AllowFail);
protected partial class ModTestPlayer : TestPlayer protected partial class ModTestPlayer : TestPlayer
{ {