1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-11 23:13:06 +08:00

Merge pull request #27154 from bdach/catch-fail-on-banana

Fix draining processor failing gameplay on bonus misses, tiny droplet misses, and ignore hits
This commit is contained in:
Dan Balasescu 2024-03-04 13:53:02 +09:00 committed by GitHub
commit e935c49a98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 280 additions and 6 deletions

View File

@ -0,0 +1,58 @@
// 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.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Scoring;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class CatchHealthProcessorTest
{
private static readonly object[][] test_cases =
[
// hitobject, starting HP, fail expected after miss
[new Fruit(), 0.01, true],
[new Droplet(), 0.01, true],
[new TinyDroplet(), 0, false],
[new Banana(), 0, false],
];
[TestCaseSource(nameof(test_cases))]
public void TestFailAfterMinResult(CatchHitObject hitObject, double startingHealth, bool failExpected)
{
var healthProcessor = new CatchHealthProcessor(0);
healthProcessor.ApplyBeatmap(new CatchBeatmap
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MinResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
}
[TestCaseSource(nameof(test_cases))]
public void TestNoFailAfterMaxResult(CatchHitObject hitObject, double startingHealth, bool _)
{
var healthProcessor = new CatchHealthProcessor(0);
healthProcessor.ApplyBeatmap(new CatchBeatmap
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MaxResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.False);
}
}
}

View File

@ -5,6 +5,7 @@ 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;
@ -21,6 +22,19 @@ namespace osu.Game.Rulesets.Catch.Scoring
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
protected override bool CheckDefaultFailCondition(JudgementResult result)
{
// matches stable.
// see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967
// the above early-return skips the failure check at the end of the same method:
// https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232
// making it impossible to fail on a tiny droplet regardless of result.
if (result.Type == HitResult.SmallTickMiss)
return false;
return base.CheckDefaultFailCondition(result);
}
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
{
double increase = 0;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests
// No matter what, mania doesn't have passive HP drain.
Assert.That(processor.DrainRate, Is.Zero);
}
private static readonly object[][] test_cases =
[
// hitobject, starting HP, fail expected after miss
[new Note(), 0.01, true],
[new HeadNote(), 0.01, true],
[new TailNote(), 0.01, true],
[new HoldNoteBody(), 0, true], // hold note break
[new HoldNote(), 0, true],
];
[TestCaseSource(nameof(test_cases))]
public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected)
{
var healthProcessor = new ManiaHealthProcessor(0);
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MinResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
}
[TestCaseSource(nameof(test_cases))]
public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _)
{
var healthProcessor = new ManiaHealthProcessor(0);
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MaxResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.False);
}
}
}

View File

@ -0,0 +1,66 @@
// 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.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class OsuHealthProcessorTest
{
private static readonly object[][] test_cases =
[
// hitobject, starting HP, fail expected after miss
[new HitCircle(), 0.01, true],
[new SliderHeadCircle(), 0.01, true],
[new SliderHeadCircle { ClassicSliderBehaviour = true }, 0.01, true],
[new SliderTick(), 0.01, true],
[new SliderRepeat(new Slider()), 0.01, true],
[new SliderTailCircle(new Slider()), 0, true],
[new SliderTailCircle(new Slider()) { ClassicSliderBehaviour = true }, 0.01, true],
[new Slider(), 0, true],
[new Slider { ClassicSliderBehaviour = true }, 0.01, true],
[new SpinnerTick(), 0, false],
[new SpinnerBonusTick(), 0, false],
[new Spinner(), 0.01, true],
];
[TestCaseSource(nameof(test_cases))]
public void TestFailAfterMinResult(OsuHitObject hitObject, double startingHealth, bool failExpected)
{
var healthProcessor = new OsuHealthProcessor(0);
healthProcessor.ApplyBeatmap(new OsuBeatmap
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MinResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
}
[TestCaseSource(nameof(test_cases))]
public void TestNoFailAfterMaxResult(OsuHitObject hitObject, double startingHealth, bool _)
{
var healthProcessor = new OsuHealthProcessor(0);
healthProcessor.ApplyBeatmap(new OsuBeatmap
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MaxResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.False);
}
}
}

View File

@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
HitObjects =
{
new DrumRoll { Duration = 2000 }
new Swell { Duration = 2000 }
}
};
@ -172,5 +172,85 @@ namespace osu.Game.Rulesets.Taiko.Tests
Assert.That(healthProcessor.HasFailed, Is.False);
});
}
[Test]
public void TestMissHitAndHitSwell()
{
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit(),
new Swell { Duration = 2000 }
}
};
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(beatmap);
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
foreach (var nested in beatmap.HitObjects[1].NestedHitObjects)
{
var nestedJudgement = nested.CreateJudgement();
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
}
var judgement = beatmap.HitObjects[1].CreateJudgement();
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], judgement) { Type = judgement.MaxResult });
Assert.Multiple(() =>
{
Assert.That(healthProcessor.Health.Value, Is.EqualTo(0));
Assert.That(healthProcessor.HasFailed, Is.True);
});
}
private static readonly object[][] test_cases =
[
// hitobject, fail expected after miss
[new Hit(), true],
[new Hit.StrongNestedHit(new Hit()), false],
[new DrumRollTick(new DrumRoll()), false],
[new DrumRollTick.StrongNestedHit(new DrumRollTick(new DrumRoll())), false],
[new DrumRoll(), false],
[new SwellTick(), false],
[new Swell(), false]
];
[TestCaseSource(nameof(test_cases))]
public void TestFailAfterMinResult(TaikoHitObject hitObject, bool failExpected)
{
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(new TaikoBeatmap
{
HitObjects = { hitObject }
});
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MinResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
}
[TestCaseSource(nameof(test_cases))]
public void TestNoFailAfterMaxResult(TaikoHitObject hitObject, bool _)
{
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(new TaikoBeatmap
{
HitObjects = { hitObject }
});
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MaxResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.False);
}
}
}

View File

@ -1,6 +1,8 @@
// 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.Rulesets.Judgements;
namespace osu.Game.Rulesets.Scoring
{
/// <summary>
@ -9,7 +11,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public partial class AccumulatingHealthProcessor : HealthProcessor
{
protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value < requiredHealth;
protected override bool CheckDefaultFailCondition(JudgementResult _) => JudgedHits == MaxHits && Health.Value < requiredHealth;
private readonly double requiredHealth;

View File

@ -142,6 +142,14 @@ namespace osu.Game.Rulesets.Scoring
}
}
protected override bool CheckDefaultFailCondition(JudgementResult result)
{
if (result.Judgement.MaxResult.IsBonus() || result.Type == HitResult.IgnoreHit)
return false;
return base.CheckDefaultFailCondition(result);
}
protected override void Reset(bool storeResults)
{
base.Reset(storeResults);

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Scoring
public event Func<bool>? Failed;
/// <summary>
/// Additional conditions on top of <see cref="DefaultFailCondition"/> that cause a failing state.
/// Additional conditions on top of <see cref="CheckDefaultFailCondition"/> that cause a failing state.
/// </summary>
public event Func<HealthProcessor, JudgementResult, bool>? FailConditions;
@ -69,9 +69,10 @@ namespace osu.Game.Rulesets.Scoring
protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.HealthIncrease;
/// <summary>
/// The default conditions for failing.
/// Checks whether the default conditions for failing are met.
/// </summary>
protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value);
/// <returns><see langword="true"/> if failure should be invoked.</returns>
protected virtual bool CheckDefaultFailCondition(JudgementResult result) => Precision.AlmostBigger(Health.MinValue, Health.Value);
/// <summary>
/// Whether the current state of <see cref="HealthProcessor"/> or the provided <paramref name="result"/> meets any fail condition.
@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="result">The judgement result.</param>
private bool meetsAnyFailCondition(JudgementResult result)
{
if (DefaultFailCondition)
if (CheckDefaultFailCondition(result))
return true;
if (FailConditions != null)