diff --git a/osu.Android.props b/osu.Android.props
index e7f90af5fd..0b41d5cda4 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -53,7 +53,7 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 58212e29ef..dc8df28e6a 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -28,9 +28,10 @@ namespace osu.Game.Rulesets.Catch
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
- public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new CatchScoreProcessor(beatmap);
+ public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this);
+
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap);
public const string SHORT_NAME = "fruits";
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index 4c0f5d510e..8377b3786a 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
base.TransferSettings(difficulty);
- CircleSize.Value = CircleSize.Default = difficulty.CircleSize;
- ApproachRate.Value = ApproachRate.Default = difficulty.ApproachRate;
+ TransferSetting(CircleSize, difficulty.CircleSize);
+ TransferSetting(ApproachRate, difficulty.ApproachRate);
}
protected override void ApplySettings(BeatmapDifficulty difficulty)
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index f67ca1213e..4c7bc4ab73 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -1,40 +1,12 @@
// Copyright (c) ppy Pty Ltd . 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.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchScoreProcessor : ScoreProcessor
{
- public CatchScoreProcessor(IBeatmap beatmap)
- : base(beatmap)
- {
- }
-
- private float hpDrainRate;
-
- protected override void ApplyBeatmap(IBeatmap beatmap)
- {
- base.ApplyBeatmap(beatmap);
-
- hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate;
- }
-
- protected override double HealthAdjustmentFactorFor(JudgementResult result)
- {
- switch (result.Type)
- {
- case HitResult.Miss:
- return hpDrainRate;
-
- default:
- return 10.2 - hpDrainRate; // Award less HP as drain rate is increased
- }
- }
-
public override HitWindows CreateHitWindows() => new CatchHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs
new file mode 100644
index 0000000000..80b1b3df8e
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public abstract class SkinnableTestScene : OsuGridTestScene
+ {
+ private Skin defaultSkin;
+
+ protected SkinnableTestScene()
+ : base(1, 2)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio, SkinManager skinManager)
+ {
+ defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
+ }
+
+ public void SetContents(Func creationFunction)
+ {
+ Cell(0).Child = createProvider(null, creationFunction);
+ Cell(1).Child = createProvider(defaultSkin, creationFunction);
+ }
+
+ private Drawable createProvider(Skin skin, Func creationFunction)
+ {
+ var mainProvider = new SkinProvidingContainer(skin);
+
+ return mainProvider
+ .WithChild(new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider))
+ {
+ Child = creationFunction()
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs
new file mode 100644
index 0000000000..eea1a36a19
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . 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.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneDrawableJudgement : SkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(DrawableJudgement),
+ typeof(DrawableManiaJudgement)
+ };
+
+ public TestSceneDrawableJudgement()
+ {
+ foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1))
+ {
+ AddStep("Show " + result.GetDescription(), () => SetContents(() =>
+ new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
new file mode 100644
index 0000000000..7b0cf40d45
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -0,0 +1,314 @@
+// Copyright (c) ppy Pty Ltd . 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.Framework.Screens;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneHoldNoteInput : RateAdjustedBeatmapTestScene
+ {
+ private const double time_before_head = 250;
+ private const double time_head = 1500;
+ private const double time_during_hold_1 = 2500;
+ private const double time_tail = 4000;
+ private const double time_after_tail = 5250;
+
+ private List judgementResults;
+ private bool allJudgedFired;
+
+ ///
+ /// -----[ ]-----
+ /// o o
+ ///
+ [Test]
+ public void TestNoInput()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ assertNoteJudgement(HitResult.Perfect);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestPressTooEarlyAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail, ManiaAction.Key1),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestPressTooEarlyAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o
+ ///
+ [Test]
+ public void TestPressTooEarlyThenPressAtStartAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_before_head + 10),
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o
+ ///
+ [Test]
+ public void TestPressTooEarlyThenPressAtStartAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_before_head + 10),
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Perfect);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAtStartAndBreak()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 10),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o
+ ///
+ [Test]
+ public void TestPressAtStartThenBreakThenRepressAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 10),
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo x o o
+ ///
+ [Test]
+ public void TestPressAtStartThenBreakThenRepressAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + 10),
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o
+ ///
+ [Test]
+ public void TestPressDuringNoteAndReleaseAfterTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_after_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// x o o
+ ///
+ [Test]
+ public void TestPressDuringNoteAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Perfect);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ ///
+ /// -----[ ]-----
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAndReleaseAtTail()
+ {
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_tail, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail + 10),
+ });
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.Miss);
+ assertTailJudgement(HitResult.Meh);
+ }
+
+ private void assertHeadJudgement(HitResult result)
+ => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result);
+
+ private void assertTailJudgement(HitResult result)
+ => AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result);
+
+ private void assertNoteJudgement(HitResult result)
+ => AddAssert($"hold note judged as {result}", () => judgementResults[^1].Type == result);
+
+ private void assertTickJudgement(HitResult result)
+ => AddAssert($"tick judged as {result}", () => judgementResults[6].Type == result); // arbitrary tick
+
+ private ScoreAccessibleReplayPlayer currentPlayer;
+
+ private void performTest(List frames)
+ {
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = time_tail - time_head,
+ Column = 0,
+ }
+ },
+ BeatmapInfo =
+ {
+ BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ });
+
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+
+ var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
+
+ p.OnLoadComplete += _ =>
+ {
+ p.ScoreProcessor.NewJudgement += result =>
+ {
+ if (currentPlayer == p) judgementResults.Add(result);
+ };
+ p.ScoreProcessor.AllJudged += () =>
+ {
+ if (currentPlayer == p) allJudgedFired = true;
+ };
+ };
+
+ LoadScreen(currentPlayer = p);
+ allJudgedFired = false;
+ judgementResults = new List();
+ });
+
+ AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
+ AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+ AddUntilStep("Wait for all judged", () => allJudgedFired);
+ }
+
+ private class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ protected override bool PauseOnFocusLost => false;
+
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score, false, false)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs
new file mode 100644
index 0000000000..cd25d162d0
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestScenePlayer : PlayerTestScene
+ {
+ public TestScenePlayer()
+ : base(new ManiaRuleset())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs
index acce41db6f..4e73883de0 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
if (DrawableObject.IsLoaded)
{
- DrawableNote note = position == HoldNotePosition.Start ? DrawableObject.Head : DrawableObject.Tail;
+ DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)DrawableObject.Head : DrawableObject.Tail;
Anchor = note.Anchor;
Origin = note.Origin;
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index b07e1d8f54..c50f4314a3 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -26,7 +26,9 @@ using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.Scoring;
+using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania
@@ -35,7 +37,7 @@ namespace osu.Game.Rulesets.Mania
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
- public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new ManiaScoreProcessor(beatmap);
+ public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
@@ -45,6 +47,8 @@ namespace osu.Game.Rulesets.Mania
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
+ public override ISkin CreateLegacySkinProvider(ISkinSource source) => new ManiaLegacySkinTransformer(source);
+
public override IEnumerable ConvertLegacyMods(LegacyMods mods)
{
if (mods.HasFlag(LegacyMods.Nightcore))
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 87b9633c80..155adb958b 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
@@ -21,11 +20,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public override bool DisplayResult => false;
- public DrawableNote Head => headContainer.Child;
- public DrawableNote Tail => tailContainer.Child;
+ public DrawableHoldNoteHead Head => headContainer.Child;
+ public DrawableHoldNoteTail Tail => tailContainer.Child;
- private readonly Container headContainer;
- private readonly Container tailContainer;
+ private readonly Container headContainer;
+ private readonly Container tailContainer;
private readonly Container tickContainer;
private readonly BodyPiece bodyPiece;
@@ -33,12 +32,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
///
- private double? holdStartTime;
+ public double? HoldStartTime { get; private set; }
///
/// Whether the hold note has been released too early and shouldn't give full score for the release.
///
- private bool hasBroken;
+ public bool HasBroken { get; private set; }
public DrawableHoldNote(HoldNote hitObject)
: base(hitObject)
@@ -49,8 +48,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
bodyPiece = new BodyPiece { RelativeSizeAxes = Axes.X },
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
- headContainer = new Container { RelativeSizeAxes = Axes.Both },
- tailContainer = new Container { RelativeSizeAxes = Axes.Both },
+ headContainer = new Container { RelativeSizeAxes = Axes.Both },
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both },
});
AccentColour.BindValueChanged(colour =>
@@ -65,11 +64,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
switch (hitObject)
{
- case DrawableHeadNote head:
+ case DrawableHoldNoteHead head:
headContainer.Child = head;
break;
- case DrawableTailNote tail:
+ case DrawableHoldNoteTail tail:
tailContainer.Child = tail;
break;
@@ -92,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
switch (hitObject)
{
case TailNote _:
- return new DrawableTailNote(this)
+ return new DrawableHoldNoteTail(this)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
};
case Note _:
- return new DrawableHeadNote(this)
+ return new DrawableHoldNoteHead(this)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@@ -110,7 +109,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
case HoldNoteTick tick:
return new DrawableHoldNoteTick(tick)
{
- HoldStartTime = () => holdStartTime,
+ HoldStartTime = () => HoldStartTime,
AccentColour = { BindTarget = AccentColour }
};
}
@@ -125,12 +124,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
}
- protected override void CheckForResult(bool userTriggered, double timeOffset)
- {
- if (Tail.AllJudged)
- ApplyResult(r => r.Type = HitResult.Perfect);
- }
-
protected override void Update()
{
base.Update();
@@ -146,146 +139,64 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
base.UpdateStateTransforms(state);
}
- protected void BeginHold()
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
{
- holdStartTime = Time.Current;
- bodyPiece.Hitting = true;
- }
+ if (Tail.AllJudged)
+ ApplyResult(r => r.Type = HitResult.Perfect);
- protected void EndHold()
- {
- holdStartTime = null;
- bodyPiece.Hitting = false;
+ if (Tail.Result.Type == HitResult.Miss)
+ HasBroken = true;
}
public bool OnPressed(ManiaAction action)
{
- // Make sure the action happened within the body of the hold note
- if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime)
+ if (AllJudged)
return false;
if (action != Action.Value)
return false;
- // The user has pressed during the body of the hold note, after the head note and its hit windows have passed
- // and within the limited range of the above if-statement. This state will be managed by the head note if the
- // user has pressed during the hit windows of the head note.
- BeginHold();
+ beginHoldAt(Time.Current - Head.HitObject.StartTime);
+ Head.UpdateResult();
+
return true;
}
+ private void beginHoldAt(double timeOffset)
+ {
+ if (timeOffset < -Head.HitObject.HitWindows.WindowFor(HitResult.Miss))
+ return;
+
+ HoldStartTime = Time.Current;
+ bodyPiece.Hitting = true;
+ }
+
public bool OnReleased(ManiaAction action)
{
- // Make sure that the user started holding the key during the hold note
- if (!holdStartTime.HasValue)
+ if (AllJudged)
return false;
if (action != Action.Value)
return false;
- EndHold();
+ // Make sure a hold was started
+ if (HoldStartTime == null)
+ return false;
+
+ Tail.UpdateResult();
+ endHold();
// If the key has been released too early, the user should not receive full score for the release
if (!Tail.IsHit)
- hasBroken = true;
+ HasBroken = true;
return true;
}
- ///
- /// The head note of a hold.
- ///
- private class DrawableHeadNote : DrawableNote
+ private void endHold()
{
- private readonly DrawableHoldNote holdNote;
-
- public DrawableHeadNote(DrawableHoldNote holdNote)
- : base(holdNote.HitObject.Head)
- {
- this.holdNote = holdNote;
- }
-
- public override bool OnPressed(ManiaAction action)
- {
- if (!base.OnPressed(action))
- return false;
-
- // If the key has been released too early, the user should not receive full score for the release
- if (Result.Type == HitResult.Miss)
- holdNote.hasBroken = true;
-
- // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held
- // The body doesn't handle these early early hits, so we have to explicitly set the holding state here
- holdNote.BeginHold();
-
- return true;
- }
- }
-
- ///
- /// The tail note of a hold.
- ///
- private class DrawableTailNote : DrawableNote
- {
- ///
- /// Lenience of release hit windows. This is to make cases where the hold note release
- /// is timed alongside presses of other hit objects less awkward.
- /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
- ///
- private const double release_window_lenience = 1.5;
-
- private readonly DrawableHoldNote holdNote;
-
- public DrawableTailNote(DrawableHoldNote holdNote)
- : base(holdNote.HitObject.Tail)
- {
- this.holdNote = holdNote;
- }
-
- protected override void CheckForResult(bool userTriggered, double timeOffset)
- {
- Debug.Assert(HitObject.HitWindows != null);
-
- // Factor in the release lenience
- timeOffset /= release_window_lenience;
-
- if (!userTriggered)
- {
- if (!HitObject.HitWindows.CanBeHit(timeOffset))
- ApplyResult(r => r.Type = HitResult.Miss);
-
- return;
- }
-
- var result = HitObject.HitWindows.ResultFor(timeOffset);
- if (result == HitResult.None)
- return;
-
- ApplyResult(r =>
- {
- if (holdNote.hasBroken && (result == HitResult.Perfect || result == HitResult.Perfect))
- result = HitResult.Good;
-
- r.Type = result;
- });
- }
-
- public override bool OnPressed(ManiaAction action) => false; // Tail doesn't handle key down
-
- public override bool OnReleased(ManiaAction action)
- {
- // Make sure that the user started holding the key during the hold note
- if (!holdNote.holdStartTime.HasValue)
- return false;
-
- if (action != Action.Value)
- return false;
-
- UpdateResult(true);
-
- // Handled by the hold note, which will set holding = false
- return false;
- }
+ HoldStartTime = null;
+ bodyPiece.Hitting = false;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
new file mode 100644
index 0000000000..a5d03bf765
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Mania.Objects.Drawables
+{
+ ///
+ /// The head of a .
+ ///
+ public class DrawableHoldNoteHead : DrawableNote
+ {
+ public DrawableHoldNoteHead(DrawableHoldNote holdNote)
+ : base(holdNote.HitObject.Head)
+ {
+ }
+
+ public void UpdateResult() => base.UpdateResult(true);
+
+ public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
+
+ public override bool OnReleased(ManiaAction action) => false; // Handled by the hold note
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
new file mode 100644
index 0000000000..a660144dd1
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Mania.Objects.Drawables
+{
+ ///
+ /// The tail of a .
+ ///
+ public class DrawableHoldNoteTail : DrawableNote
+ {
+ ///
+ /// Lenience of release hit windows. This is to make cases where the hold note release
+ /// is timed alongside presses of other hit objects less awkward.
+ /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
+ ///
+ private const double release_window_lenience = 1.5;
+
+ private readonly DrawableHoldNote holdNote;
+
+ public DrawableHoldNoteTail(DrawableHoldNote holdNote)
+ : base(holdNote.HitObject.Tail)
+ {
+ this.holdNote = holdNote;
+ }
+
+ public void UpdateResult() => base.UpdateResult(true);
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ Debug.Assert(HitObject.HitWindows != null);
+
+ // Factor in the release lenience
+ timeOffset /= release_window_lenience;
+
+ if (!userTriggered)
+ {
+ if (!HitObject.HitWindows.CanBeHit(timeOffset))
+ ApplyResult(r => r.Type = HitResult.Miss);
+
+ return;
+ }
+
+ var result = HitObject.HitWindows.ResultFor(timeOffset);
+ if (result == HitResult.None)
+ return;
+
+ ApplyResult(r =>
+ {
+ // If the head wasn't hit or the hold note was broken, cap the max score to Meh.
+ if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken))
+ result = HitResult.Meh;
+
+ r.Type = result;
+ });
+ }
+
+ public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
+
+ public override bool OnReleased(ManiaAction action) => false; // Handled by the hold note
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index a678ef60e7..9b54b48de3 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -1,87 +1,12 @@
// Copyright (c) ppy Pty Ltd . 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.Scoring;
namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor
{
- ///
- /// The hit HP multiplier at OD = 0.
- ///
- private const double hp_multiplier_min = 0.75;
-
- ///
- /// The hit HP multiplier at OD = 0.
- ///
- private const double hp_multiplier_mid = 0.85;
-
- ///
- /// The hit HP multiplier at OD = 0.
- ///
- private const double hp_multiplier_max = 1;
-
- ///
- /// The MISS HP multiplier at OD = 0.
- ///
- private const double hp_multiplier_miss_min = 0.5;
-
- ///
- /// The MISS HP multiplier at OD = 5.
- ///
- private const double hp_multiplier_miss_mid = 0.75;
-
- ///
- /// The MISS HP multiplier at OD = 10.
- ///
- private const double hp_multiplier_miss_max = 1;
-
- ///
- /// The MISS HP multiplier. This is multiplied to the miss hp increase.
- ///
- private double hpMissMultiplier = 1;
-
- ///
- /// The HIT HP multiplier. This is multiplied to hit hp increases.
- ///
- private double hpMultiplier = 1;
-
- public ManiaScoreProcessor(IBeatmap beatmap)
- : base(beatmap)
- {
- }
-
- protected override void ApplyBeatmap(IBeatmap beatmap)
- {
- base.ApplyBeatmap(beatmap);
-
- BeatmapDifficulty difficulty = beatmap.BeatmapInfo.BaseDifficulty;
- hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max);
- hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max);
- }
-
- protected override void SimulateAutoplay(IBeatmap beatmap)
- {
- while (true)
- {
- base.SimulateAutoplay(beatmap);
-
- if (!HasFailed)
- break;
-
- hpMultiplier *= 1.01;
- hpMissMultiplier *= 0.98;
-
- Reset(false);
- }
- }
-
- protected override double HealthAdjustmentFactorFor(JudgementResult result)
- => result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier;
-
public override HitWindows CreateHitWindows() => new ManiaHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
new file mode 100644
index 0000000000..f3739ce7c2
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
@@ -0,0 +1,67 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Audio;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class ManiaLegacySkinTransformer : ISkin
+ {
+ private readonly ISkin source;
+
+ public ManiaLegacySkinTransformer(ISkin source)
+ {
+ this.source = source;
+ }
+
+ public Drawable GetDrawableComponent(ISkinComponent component)
+ {
+ switch (component)
+ {
+ case GameplaySkinComponent resultComponent:
+ return getResult(resultComponent);
+ }
+
+ return null;
+ }
+
+ private Drawable getResult(GameplaySkinComponent resultComponent)
+ {
+ switch (resultComponent.Component)
+ {
+ case HitResult.Miss:
+ return this.GetAnimation("mania-hit0", true, true);
+
+ case HitResult.Meh:
+ return this.GetAnimation("mania-hit50", true, true);
+
+ case HitResult.Ok:
+ return this.GetAnimation("mania-hit100", true, true);
+
+ case HitResult.Good:
+ return this.GetAnimation("mania-hit200", true, true);
+
+ case HitResult.Great:
+ return this.GetAnimation("mania-hit300", true, true);
+
+ case HitResult.Perfect:
+ return this.GetAnimation("mania-hit300g", true, true);
+ }
+
+ return null;
+ }
+
+ public Texture GetTexture(string componentName) => source.GetTexture(componentName);
+
+ public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
+
+ public IBindable GetConfig(TLookup lookup) =>
+ source.GetConfig(lookup);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
index 386bcbb724..ee2cec1bbd 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
@@ -22,26 +22,15 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private readonly IBindable direction = new Bindable();
- private readonly Container hitTargetLine;
- private readonly Drawable hitTargetBar;
+ private readonly Drawable hitTarget;
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
{
InternalChildren = new[]
{
- hitTargetBar = new Box
+ hitTarget = new DefaultHitTarget
{
RelativeSizeAxes = Axes.X,
- Height = NotePiece.NOTE_HEIGHT,
- Alpha = 0.6f,
- Colour = Color4.Black
- },
- hitTargetLine = new Container
- {
- RelativeSizeAxes = Axes.X,
- Height = hit_target_bar_height,
- Masking = true,
- Child = new Box { RelativeSizeAxes = Axes.Both }
},
hitObjectContainer
};
@@ -55,17 +44,10 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
- hitTargetBar.Anchor = hitTargetBar.Origin = anchor;
- hitTargetLine.Anchor = hitTargetLine.Origin = anchor;
+ hitTarget.Anchor = hitTarget.Origin = anchor;
}, true);
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
- updateColours();
- }
-
private Color4 accentColour;
public Color4 AccentColour
@@ -78,21 +60,86 @@ namespace osu.Game.Rulesets.Mania.UI.Components
accentColour = value;
- updateColours();
+ if (hitTarget is IHasAccentColour colouredHitTarget)
+ colouredHitTarget.AccentColour = accentColour;
}
}
- private void updateColours()
+ private class DefaultHitTarget : CompositeDrawable, IHasAccentColour
{
- if (!IsLoaded)
- return;
+ private readonly IBindable direction = new Bindable();
- hitTargetLine.EdgeEffect = new EdgeEffectParameters
+ private readonly Container hitTargetLine;
+ private readonly Drawable hitTargetBar;
+
+ public DefaultHitTarget()
{
- Type = EdgeEffectType.Glow,
- Radius = 5,
- Colour = accentColour.Opacity(0.5f),
- };
+ InternalChildren = new[]
+ {
+ hitTargetBar = new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = NotePiece.NOTE_HEIGHT,
+ Alpha = 0.6f,
+ Colour = Color4.Black
+ },
+ hitTargetLine = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = hit_target_bar_height,
+ Masking = true,
+ Child = new Box { RelativeSizeAxes = Axes.Both }
+ },
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(dir =>
+ {
+ Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
+
+ hitTargetBar.Anchor = hitTargetBar.Origin = anchor;
+ hitTargetLine.Anchor = hitTargetLine.Origin = anchor;
+ }, true);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateColours();
+ }
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ if (accentColour == value)
+ return;
+
+ accentColour = value;
+
+ updateColours();
+ }
+ }
+
+ private void updateColours()
+ {
+ if (!IsLoaded)
+ return;
+
+ hitTargetLine.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 5,
+ Colour = accentColour.Opacity(0.5f),
+ };
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
index eff4d919b0..c9b3d08a22 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
@@ -33,8 +33,8 @@ namespace osu.Game.Rulesets.Osu.Tests
typeof(CircularDistanceSnapGrid)
};
- [Cached(typeof(IEditorBeatmap))]
- private readonly EditorBeatmap editorBeatmap;
+ [Cached(typeof(EditorBeatmap))]
+ private readonly EditorBeatmap editorBeatmap;
[Cached]
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public TestSceneOsuDistanceSnapGrid()
{
- editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ editorBeatmap = new EditorBeatmap(new OsuBeatmap());
}
[SetUp]
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index a2c1a5f5f4..49624ea733 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -91,10 +91,10 @@ namespace osu.Game.Rulesets.Osu.Edit
if (sourceIndex == -1)
return null;
- OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
+ HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
int targetIndex = sourceIndex + targetOffset;
- OsuHitObject targetObject = null;
+ HitObject targetObject = null;
// Keep advancing the target object while its start time falls before the end time of the source object
while (true)
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Edit
targetIndex++;
}
- return new OsuDistanceSnapGrid(sourceObject, targetObject);
+ return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs
index 7a5b98864c..bf30fbc351 100644
--- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs
@@ -27,22 +27,5 @@ namespace osu.Game.Rulesets.Osu.Judgements
return 300;
}
}
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- case HitResult.Miss:
- return -0.02;
-
- case HitResult.Meh:
- case HitResult.Good:
- case HitResult.Great:
- return 0.01;
-
- default:
- return 0;
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index 63110b2797..831e4a700f 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -18,7 +18,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModBlinds : Mod, IApplicableToDrawableRuleset, IApplicableToScoreProcessor
+ public class OsuModBlinds : Mod, IApplicableToDrawableRuleset, IApplicableToHealthProcessor
{
public override string Name => "Blinds";
public override string Description => "Play with blinds on your screen.";
@@ -37,9 +37,9 @@ namespace osu.Game.Rulesets.Osu.Mods
drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield.HitObjectContainer, drawableRuleset.Beatmap));
}
- public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
{
- scoreProcessor.Health.ValueChanged += health => { blinds.AnimateClosedness((float)health.NewValue); };
+ healthProcessor.Health.ValueChanged += health => { blinds.AnimateClosedness((float)health.NewValue); };
}
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
index 0514e2ab34..7eee71be81 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
@@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
base.TransferSettings(difficulty);
- CircleSize.Value = CircleSize.Default = difficulty.CircleSize;
- ApproachRate.Value = ApproachRate.Default = difficulty.ApproachRate;
+ TransferSetting(CircleSize, difficulty.CircleSize);
+ TransferSetting(ApproachRate, difficulty.ApproachRate);
}
protected override void ApplySettings(BeatmapDifficulty difficulty)
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index c8a156dc57..36346eb78a 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
- public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new OsuScoreProcessor(beatmap);
+ public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this);
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
index 6779271cb3..1de7d488f3 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . 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.Osu.Judgements;
@@ -11,44 +10,6 @@ namespace osu.Game.Rulesets.Osu.Scoring
{
internal class OsuScoreProcessor : ScoreProcessor
{
- public OsuScoreProcessor(IBeatmap beatmap)
- : base(beatmap)
- {
- }
-
- private float hpDrainRate;
-
- protected override void ApplyBeatmap(IBeatmap beatmap)
- {
- base.ApplyBeatmap(beatmap);
-
- hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate;
- }
-
- protected override double HealthAdjustmentFactorFor(JudgementResult result)
- {
- switch (result.Type)
- {
- case HitResult.Great:
- return 10.2 - hpDrainRate;
-
- case HitResult.Good:
- return 8 - hpDrainRate;
-
- case HitResult.Meh:
- return 4 - hpDrainRate;
-
- // case HitResult.SliderTick:
- // return Math.Max(7 - hpDrainRate, 0) * 0.01;
-
- case HitResult.Miss:
- return hpDrainRate;
-
- default:
- return 0;
- }
- }
-
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement);
public override HitWindows CreateHitWindows() => new OsuHitWindows();
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
new file mode 100644
index 0000000000..edb089dbac
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Scoring
+{
+ ///
+ /// A for the taiko ruleset.
+ /// Taiko fails if the player has not half-filled their health by the end of the map.
+ ///
+ public class TaikoHealthProcessor : AccumulatingHealthProcessor
+ {
+ ///
+ /// A value used for calculating .
+ ///
+ private const double object_count_factor = 3;
+
+ ///
+ /// HP multiplier for a successful .
+ ///
+ private double hpMultiplier;
+
+ ///
+ /// HP multiplier for a .
+ ///
+ private double hpMissMultiplier;
+
+ public TaikoHealthProcessor()
+ : base(0.5)
+ {
+ }
+
+ public override void ApplyBeatmap(IBeatmap beatmap)
+ {
+ base.ApplyBeatmap(beatmap);
+
+ hpMultiplier = 1 / (object_count_factor * beatmap.HitObjects.OfType().Count() * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98));
+ hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120);
+ }
+
+ protected override double GetHealthIncreaseFor(JudgementResult result)
+ => base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier);
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
index ae593d2e3a..003d40af56 100644
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
@@ -1,60 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Linq;
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Scoring
{
internal class TaikoScoreProcessor : ScoreProcessor
{
- ///
- /// A value used for calculating .
- ///
- private const double object_count_factor = 3;
-
- ///
- /// Taiko fails at the end of the map if the player has not half-filled their HP bar.
- ///
- protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value <= 0.5;
-
- ///
- /// HP multiplier for a successful .
- ///
- private double hpMultiplier;
-
- ///
- /// HP multiplier for a .
- ///
- private double hpMissMultiplier;
-
- public TaikoScoreProcessor(IBeatmap beatmap)
- : base(beatmap)
- {
- }
-
- protected override void ApplyBeatmap(IBeatmap beatmap)
- {
- base.ApplyBeatmap(beatmap);
-
- hpMultiplier = 1 / (object_count_factor * beatmap.HitObjects.OfType().Count() * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98));
-
- hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120);
- }
-
- protected override double HealthAdjustmentFactorFor(JudgementResult result)
- => result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier;
-
- protected override void Reset(bool storeResults)
- {
- base.Reset(storeResults);
-
- Health.Value = 0;
- }
-
public override HitWindows CreateHitWindows() => new TaikoHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 5890ed2976..777b68a993 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -28,7 +28,9 @@ namespace osu.Game.Rulesets.Taiko
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
- public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new TaikoScoreProcessor(beatmap);
+ public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor();
+
+ public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TaikoHealthProcessor();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this);
diff --git a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs b/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs
index 98e630abd2..12d729d09f 100644
--- a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestHitObjectAddEvent()
{
- var editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap());
HitObject addedObject = null;
editorBeatmap.HitObjectAdded += h => addedObject = h;
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Beatmaps
public void HitObjectRemoveEvent()
{
var hitCircle = new HitCircle();
- var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject removedObject = null;
editorBeatmap.HitObjectRemoved += h => removedObject = h;
@@ -55,7 +55,7 @@ namespace osu.Game.Tests.Beatmaps
public void TestInitialHitObjectStartTimeChangeEvent()
{
var hitCircle = new HitCircle();
- var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject changedObject = null;
editorBeatmap.StartTimeChanged += h => changedObject = h;
@@ -71,7 +71,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestAddedHitObjectStartTimeChangeEvent()
{
- var editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap());
HitObject changedObject = null;
editorBeatmap.StartTimeChanged += h => changedObject = h;
@@ -92,7 +92,7 @@ namespace osu.Game.Tests.Beatmaps
public void TestRemovedHitObjectStartTimeChangeEvent()
{
var hitCircle = new HitCircle();
- var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject changedObject = null;
editorBeatmap.StartTimeChanged += h => changedObject = h;
@@ -110,7 +110,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestAddHitObjectInMiddle()
{
- var editorBeatmap = new EditorBeatmap(new OsuBeatmap
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap
{
HitObjects =
{
@@ -134,7 +134,7 @@ namespace osu.Game.Tests.Beatmaps
public void TestResortWhenStartTimeChanged()
{
var hitCircle = new HitCircle { StartTime = 1000 };
- var editorBeatmap = new EditorBeatmap(new OsuBeatmap
+ var editorBeatmap = new EditorBeatmap(new OsuBeatmap
{
HitObjects =
{
diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
index fe3cc375ea..2d336bd19c 100644
--- a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -2,11 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
-using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
@@ -17,6 +18,9 @@ namespace osu.Game.Tests.Editor
{
private TestHitObjectComposer composer;
+ [Cached(typeof(EditorBeatmap))]
+ private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+
[SetUp]
public void Setup() => Schedule(() =>
{
@@ -183,7 +187,7 @@ namespace osu.Game.Tests.Editor
private class TestHitObjectComposer : OsuHitObjectComposer
{
- public new EditorBeatmap EditorBeatmap => base.EditorBeatmap;
+ public new EditorBeatmap EditorBeatmap => base.EditorBeatmap;
public TestHitObjectComposer()
: base(new OsuRuleset())
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
new file mode 100644
index 0000000000..eec52669ff
--- /dev/null
+++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
@@ -0,0 +1,159 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.MathUtils;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneDrainingHealthProcessor : OsuTestScene
+ {
+ private Bindable breakTime;
+ private HealthProcessor processor;
+ private ManualClock clock;
+
+ [Test]
+ public void TestInitialHealthStartsAtOne()
+ {
+ createProcessor(createBeatmap(1000, 2000));
+
+ assertHealthEqualTo(1);
+ }
+
+ [Test]
+ public void TestHealthNotDrainedBeforeGameplayStart()
+ {
+ createProcessor(createBeatmap(1000, 2000));
+
+ setTime(100);
+ assertHealthEqualTo(1);
+ setTime(900);
+ assertHealthEqualTo(1);
+ }
+
+ [Test]
+ public void TestHealthNotDrainedAfterGameplayEnd()
+ {
+ createProcessor(createBeatmap(1000, 2000));
+ setTime(2001); // After the hitobjects
+ setHealth(1); // Reset the current health for assertions to take place
+
+ setTime(2100);
+ assertHealthEqualTo(1);
+ setTime(3000);
+ assertHealthEqualTo(1);
+ }
+
+ [Test]
+ public void TestHealthNotDrainedDuringBreak()
+ {
+ createProcessor(createBeatmap(0, 2000));
+ setBreak(true);
+
+ setTime(700);
+ assertHealthEqualTo(1);
+ setTime(900);
+ assertHealthEqualTo(1);
+ }
+
+ [Test]
+ public void TestHealthDrainedDuringGameplay()
+ {
+ createProcessor(createBeatmap(0, 1000));
+
+ setTime(500);
+ assertHealthNotEqualTo(1);
+ }
+
+ [Test]
+ public void TestHealthGainedAfterRewind()
+ {
+ createProcessor(createBeatmap(0, 1000));
+ setTime(500);
+
+ setTime(0);
+ assertHealthEqualTo(1);
+ }
+
+ [Test]
+ public void TestHealthGainedOnHit()
+ {
+ Beatmap beatmap = createBeatmap(0, 1000);
+
+ createProcessor(beatmap);
+ setTime(10); // Decrease health slightly
+ assertHealthNotEqualTo(1);
+
+ AddStep("apply hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
+ assertHealthEqualTo(1);
+ }
+
+ [Test]
+ public void TestHealthRemovedOnRevert()
+ {
+ var beatmap = createBeatmap(0, 1000);
+ JudgementResult result = null;
+
+ createProcessor(beatmap);
+ setTime(10); // Decrease health slightly
+ AddStep("apply hit result", () => processor.ApplyResult(result = new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
+
+ AddStep("revert hit result", () => processor.RevertResult(result));
+ assertHealthNotEqualTo(1);
+ }
+
+ private Beatmap createBeatmap(double startTime, double endTime)
+ {
+ var beatmap = new Beatmap
+ {
+ BeatmapInfo = { BaseDifficulty = { DrainRate = 5 } },
+ };
+
+ for (double time = startTime; time <= endTime; time += 100)
+ beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = time });
+
+ return beatmap;
+ }
+
+ private void createProcessor(Beatmap beatmap) => AddStep("create processor", () =>
+ {
+ breakTime = new Bindable();
+
+ Child = processor = new DrainingHealthProcessor(beatmap.HitObjects[0].StartTime).With(d =>
+ {
+ d.RelativeSizeAxes = Axes.Both;
+ d.Clock = new FramedClock(clock = new ManualClock());
+ });
+
+ processor.IsBreakTime.BindTo(breakTime);
+ processor.ApplyBeatmap(beatmap);
+ });
+
+ private void setTime(double time) => AddStep($"set time = {time}", () => clock.CurrentTime = time);
+
+ private void setHealth(double health) => AddStep($"set health = {health}", () => processor.Health.Value = health);
+
+ private void setBreak(bool enabled) => AddStep($"{(enabled ? "enable" : "disable")} break", () => breakTime.Value = enabled);
+
+ private void assertHealthEqualTo(double value)
+ => AddAssert($"health = {value}", () => Precision.AlmostEquals(value, processor.Health.Value, 0.0001f));
+
+ private void assertHealthNotEqualTo(double value)
+ => AddAssert($"health != {value}", () => !Precision.AlmostEquals(value, processor.Health.Value, 0.0001f));
+
+ private class JudgeableHitObject : HitObject
+ {
+ public override Judgement CreateJudgement() => new Judgement();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs
index 9f16e1d781..3562689482 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs
@@ -4,6 +4,8 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
namespace osu.Game.Tests.Visual.Editor
@@ -11,10 +13,21 @@ namespace osu.Game.Tests.Visual.Editor
[TestFixture]
public class TestSceneComposeScreen : EditorClockTestScene
{
+ [Cached(typeof(EditorBeatmap))]
+ private readonly EditorBeatmap editorBeatmap =
+ new EditorBeatmap(new OsuBeatmap
+ {
+ BeatmapInfo =
+ {
+ Ruleset = new OsuRuleset().RulesetInfo
+ }
+ });
+
[BackgroundDependencyLoader]
private void load()
{
- Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
+
Child = new ComposeScreen();
}
}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
index 39b4bf7218..847d168e51 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps;
-using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -21,15 +20,15 @@ namespace osu.Game.Tests.Visual.Editor
private const double beat_length = 100;
private static readonly Vector2 grid_position = new Vector2(512, 384);
- [Cached(typeof(IEditorBeatmap))]
- private readonly EditorBeatmap editorBeatmap;
+ [Cached(typeof(EditorBeatmap))]
+ private readonly EditorBeatmap editorBeatmap;
[Cached(typeof(IDistanceSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider();
public TestSceneDistanceSnapGrid()
{
- editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ editorBeatmap = new EditorBeatmap(new OsuBeatmap());
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
index ed6bc5fe0c..29575cb42e 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Editor
{
Beatmap.Value = new WaveformTestBeatmap(audio);
- var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap);
+ var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap);
Children = new Drawable[]
{
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
index b7c7028b52..c001c83877 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
@@ -16,6 +16,7 @@ using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -59,9 +60,12 @@ namespace osu.Game.Tests.Visual.Editor
},
});
+ var editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
+
var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
Dependencies.CacheAs(clock);
Dependencies.CacheAs(clock);
+ Dependencies.CacheAs(editorBeatmap);
Child = new OsuHitObjectComposer(new OsuRuleset());
}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
index 121853d8d0..adfed9a299 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
@@ -5,7 +5,8 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Tests.Visual.Editor
@@ -25,10 +26,13 @@ namespace osu.Game.Tests.Visual.Editor
typeof(RowAttribute)
};
+ [Cached(typeof(EditorBeatmap))]
+ private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+
[BackgroundDependencyLoader]
private void load()
{
- Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Child = new TimingScreen();
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
index 992c47f856..81050b1637 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void LoadComplete()
{
base.LoadComplete();
- ScoreProcessor.FailConditions += (_, __) => true;
+ HealthProcessor.FailConditions += (_, __) => true;
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
index 1580aac8c5..2045072c79 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
@@ -22,12 +22,12 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("wait for multiple judged objects", () => ((FailPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.Count(h => h.AllJudged) > 1);
- AddAssert("total judgements == 1", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits == 1);
+ AddAssert("total judgements == 1", () => ((FailPlayer)Player).HealthProcessor.JudgedHits >= 1);
}
private class FailPlayer : TestPlayer
{
- public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+ public new HealthProcessor HealthProcessor => base.HealthProcessor;
public FailPlayer()
: base(false, false)
@@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void LoadComplete()
{
base.LoadComplete();
- ScoreProcessor.FailConditions += (_, __) => true;
+ HealthProcessor.FailConditions += (_, __) => true;
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index 39c42980ab..ee58219cd3 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create overlay", () =>
{
- Child = hudOverlay = new HUDOverlay(null, null, Array.Empty());
+ Child = hudOverlay = new HUDOverlay(null, null, null, Array.Empty());
action?.Invoke(hudOverlay);
});
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index e04315894e..1a83e35e4f 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestResumeWithResumeOverlay()
{
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
- AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1);
+ AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1);
pauseAndConfirm();
resume();
@@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestPauseWithResumeOverlay()
{
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
- AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1);
+ AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1);
pauseAndConfirm();
@@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("move cursor to button", () =>
InputManager.MoveMouseTo(Player.HUDOverlay.HoldToQuit.Children.OfType().First().ScreenSpaceDrawQuad.Centre));
- AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1);
+ AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1);
pauseAndConfirm();
resumeAndConfirm();
@@ -285,7 +285,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected class PausePlayer : TestPlayer
{
- public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+ public new HealthProcessor HealthProcessor => base.HealthProcessor;
public new HUDOverlay HUDOverlay => base.HUDOverlay;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index aa80819694..8629522dc2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -3,12 +3,14 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -28,12 +30,16 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached(typeof(IReadOnlyList))]
private IReadOnlyList mods { get; set; } = Array.Empty();
+ private const int spawn_interval = 5000;
+
private readonly ScrollingTestContainer[] scrollContainers = new ScrollingTestContainer[4];
private readonly TestPlayfield[] playfields = new TestPlayfield[4];
+ private ScheduledDelegate hitObjectSpawnDelegate;
- public TestSceneScrollingHitObjects()
+ [SetUp]
+ public void Setup() => Schedule(() =>
{
- Add(new GridContainer
+ Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
@@ -43,48 +49,66 @@ namespace osu.Game.Tests.Visual.Gameplay
scrollContainers[0] = new ScrollingTestContainer(ScrollingDirection.Up)
{
RelativeSizeAxes = Axes.Both,
- Child = playfields[0] = new TestPlayfield()
+ Child = playfields[0] = new TestPlayfield(),
+ TimeRange = spawn_interval
},
- scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Up)
+ scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Down)
{
RelativeSizeAxes = Axes.Both,
- Child = playfields[1] = new TestPlayfield()
+ Child = playfields[1] = new TestPlayfield(),
+ TimeRange = spawn_interval
},
},
new Drawable[]
{
- scrollContainers[2] = new ScrollingTestContainer(ScrollingDirection.Up)
+ scrollContainers[2] = new ScrollingTestContainer(ScrollingDirection.Left)
{
RelativeSizeAxes = Axes.Both,
- Child = playfields[2] = new TestPlayfield()
+ Child = playfields[2] = new TestPlayfield(),
+ TimeRange = spawn_interval
},
- scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Up)
+ scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Right)
{
RelativeSizeAxes = Axes.Both,
- Child = playfields[3] = new TestPlayfield()
+ Child = playfields[3] = new TestPlayfield(),
+ TimeRange = spawn_interval
}
}
}
- });
+ };
+ setUpHitObjects();
+ });
+
+ private void setUpHitObjects()
+ {
+ scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0)));
+
+ for (int i = 0; i <= spawn_interval; i += 1000)
+ addHitObject(Time.Current + i);
+
+ hitObjectSpawnDelegate?.Cancel();
+ hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + spawn_interval), 1000, true);
+ }
+
+ [Test]
+ public void TestScrollAlgorithms()
+ {
AddStep("Constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
AddStep("Overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
AddStep("Sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
- AddSliderStep("Time range", 100, 10000, 5000, v => scrollContainers.ForEach(c => c.TimeRange = v));
- AddStep("Add control point", () => addControlPoint(Time.Current + 5000));
+ AddSliderStep("Time range", 100, 10000, spawn_interval, v => scrollContainers.Where(c => c != null).ForEach(c => c.TimeRange = v));
+ AddStep("Add control point", () => addControlPoint(Time.Current + spawn_interval));
}
- protected override void LoadComplete()
+ [Test]
+ public void TestScrollLifetime()
{
- base.LoadComplete();
-
- scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0)));
-
- for (int i = 0; i <= 5000; i += 1000)
- addHitObject(Time.Current + i);
-
- Scheduler.AddDelayed(() => addHitObject(Time.Current + 5000), 1000, true);
+ AddStep("Set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
+ // scroll container time range must be less than the rate of spawning hitobjects
+ // otherwise the hitobjects will spawn already partly visible on screen and look wrong
+ AddStep("Set time range", () => scrollContainers.ForEach(c => c.TimeRange = spawn_interval / 2.0));
}
private void addHitObject(double time)
@@ -207,7 +231,9 @@ namespace osu.Game.Tests.Visual.Gameplay
public TestDrawableHitObject(double time)
: base(new HitObject { StartTime = time })
{
- Origin = Anchor.Centre;
+ Origin = Anchor.Custom;
+ OriginPosition = new Vector2(75 / 4.0f);
+
AutoSizeAxes = Axes.Both;
AddInternal(new Box { Size = new Vector2(75) });
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
index d8a4514df1..35e5f9719c 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
@@ -67,9 +67,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddRepeatStep(@"add many simple", sendManyNotifications, 3);
- AddWaitStep("wait some", 5);
-
- checkProgressingCount(0);
+ waitForCompletion();
AddStep(@"progress #3", sendUploadProgress);
@@ -77,9 +75,7 @@ namespace osu.Game.Tests.Visual.UserInterface
checkDisplayedCount(33);
- AddWaitStep("wait some", 10);
-
- checkProgressingCount(0);
+ waitForCompletion();
}
[Test]
@@ -109,9 +105,9 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep(@"background progress #1", sendBackgroundUploadProgress);
- AddWaitStep("wait some", 5);
+ checkProgressingCount(1);
- checkProgressingCount(0);
+ waitForCompletion();
checkDisplayedCount(2);
@@ -190,6 +186,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private void checkProgressingCount(int expected) => AddAssert($"progressing count is {expected}", () => progressingNotifications.Count == expected);
+ private void waitForCompletion() => AddUntilStep("wait for notification progress completion", () => progressingNotifications.Count == 0);
+
private void sendBarrage()
{
switch (RNG.Next(0, 4))
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index fd455d7cd5..45fe034a70 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -259,6 +259,9 @@ namespace osu.Game.Database
///
/// Create a SHA-2 hash from the provided archive based on file content of all files matching .
///
+ ///
+ /// In the case of no matching files, a hash will be generated from the passed archive's .
+ ///
private string computeHash(ArchiveReader reader)
{
// for now, concatenate all .osu files in the set to create a unique hash.
@@ -270,7 +273,7 @@ namespace osu.Game.Database
s.CopyTo(hashable);
}
- return hashable.Length > 0 ? hashable.ComputeSHA2Hash() : null;
+ return hashable.Length > 0 ? hashable.ComputeSHA2Hash() : reader.Name.ComputeSHA2Hash();
}
///
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index 563dc2dad9..958390d5d2 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
@@ -31,6 +32,7 @@ namespace osu.Game.Graphics.UserInterface
protected readonly Nub Nub;
private readonly Box leftBox;
private readonly Box rightBox;
+ private readonly Container nubContainer;
public virtual string TooltipText { get; private set; }
@@ -72,10 +74,15 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.CentreRight,
Alpha = 0.5f,
},
- Nub = new Nub
+ nubContainer = new Container
{
- Origin = Anchor.TopCentre,
- Expanded = true,
+ RelativeSizeAxes = Axes.Both,
+ Child = Nub = new Nub
+ {
+ Origin = Anchor.TopCentre,
+ RelativePositionAxes = Axes.X,
+ Expanded = true,
+ },
},
new HoverClickSounds()
};
@@ -90,6 +97,13 @@ namespace osu.Game.Graphics.UserInterface
AccentColour = colours.Pink;
}
+ protected override void Update()
+ {
+ base.Update();
+
+ nubContainer.Padding = new MarginPadding { Horizontal = RangePadding };
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -176,14 +190,14 @@ namespace osu.Game.Graphics.UserInterface
{
base.UpdateAfterChildren();
leftBox.Scale = new Vector2(Math.Clamp(
- Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, DrawWidth), 1);
+ RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, DrawWidth), 1);
rightBox.Scale = new Vector2(Math.Clamp(
- DrawWidth - Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, DrawWidth), 1);
+ DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2, 0, DrawWidth), 1);
}
protected override void UpdateValue(float value)
{
- Nub.MoveToX(RangePadding + UsableWidth * value, 250, Easing.OutQuint);
+ Nub.MoveToX(value, 250, Easing.OutQuint);
}
///
diff --git a/osu.Game/Graphics/UserInterface/ScreenTitle.cs b/osu.Game/Graphics/UserInterface/ScreenTitle.cs
index 10fc312d8b..ecd0508258 100644
--- a/osu.Game/Graphics/UserInterface/ScreenTitle.cs
+++ b/osu.Game/Graphics/UserInterface/ScreenTitle.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
using osuTK;
@@ -13,15 +14,15 @@ namespace osu.Game.Graphics.UserInterface
{
public abstract class ScreenTitle : CompositeDrawable, IHasAccentColour
{
- public const float ICON_WIDTH = ICON_SIZE + icon_spacing;
+ public const float ICON_WIDTH = ICON_SIZE + spacing;
public const float ICON_SIZE = 25;
+ private const float spacing = 6;
+ private const int text_offset = 2;
private SpriteIcon iconSprite;
private readonly OsuSpriteText titleText, pageText;
- private const float icon_spacing = 10;
-
protected IconUsage Icon
{
set
@@ -63,26 +64,35 @@ namespace osu.Game.Graphics.UserInterface
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
- Spacing = new Vector2(icon_spacing, 0),
+ Spacing = new Vector2(spacing, 0),
+ Direction = FillDirection.Horizontal,
Children = new[]
{
- CreateIcon(),
- new FillFlowContainer
+ CreateIcon().With(t =>
{
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(6, 0),
- Children = new[]
- {
- titleText = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light),
- },
- pageText = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light),
- }
- }
+ t.Anchor = Anchor.Centre;
+ t.Origin = Anchor.Centre;
+ }),
+ titleText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold),
+ Margin = new MarginPadding { Bottom = text_offset }
+ },
+ new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(4),
+ Colour = Color4.Gray,
+ },
+ pageText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.GetFont(size: 20),
+ Margin = new MarginPadding { Bottom = text_offset }
}
}
},
diff --git a/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs b/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs
index f590e7e357..c2a13970de 100644
--- a/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs
+++ b/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs
@@ -4,7 +4,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osuTK;
@@ -16,8 +15,6 @@ namespace osu.Game.Graphics.UserInterface
///
public class ScreenTitleTextureIcon : CompositeDrawable
{
- private const float circle_allowance = 0.8f;
-
private readonly string textureName;
public ScreenTitleTextureIcon(string textureName)
@@ -26,38 +23,17 @@ namespace osu.Game.Graphics.UserInterface
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures, OsuColour colours)
+ private void load(TextureStore textures)
{
- Size = new Vector2(ScreenTitle.ICON_SIZE / circle_allowance);
+ Size = new Vector2(ScreenTitle.ICON_SIZE);
- InternalChildren = new Drawable[]
+ InternalChild = new Sprite
{
- new CircularContainer
- {
- Masking = true,
- BorderColour = colours.Violet,
- BorderThickness = 3,
- MaskingSmoothness = 1,
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- new Sprite
- {
- RelativeSizeAxes = Axes.Both,
- Texture = textures.Get(textureName),
- Size = new Vector2(circle_allowance),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = colours.Violet,
- Alpha = 0,
- AlwaysPresent = true,
- },
- }
- },
+ RelativeSizeAxes = Axes.Both,
+ Texture = textures.Get(textureName),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ FillMode = FillMode.Fit
};
}
}
diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs
index ff3618b263..fe8756a4d2 100644
--- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs
@@ -34,11 +34,21 @@ namespace osu.Game.Graphics.UserInterface
public override bool OnPressed(PlatformAction action)
{
- // Shift+delete is handled via PlatformAction on macOS. this is not so useful in the context of a SearchTextBox
- // as we do not allow arrow key navigation in the first place (ie. the caret should always be at the end of text)
- // Avoid handling it here to allow other components to potentially consume the shortcut.
- if (action.ActionType == PlatformActionType.CharNext && action.ActionMethod == PlatformActionMethod.Delete)
- return false;
+ switch (action.ActionType)
+ {
+ case PlatformActionType.LineEnd:
+ case PlatformActionType.LineStart:
+ return false;
+
+ // Shift+delete is handled via PlatformAction on macOS. this is not so useful in the context of a SearchTextBox
+ // as we do not allow arrow key navigation in the first place (ie. the caret should always be at the end of text)
+ // Avoid handling it here to allow other components to potentially consume the shortcut.
+ case PlatformActionType.CharNext:
+ if (action.ActionMethod == PlatformActionMethod.Delete)
+ return false;
+
+ break;
+ }
return base.OnPressed(action);
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index c7c746bed3..84aba4af52 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -39,6 +39,7 @@ using osu.Game.Online.Chat;
using osu.Game.Skinning;
using osuTK.Graphics;
using osu.Game.Overlays.Volume;
+using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Select;
using osu.Game.Utils;
@@ -204,6 +205,7 @@ namespace osu.Game
Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade);
+ SelectedMods.BindValueChanged(modsChanged);
Beatmap.BindValueChanged(beatmapChanged, true);
}
@@ -403,9 +405,29 @@ namespace osu.Game
oldBeatmap.Track.Completed -= currentTrackCompleted;
}
+ updateModDefaults();
+
nextBeatmap?.LoadBeatmapAsync();
}
+ private void modsChanged(ValueChangedEvent> mods)
+ {
+ updateModDefaults();
+ }
+
+ private void updateModDefaults()
+ {
+ BeatmapDifficulty baseDifficulty = Beatmap.Value.BeatmapInfo.BaseDifficulty;
+
+ if (baseDifficulty != null && SelectedMods.Value.Any(m => m is IApplicableToDifficulty))
+ {
+ var adjustedDifficulty = baseDifficulty.Clone();
+
+ foreach (var mod in SelectedMods.Value.OfType())
+ mod.ReadFromDifficulty(adjustedDifficulty);
+ }
+ }
+
private void currentTrackCompleted() => Schedule(() =>
{
if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled)
diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs
index b2e9be24b3..3b6f0d778d 100644
--- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs
@@ -47,6 +47,8 @@ namespace osu.Game.Overlays.Changelog
private void load(OsuColour colours)
{
TabControl.AccentColour = colours.Violet;
+ TitleBackgroundColour = colours.GreyVioletDarker;
+ ControlBackgroundColour = colours.GreyVioletDark;
}
private ChangelogHeaderTitle title;
@@ -111,7 +113,7 @@ namespace osu.Game.Overlays.Changelog
public ChangelogHeaderTitle()
{
- Title = "Changelog";
+ Title = "changelog";
Version = null;
}
diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs
index c1c5113c5e..4ad8e95512 100644
--- a/osu.Game/Overlays/Direct/DirectPanel.cs
+++ b/osu.Game/Overlays/Direct/DirectPanel.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . 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.Diagnostics;
using System.Linq;
@@ -9,21 +10,25 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Direct
{
- public abstract class DirectPanel : Container
+ public abstract class DirectPanel : OsuClickableContainer, IHasContextMenu
{
public readonly BeatmapSetInfo SetInfo;
@@ -32,8 +37,6 @@ namespace osu.Game.Overlays.Direct
private Container content;
- private BeatmapSetOverlay beatmapSetOverlay;
-
public PreviewTrack Preview => PlayButton.Preview;
public Bindable PreviewPlaying => PlayButton?.Playing;
@@ -44,6 +47,8 @@ namespace osu.Game.Overlays.Direct
protected override Container Content => content;
+ protected Action ViewBeatmap;
+
protected DirectPanel(BeatmapSetInfo setInfo)
{
Debug.Assert(setInfo.OnlineBeatmapSetID != null);
@@ -70,8 +75,6 @@ namespace osu.Game.Overlays.Direct
[BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapManager beatmaps, OsuColour colours, BeatmapSetOverlay beatmapSetOverlay)
{
- this.beatmapSetOverlay = beatmapSetOverlay;
-
AddInternal(content = new Container
{
RelativeSizeAxes = Axes.Both,
@@ -88,6 +91,12 @@ namespace osu.Game.Overlays.Direct
},
}
});
+
+ Action = ViewBeatmap = () =>
+ {
+ Debug.Assert(SetInfo.OnlineBeatmapSetID != null);
+ beatmapSetOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value);
+ };
}
protected override void Update()
@@ -120,13 +129,6 @@ namespace osu.Game.Overlays.Direct
base.OnHoverLost(e);
}
- protected override bool OnClick(ClickEvent e)
- {
- Debug.Assert(SetInfo.OnlineBeatmapSetID != null);
- beatmapSetOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value);
- return true;
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
@@ -203,5 +205,10 @@ namespace osu.Game.Overlays.Direct
Value = value;
}
}
+
+ public MenuItem[] ContextMenuItems => new MenuItem[]
+ {
+ new OsuMenuItem("View Beatmap", MenuItemType.Highlighted, ViewBeatmap),
+ };
}
}
diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs
index 27620ab523..e3cf58ed0a 100644
--- a/osu.Game/Overlays/News/NewsHeader.cs
+++ b/osu.Game/Overlays/News/NewsHeader.cs
@@ -4,7 +4,6 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
@@ -15,7 +14,7 @@ namespace osu.Game.Overlays.News
{
public class NewsHeader : OverlayHeader
{
- private const string front_page_string = "Front Page";
+ private const string front_page_string = "frontpage";
private NewsHeaderTitle title;
@@ -33,16 +32,18 @@ namespace osu.Game.Overlays.News
ShowFrontPage?.Invoke();
};
- Current.ValueChanged += showArticle;
+ Current.ValueChanged += showPost;
}
[BackgroundDependencyLoader]
- private void load(OsuColour colour)
+ private void load(OsuColour colours)
{
- TabControl.AccentColour = colour.Violet;
+ TabControl.AccentColour = colours.Violet;
+ TitleBackgroundColour = colours.GreyVioletDarker;
+ ControlBackgroundColour = colours.GreyVioletDark;
}
- private void showArticle(ValueChangedEvent e)
+ private void showPost(ValueChangedEvent e)
{
if (e.OldValue != null)
TabControl.RemoveItem(e.OldValue);
@@ -52,19 +53,17 @@ namespace osu.Game.Overlays.News
TabControl.AddItem(e.NewValue);
TabControl.Current.Value = e.NewValue;
- title.IsReadingArticle = true;
+ title.IsReadingPost = true;
}
else
{
TabControl.Current.Value = front_page_string;
- title.IsReadingArticle = false;
+ title.IsReadingPost = false;
}
}
protected override Drawable CreateBackground() => new NewsHeaderBackground();
- protected override Drawable CreateContent() => new Container();
-
protected override ScreenTitle CreateTitle() => title = new NewsHeaderTitle();
private class NewsHeaderBackground : Sprite
@@ -84,17 +83,17 @@ namespace osu.Game.Overlays.News
private class NewsHeaderTitle : ScreenTitle
{
- private const string article_string = "Article";
+ private const string post_string = "post";
- public bool IsReadingArticle
+ public bool IsReadingPost
{
- set => Section = value ? article_string : front_page_string;
+ set => Section = value ? post_string : front_page_string;
}
public NewsHeaderTitle()
{
- Title = "News";
- IsReadingArticle = false;
+ Title = "news";
+ IsReadingPost = false;
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/news");
diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs
index 2e032db2ba..7a397d10c6 100644
--- a/osu.Game/Overlays/OverlayHeader.cs
+++ b/osu.Game/Overlays/OverlayHeader.cs
@@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
+using osuTK.Graphics;
namespace osu.Game.Overlays
{
@@ -11,59 +14,96 @@ namespace osu.Game.Overlays
{
protected readonly OverlayHeaderTabControl TabControl;
- private const float cover_height = 150;
- private const float cover_info_height = 75;
+ private readonly Box titleBackground;
+ private readonly Box controlBackground;
+ private readonly Container background;
+
+ protected Color4 TitleBackgroundColour
+ {
+ set => titleBackground.Colour = value;
+ }
+
+ protected Color4 ControlBackgroundColour
+ {
+ set => controlBackground.Colour = value;
+ }
+
+ protected float BackgroundHeight
+ {
+ set => background.Height = value;
+ }
protected OverlayHeader()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Children = new Drawable[]
+ Add(new FillFlowContainer
{
- new Container
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new[]
{
- RelativeSizeAxes = Axes.X,
- Height = cover_height,
- Masking = true,
- Child = CreateBackground()
- },
- new Container
- {
- Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN },
- Y = cover_height,
- Height = cover_info_height,
- RelativeSizeAxes = Axes.X,
- Anchor = Anchor.TopLeft,
- Origin = Anchor.BottomLeft,
- Depth = -float.MaxValue,
- Children = new Drawable[]
+ background = new Container
{
- CreateTitle().With(t => t.X = -ScreenTitle.ICON_WIDTH),
- TabControl = new OverlayHeaderTabControl
+ RelativeSizeAxes = Axes.X,
+ Height = 80,
+ Masking = true,
+ Child = CreateBackground()
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
{
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- RelativeSizeAxes = Axes.X,
- Height = cover_info_height - 30,
- Margin = new MarginPadding { Left = -UserProfileOverlay.CONTENT_X_MARGIN },
- Padding = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN }
+ titleBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Gray,
+ },
+ CreateTitle().With(title =>
+ {
+ title.Margin = new MarginPadding
+ {
+ Vertical = 10,
+ Left = UserProfileOverlay.CONTENT_X_MARGIN
+ };
+ })
}
- }
- },
- new Container
- {
- Margin = new MarginPadding { Top = cover_height },
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Child = CreateContent()
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Depth = -float.MaxValue,
+ Children = new Drawable[]
+ {
+ controlBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Gray,
+ },
+ TabControl = new OverlayHeaderTabControl
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.X,
+ Height = 30,
+ Padding = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN },
+ }
+ }
+ },
+ CreateContent()
}
- };
+ });
}
protected abstract Drawable CreateBackground();
- protected abstract Drawable CreateContent();
+ [NotNull]
+ protected virtual Drawable CreateContent() => new Container();
protected abstract ScreenTitle CreateTitle();
}
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index 76613c156d..4a792f7375 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -26,6 +26,8 @@ namespace osu.Game.Overlays.Profile
public ProfileHeader()
{
+ BackgroundHeight = 150;
+
User.ValueChanged += e => updateDisplay(e.NewValue);
TabControl.AddItem("Info");
@@ -38,6 +40,8 @@ namespace osu.Game.Overlays.Profile
private void load(OsuColour colours)
{
TabControl.AccentColour = colours.Seafoam;
+ TitleBackgroundColour = colours.GreySeafoamDarker;
+ ControlBackgroundColour = colours.GreySeafoam;
}
protected override Drawable CreateBackground() =>
@@ -101,8 +105,8 @@ namespace osu.Game.Overlays.Profile
{
public ProfileHeaderTitle()
{
- Title = "Player";
- Section = "Info";
+ Title = "player";
+ Section = "info";
}
[BackgroundDependencyLoader]
@@ -110,6 +114,8 @@ namespace osu.Game.Overlays.Profile
{
AccentColour = colours.Seafoam;
}
+
+ protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/profile");
}
}
}
diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs
index 37478d902b..5975e94ffc 100644
--- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs
+++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Cursor;
namespace osu.Game.Overlays.SearchableList
{
@@ -61,21 +62,20 @@ namespace osu.Game.Overlays.SearchableList
scrollContainer = new Container
{
RelativeSizeAxes = Axes.Both,
- Children = new[]
+ Child = new OsuContextMenuContainer
{
- new OsuScrollContainer
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Child = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
- Children = new[]
+ Child = ScrollFlow = new FillFlowContainer
{
- ScrollFlow = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = WIDTH_PADDING, Bottom = 50 },
- Direction = FillDirection.Vertical,
- },
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = WIDTH_PADDING, Bottom = 50 },
+ Direction = FillDirection.Vertical,
},
},
},
diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs
index 4710465536..89e7866707 100644
--- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs
+++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Edit
private readonly DrawableRuleset drawableRuleset;
[Resolved]
- private IEditorBeatmap beatmap { get; set; }
+ private EditorBeatmap beatmap { get; set; }
public DrawableEditRulesetWrapper(DrawableRuleset drawableRuleset)
{
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 22d94abcb9..bfaa7e8872 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
@@ -35,20 +34,20 @@ namespace osu.Game.Rulesets.Edit
{
protected IRulesetConfigManager Config { get; private set; }
- protected new EditorBeatmap EditorBeatmap { get; private set; }
-
protected readonly Ruleset Ruleset;
[Resolved]
protected IFrameBasedClock EditorClock { get; private set; }
+ [Resolved]
+ protected EditorBeatmap EditorBeatmap { get; private set; }
+
[Resolved]
private IAdjustableClock adjustableClock { get; set; }
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; }
- private Beatmap playableBeatmap;
private IBeatmapProcessor beatmapProcessor;
private DrawableEditRulesetWrapper drawableRulesetWrapper;
@@ -68,9 +67,17 @@ namespace osu.Game.Rulesets.Edit
[BackgroundDependencyLoader]
private void load(IFrameBasedClock framedClock)
{
+ beatmapProcessor = Ruleset.CreateBeatmapProcessor(EditorBeatmap.PlayableBeatmap);
+
+ EditorBeatmap.HitObjectAdded += addHitObject;
+ EditorBeatmap.HitObjectRemoved += removeHitObject;
+ EditorBeatmap.StartTimeChanged += UpdateHitObject;
+
+ Config = Dependencies.Get().GetConfigFor(Ruleset);
+
try
{
- drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, playableBeatmap))
+ drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap))
{
Clock = framedClock,
ProcessCustomClock = false
@@ -140,28 +147,6 @@ namespace osu.Game.Rulesets.Edit
blueprintContainer.SelectionChanged += selectionChanged;
}
- protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
- {
- var parentWorkingBeatmap = parent.Get>().Value;
-
- playableBeatmap = (Beatmap)parentWorkingBeatmap.GetPlayableBeatmap(Ruleset.RulesetInfo);
-
- beatmapProcessor = Ruleset.CreateBeatmapProcessor(playableBeatmap);
-
- base.EditorBeatmap = EditorBeatmap = new EditorBeatmap(playableBeatmap);
- EditorBeatmap.HitObjectAdded += addHitObject;
- EditorBeatmap.HitObjectRemoved += removeHitObject;
- EditorBeatmap.StartTimeChanged += UpdateHitObject;
-
- var dependencies = new DependencyContainer(parent);
- dependencies.CacheAs(EditorBeatmap);
- dependencies.CacheAs>(EditorBeatmap);
-
- Config = dependencies.Get().GetConfigFor(Ruleset);
-
- return base.CreateChildDependencies(dependencies);
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
@@ -234,7 +219,7 @@ namespace osu.Game.Rulesets.Edit
scheduledUpdate = Schedule(() =>
{
beatmapProcessor?.PreProcess();
- hitObject?.ApplyDefaults(playableBeatmap.ControlPointInfo, playableBeatmap.BeatmapInfo.BaseDifficulty);
+ hitObject?.ApplyDefaults(EditorBeatmap.ControlPointInfo, EditorBeatmap.BeatmapInfo.BaseDifficulty);
beatmapProcessor?.PostProcess();
});
}
@@ -333,11 +318,6 @@ namespace osu.Game.Rulesets.Edit
///
public abstract IEnumerable HitObjects { get; }
- ///
- /// An editor-specific beatmap, exposing mutation events.
- ///
- public IEditorBeatmap EditorBeatmap { get; protected set; }
-
///
/// Whether the user's cursor is currently in an area of the that is valid for placement.
///
diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs
index f07f76a2b8..599135ba54 100644
--- a/osu.Game/Rulesets/Judgements/Judgement.cs
+++ b/osu.Game/Rulesets/Judgements/Judgement.cs
@@ -11,6 +11,12 @@ namespace osu.Game.Rulesets.Judgements
///
public class Judgement
{
+ ///
+ /// The default health increase for a maximum judgement, as a proportion of total health.
+ /// By default, each maximum judgement restores 5% of total health.
+ ///
+ protected const double DEFAULT_MAX_HEALTH_INCREASE = 0.05;
+
///
/// The maximum that can be achieved.
///
@@ -55,7 +61,32 @@ namespace osu.Game.Rulesets.Judgements
///
/// The to find the numeric health increase for.
/// The numeric health increase of .
- protected virtual double HealthIncreaseFor(HitResult result) => 0;
+ protected virtual double HealthIncreaseFor(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Miss:
+ return -DEFAULT_MAX_HEALTH_INCREASE;
+
+ case HitResult.Meh:
+ return -DEFAULT_MAX_HEALTH_INCREASE * 0.05;
+
+ case HitResult.Ok:
+ return -DEFAULT_MAX_HEALTH_INCREASE * 0.01;
+
+ case HitResult.Good:
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.3;
+
+ case HitResult.Great:
+ return DEFAULT_MAX_HEALTH_INCREASE;
+
+ case HitResult.Perfect:
+ return DEFAULT_MAX_HEALTH_INCREASE * 1.05;
+
+ default:
+ return 0;
+ }
+ }
///
/// Retrieves the numeric health increase of a .
diff --git a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs
index 4d4cd75434..34198da722 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs
@@ -10,6 +10,17 @@ namespace osu.Game.Rulesets.Mods
///
public interface IApplicableToDifficulty : IApplicableMod
{
+ ///
+ /// Called when a beatmap is changed. Can be used to read default values.
+ /// Any changes made will not be preserved.
+ ///
+ /// The difficulty to read from.
+ void ReadFromDifficulty(BeatmapDifficulty difficulty);
+
+ ///
+ /// Called post beatmap conversion. Can be used to apply changes to difficulty attributes.
+ ///
+ /// The difficulty to mutate.
void ApplyToDifficulty(BeatmapDifficulty difficulty);
}
}
diff --git a/osu.Game/Rulesets/Mods/IApplicableToHealthProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableToHealthProcessor.cs
new file mode 100644
index 0000000000..a181955653
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/IApplicableToHealthProcessor.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Mods
+{
+ public interface IApplicableToHealthProcessor : IApplicableMod
+ {
+ ///
+ /// Provide a to a mod. Called once on initialisation of a play instance.
+ ///
+ void ApplyToHealthProcessor(HealthProcessor healthProcessor);
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
index 224fc78508..c5b8a1bc73 100644
--- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
@@ -5,6 +5,7 @@ using osu.Game.Beatmaps;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using System;
+using System.Collections.Generic;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods
@@ -47,25 +48,48 @@ namespace osu.Game.Rulesets.Mods
private BeatmapDifficulty difficulty;
- public void ApplyToDifficulty(BeatmapDifficulty difficulty)
+ public void ReadFromDifficulty(BeatmapDifficulty difficulty)
{
if (this.difficulty == null || this.difficulty.ID != difficulty.ID)
{
- this.difficulty = difficulty;
TransferSettings(difficulty);
+ this.difficulty = difficulty;
}
- else
- ApplySettings(difficulty);
}
+ public void ApplyToDifficulty(BeatmapDifficulty difficulty) => ApplySettings(difficulty);
+
///
/// Transfer initial settings from the beatmap to settings.
///
/// The beatmap's initial values.
protected virtual void TransferSettings(BeatmapDifficulty difficulty)
{
- DrainRate.Value = DrainRate.Default = difficulty.DrainRate;
- OverallDifficulty.Value = OverallDifficulty.Default = difficulty.OverallDifficulty;
+ TransferSetting(DrainRate, difficulty.DrainRate);
+ TransferSetting(OverallDifficulty, difficulty.OverallDifficulty);
+ }
+
+ private readonly Dictionary userChangedSettings = new Dictionary();
+
+ ///
+ /// Transfer a setting from to a configuration bindable.
+ /// Only performs the transfer if the user it not currently overriding..
+ ///
+ protected void TransferSetting(BindableNumber bindable, T beatmapDefault)
+ where T : struct, IComparable, IConvertible, IEquatable
+ {
+ bindable.UnbindEvents();
+
+ userChangedSettings.TryAdd(bindable, false);
+
+ bindable.Default = beatmapDefault;
+
+ // users generally choose a difficulty setting and want it to stick across multiple beatmap changes.
+ // we only want to value transfer if the user hasn't changed the value previously.
+ if (!userChangedSettings[bindable])
+ bindable.Value = beatmapDefault;
+
+ bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault;
}
///
diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs
index a91e4dfd5c..ec0f50c0be 100644
--- a/osu.Game/Rulesets/Mods/ModEasy.cs
+++ b/osu.Game/Rulesets/Mods/ModEasy.cs
@@ -8,11 +8,10 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModEasy : Mod, IApplicableToDifficulty, IApplicableFailOverride, IApplicableToScoreProcessor
+ public abstract class ModEasy : Mod, IApplicableToDifficulty, IApplicableFailOverride, IApplicableToHealthProcessor
{
public override string Name => "Easy";
public override string Acronym => "EZ";
@@ -33,6 +32,8 @@ namespace osu.Game.Rulesets.Mods
private BindableNumber health;
+ public void ReadFromDifficulty(BeatmapDifficulty difficulty) { }
+
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
const float ratio = 0.5f;
@@ -59,11 +60,9 @@ namespace osu.Game.Rulesets.Mods
public bool RestartOnFail => false;
- public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
{
- health = scoreProcessor.Health.GetBoundCopy();
+ health = healthProcessor.Health.GetBoundCopy();
}
-
- public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
}
}
diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs
index 2bcac3e4a9..a613d41cf4 100644
--- a/osu.Game/Rulesets/Mods/ModHardRock.cs
+++ b/osu.Game/Rulesets/Mods/ModHardRock.cs
@@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Mods
public override string Description => "Everything just got a bit harder...";
public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) };
+ public void ReadFromDifficulty(BeatmapDifficulty difficulty) { }
+
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
const float ratio = 1.4f;
diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs
index 0994d1f7d3..afa263f1c9 100644
--- a/osu.Game/Rulesets/Mods/ModPerfect.cs
+++ b/osu.Game/Rulesets/Mods/ModPerfect.cs
@@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage Icon => OsuIcon.ModPerfect;
public override string Description => "SS or quit.";
- protected override bool FailCondition(ScoreProcessor scoreProcessor, JudgementResult result) => scoreProcessor.Accuracy.Value != 1;
+ protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => result.Type != result.Judgement.MaxResult;
}
}
diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
index c4c4ab1f04..a4d0631d8c 100644
--- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
+++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
@@ -6,11 +6,10 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModSuddenDeath : Mod, IApplicableToScoreProcessor, IApplicableFailOverride
+ public abstract class ModSuddenDeath : Mod, IApplicableToHealthProcessor, IApplicableFailOverride
{
public override string Name => "Sudden Death";
public override string Acronym => "SD";
@@ -24,13 +23,11 @@ namespace osu.Game.Rulesets.Mods
public bool AllowFail => true;
public bool RestartOnFail => true;
- public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
{
- scoreProcessor.FailConditions += FailCondition;
+ healthProcessor.FailConditions += FailCondition;
}
- public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
-
- protected virtual bool FailCondition(ScoreProcessor scoreProcessor, JudgementResult result) => scoreProcessor.Combo.Value == 0 && result.Judgement.AffectsCombo;
+ protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => !result.IsHit && result.Judgement.AffectsCombo;
}
}
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index 107633194f..67ec6d15ea 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -71,10 +71,16 @@ namespace osu.Game.Rulesets
public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null);
///
- /// Creates a for a beatmap converted to this ruleset.
+ /// Creates a for this .
///
/// The score processor.
- public virtual ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new ScoreProcessor(beatmap);
+ public virtual ScoreProcessor CreateScoreProcessor() => new ScoreProcessor();
+
+ ///
+ /// Creates a for this .
+ ///
+ /// The health processor.
+ public virtual HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime);
///
/// Creates a to convert a to one that is applicable for this .
diff --git a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs
new file mode 100644
index 0000000000..5dfb5167f4
--- /dev/null
+++ b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Scoring
+{
+ ///
+ /// A that accumulates health and causes a fail if the final health
+ /// is less than a value required to pass the beatmap.
+ ///
+ public class AccumulatingHealthProcessor : HealthProcessor
+ {
+ protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value < requiredHealth;
+
+ private readonly double requiredHealth;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The minimum amount of health required to beatmap.
+ public AccumulatingHealthProcessor(double requiredHealth)
+ {
+ this.requiredHealth = requiredHealth;
+ }
+
+ protected override void Reset(bool storeResults)
+ {
+ base.Reset(storeResults);
+
+ Health.Value = 0;
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
new file mode 100644
index 0000000000..fffcbb3c9f
--- /dev/null
+++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
@@ -0,0 +1,157 @@
+// Copyright (c) ppy Pty Ltd . 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
+{
+ ///
+ /// A which continuously drains health.
+ /// At HP=0, the minimum health reached for a perfect play is 95%.
+ /// At HP=5, the minimum health reached for a perfect play is 70%.
+ /// At HP=10, the minimum health reached for a perfect play is 30%.
+ ///
+ public class DrainingHealthProcessor : HealthProcessor
+ {
+ ///
+ /// A reasonable allowable error for the minimum health offset from . A 1% error is unnoticeable.
+ ///
+ private const double minimum_health_error = 0.01;
+
+ ///
+ /// The minimum health target at an HP drain rate of 0.
+ ///
+ private const double min_health_target = 0.95;
+
+ ///
+ /// The minimum health target at an HP drain rate of 5.
+ ///
+ private const double mid_health_target = 0.70;
+
+ ///
+ /// The minimum health target at an HP drain rate of 10.
+ ///
+ private const double max_health_target = 0.30;
+
+ private IBeatmap beatmap;
+
+ private double gameplayEndTime;
+
+ private readonly double drainStartTime;
+
+ private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>();
+ private double targetMinimumHealth;
+ private double drainRate = 1;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The time after which draining should begin.
+ public DrainingHealthProcessor(double drainStartTime)
+ {
+ this.drainStartTime = drainStartTime;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!IsBreakTime.Value)
+ {
+ // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time
+ double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime);
+ double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime);
+
+ Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime);
+ }
+ }
+
+ public override void ApplyBeatmap(IBeatmap beatmap)
+ {
+ this.beatmap = beatmap;
+
+ if (beatmap.HitObjects.Count > 0)
+ gameplayEndTime = beatmap.HitObjects[^1].GetEndTime();
+
+ targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target);
+
+ base.ApplyBeatmap(beatmap);
+ }
+
+ protected override void ApplyResultInternal(JudgementResult result)
+ {
+ base.ApplyResultInternal(result);
+ healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result)));
+ }
+
+ protected override void Reset(bool storeResults)
+ {
+ base.Reset(storeResults);
+
+ drainRate = 1;
+
+ if (storeResults)
+ drainRate = computeDrainRate();
+
+ healthIncreases.Clear();
+ }
+
+ private double computeDrainRate()
+ {
+ if (healthIncreases.Count == 0)
+ return 0;
+
+ int adjustment = 1;
+ double result = 1;
+
+ // Although we expect the following loop to converge within 30 iterations (health within 1/2^31 accuracy of the target),
+ // we'll still keep a safety measure to avoid infinite loops by detecting overflows.
+ while (adjustment > 0)
+ {
+ double currentHealth = 1;
+ double lowestHealth = 1;
+ int currentBreak = -1;
+
+ for (int i = 0; i < healthIncreases.Count; i++)
+ {
+ double currentTime = healthIncreases[i].time;
+ double lastTime = i > 0 ? healthIncreases[i - 1].time : drainStartTime;
+
+ // Subtract any break time from the duration since the last object
+ if (beatmap.Breaks.Count > 0)
+ {
+ // Advance the last break occuring before the current time
+ while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime)
+ currentBreak++;
+
+ if (currentBreak >= 0)
+ lastTime = Math.Max(lastTime, beatmap.Breaks[currentBreak].EndTime);
+ }
+
+ // Apply health adjustments
+ currentHealth -= (healthIncreases[i].time - lastTime) * result;
+ lowestHealth = Math.Min(lowestHealth, currentHealth);
+ currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health);
+
+ // Common scenario for when the drain rate is definitely too harsh
+ if (lowestHealth < 0)
+ break;
+ }
+
+ // Stop if the resulting health is within a reasonable offset from the target
+ if (Math.Abs(lowestHealth - targetMinimumHealth) <= minimum_health_error)
+ break;
+
+ // This effectively works like a binary search - each iteration the search space moves closer to the target, but may exceed it.
+ adjustment *= 2;
+ result += 1.0 / adjustment * Math.Sign(lowestHealth - targetMinimumHealth);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
new file mode 100644
index 0000000000..0c6b3f67b4
--- /dev/null
+++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
@@ -0,0 +1,83 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Bindables;
+using osu.Framework.MathUtils;
+using osu.Game.Rulesets.Judgements;
+
+namespace osu.Game.Rulesets.Scoring
+{
+ public abstract class HealthProcessor : JudgementProcessor
+ {
+ ///
+ /// Invoked when the is in a failed state.
+ /// Return true if the fail was permitted.
+ ///
+ public event Func Failed;
+
+ ///
+ /// Additional conditions on top of that cause a failing state.
+ ///
+ public event Func FailConditions;
+
+ ///
+ /// The current health.
+ ///
+ public readonly BindableDouble Health = new BindableDouble(1) { MinValue = 0, MaxValue = 1 };
+
+ ///
+ /// Whether gameplay is currently in a break.
+ ///
+ public readonly IBindable IsBreakTime = new Bindable();
+
+ ///
+ /// Whether this ScoreProcessor has already triggered the failed state.
+ ///
+ public bool HasFailed { get; private set; }
+
+ protected override void ApplyResultInternal(JudgementResult result)
+ {
+ result.HealthAtJudgement = Health.Value;
+ result.FailedAtJudgement = HasFailed;
+
+ if (HasFailed)
+ return;
+
+ Health.Value += GetHealthIncreaseFor(result);
+
+ if (!DefaultFailCondition && FailConditions?.Invoke(this, result) != true)
+ return;
+
+ if (Failed?.Invoke() != false)
+ HasFailed = true;
+ }
+
+ protected override void RevertResultInternal(JudgementResult result)
+ {
+ Health.Value = result.HealthAtJudgement;
+
+ // Todo: Revert HasFailed state with proper player support
+ }
+
+ ///
+ /// Retrieves the health increase for a .
+ ///
+ /// The .
+ /// The health increase.
+ protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.Judgement.HealthIncreaseFor(result);
+
+ ///
+ /// The default conditions for failing.
+ ///
+ protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value);
+
+ protected override void Reset(bool storeResults)
+ {
+ base.Reset(storeResults);
+
+ Health.Value = 1;
+ HasFailed = false;
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
new file mode 100644
index 0000000000..3016007f98
--- /dev/null
+++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
@@ -0,0 +1,140 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Scoring
+{
+ public abstract class JudgementProcessor : Component
+ {
+ ///
+ /// Invoked when all s have been judged by this .
+ ///
+ public event Action AllJudged;
+
+ ///
+ /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by this .
+ ///
+ public event Action NewJudgement;
+
+ ///
+ /// The maximum number of hits that can be judged.
+ ///
+ protected int MaxHits { get; private set; }
+
+ ///
+ /// The total number of judged s at the current point in time.
+ ///
+ public int JudgedHits { get; private set; }
+
+ ///
+ /// Whether all s have been processed.
+ ///
+ public bool HasCompleted => JudgedHits == MaxHits;
+
+ ///
+ /// Applies a to this .
+ ///
+ /// The to read properties from.
+ public virtual void ApplyBeatmap(IBeatmap beatmap)
+ {
+ Reset(false);
+ SimulateAutoplay(beatmap);
+ Reset(true);
+ }
+
+ ///
+ /// Applies the score change of a to this .
+ ///
+ /// The to apply.
+ public void ApplyResult(JudgementResult result)
+ {
+ JudgedHits++;
+
+ ApplyResultInternal(result);
+
+ NewJudgement?.Invoke(result);
+
+ if (HasCompleted)
+ AllJudged?.Invoke();
+ }
+
+ ///
+ /// Reverts the score change of a that was applied to this .
+ ///
+ /// The judgement scoring result.
+ public void RevertResult(JudgementResult result)
+ {
+ JudgedHits--;
+
+ RevertResultInternal(result);
+ }
+
+ ///
+ /// Applies the score change of a to this .
+ ///
+ ///
+ /// Any changes applied via this method can be reverted via .
+ ///
+ /// The to apply.
+ protected abstract void ApplyResultInternal(JudgementResult result);
+
+ ///
+ /// Reverts the score change of a that was applied to this via .
+ ///
+ /// The judgement scoring result.
+ protected abstract void RevertResultInternal(JudgementResult result);
+
+ ///
+ /// Resets this to a default state.
+ ///
+ /// Whether to store the current state of the for future use.
+ protected virtual void Reset(bool storeResults)
+ {
+ if (storeResults)
+ MaxHits = JudgedHits;
+
+ JudgedHits = 0;
+ }
+
+ ///
+ /// Creates the that represents the scoring result for a .
+ ///
+ /// The which was judged.
+ /// The that provides the scoring information.
+ protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement);
+
+ ///
+ /// Simulates an autoplay of the to determine scoring values.
+ ///
+ /// This provided temporarily. DO NOT USE.
+ /// The to simulate.
+ protected virtual void SimulateAutoplay(IBeatmap beatmap)
+ {
+ foreach (var obj in beatmap.HitObjects)
+ simulate(obj);
+
+ void simulate(HitObject obj)
+ {
+ foreach (var nested in obj.NestedHitObjects)
+ simulate(nested);
+
+ var judgement = obj.CreateJudgement();
+ if (judgement == null)
+ return;
+
+ var result = CreateResult(obj, judgement);
+ if (result == null)
+ throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
+
+ result.Type = judgement.MaxResult;
+ ApplyResult(result);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index a8a2294498..8ccc2af93b 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -7,44 +7,18 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
-using osu.Framework.Extensions.TypeExtensions;
-using osu.Framework.MathUtils;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Scoring
{
- public class ScoreProcessor
+ public class ScoreProcessor : JudgementProcessor
{
private const double base_portion = 0.3;
private const double combo_portion = 0.7;
private const double max_score = 1000000;
- ///
- /// Invoked when the is in a failed state.
- /// This may occur regardless of whether an event is invoked.
- /// Return true if the fail was permitted.
- ///
- public event Func Failed;
-
- ///
- /// Invoked when all s have been judged.
- ///
- public event Action AllJudged;
-
- ///
- /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by the .
- ///
- public event Action NewJudgement;
-
- ///
- /// Additional conditions on top of that cause a failing state.
- ///
- public event Func FailConditions;
-
///
/// The current total score.
///
@@ -55,11 +29,6 @@ namespace osu.Game.Rulesets.Scoring
///
public readonly BindableDouble Accuracy = new BindableDouble(1) { MinValue = 0, MaxValue = 1 };
- ///
- /// The current health.
- ///
- public readonly BindableDouble Health = new BindableDouble(1) { MinValue = 0, MaxValue = 1 };
-
///
/// The current combo.
///
@@ -85,26 +54,6 @@ namespace osu.Game.Rulesets.Scoring
///
public readonly Bindable Mode = new Bindable();
- ///
- /// Whether all s have been processed.
- ///
- public bool HasCompleted => JudgedHits == MaxHits;
-
- ///
- /// Whether this ScoreProcessor has already triggered the failed state.
- ///
- public bool HasFailed { get; private set; }
-
- ///
- /// The maximum number of hits that can be judged.
- ///
- protected int MaxHits { get; private set; }
-
- ///
- /// The total number of judged s at the current point in time.
- ///
- public int JudgedHits { get; private set; }
-
private double maxHighestCombo;
private double maxBaseScore;
@@ -114,7 +63,7 @@ namespace osu.Game.Rulesets.Scoring
private double scoreMultiplier = 1;
- public ScoreProcessor(IBeatmap beatmap)
+ public ScoreProcessor()
{
Debug.Assert(base_portion + combo_portion == 1.0);
@@ -126,18 +75,6 @@ namespace osu.Game.Rulesets.Scoring
Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue);
};
- ApplyBeatmap(beatmap);
-
- Reset(false);
- SimulateAutoplay(beatmap);
- Reset(true);
-
- if (maxBaseScore == 0 || maxHighestCombo == 0)
- {
- Mode.Value = ScoringMode.Classic;
- Mode.Disabled = true;
- }
-
Mode.ValueChanged += _ => updateScore();
Mods.ValueChanged += mods =>
{
@@ -150,91 +87,16 @@ namespace osu.Game.Rulesets.Scoring
};
}
- ///
- /// Applies any properties of the which affect scoring to this .
- ///
- /// The to read properties from.
- protected virtual void ApplyBeatmap(IBeatmap beatmap)
- {
- }
-
- ///
- /// Simulates an autoplay of the to determine scoring values.
- ///
- /// This provided temporarily. DO NOT USE.
- /// The to simulate.
- protected virtual void SimulateAutoplay(IBeatmap beatmap)
- {
- foreach (var obj in beatmap.HitObjects)
- simulate(obj);
-
- void simulate(HitObject obj)
- {
- foreach (var nested in obj.NestedHitObjects)
- simulate(nested);
-
- var judgement = obj.CreateJudgement();
- if (judgement == null)
- return;
-
- var result = CreateResult(obj, judgement);
- if (result == null)
- throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
-
- result.Type = judgement.MaxResult;
-
- ApplyResult(result);
- }
- }
-
- ///
- /// Applies the score change of a to this .
- ///
- /// The to apply.
- public void ApplyResult(JudgementResult result)
- {
- ApplyResultInternal(result);
-
- updateScore();
- updateFailed(result);
-
- NewJudgement?.Invoke(result);
-
- if (HasCompleted)
- AllJudged?.Invoke();
- }
-
- ///
- /// Reverts the score change of a that was applied to this .
- ///
- /// The judgement scoring result.
- public void RevertResult(JudgementResult result)
- {
- RevertResultInternal(result);
- updateScore();
- }
-
private readonly Dictionary scoreResultCounts = new Dictionary();
- ///
- /// Applies the score change of a to this .
- ///
- ///
- /// Any changes applied via this method can be reverted via .
- ///
- /// The to apply.
- protected virtual void ApplyResultInternal(JudgementResult result)
+ protected sealed override void ApplyResultInternal(JudgementResult result)
{
result.ComboAtJudgement = Combo.Value;
result.HighestComboAtJudgement = HighestCombo.Value;
- result.HealthAtJudgement = Health.Value;
- result.FailedAtJudgement = HasFailed;
- if (HasFailed)
+ if (result.FailedAtJudgement)
return;
- JudgedHits++;
-
if (result.Judgement.AffectsCombo)
{
switch (result.Type)
@@ -266,26 +128,17 @@ namespace osu.Game.Rulesets.Scoring
rollingMaxBaseScore += result.Judgement.MaxNumericResult;
}
- Health.Value += HealthAdjustmentFactorFor(result) * result.Judgement.HealthIncreaseFor(result);
+ updateScore();
}
- ///
- /// Reverts the score change of a that was applied to this via .
- ///
- /// The judgement scoring result.
- protected virtual void RevertResultInternal(JudgementResult result)
+ protected sealed override void RevertResultInternal(JudgementResult result)
{
Combo.Value = result.ComboAtJudgement;
HighestCombo.Value = result.HighestComboAtJudgement;
- Health.Value = result.HealthAtJudgement;
-
- // Todo: Revert HasFailed state with proper player support
if (result.FailedAtJudgement)
return;
- JudgedHits--;
-
if (result.Judgement.IsBonus)
{
if (result.IsHit)
@@ -299,14 +152,9 @@ namespace osu.Game.Rulesets.Scoring
baseScore -= result.Judgement.NumericResultFor(result);
rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
}
- }
- ///
- /// An adjustment factor which is multiplied into the health increase provided by a .
- ///
- /// The for which the adjustment should apply.
- /// The adjustment factor.
- protected virtual double HealthAdjustmentFactorFor(JudgementResult result) => 1;
+ updateScore();
+ }
private void updateScore()
{
@@ -330,24 +178,6 @@ namespace osu.Game.Rulesets.Scoring
}
}
- ///
- /// Checks if the score is in a failed state and notifies subscribers.
- ///
- /// This can only ever notify subscribers once.
- ///
- ///
- private void updateFailed(JudgementResult result)
- {
- if (HasFailed)
- return;
-
- if (!DefaultFailCondition && FailConditions?.Invoke(this, result) != true)
- return;
-
- if (Failed?.Invoke() != false)
- HasFailed = true;
- }
-
private ScoreRank rankFrom(double acc)
{
if (acc == 1)
@@ -372,30 +202,33 @@ namespace osu.Game.Rulesets.Scoring
/// Resets this ScoreProcessor to a default state.
///
/// Whether to store the current state of the for future use.
- protected virtual void Reset(bool storeResults)
+ protected override void Reset(bool storeResults)
{
+ base.Reset(storeResults);
+
scoreResultCounts.Clear();
if (storeResults)
{
- MaxHits = JudgedHits;
maxHighestCombo = HighestCombo.Value;
maxBaseScore = baseScore;
+
+ if (maxBaseScore == 0 || maxHighestCombo == 0)
+ {
+ Mode.Value = ScoringMode.Classic;
+ Mode.Disabled = true;
+ }
}
- JudgedHits = 0;
baseScore = 0;
rollingMaxBaseScore = 0;
bonusScore = 0;
TotalScore.Value = 0;
Accuracy.Value = 1;
- Health.Value = 1;
Combo.Value = 0;
Rank.Value = ScoreRank.X;
HighestCombo.Value = 0;
-
- HasFailed = false;
}
///
@@ -416,22 +249,10 @@ namespace osu.Game.Rulesets.Scoring
score.Statistics[result] = GetStatistic(result);
}
- ///
- /// The default conditions for failing.
- ///
- protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value);
-
///
/// Create a for this processor.
///
public virtual HitWindows CreateHitWindows() => new HitWindows();
-
- ///
- /// Creates the that represents the scoring result for a .
- ///
- /// The which was judged.
- /// The that provides the scoring information.
- protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement);
}
public enum ScoringMode
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index f318539bb7..e624fb80fa 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -72,10 +72,9 @@ namespace osu.Game.Rulesets.UI
///
public override Playfield Playfield => playfield.Value;
- ///
- /// Place to put drawables above hit objects but below UI.
- ///
- public Container Overlays { get; private set; }
+ private Container overlays;
+
+ public override Container Overlays => overlays;
public override GameplayClock FrameStableClock => frameStabilityContainer.GameplayClock;
@@ -185,12 +184,15 @@ namespace osu.Game.Rulesets.UI
frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime)
{
FrameStablePlayback = FrameStablePlayback,
- Child = KeyBindingInputManager
- .WithChild(CreatePlayfieldAdjustmentContainer()
- .WithChild(Playfield)
- )
+ Children = new Drawable[]
+ {
+ KeyBindingInputManager
+ .WithChild(CreatePlayfieldAdjustmentContainer()
+ .WithChild(Playfield)
+ ),
+ overlays = new Container { RelativeSizeAxes = Axes.Both }
+ }
},
- Overlays = new Container { RelativeSizeAxes = Axes.Both }
};
if ((ResumeOverlay = CreateResumeOverlay()) != null)
@@ -385,6 +387,11 @@ namespace osu.Game.Rulesets.UI
///
public abstract Playfield Playfield { get; }
+ ///
+ /// Place to put drawables above hit objects but below UI.
+ ///
+ public abstract Container Overlays { get; }
+
///
/// The frame-stable clock which is being used for playfield display.
///
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 857929ff9e..04b4374fc4 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
private void computeLifetimeStartRecursive(DrawableHitObject hitObject)
{
- hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value);
+ hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
foreach (var obj in hitObject.NestedHitObjects)
computeLifetimeStartRecursive(obj);
@@ -108,6 +108,35 @@ namespace osu.Game.Rulesets.UI.Scrolling
private readonly Dictionary hitObjectInitialStateCache = new Dictionary();
+ private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
+ {
+ float originAdjustment = 0.0f;
+
+ // calculate the dimension of the part of the hitobject that should already be visible
+ // when the hitobject origin first appears inside the scrolling container
+ switch (direction.Value)
+ {
+ case ScrollingDirection.Up:
+ originAdjustment = hitObject.OriginPosition.Y;
+ break;
+
+ case ScrollingDirection.Down:
+ originAdjustment = hitObject.DrawHeight - hitObject.OriginPosition.Y;
+ break;
+
+ case ScrollingDirection.Left:
+ originAdjustment = hitObject.OriginPosition.X;
+ break;
+
+ case ScrollingDirection.Right:
+ originAdjustment = hitObject.DrawWidth - hitObject.OriginPosition.X;
+ break;
+ }
+
+ var adjustedStartTime = scrollingInfo.Algorithm.TimeAt(-originAdjustment, hitObject.HitObject.StartTime, timeRange.Value, scrollLength);
+ return scrollingInfo.Algorithm.GetDisplayStartTime(adjustedStartTime, timeRange.Value);
+ }
+
// Cant use AddOnce() since the delegate is re-constructed every invocation
private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 195bc663f1..cafaddc39e 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private HitObjectComposer composer { get; set; }
[Resolved]
- private IEditorBeatmap beatmap { get; set; }
+ private EditorBeatmap beatmap { get; set; }
public BlueprintContainer()
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 00326d04f7..53c5cf97fa 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected IDistanceSnapProvider SnapProvider { get; private set; }
[Resolved]
- private IEditorBeatmap beatmap { get; set; }
+ private EditorBeatmap beatmap { get; set; }
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; }
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs
index db4aca75e5..b20f2fa11d 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs
@@ -16,9 +16,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
internal class TimelineHitObjectDisplay : TimelinePart
{
- private IEditorBeatmap beatmap { get; }
+ private EditorBeatmap beatmap { get; }
- public TimelineHitObjectDisplay(IEditorBeatmap beatmap)
+ public TimelineHitObjectDisplay(EditorBeatmap beatmap)
{
RelativeSizeAxes = Axes.Both;
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index 5d9757778d..1a6aae294a 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -32,6 +32,6 @@ namespace osu.Game.Screens.Edit.Compose
return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer));
}
- protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineHitObjectDisplay(composer.EditorBeatmap);
+ protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineHitObjectDisplay(EditorBeatmap);
}
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 1b4964c068..14d69cddd1 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -23,6 +23,7 @@ using osuTK.Input;
using System.Collections.Generic;
using osu.Framework;
using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor;
using osu.Game.Input.Bindings;
using osu.Game.Screens.Edit.Compose;
@@ -49,9 +50,11 @@ namespace osu.Game.Screens.Edit
private EditorScreen currentScreen;
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
-
private EditorClock clock;
+ private IBeatmap playableBeatmap;
+ private EditorBeatmap editorBeatmap;
+
private DependencyContainer dependencies;
private GameHost host;
@@ -73,9 +76,13 @@ namespace osu.Game.Screens.Edit
clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false };
clock.ChangeSource(sourceClock);
+ playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
+ editorBeatmap = new EditorBeatmap(playableBeatmap);
+
dependencies.CacheAs(clock);
dependencies.CacheAs(clock);
dependencies.Cache(beatDivisor);
+ dependencies.CacheAs(editorBeatmap);
EditorMenuBar menuBar;
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index c3a322ea36..6ed74dfdb0 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@@ -11,30 +12,30 @@ using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit
{
- public class EditorBeatmap : IEditorBeatmap
- where T : HitObject
+ public class EditorBeatmap : IBeatmap
{
///
- /// Invoked when a is added to this .
+ /// Invoked when a is added to this .
///
public event Action HitObjectAdded;
///
- /// Invoked when a is removed from this .
+ /// Invoked when a is removed from this .
///
public event Action HitObjectRemoved;
///
- /// Invoked when the start time of a in this was changed.
+ /// Invoked when the start time of a in this was changed.
///
public event Action StartTimeChanged;
- private readonly Dictionary> startTimeBindables = new Dictionary>();
- private readonly Beatmap beatmap;
+ public readonly IBeatmap PlayableBeatmap;
- public EditorBeatmap(Beatmap beatmap)
+ private readonly Dictionary> startTimeBindables = new Dictionary>();
+
+ public EditorBeatmap(IBeatmap playableBeatmap)
{
- this.beatmap = beatmap;
+ PlayableBeatmap = playableBeatmap;
foreach (var obj in HitObjects)
trackStartTime(obj);
@@ -42,82 +43,83 @@ namespace osu.Game.Screens.Edit
public BeatmapInfo BeatmapInfo
{
- get => beatmap.BeatmapInfo;
- set => beatmap.BeatmapInfo = value;
+ get => PlayableBeatmap.BeatmapInfo;
+ set => PlayableBeatmap.BeatmapInfo = value;
}
- public BeatmapMetadata Metadata => beatmap.Metadata;
+ public BeatmapMetadata Metadata => PlayableBeatmap.Metadata;
- public ControlPointInfo ControlPointInfo => beatmap.ControlPointInfo;
+ public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo;
- public List Breaks => beatmap.Breaks;
+ public List Breaks => PlayableBeatmap.Breaks;
- public double TotalBreakTime => beatmap.TotalBreakTime;
+ public double TotalBreakTime => PlayableBeatmap.TotalBreakTime;
- public IReadOnlyList HitObjects => beatmap.HitObjects;
+ public IReadOnlyList HitObjects => PlayableBeatmap.HitObjects;
- IReadOnlyList IBeatmap.HitObjects => beatmap.HitObjects;
+ public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics();
- public IEnumerable GetStatistics() => beatmap.GetStatistics();
+ public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone();
- public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone();
+ private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;
///
- /// Adds a to this .
+ /// Adds a to this .
///
/// The to add.
- public void Add(T hitObject)
+ public void Add(HitObject hitObject)
{
trackStartTime(hitObject);
// Preserve existing sorting order in the beatmap
- var insertionIndex = beatmap.HitObjects.FindLastIndex(h => h.StartTime <= hitObject.StartTime);
- beatmap.HitObjects.Insert(insertionIndex + 1, hitObject);
+ var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime);
+ mutableHitObjects.Insert(insertionIndex + 1, hitObject);
HitObjectAdded?.Invoke(hitObject);
}
///
- /// Removes a from this .
+ /// Removes a from this .
///
/// The to add.
- public void Remove(T hitObject)
+ public void Remove(HitObject hitObject)
{
- if (beatmap.HitObjects.Remove(hitObject))
- {
- var bindable = startTimeBindables[hitObject];
- bindable.UnbindAll();
+ if (!mutableHitObjects.Contains(hitObject))
+ return;
- startTimeBindables.Remove(hitObject);
- HitObjectRemoved?.Invoke(hitObject);
- }
+ mutableHitObjects.Remove(hitObject);
+
+ var bindable = startTimeBindables[hitObject];
+ bindable.UnbindAll();
+
+ startTimeBindables.Remove(hitObject);
+ HitObjectRemoved?.Invoke(hitObject);
}
- private void trackStartTime(T hitObject)
+ private void trackStartTime(HitObject hitObject)
{
startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy();
startTimeBindables[hitObject].ValueChanged += _ =>
{
// For now we'll remove and re-add the hitobject. This is not optimal and can be improved if required.
- beatmap.HitObjects.Remove(hitObject);
+ mutableHitObjects.Remove(hitObject);
- var insertionIndex = beatmap.HitObjects.FindLastIndex(h => h.StartTime <= hitObject.StartTime);
- beatmap.HitObjects.Insert(insertionIndex + 1, hitObject);
+ var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime);
+ mutableHitObjects.Insert(insertionIndex + 1, hitObject);
StartTimeChanged?.Invoke(hitObject);
};
}
- ///
- /// Adds a to this .
- ///
- /// The to add.
- public void Add(HitObject hitObject) => Add((T)hitObject);
+ private int findInsertionIndex(IReadOnlyList list, double startTime)
+ {
+ for (int i = 0; i < list.Count; i++)
+ {
+ if (list[i].StartTime > startTime)
+ return i - 1;
+ }
- ///
- /// Removes a from this .
- ///
- /// The to add.
- public void Remove(HitObject hitObject) => Remove((T)hitObject);
+ return list.Count - 1;
+ }
}
}
diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs
index 1b57c703ae..d42447ac4b 100644
--- a/osu.Game/Screens/Edit/EditorScreen.cs
+++ b/osu.Game/Screens/Edit/EditorScreen.cs
@@ -17,6 +17,9 @@ namespace osu.Game.Screens.Edit
[Resolved]
protected IBindable Beatmap { get; private set; }
+ [Resolved]
+ protected EditorBeatmap EditorBeatmap { get; private set; }
+
protected override Container Content => content;
private readonly Container content;
diff --git a/osu.Game/Screens/Edit/IEditorBeatmap.cs b/osu.Game/Screens/Edit/IEditorBeatmap.cs
deleted file mode 100644
index 3e3418ef79..0000000000
--- a/osu.Game/Screens/Edit/IEditorBeatmap.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects;
-
-namespace osu.Game.Screens.Edit
-{
- ///
- /// Interface for the contained by the see .
- /// Children of may resolve the beatmap via or .
- ///
- public interface IEditorBeatmap : IBeatmap
- {
- ///
- /// Invoked when a is added to this .
- ///
- event Action HitObjectAdded;
-
- ///
- /// Invoked when a is removed from this .
- ///
- event Action HitObjectRemoved;
-
- ///
- /// Invoked when the start time of a in this was changed.
- ///
- event Action StartTimeChanged;
- }
-
- ///
- /// Interface for the contained by the see .
- /// Children of may resolve the beatmap via or .
- ///
- public interface IEditorBeatmap : IEditorBeatmap, IBeatmap
- where T : HitObject
- {
- }
-}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index e2f362780d..236bdc8442 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -41,6 +41,7 @@ namespace osu.Game.Screens.Play
public Bindable ShowHealthbar = new Bindable(true);
private readonly ScoreProcessor scoreProcessor;
+ private readonly HealthProcessor healthProcessor;
private readonly DrawableRuleset drawableRuleset;
private readonly IReadOnlyList mods;
@@ -63,9 +64,10 @@ namespace osu.Game.Screens.Play
private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter };
- public HUDOverlay(ScoreProcessor scoreProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods)
+ public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods)
{
this.scoreProcessor = scoreProcessor;
+ this.healthProcessor = healthProcessor;
this.drawableRuleset = drawableRuleset;
this.mods = mods;
@@ -119,7 +121,10 @@ namespace osu.Game.Screens.Play
private void load(OsuConfigManager config, NotificationOverlay notificationOverlay)
{
if (scoreProcessor != null)
- BindProcessor(scoreProcessor);
+ BindScoreProcessor(scoreProcessor);
+
+ if (healthProcessor != null)
+ BindHealthProcessor(healthProcessor);
if (drawableRuleset != null)
{
@@ -288,15 +293,19 @@ namespace osu.Game.Screens.Play
protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();
- protected virtual void BindProcessor(ScoreProcessor processor)
+ protected virtual void BindScoreProcessor(ScoreProcessor processor)
{
ScoreCounter?.Current.BindTo(processor.TotalScore);
AccuracyCounter?.Current.BindTo(processor.Accuracy);
ComboCounter?.Current.BindTo(processor.Combo);
- HealthDisplay?.Current.BindTo(processor.Health);
if (HealthDisplay is StandardHealthDisplay shd)
processor.NewJudgement += shd.Flash;
}
+
+ protected virtual void BindHealthProcessor(HealthProcessor processor)
+ {
+ HealthDisplay?.Current.BindTo(processor.Health);
+ }
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 8970f9ac88..7228e22382 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -72,6 +72,9 @@ namespace osu.Game.Screens.Play
public BreakOverlay BreakOverlay;
protected ScoreProcessor ScoreProcessor { get; private set; }
+
+ protected HealthProcessor HealthProcessor { get; private set; }
+
protected DrawableRuleset DrawableRuleset { get; private set; }
protected HUDOverlay HUDOverlay { get; private set; }
@@ -128,9 +131,13 @@ namespace osu.Game.Screens.Play
DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value);
- ScoreProcessor = ruleset.CreateScoreProcessor(playableBeatmap);
+ ScoreProcessor = ruleset.CreateScoreProcessor();
+ ScoreProcessor.ApplyBeatmap(playableBeatmap);
ScoreProcessor.Mods.BindTo(Mods);
+ HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
+ HealthProcessor.ApplyBeatmap(playableBeatmap);
+
if (!ScoreProcessor.Mode.Disabled)
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
@@ -145,15 +152,28 @@ namespace osu.Game.Screens.Play
// bind clock into components that require it
DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused);
- DrawableRuleset.OnNewResult += ScoreProcessor.ApplyResult;
- DrawableRuleset.OnRevertResult += ScoreProcessor.RevertResult;
+ DrawableRuleset.OnNewResult += r =>
+ {
+ HealthProcessor.ApplyResult(r);
+ ScoreProcessor.ApplyResult(r);
+ };
- // Bind ScoreProcessor to ourselves
+ DrawableRuleset.OnRevertResult += r =>
+ {
+ HealthProcessor.RevertResult(r);
+ ScoreProcessor.RevertResult(r);
+ };
+
+ // Bind the judgement processors to ourselves
ScoreProcessor.AllJudged += onCompletion;
- ScoreProcessor.Failed += onFail;
+ HealthProcessor.Failed += onFail;
foreach (var mod in Mods.Value.OfType())
mod.ApplyToScoreProcessor(ScoreProcessor);
+
+ foreach (var mod in Mods.Value.OfType())
+ mod.ApplyToHealthProcessor(HealthProcessor);
+
BreakOverlay.IsBreakTime.ValueChanged += _ => updatePauseOnFocusLostState();
}
@@ -188,16 +208,10 @@ namespace osu.Game.Screens.Play
{
target.AddRange(new[]
{
- BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Breaks = working.Beatmap.Breaks
- },
// display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
- HUDOverlay = new HUDOverlay(ScoreProcessor, DrawableRuleset, Mods.Value)
+ HUDOverlay = new HUDOverlay(ScoreProcessor, HealthProcessor, DrawableRuleset, Mods.Value)
{
HoldToQuit =
{
@@ -248,6 +262,18 @@ namespace osu.Game.Screens.Play
},
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }
});
+
+ DrawableRuleset.Overlays.Add(BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Breaks = working.Beatmap.Breaks
+ });
+
+ DrawableRuleset.Overlays.Add(ScoreProcessor);
+ DrawableRuleset.Overlays.Add(HealthProcessor);
+
+ HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime);
}
private void updatePauseOnFocusLostState() =>
@@ -342,7 +368,7 @@ namespace osu.Game.Screens.Play
private void onCompletion()
{
// Only show the completion screen if the player hasn't failed
- if (ScoreProcessor.HasFailed || completionProgressDelegate != null)
+ if (HealthProcessor.HasFailed || completionProgressDelegate != null)
return;
ValidForResume = false;
@@ -350,18 +376,7 @@ namespace osu.Game.Screens.Play
if (!showResults) return;
using (BeginDelayedSequence(1000))
- {
- completionProgressDelegate = Schedule(delegate
- {
- if (!this.IsCurrentScreen()) return;
-
- var score = CreateScore();
- if (DrawableRuleset.ReplayScore == null)
- scoreManager.Import(score).Wait();
-
- this.Push(CreateResults(score));
- });
- }
+ scheduleGotoRanking();
}
protected virtual ScoreInfo CreateScore()
@@ -542,7 +557,7 @@ namespace osu.Game.Screens.Play
if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed)
{
// proceed to result screen if beatmap already finished playing
- completionProgressDelegate.RunTask();
+ scheduleGotoRanking();
return true;
}
@@ -577,6 +592,19 @@ namespace osu.Game.Screens.Play
storyboardReplacesBackground.Value = false;
}
+ private void scheduleGotoRanking()
+ {
+ completionProgressDelegate?.Cancel();
+ completionProgressDelegate = Schedule(delegate
+ {
+ var score = CreateScore();
+ if (DrawableRuleset.ReplayScore == null)
+ scoreManager.Import(score).Wait();
+
+ this.Push(CreateResults(score));
+ });
+ }
+
#endregion
}
}
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 57021dfc68..64fcc48004 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -118,8 +118,6 @@ namespace osu.Game.Screens.Play
},
idleTracker = new IdleTracker(750)
});
-
- loadNewPlayer();
}
protected override void LoadComplete()
@@ -127,6 +125,21 @@ namespace osu.Game.Screens.Play
base.LoadComplete();
inputManager = GetContainingInputManager();
+ }
+
+ public override void OnEntering(IScreen last)
+ {
+ base.OnEntering(last);
+
+ loadNewPlayer();
+
+ content.ScaleTo(0.7f);
+ Background?.FadeColour(Color4.White, 800, Easing.OutQuint);
+
+ contentIn();
+
+ info.Delay(750).FadeIn(500);
+ this.Delay(1800).Schedule(pushWhenLoaded);
if (!muteWarningShownOnce.Value)
{
@@ -179,19 +192,6 @@ namespace osu.Game.Screens.Play
content.FadeOut(250);
}
- public override void OnEntering(IScreen last)
- {
- base.OnEntering(last);
-
- content.ScaleTo(0.7f);
- Background?.FadeColour(Color4.White, 800, Easing.OutQuint);
-
- contentIn();
-
- info.Delay(750).FadeIn(500);
- this.Delay(1800).Schedule(pushWhenLoaded);
- }
-
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
base.LogoArriving(logo, resuming);
diff --git a/osu.Game/Screens/Play/ReplayPlayerLoader.cs b/osu.Game/Screens/Play/ReplayPlayerLoader.cs
index c8ca604902..4572570437 100644
--- a/osu.Game/Screens/Play/ReplayPlayerLoader.cs
+++ b/osu.Game/Screens/Play/ReplayPlayerLoader.cs
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Allocation;
+using osu.Framework.Screens;
using osu.Game.Scoring;
namespace osu.Game.Screens.Play
@@ -20,15 +20,13 @@ namespace osu.Game.Screens.Play
scoreInfo = score.ScoreInfo;
}
- protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ public override void OnEntering(IScreen last)
{
- var dependencies = base.CreateChildDependencies(parent);
-
// these will be reverted thanks to PlayerLoader's lease.
Mods.Value = scoreInfo.Mods;
Ruleset.Value = scoreInfo.Ruleset;
- return dependencies;
+ base.OnEntering(last);
}
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 0c0a58d533..565608b40f 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -22,8 +22,8 @@
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index edeeea239e..60355b8592 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -73,8 +73,8 @@
-
-
+
+
@@ -82,7 +82,7 @@
-
+