diff --git a/osu.Android.props b/osu.Android.props
index dc3e14c141..d7817cf4cf 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 9839d16030..db73bb7e7f 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -9,7 +9,7 @@ using osu.Framework.Android;
namespace osu.Android
{
- [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
+ [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuGameAndroid();
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
index c1b7214d72..3e06e78dba 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss);
// We only care about testing misses, hits are tested via JuiceStream
- [TestCase(false)]
+ [TestCase(true)]
public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss);
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
index e79792e04a..c7b322c8a0 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestCatchComboCounter()
{
- AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20);
+ AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 20);
AddStep("perform miss", () => performJudgement(HitResult.Miss));
AddStep("randomize judged object colour", () =>
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index d700f79e5b..a4b9ca35eb 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
mods = Score.Mods;
- fruitsHit = Score.Statistics.GetOrDefault(HitResult.Perfect);
+ fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great);
ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit);
tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit);
tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss);
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
index a7449ba4e1..b919102215 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
@@ -8,31 +8,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchBananaJudgement : CatchJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 1100;
- }
- }
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
- }
- }
+ public override HitResult MaxResult => HitResult.LargeBonus;
public override bool ShouldExplodeFor(JudgementResult result) => true;
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
index e87ecba749..8fd7b93e4c 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
@@ -7,16 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchDropletJudgement : CatchJudgement
{
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 30;
- }
- }
+ public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
index 2149ed9712..ccafe0abc4 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
@@ -9,19 +9,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchJudgement : Judgement
{
- public override HitResult MaxResult => HitResult.Perfect;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 300;
- }
- }
+ public override HitResult MaxResult => HitResult.Great;
///
/// Whether fruit on the platter should explode or drop.
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
index d607b49ea4..d957d4171b 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
@@ -7,30 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchTinyDropletJudgement : CatchJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 10;
- }
- }
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 0.02;
- }
- }
+ public override HitResult MaxResult => HitResult.SmallTickHit;
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index 2fe017dc62..d03a764bda 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Catch.UI;
using osuTK;
using osuTK.Graphics;
@@ -86,7 +85,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
if (CheckPosition == null) return;
if (timeOffset >= 0 && Result != null)
- ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss);
+ ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
protected override void UpdateStateTransforms(ArmedState state)
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
index ff793a372e..0a444d923e 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
switch (result)
{
- case HitResult.Perfect:
+ case HitResult.Great:
case HitResult.Miss:
return true;
}
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index 4c7bc4ab73..2cc05826b4 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchScoreProcessor : ScoreProcessor
{
- public override HitWindows CreateHitWindows() => new CatchHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
index 58a3140bb5..75feb21298 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
@@ -33,10 +33,10 @@ namespace osu.Game.Rulesets.Catch.UI
public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result)
{
- if (!result.Judgement.AffectsCombo || !result.HasResult)
+ if (!result.Type.AffectsCombo() || !result.HasResult)
return;
- if (result.Type == HitResult.Miss)
+ if (!result.IsHit)
{
updateCombo(0, null);
return;
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Catch.UI
public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result)
{
- if (!result.Judgement.AffectsCombo || !result.HasResult)
+ if (!result.Type.AffectsCombo() || !result.HasResult)
return;
updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index d3e63b0333..5e794a76aa 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osuTK;
@@ -52,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI
public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result)
{
- if (result.Judgement is IgnoreJudgement)
+ if (!result.Type.IsScorable())
return;
void runAfterLoaded(Action action)
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
index 95e86de884..9c4c2b3d5b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public class TestSceneHoldNote : ManiaHitObjectTestScene
{
- public TestSceneHoldNote()
+ [Test]
+ public void TestHoldNote()
{
AddToggleStep("toggle hitting", v =>
{
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 95072cf4f8..5cb1519196 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
- assertNoteJudgement(HitResult.Perfect);
+ assertNoteJudgement(HitResult.IgnoreHit);
}
///
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
@@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
@@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Perfect);
}
@@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
@@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Meh);
}
@@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Meh);
}
@@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Meh);
}
@@ -280,10 +280,10 @@ namespace osu.Game.Rulesets.Mania.Tests
}, beatmap);
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
- .All(j => j.Type == HitResult.Miss));
+ .All(j => !j.Type.IsHit()));
AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
- .All(j => j.Type == HitResult.Perfect));
+ .All(j => j.Type.IsHit()));
}
private void assertHeadJudgement(HitResult result)
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
index dd5fd93710..6b8f5d5d9d 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
@@ -28,25 +28,33 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture]
public class TestSceneNotes : OsuTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void TestVariousNotes()
{
- Child = new FillFlowContainer
+ DrawableNote note1 = null;
+ DrawableNote note2 = null;
+ DrawableHoldNote holdNote1 = null;
+ DrawableHoldNote holdNote2 = null;
+
+ AddStep("create notes", () =>
{
- Clock = new FramedClock(new ManualClock()),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(20),
- Children = new[]
+ Child = new FillFlowContainer
{
- createNoteDisplay(ScrollingDirection.Down, 1, out var note1),
- createNoteDisplay(ScrollingDirection.Up, 2, out var note2),
- createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1),
- createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2),
- }
- };
+ Clock = new FramedClock(new ManualClock()),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(20),
+ Children = new[]
+ {
+ createNoteDisplay(ScrollingDirection.Down, 1, out note1),
+ createNoteDisplay(ScrollingDirection.Up, 2, out note2),
+ createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1),
+ createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2),
+ }
+ };
+ });
AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2));
AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0));
diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
index 28e5d2cc1b..ee6cbbc828 100644
--- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
@@ -7,18 +7,6 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTickJudgement : ManiaJudgement
{
- protected override int NumericResultFor(HitResult result) => result == MaxResult ? 20 : 0;
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 0.01;
- }
- }
+ public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
index 53967ffa05..d28b7bdf58 100644
--- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
@@ -8,27 +8,33 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
public class ManiaJudgement : Judgement
{
- protected override int NumericResultFor(HitResult result)
+ protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
{
- default:
- return 0;
+ case HitResult.LargeTickHit:
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
+
+ case HitResult.LargeTickMiss:
+ return -DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.Meh:
- return 50;
+ return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
case HitResult.Ok:
- return 100;
+ return -DEFAULT_MAX_HEALTH_INCREASE * 0.3;
case HitResult.Good:
- return 200;
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.Great:
- return 300;
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.8;
case HitResult.Perfect:
- return 350;
+ return DEFAULT_MAX_HEALTH_INCREASE;
+
+ default:
+ return base.HealthIncreaseFor(result);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 2ebcc5451a..f6d539c91b 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -239,11 +239,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
if (Tail.AllJudged)
{
- ApplyResult(r => r.Type = HitResult.Perfect);
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
endHold();
}
- if (Tail.Result.Type == HitResult.Miss)
+ if (Tail.Judged && !Tail.IsHit)
HasBroken = true;
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index 31e43d3ee2..c780c0836e 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
index 9b0322a6cd..f265419aa0 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public class DrawableHoldNoteTick : DrawableManiaHitObject
{
+ public override bool DisplayResult => false;
+
///
/// References the time at which the user started holding the hold note.
///
@@ -73,9 +74,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
var startTime = HoldStartTime?.Invoke();
if (startTime == null || startTime > HitObject.StartTime)
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
else
- ApplyResult(r => r.Type = HitResult.Perfect);
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 08c41b0d75..27960b3f3a 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -136,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
/// Causes this to get missed, disregarding all conditions in implementations of .
///
- public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
+ public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
}
public abstract class DrawableManiaHitObject : DrawableManiaHitObject
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index 973dc06e05..b3402d13e4 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index 4b2f643333..71cc0bdf1f 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -10,7 +10,5 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override double DefaultAccuracyPortion => 0.95;
protected override double DefaultComboPortion => 0.05;
-
- public override HitWindows CreateHitWindows() => new ManiaHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
index 439e6f7df2..3724269f4d 100644
--- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
@@ -130,6 +130,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
private Drawable getResult(HitResult result)
{
+ if (!hitresult_mapping.ContainsKey(result))
+ return null;
+
string filename = this.GetManiaSkinConfig(hitresult_mapping[result])?.Value
?? default_hitresult_skin_filenames[result];
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 9aabcc6699..c28a1c13d8 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (result.IsHit)
hitPolicy.HandleHit(judgedObject);
- if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
+ if (!result.IsHit || !DisplayJudgements.Value)
return;
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png
new file mode 100644
index 0000000000..c6c3771593
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png
new file mode 100644
index 0000000000..232560a1d4
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index 37df0d6e37..596bc06c68 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
- public TestSceneHitCircle()
+ [Test]
+ public void TestVariousHitCircles()
{
AddStep("Miss Big Single", () => SetContents(() => testSingle(2)));
AddStep("Miss Medium Single", () => SetContents(() => testSingle(5)));
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
index f3221ffe32..39deba2f57 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
HitObjects = { new HitCircle { Position = new Vector2(256, 192) } }
},
- PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss
+ PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit
});
}
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
Autoplay = false,
Beatmap = beatmap,
- PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss
+ PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit
});
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
index 854626d362..32a36ab317 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
@@ -209,9 +209,9 @@ namespace osu.Game.Rulesets.Osu.Tests
});
addJudgementAssert(hitObjects[0], HitResult.Great);
- addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss);
- addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
}
///
@@ -252,9 +252,9 @@ namespace osu.Game.Rulesets.Osu.Tests
});
addJudgementAssert(hitObjects[0], HitResult.Great);
- addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
- addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
}
///
@@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
addJudgementAssert(hitObjects[0], HitResult.Great);
- addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
}
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index 6a689a1f80..c9e112f76d 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
- public TestSceneSlider()
+ [Test]
+ public void TestVariousSliders()
{
AddStep("Big Single", () => SetContents(() => testSimpleBig()));
AddStep("Medium Single", () => SetContents(() => testSimpleMedium()));
@@ -164,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
+ StartTime = Time.Current + time_offset,
Position = new Vector2(239, 176),
Path = new SliderPath(PathType.PerfectCurve, new[]
{
@@ -185,22 +186,26 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable testSlowSpeed() => createSlider(speedMultiplier: 0.5);
- private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5);
+ private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 0.5);
private Drawable testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15);
- private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15);
+ private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 15);
- private Drawable createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
+ private const double time_offset = 1500;
+
+ private const float max_length = 200;
+
+ private Drawable createSlider(float circleSize = 2, float distance = max_length, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
- Position = new Vector2(-(distance / 2), 0),
+ StartTime = Time.Current + time_offset,
+ Position = new Vector2(0, -(distance / 2)),
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
- new Vector2(distance, 0),
+ new Vector2(0, distance),
}, distance),
RepeatCount = repeats,
StackHeight = stackHeight
@@ -213,14 +218,14 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
- Position = new Vector2(-200, 0),
+ StartTime = Time.Current + time_offset,
+ Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
- new Vector2(200, 200),
- new Vector2(400, 0)
- }, 600),
+ new Vector2(max_length / 2, max_length / 2),
+ new Vector2(max_length, 0)
+ }, max_length * 1.5f),
RepeatCount = repeats,
};
@@ -233,16 +238,16 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
- Position = new Vector2(-200, 0),
+ StartTime = Time.Current + time_offset,
+ Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
- new Vector2(150, 75),
- new Vector2(200, 0),
- new Vector2(300, -200),
- new Vector2(400, 0),
- new Vector2(430, 0)
+ new Vector2(max_length * 0.375f, max_length * 0.18f),
+ new Vector2(max_length / 2, 0),
+ new Vector2(max_length * 0.75f, -max_length / 2),
+ new Vector2(max_length * 0.95f, 0),
+ new Vector2(max_length, 0)
}),
RepeatCount = repeats,
};
@@ -256,15 +261,15 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
- Position = new Vector2(-200, 0),
+ StartTime = Time.Current + time_offset,
+ Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Bezier, new[]
{
Vector2.Zero,
- new Vector2(150, 75),
- new Vector2(200, 100),
- new Vector2(300, -200),
- new Vector2(430, 0)
+ new Vector2(max_length * 0.375f, max_length * 0.18f),
+ new Vector2(max_length / 2, max_length / 4),
+ new Vector2(max_length * 0.75f, -max_length / 2),
+ new Vector2(max_length, 0)
}),
RepeatCount = repeats,
};
@@ -278,16 +283,16 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
+ StartTime = Time.Current + time_offset,
Position = new Vector2(0, 0),
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
- new Vector2(-200, 0),
+ new Vector2(-max_length / 2, 0),
new Vector2(0, 0),
- new Vector2(0, -200),
- new Vector2(-200, -200),
- new Vector2(0, -200)
+ new Vector2(0, -max_length / 2),
+ new Vector2(-max_length / 2, -max_length / 2),
+ new Vector2(0, -max_length / 2)
}),
RepeatCount = repeats,
};
@@ -305,14 +310,14 @@ namespace osu.Game.Rulesets.Osu.Tests
var slider = new Slider
{
- StartTime = Time.Current + 1000,
- Position = new Vector2(-100, 0),
+ StartTime = Time.Current + time_offset,
+ Position = new Vector2(-max_length / 4, 0),
Path = new SliderPath(PathType.Catmull, new[]
{
Vector2.Zero,
- new Vector2(50, -50),
- new Vector2(150, 50),
- new Vector2(200, 0)
+ new Vector2(max_length * 0.125f, max_length * 0.125f),
+ new Vector2(max_length * 0.375f, max_length * 0.125f),
+ new Vector2(max_length / 2, 0)
}),
RepeatCount = repeats,
NodeSamples = repeatSamples
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index b543b6fa94..0164fb8bf4 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 },
});
- AddAssert("Tracking retained", assertGreatJudge);
+ AddAssert("Tracking retained", assertMaxJudge);
}
///
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
});
- AddAssert("Tracking retained", assertGreatJudge);
+ AddAssert("Tracking retained", assertMaxJudge);
}
///
@@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
});
- AddAssert("Tracking retained", assertGreatJudge);
+ AddAssert("Tracking retained", assertMaxJudge);
}
///
@@ -288,7 +288,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.199f), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
});
- AddAssert("Tracking kept", assertGreatJudge);
+ AddAssert("Tracking kept", assertMaxJudge);
}
///
@@ -312,13 +312,13 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("Tracking dropped", assertMidSliderJudgementFail);
}
- private bool assertGreatJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == HitResult.Great);
+ private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult);
- private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss;
+ private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit;
- private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.Great;
+ private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit;
- private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.Miss;
+ private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss;
private ScoreAccessibleReplayPlayer currentPlayer;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index f7909071ea..53bf1ea566 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
@@ -146,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
- return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * SpinnerTick.SCORE_PER_TICK;
+ return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
});
addSeekStep(0);
@@ -194,13 +193,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(0);
- AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
- // autoplay replay frames use track time;
- // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time.
- // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time,
- // as real time is what we care about for spinners
- // (so we're making the spin take 1000ms in real time *always*, regardless of the track clock's rate).
- transformReplay(replay => applyRateAdjustment(replay, rate));
+ AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate);
addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 6f4c0f9cfa..02577461f0 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double accuracy;
private int scoreMaxCombo;
private int countGreat;
- private int countGood;
+ private int countOk;
private int countMeh;
private int countMiss;
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
accuracy = Score.Accuracy;
scoreMaxCombo = Score.MaxCombo;
countGreat = Score.Statistics.GetOrDefault(HitResult.Great);
- countGood = Score.Statistics.GetOrDefault(HitResult.Good);
+ countOk = Score.Statistics.GetOrDefault(HitResult.Ok);
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
@@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
int amountHitObjectsWithAccuracy = countHitCircles;
if (amountHitObjectsWithAccuracy > 0)
- betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countGood * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
+ betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
else
betterAccuracyPercentage = 0;
@@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return accuracyValue;
}
- private int totalHits => countGreat + countGood + countMeh + countMiss;
- private int totalSuccessfulHits => countGreat + countGood + countMeh;
+ private int totalHits => countGreat + countOk + countMeh + countMiss;
+ private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 2bd4bc5015..7ae0730e39 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -222,8 +222,8 @@ namespace osu.Game.Rulesets.Osu.Edit
point.Y -= origin.Y;
Vector2 ret;
- ret.X = (float)(point.X * Math.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * Math.Sin(angle / 180f * Math.PI));
- ret.Y = (float)(point.X * -Math.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * Math.Cos(angle / 180f * Math.PI));
+ ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
+ ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;
diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs
index e528f65dca..1999785efe 100644
--- a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs
@@ -7,10 +7,6 @@ namespace osu.Game.Rulesets.Osu.Judgements
{
public class OsuIgnoreJudgement : OsuJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result) => 0;
-
- protected override double HealthIncreaseFor(HitResult result) => 0;
+ public override HitResult MaxResult => HitResult.IgnoreHit;
}
}
diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs
index bf30fbc351..1a88e2a8b2 100644
--- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs
@@ -9,23 +9,5 @@ namespace osu.Game.Rulesets.Osu.Judgements
public class OsuJudgement : Judgement
{
public override HitResult MaxResult => HitResult.Great;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Meh:
- return 50;
-
- case HitResult.Good:
- return 100;
-
- case HitResult.Great:
- return 300;
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 08fd13915d..80e40af717 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods
base.ApplyToDrawableHitObjects(drawables);
}
+ private double lastSliderHeadFadeOutStartTime;
+ private double lastSliderHeadFadeOutDuration;
+
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
{
if (!(drawable is DrawableOsuHitObject d))
@@ -54,7 +57,35 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
+ case DrawableSliderTail sliderTail:
+ // use stored values from head circle to achieve same fade sequence.
+ fadeOutDuration = lastSliderHeadFadeOutDuration;
+ fadeOutStartTime = lastSliderHeadFadeOutStartTime;
+
+ using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
+ sliderTail.FadeOut(fadeOutDuration);
+
+ break;
+
+ case DrawableSliderRepeat sliderRepeat:
+ // use stored values from head circle to achieve same fade sequence.
+ fadeOutDuration = lastSliderHeadFadeOutDuration;
+ fadeOutStartTime = lastSliderHeadFadeOutStartTime;
+
+ using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
+ // only apply to circle piece – reverse arrow is not affected by hidden.
+ sliderRepeat.CirclePiece.FadeOut(fadeOutDuration);
+
+ break;
+
case DrawableHitCircle circle:
+
+ if (circle is DrawableSliderHead)
+ {
+ lastSliderHeadFadeOutDuration = fadeOutDuration;
+ lastSliderHeadFadeOutStartTime = fadeOutStartTime;
+ }
+
// we don't want to see the approach circle
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
circle.ApproachCircle.Hide();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index a438dc8be4..b5ac26c824 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var circleResult = (OsuHitCircleJudgementResult)r;
// Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
- if (result != HitResult.Miss)
+ if (result.IsHit())
{
var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 2946331bc6..45c664ba3b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -8,7 +8,6 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Osu.UI;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
/// Causes this to get missed, disregarding all conditions in implementations of .
///
- public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
+ public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 012d9f8878..49535e7fff 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -8,7 +8,6 @@ using osu.Game.Configuration;
using osuTK;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK.Graphics;
@@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (JudgedObject != null)
{
lightingColour = JudgedObject.AccentColour.GetBoundCopy();
- lightingColour.BindValueChanged(colour => Lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true);
+ lightingColour.BindValueChanged(colour => Lighting.Colour = Result.IsHit ? colour.NewValue : Color4.Transparent, true);
}
else
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 07f40f763b..b00d12983d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI;
-using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
using osu.Game.Skinning;
@@ -52,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
InternalChildren = new Drawable[]
{
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both },
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
Ball = new SliderBall(s, this)
@@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Alpha = 0
},
headContainer = new Container { RelativeSizeAxes = Axes.Both },
- tailContainer = new Container { RelativeSizeAxes = Axes.Both },
};
}
@@ -87,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.BindValueChanged(updateSlidingSample);
}
- private SkinnableSound slidingSample;
+ private PausableSkinnableSound slidingSample;
protected override void LoadSamples()
{
@@ -103,19 +102,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "sliderslide";
- AddInternal(slidingSample = new SkinnableSound(clone)
+ AddInternal(slidingSample = new PausableSkinnableSound(clone)
{
Looping = true
});
}
}
+ public override void StopAllSamples()
+ {
+ base.StopAllSamples();
+ slidingSample?.Stop();
+ }
+
private void updateSlidingSample(ValueChangedEvent tracking)
{
- // note that samples will not start playing if exiting a seek operation in the middle of a slider.
- // may be something we want to address at a later point, but not so easy to make happen right now
- // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
- if (tracking.NewValue && ShouldPlaySamples)
+ if (tracking.NewValue)
slidingSample?.Play();
else
slidingSample?.Stop();
@@ -247,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
// rather than doing it this way, we should probably attach the sample to the tail circle.
// this can only be done after we stop using LegacyLastTick.
- if (TailCircle.Result.Type != HitResult.Miss)
+ if (TailCircle.IsHit)
base.PlaySamples();
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index d79ecb7b4e..2a88f11f69 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -6,10 +6,11 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Drawable scaleContainer;
+ public readonly Drawable CirclePiece;
+
public override bool DisplayResult => false;
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
@@ -35,7 +38,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
- InternalChild = scaleContainer = new ReverseArrowPiece();
+ InternalChild = scaleContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new[]
+ {
+ // no default for this; only visible in legacy skins.
+ CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()),
+ arrow = new ReverseArrowPiece(),
+ }
+ };
}
private readonly IBindable scaleBindable = new BindableFloat();
@@ -50,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (sliderRepeat.StartTime <= Time.Current)
- ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? HitResult.Great : HitResult.Miss);
+ ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
protected override void UpdateInitialTransforms()
@@ -86,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private bool hasRotation;
+ private readonly ReverseArrowPiece arrow;
+
public void UpdateSnakingPosition(Vector2 start, Vector2 end)
{
// When the repeat is hit, the arrow should fade out on spot rather than following the slider
@@ -115,18 +131,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
- while (Math.Abs(aimRotation - Rotation) > 180)
- aimRotation += aimRotation < Rotation ? 360 : -360;
+ while (Math.Abs(aimRotation - arrow.Rotation) > 180)
+ aimRotation += aimRotation < arrow.Rotation ? 360 : -360;
if (!hasRotation)
{
- Rotation = aimRotation;
+ arrow.Rotation = aimRotation;
hasRotation = true;
}
else
{
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
- Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint);
+ arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index 29a4929c1b..f5bcecccdf 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -1,16 +1,20 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Scoring;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
+ public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking
{
- private readonly Slider slider;
+ private readonly SliderTailCircle tailCircle;
///
/// The judgement text is provided by the .
@@ -19,36 +23,82 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool Tracking { get; set; }
- private readonly IBindable positionBindable = new Bindable();
- private readonly IBindable pathVersion = new Bindable();
+ private readonly IBindable scaleBindable = new BindableFloat();
- public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
- : base(hitCircle)
+ private readonly SkinnableDrawable circlePiece;
+
+ private readonly Container scaleContainer;
+
+ public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle)
+ : base(tailCircle)
{
- this.slider = slider;
-
+ this.tailCircle = tailCircle;
Origin = Anchor.Centre;
- RelativeSizeAxes = Axes.Both;
- FillMode = FillMode.Fit;
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
- AlwaysPresent = true;
+ InternalChildren = new Drawable[]
+ {
+ scaleContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ // no default for this; only visible in legacy skins.
+ circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
+ }
+ },
+ };
+ }
- positionBindable.BindTo(hitCircle.PositionBindable);
- pathVersion.BindTo(slider.Path.Version);
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
+ scaleBindable.BindTo(HitObject.ScaleBindable);
+ }
- positionBindable.BindValueChanged(_ => updatePosition());
- pathVersion.BindValueChanged(_ => updatePosition(), true);
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
- // TODO: This has no drawable content. Support for skins should be added.
+ circlePiece.FadeInFromZero(HitObject.TimeFadeIn);
+ }
+
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ base.UpdateStateTransforms(state);
+
+ Debug.Assert(HitObject.HitWindows != null);
+
+ switch (state)
+ {
+ case ArmedState.Idle:
+ this.Delay(HitObject.TimePreempt).FadeOut(500);
+
+ Expire(true);
+ break;
+
+ case ArmedState.Miss:
+ this.FadeOut(100);
+ break;
+
+ case ArmedState.Hit:
+ // todo: temporary / arbitrary
+ this.Delay(800).FadeOut();
+ break;
+ }
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (!userTriggered && timeOffset >= 0)
- ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss);
+ ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
- private void updatePosition() => Position = HitObject.Position - slider.Position;
+ public void UpdateSnakingPosition(Vector2 start, Vector2 end) =>
+ Position = tailCircle.RepeatIndex % 2 == 0 ? end : start;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
index 66eb60aa28..9b68b446a4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
@@ -10,7 +10,6 @@ using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Skinning;
using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -64,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (timeOffset >= 0)
- ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss);
+ ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
protected override void UpdateInitialTransforms()
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index a57bb466c7..936bfaeb86 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample);
}
- private SkinnableSound spinningSample;
+ private PausableSkinnableSound spinningSample;
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
@@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "spinnerspin";
- AddInternal(spinningSample = new SkinnableSound(clone)
+ AddInternal(spinningSample = new PausableSkinnableSound(clone)
{
Volume = { Value = 0 },
Looping = true,
@@ -113,10 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateSpinningSample(ValueChangedEvent tracking)
{
- // note that samples will not start playing if exiting a seek operation in the middle of a spinner.
- // may be something we want to address at a later point, but not so easy to make happen right now
- // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
- if (tracking.NewValue && ShouldPlaySamples)
+ if (tracking.NewValue)
{
spinningSample?.Play();
spinningSample?.VolumeTo(1, 200);
@@ -127,6 +124,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
+ public override void StopAllSamples()
+ {
+ base.StopAllSamples();
+ spinningSample?.Stop();
+ }
+
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
@@ -209,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return;
// Trigger a miss result for remaining ticks to avoid infinite gameplay.
- foreach (var tick in ticks.Where(t => !t.IsHit))
+ foreach (var tick in ticks.Where(t => !t.Result.HasResult))
tick.TriggerResult(false);
ApplyResult(r =>
@@ -217,11 +220,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Progress >= 1)
r.Type = HitResult.Great;
else if (Progress > .9)
- r.Type = HitResult.Good;
+ r.Type = HitResult.Ok;
else if (Progress > .75)
r.Type = HitResult.Meh;
else if (Time.Current >= Spinner.EndTime)
- r.Type = HitResult.Miss;
+ r.Type = r.Judgement.MinResult;
});
}
@@ -265,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
while (wholeSpins != spins)
{
- var tick = ticks.FirstOrDefault(t => !t.IsHit);
+ var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
// tick may be null if we've hit the spin limit.
if (tick != null)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
index c390b673be..e9cede1398 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
@@ -1,8 +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.Rulesets.Scoring;
-
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSpinnerTick : DrawableOsuHitObject
@@ -18,6 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// Apply a judgement result.
///
/// Whether this tick was reached.
- internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss);
+ internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
index aab01f45d4..e95cdc7ee3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
@@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures)
+ private void load(TextureStore textures, DrawableHitObject drawableHitObject)
{
InternalChildren = new Drawable[]
{
@@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Origin = Anchor.Centre,
Texture = textures.Get(@"Gameplay/osu/disc"),
},
- new TrianglesPiece
+ new TrianglesPiece((int)drawableHitObject.HitObject.StartTime)
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
index 2862fe49bd..e855317544 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private Spinner spinner;
+ private const float initial_scale = 1.3f;
private const float idle_alpha = 0.2f;
private const float tracking_alpha = 0.4f;
@@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
// we are slightly bigger than our parent, to clip the top and bottom of the circle
// this should probably be revisited when scaled spinners are a thing.
- Scale = new Vector2(1.3f);
+ Scale = new Vector2(initial_scale);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@@ -93,6 +94,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
protected override void Update()
@@ -115,8 +118,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
}
- const float initial_scale = 0.2f;
- float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress;
+ const float initial_fill_scale = 0.2f;
+ float targetScale = initial_fill_scale + (1 - initial_fill_scale) * drawableSpinner.Progress;
fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation;
@@ -124,41 +127,57 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
- centre.ScaleTo(0);
- mainContainer.ScaleTo(0);
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
{
- // constant ambient rotation to give the spinner "spinning" character.
- this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
-
- centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
- mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
+ this.ScaleTo(initial_scale);
+ this.RotateTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
{
- centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
- mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
+ // constant ambient rotation to give the spinner "spinning" character.
+ this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
+ }
+
+ using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true))
+ {
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
+ this.RotateTo(mainContainer.Rotation + 180, 320);
+ break;
+
+ case ArmedState.Miss:
+ this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
+ break;
+ }
+ }
+ }
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ {
+ centre.ScaleTo(0);
+ mainContainer.ScaleTo(0);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ {
+ centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
+ mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ {
+ centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
+ mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
+ }
}
}
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
- updateComplete(state == ArmedState.Hit, 0);
-
- using (BeginDelayedSequence(spinner.Duration, true))
- {
- switch (state)
- {
- case ArmedState.Hit:
- this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
- this.RotateTo(mainContainer.Rotation + 180, 320);
- break;
-
- case ArmedState.Miss:
- this.ScaleTo(Scale * 0.8f, 320, Easing.In);
- break;
- }
- }
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ updateComplete(state == ArmedState.Hit, 0);
}
private void updateComplete(bool complete, double duration)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
index b499d7a92b..f483bb1b26 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
@@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
///
public class SpinnerBonusDisplay : CompositeDrawable
{
+ private static readonly int score_per_tick = new SpinnerBonusTick().CreateJudgement().MaxNumericResult;
+
private readonly OsuSpriteText bonusCounter;
public SpinnerBonusDisplay()
@@ -36,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
return;
displayedCount = count;
- bonusCounter.Text = $"{SpinnerBonusTick.SCORE_PER_TICK * count}";
+ bonusCounter.Text = $"{score_per_tick * count}";
bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
index f1a782cbb5..05ed38d241 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
@@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
+using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
@@ -77,6 +79,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private bool rotationTransferred;
+ [Resolved(canBeNull: true)]
+ private GameplayClock gameplayClock { get; set; }
+
protected override void Update()
{
base.Update();
@@ -126,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
currentRotation += angle;
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
// (see: ModTimeRamp)
- RateAdjustedRotation += (float)(Math.Abs(angle) * Clock.Rate);
+ RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate));
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
index 0e29a1dcd8..6cdb0d3df3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
@@ -11,7 +11,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
protected override bool CreateNewTriangles => false;
protected override float SpawnRatio => 0.5f;
- public TrianglesPiece()
+ public TrianglesPiece(int? seed = null)
+ : base(seed)
{
TriangleScale = 1.2f;
HideAlphaDiscrepancies = false;
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 51f6a44a87..917382eccf 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -176,6 +176,7 @@ namespace osu.Game.Rulesets.Osu.Objects
// if this is to change, we should revisit this.
AddNested(TailCircle = new SliderTailCircle(this)
{
+ RepeatIndex = e.SpanIndex,
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
@@ -183,10 +184,9 @@ namespace osu.Game.Rulesets.Osu.Objects
break;
case SliderEventType.Repeat:
- AddNested(new SliderRepeat
+ AddNested(new SliderRepeat(this)
{
RepeatIndex = e.SpanIndex,
- SpanDuration = SpanDuration,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs
deleted file mode 100644
index 151902a752..0000000000
--- a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-// 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.Osu.Objects
-{
- public class SliderCircle : HitCircle
- {
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
new file mode 100644
index 0000000000..a6aed2c00e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
@@ -0,0 +1,50 @@
+// 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.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Objects
+{
+ ///
+ /// A hit circle which is at the end of a slider path (either repeat or final tail).
+ ///
+ public abstract class SliderEndCircle : HitCircle
+ {
+ private readonly Slider slider;
+
+ protected SliderEndCircle(Slider slider)
+ {
+ this.slider = slider;
+ }
+
+ public int RepeatIndex { get; set; }
+
+ public double SpanDuration => slider.SpanDuration;
+
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+
+ if (RepeatIndex > 0)
+ {
+ // Repeat points after the first span should appear behind the still-visible one.
+ TimeFadeIn = 0;
+
+ // The next end circle should appear exactly after the previous circle (on the same end) is hit.
+ TimePreempt = SpanDuration * 2;
+ }
+ else
+ {
+ // taken from osu-stable
+ const float first_end_circle_preempt_adjust = 2 / 3f;
+
+ // The first end circle should fade in with the slider.
+ TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust;
+ }
+ }
+
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
index ac6c6905e4..cca86361c2 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
@@ -1,40 +1,24 @@
// 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.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
- public class SliderRepeat : OsuHitObject
+ public class SliderRepeat : SliderEndCircle
{
- public int RepeatIndex { get; set; }
- public double SpanDuration { get; set; }
-
- protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ public SliderRepeat(Slider slider)
+ : base(slider)
{
- base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
-
- // Out preempt should be one span early to give the user ample warning.
- TimePreempt += SpanDuration;
-
- // We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders
- // we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time.
- if (RepeatIndex > 0)
- TimePreempt = Math.Min(SpanDuration * 2, TimePreempt);
}
- protected override HitWindows CreateHitWindows() => HitWindows.Empty;
-
public override Judgement CreateJudgement() => new SliderRepeatJudgement();
public class SliderRepeatJudgement : OsuJudgement
{
- protected override int NumericResultFor(HitResult result) => result == MaxResult ? 30 : 0;
+ public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
index 1e54b576f1..f9450062f4 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.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.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
@@ -13,25 +12,18 @@ namespace osu.Game.Rulesets.Osu.Objects
/// Note that this should not be used for timing correctness.
/// See usage in for more information.
///
- public class SliderTailCircle : SliderCircle
+ public class SliderTailCircle : SliderEndCircle
{
- private readonly IBindable pathVersion = new Bindable();
-
public SliderTailCircle(Slider slider)
+ : base(slider)
{
- pathVersion.BindTo(slider.Path.Version);
- pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
}
- protected override HitWindows CreateHitWindows() => HitWindows.Empty;
-
public override Judgement CreateJudgement() => new SliderTailJudgement();
public class SliderTailJudgement : OsuJudgement
{
- protected override int NumericResultFor(HitResult result) => 0;
-
- public override bool AffectsCombo => false;
+ public override HitResult MaxResult => HitResult.SmallTickHit;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
index 22f3f559db..a427ee1955 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public class SliderTickJudgement : OsuJudgement
{
- protected override int NumericResultFor(HitResult result) => result == MaxResult ? 10 : 0;
+ public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
index 0b1232b8db..235dc8710a 100644
--- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
@@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerBonusTick : SpinnerTick
{
- public new const int SCORE_PER_TICK = 50;
-
public SpinnerBonusTick()
{
Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
@@ -20,9 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
{
- protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0;
-
- protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
+ public override HitResult MaxResult => HitResult.LargeBonus;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
index f54e7a9a15..d715b9a428 100644
--- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
@@ -9,19 +9,13 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerTick : OsuHitObject
{
- public const int SCORE_PER_TICK = 10;
-
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public class OsuSpinnerTickJudgement : OsuJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0;
-
- protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
+ public override HitResult MaxResult => HitResult.SmallBonus;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index 5468764692..2883f0c187 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu
ReverseArrow,
HitCircleText,
SliderHeadHitCircle,
+ SliderTailHitCircle,
SliderFollowCircle,
SliderBall,
SliderBody,
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index 9b350278f3..954a217473 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -137,13 +137,13 @@ namespace osu.Game.Rulesets.Osu.Replays
if (!(h is Spinner))
AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y)));
}
- else if (h.StartTime - hitWindows.WindowFor(HitResult.Good) > endTime + hitWindows.WindowFor(HitResult.Good) + 50)
+ else if (h.StartTime - hitWindows.WindowFor(HitResult.Ok) > endTime + hitWindows.WindowFor(HitResult.Ok) + 50)
{
if (!(prev is Spinner) && h.StartTime - endTime < 1000)
- AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Good), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y)));
+ AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Ok), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y)));
if (!(h is Spinner))
- AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Good), new Vector2(h.StackedPosition.X, h.StackedPosition.Y)));
+ AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Ok), new Vector2(h.StackedPosition.X, h.StackedPosition.Y)));
}
}
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
index 6f2998006f..dafe63a6d1 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
private static readonly DifficultyRange[] osu_ranges =
{
new DifficultyRange(HitResult.Great, 80, 50, 20),
- new DifficultyRange(HitResult.Good, 140, 100, 60),
+ new DifficultyRange(HitResult.Ok, 140, 100, 60),
new DifficultyRange(HitResult.Meh, 200, 150, 100),
new DifficultyRange(HitResult.Miss, 400, 400, 400),
};
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
switch (result)
{
case HitResult.Great:
- case HitResult.Good:
+ case HitResult.Ok:
case HitResult.Meh:
case HitResult.Miss:
return true;
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
index 86ec76e373..44118227d9 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
@@ -25,7 +25,5 @@ namespace osu.Game.Rulesets.Osu.Scoring
return new OsuJudgementResult(hitObject, judgement);
}
}
-
- public override HitWindows CreateHitWindows() => new OsuHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs
index 1885c76fcc..e6cd7bc59d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.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 System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning;
@@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
private bool disjointTrail;
private double lastTrailTime;
+ private IBindable cursorSize;
public LegacyCursorTrail()
{
@@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
[BackgroundDependencyLoader]
- private void load(ISkinSource skin)
+ private void load(ISkinSource skin, OsuConfigManager config)
{
Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null;
@@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
Texture.ScaleAdjust *= 1.6f;
}
+
+ cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy();
}
protected override double FadeDuration => disjointTrail ? 150 : 500;
protected override bool InterpolateMovements => !disjointTrail;
+ protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
+
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (!disjointTrail)
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
index d15a0a3203..f051cbfa3b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
public class LegacyMainCirclePiece : CompositeDrawable
{
private readonly string priorityLookup;
+ private readonly bool hasNumber;
- public LegacyMainCirclePiece(string priorityLookup = null)
+ public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true)
{
this.priorityLookup = priorityLookup;
+ this.hasNumber = hasNumber;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
@@ -70,7 +72,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
}
},
- hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
+ };
+
+ if (hasNumber)
+ {
+ AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
@@ -78,8 +84,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- },
- };
+ });
+ }
bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
@@ -107,7 +113,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
- indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
+ if (hasNumber)
+ indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
}
private void updateState(ValueChangedEvent state)
@@ -120,16 +127,19 @@ namespace osu.Game.Rulesets.Osu.Skinning
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
- var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value;
-
- if (legacyVersion >= 2.0m)
- // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
- hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
- else
+ if (hasNumber)
{
- // old skins scale and fade it normally along other pieces.
- hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
- hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value;
+
+ if (legacyVersion >= 2.0m)
+ // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
+ hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
+ else
+ {
+ // old skins scale and fade it normally along other pieces.
+ hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
+ hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ }
}
break;
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
index bcb2af8e3e..56b5571ce1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
@@ -70,20 +70,30 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.LoadComplete();
- this.FadeOut();
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
var spinner = (Spinner)drawableSpinner.HitObject;
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ this.FadeOut();
+
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
- fixedMiddle.FadeColour(Color4.White);
- using (BeginAbsoluteSequence(spinner.StartTime, true))
- fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ {
+ fixedMiddle.FadeColour(Color4.White);
+
+ using (BeginDelayedSequence(spinner.TimePreempt, true))
+ fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
+ }
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
index a45d91801d..7b0d7acbbc 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
@@ -24,12 +24,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
private Sprite metreSprite;
private Container metre;
+ private bool spinnerBlink;
+
private const float sprite_scale = 1 / 1.6f;
private const float final_metre_height = 692 * sprite_scale;
[BackgroundDependencyLoader]
private void load(ISkinSource source, DrawableHitObject drawableObject)
{
+ spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
+
drawableSpinner = (DrawableSpinner)drawableObject;
RelativeSizeAxes = Axes.Both;
@@ -84,14 +88,20 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.LoadComplete();
- this.FadeOut();
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
var spinner = drawableSpinner.HitObject;
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ this.FadeOut();
+
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
}
@@ -116,12 +126,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float getMetreHeight(float progress)
{
- progress = Math.Min(99, progress * 100);
+ progress *= 100;
+
+ // the spinner should still blink at 100% progress.
+ if (spinnerBlink)
+ progress = Math.Min(99, progress);
int barCount = (int)progress / 10;
- // todo: add SpinnerNoBlink support
- if (RNG.NextBool(((int)progress % 10) / 10f))
+ if (spinnerBlink && RNG.NextBool(((int)progress % 10) / 10f))
barCount++;
return (float)barCount / total_bars * final_metre_height;
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index 851a8d56c9..78bc26eff7 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
return null;
+ case OsuSkinComponents.SliderTailHitCircle:
+ if (hasHitCircle.Value)
+ return new LegacyMainCirclePiece("sliderendcircle", false);
+
+ return null;
+
case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderstartcircle");
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index e034e14eb0..63c9b53278 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
CursorRotate,
HitCircleOverlayAboveNumber,
HitCircleOverlayAboveNumer, // Some old skins will have this typo
- SpinnerFrequencyModulate
+ SpinnerFrequencyModulate,
+ SpinnerNoBlink
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 9bcb3abc63..0b30c28b8d 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -119,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
///
protected virtual bool InterpolateMovements => true;
+ protected virtual float IntervalMultiplier => 1.0f;
+
private Vector2? lastPosition;
private readonly InputResampler resampler = new InputResampler();
@@ -147,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length;
Vector2 direction = diff / distance;
- float interval = partSize.X / 2.5f;
+ float interval = partSize.X / 2.5f * IntervalMultiplier;
for (float d = interval; d < distance; d += interval)
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
index 36289bda93..99e103da3b 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
createDrawableRuleset();
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
- assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Idle);
+ assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
}
[Test]
@@ -102,8 +102,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
createDrawableRuleset();
- assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Kiai);
- assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Kiai);
+ assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Kiai);
+ assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Kiai);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
}
@@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
- assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle);
+ assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Idle);
}
[TestCase(true)]
@@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
AddRepeatStep("reach 49 combo", () => applyNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }), 49);
- assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Clear);
+ assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Clear);
}
[TestCase(true, TaikoMascotAnimationState.Kiai)]
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
index 45c94a8a86..fecb5d4a74 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
public void TestNormalHit()
{
AddStep("Great", () => SetContents(() => getContentFor(createHit(HitResult.Great))));
- AddStep("Good", () => SetContents(() => getContentFor(createHit(HitResult.Good))));
+ AddStep("Ok", () => SetContents(() => getContentFor(createHit(HitResult.Ok))));
AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss))));
}
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
public void TestStrongHit([Values(false, true)] bool hitBoth)
{
AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth))));
- AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth))));
+ AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Ok, hitBoth))));
}
private Drawable getContentFor(DrawableTestHit hit)
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
index 16ef5b968d..114038b81c 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
}));
AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value =
- new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss }));
+ new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Great : HitResult.Miss }));
AddToggleStep("toggle playback direction", reversed => this.reversed = reversed);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs
index 7492a76a67..63854e7ead 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
DrawableDrumRollTick h;
DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType });
- ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(tick, new TaikoDrumRollTickJudgement()) { Type = HitResult.Perfect });
+ ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(tick, new TaikoDrumRollTickJudgement()) { Type = HitResult.Great });
}
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
index b6cfe368f7..e4c0766844 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
@@ -3,7 +3,6 @@
using System;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@@ -30,8 +29,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
private readonly Random rng = new Random(1337);
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void TestVariousHits()
{
AddStep("Hit", () => addHitJudgement(false));
AddStep("Strong hit", () => addStrongHitJudgement(false));
@@ -105,7 +104,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
private void addHitJudgement(bool kiai)
{
- HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
+ HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
var cpi = new ControlPointInfo();
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
@@ -113,7 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
- var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
+ var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
DrawableRuleset.Playfield.Add(h);
@@ -122,7 +121,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
private void addStrongHitJudgement(bool kiai)
{
- HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
+ HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
var cpi = new ControlPointInfo();
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
@@ -130,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
- var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
+ var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
DrawableRuleset.Playfield.Add(h);
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
index 7c39c040b1..fcf7c529f5 100644
--- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
+++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
@@ -42,9 +42,9 @@ namespace osu.Game.Rulesets.Taiko.Audio
}
}
- private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
+ private PausableSkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
{
- var drawable = new SkinnableSound(hitSampleInfo)
+ var drawable = new PausableSkinnableSound(hitSampleInfo)
{
LifetimeStart = lifetimeStart,
LifetimeEnd = lifetimeEnd
@@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Taiko.Audio
public class DrumSample
{
- public SkinnableSound Centre;
- public SkinnableSound Rim;
+ public PausableSkinnableSound Centre;
+ public PausableSkinnableSound Rim;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index b9d95a6ba6..c04fffa2e7 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private Mod[] mods;
private int countGreat;
- private int countGood;
+ private int countOk;
private int countMeh;
private int countMiss;
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
mods = Score.Mods;
countGreat = Score.Statistics.GetOrDefault(HitResult.Great);
- countGood = Score.Statistics.GetOrDefault(HitResult.Good);
+ countOk = Score.Statistics.GetOrDefault(HitResult.Ok);
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
@@ -102,6 +102,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
}
- private int totalHits => countGreat + countGood + countMeh + countMiss;
+ private int totalHits => countGreat + countOk + countMeh + countMiss;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs
index a617028f1c..647ad7853d 100644
--- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs
@@ -7,25 +7,13 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{
public class TaikoDrumRollTickJudgement : TaikoJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- case HitResult.Great:
- return 200;
-
- default:
- return 0;
- }
- }
+ public override HitResult MaxResult => HitResult.SmallTickHit;
protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
{
- case HitResult.Great:
+ case HitResult.SmallTickHit:
return 0.15;
default:
diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs
index eb5f443365..e272c1a4ef 100644
--- a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs
@@ -10,21 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{
public override HitResult MaxResult => HitResult.Great;
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- case HitResult.Good:
- return 100;
-
- case HitResult.Great:
- return 300;
-
- default:
- return 0;
- }
- }
-
protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
@@ -32,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements
case HitResult.Miss:
return -1.0;
- case HitResult.Good:
+ case HitResult.Ok:
return 1.1;
case HitResult.Great:
diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs
index e045ea324f..06495ad9f4 100644
--- a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs
@@ -7,9 +7,9 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{
public class TaikoStrongJudgement : TaikoJudgement
{
+ public override HitResult MaxResult => HitResult.SmallBonus;
+
// MainObject already changes the HP
protected override double HealthIncreaseFor(HitResult result) => 0;
-
- public override bool AffectsCombo => false;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
index 2c1c2d2bc1..8f268dc1c7 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
@@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!(obj is DrawableDrumRollTick))
return;
- if (result.Type > HitResult.Miss)
+ if (result.IsHit)
rollingHits++;
else
rollingHits--;
@@ -129,10 +129,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (countHit >= HitObject.RequiredGoodHits)
{
- ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good);
+ ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok);
}
else
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
}
protected override void UpdateStateTransforms(ArmedState state)
@@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!MainObject.Judged)
return;
- ApplyResult(r => r.Type = MainObject.IsHit ? HitResult.Great : HitResult.Miss);
+ ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
public override bool OnPressed(TaikoAction action) => false;
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index 62405cf047..9d7dcc7218 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -4,7 +4,6 @@
using System;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
@@ -34,14 +33,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!userTriggered)
{
if (timeOffset > HitObject.HitWindow)
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
if (Math.Abs(timeOffset) > HitObject.HitWindow)
return;
- ApplyResult(r => r.Type = HitResult.Great);
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
protected override void UpdateStateTransforms(ArmedState state)
@@ -74,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!MainObject.Judged)
return;
- ApplyResult(r => r.Type = MainObject.IsHit ? HitResult.Great : HitResult.Miss);
+ ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
public override bool OnPressed(TaikoAction action) => false;
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
index 3a6eaa83db..bb42240f25 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
@@ -152,7 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return;
if (!validActionPressed)
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
else
ApplyResult(r => r.Type = result);
}
@@ -257,19 +257,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!MainObject.Result.IsHit)
{
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
if (!userTriggered)
{
if (timeOffset - MainObject.Result.TimeOffset > second_hit_window)
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
if (Math.Abs(timeOffset - MainObject.Result.TimeOffset) <= second_hit_window)
- ApplyResult(r => r.Type = MainObject.Result.Type);
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
public override bool OnPressed(TaikoAction action)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
index 7294587b10..8ee4a5db71 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
@@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
}
- nextTick?.TriggerResult(HitResult.Great);
+ nextTick?.TriggerResult(true);
var numHits = ticks.Count(r => r.IsHit);
@@ -208,12 +208,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
continue;
}
- tick.TriggerResult(HitResult.Miss);
+ tick.TriggerResult(false);
}
- var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Good : HitResult.Miss;
-
- ApplyResult(r => r.Type = hitResult);
+ ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult);
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
index 1685576f0d..6202583494 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
@@ -19,10 +18,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void UpdateInitialTransforms() => this.FadeOut();
- public void TriggerResult(HitResult type)
+ public void TriggerResult(bool hit)
{
HitObject.StartTime = Time.Current;
- ApplyResult(r => r.Type = type);
+ ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
index dd3c2289ea..f7a1d130eb 100644
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
private double hpMultiplier;
///
- /// HP multiplier for a .
+ /// HP multiplier for a that does not satisfy .
///
private double hpMissMultiplier;
@@ -45,6 +45,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring
}
protected override double GetHealthIncreaseFor(JudgementResult result)
- => base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier);
+ => base.GetHealthIncreaseFor(result) * (result.IsHit ? hpMultiplier : hpMissMultiplier);
}
}
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs
index 9d273392ff..cf806c0c97 100644
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
private static readonly DifficultyRange[] taiko_ranges =
{
new DifficultyRange(HitResult.Great, 50, 35, 20),
- new DifficultyRange(HitResult.Good, 120, 80, 50),
+ new DifficultyRange(HitResult.Ok, 120, 80, 50),
new DifficultyRange(HitResult.Miss, 135, 95, 70),
};
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
switch (result)
{
case HitResult.Great:
- case HitResult.Good:
+ case HitResult.Ok:
case HitResult.Miss:
return true;
}
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
index e29ea87d25..1829ea2513 100644
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
@@ -10,7 +10,5 @@ namespace osu.Game.Rulesets.Taiko.Scoring
protected override double DefaultAccuracyPortion => 0.75;
protected override double DefaultComboPortion => 0.25;
-
- public override HitWindows CreateHitWindows() => new TaikoHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs
index b5ec2e8def..19493271be 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs
@@ -1,21 +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.Linq;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Skinning
{
public class LegacyHitExplosion : CompositeDrawable
{
- public LegacyHitExplosion(Drawable sprite)
- {
- InternalChild = sprite;
+ private readonly Drawable sprite;
+ private readonly Drawable strongSprite;
+ private DrawableStrongNestedHit nestedStrongHit;
+ private bool switchedToStrongSprite;
+
+ ///
+ /// Creates a new legacy hit explosion.
+ ///
+ ///
+ /// Contrary to stable's, this implementation doesn't require a frame-perfect hit
+ /// for the strong sprite to be displayed.
+ ///
+ /// The normal legacy explosion sprite.
+ /// The strong legacy explosion sprite.
+ public LegacyHitExplosion(Drawable sprite, Drawable strongSprite = null)
+ {
+ this.sprite = sprite;
+ this.strongSprite = strongSprite;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject judgedObject)
+ {
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
+
+ AddInternal(sprite.With(s =>
+ {
+ s.Anchor = Anchor.Centre;
+ s.Origin = Anchor.Centre;
+ }));
+
+ if (strongSprite != null)
+ {
+ AddInternal(strongSprite.With(s =>
+ {
+ s.Alpha = 0;
+ s.Anchor = Anchor.Centre;
+ s.Origin = Anchor.Centre;
+ }));
+ }
+
+ if (judgedObject is DrawableHit hit)
+ nestedStrongHit = hit.NestedHitObjects.SingleOrDefault() as DrawableStrongNestedHit;
}
protected override void LoadComplete()
@@ -33,5 +76,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning
Expire(true);
}
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (shouldSwitchToStrongSprite() && !switchedToStrongSprite)
+ {
+ sprite.FadeOut(50, Easing.OutQuint);
+ strongSprite.FadeIn(50, Easing.OutQuint);
+ switchedToStrongSprite = true;
+ }
+ }
+
+ private bool shouldSwitchToStrongSprite()
+ {
+ if (nestedStrongHit == null || strongSprite == null)
+ return false;
+
+ return nestedStrongHit.IsHit;
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs
index 03813e0a99..e029040ef3 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs
@@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning
var r = result.NewValue;
// always ignore hitobjects that don't affect combo (drumroll ticks etc.)
- if (r?.Judgement.AffectsCombo == false)
+ if (r?.Type.AffectsCombo() == false)
return;
- passing = r == null || r.Type > HitResult.Miss;
+ passing = r == null || r.IsHit;
foreach (var sprite in InternalChildren.OfType())
sprite.Passing = passing;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index c222ccb51f..93c9deec1f 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
@@ -74,15 +74,23 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null;
- case TaikoSkinComponents.TaikoExplosionGood:
- case TaikoSkinComponents.TaikoExplosionGoodStrong:
- case TaikoSkinComponents.TaikoExplosionGreat:
- case TaikoSkinComponents.TaikoExplosionGreatStrong:
case TaikoSkinComponents.TaikoExplosionMiss:
- var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
- if (sprite != null)
- return new LegacyHitExplosion(sprite);
+ var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
+ if (missSprite != null)
+ return new LegacyHitExplosion(missSprite);
+
+ return null;
+
+ case TaikoSkinComponents.TaikoExplosionOk:
+ case TaikoSkinComponents.TaikoExplosionGreat:
+
+ var hitName = getHitName(taikoComponent.Component);
+ var hitSprite = this.GetAnimation(hitName, true, false);
+ var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
+
+ if (hitSprite != null)
+ return new LegacyHitExplosion(hitSprite, strongHitSprite);
return null;
@@ -106,20 +114,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning
case TaikoSkinComponents.TaikoExplosionMiss:
return "taiko-hit0";
- case TaikoSkinComponents.TaikoExplosionGood:
+ case TaikoSkinComponents.TaikoExplosionOk:
return "taiko-hit100";
- case TaikoSkinComponents.TaikoExplosionGoodStrong:
- return "taiko-hit100k";
-
case TaikoSkinComponents.TaikoExplosionGreat:
return "taiko-hit300";
-
- case TaikoSkinComponents.TaikoExplosionGreatStrong:
- return "taiko-hit300k";
}
- throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type");
+ throw new ArgumentOutOfRangeException(nameof(component), $"Invalid component type: {component}");
}
public override SampleChannel GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index 0d785adb4a..132d8f8868 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -16,10 +16,8 @@ namespace osu.Game.Rulesets.Taiko
PlayfieldBackgroundRight,
BarLine,
TaikoExplosionMiss,
- TaikoExplosionGood,
- TaikoExplosionGoodStrong,
+ TaikoExplosionOk,
TaikoExplosionGreat,
- TaikoExplosionGreatStrong,
Scroller,
Mascot,
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
index 7b8ab89233..3bd20e4bb4 100644
--- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Alpha = 0.15f;
Masking = true;
- if (result == HitResult.Miss)
+ if (!result.IsHit())
return;
bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
index f91bbb14e8..cbfc5a8628 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
switch (Result.Type)
{
- case HitResult.Good:
+ case HitResult.Ok:
JudgementBody.Colour = colours.GreenLight;
break;
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
index b937beae3c..6a16f311bf 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Screens.Play;
@@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.UI
lastObjectHit = true;
}
- if (!result.Judgement.AffectsCombo)
+ if (!result.Type.AffectsCombo())
return;
lastObjectHit = result.IsHit;
@@ -115,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.UI
}
private bool triggerComboClear(JudgementResult judgementResult)
- => (judgementResult.ComboAtJudgement + 1) % 50 == 0 && judgementResult.Judgement.AffectsCombo && judgementResult.IsHit;
+ => (judgementResult.ComboAtJudgement + 1) % 50 == 0 && judgementResult.Type.AffectsCombo() && judgementResult.IsHit;
private bool triggerSwellClear(JudgementResult judgementResult)
=> judgementResult.Judgement is TaikoSwellJudgement && judgementResult.IsHit;
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
index efd1b25046..247c0dde73 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -10,7 +9,6 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
-using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
@@ -50,39 +48,24 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load()
{
- Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion(JudgedObject, result));
+ Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(result)), _ => new DefaultHitExplosion(JudgedObject, result));
}
- private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject)
+ private static TaikoSkinComponents getComponentName(HitResult result)
{
switch (result)
{
case HitResult.Miss:
return TaikoSkinComponents.TaikoExplosionMiss;
- case HitResult.Good:
- return useStrongExplosion(judgedObject)
- ? TaikoSkinComponents.TaikoExplosionGoodStrong
- : TaikoSkinComponents.TaikoExplosionGood;
+ case HitResult.Ok:
+ return TaikoSkinComponents.TaikoExplosionOk;
case HitResult.Great:
- return useStrongExplosion(judgedObject)
- ? TaikoSkinComponents.TaikoExplosionGreatStrong
- : TaikoSkinComponents.TaikoExplosionGreat;
+ return TaikoSkinComponents.TaikoExplosionGreat;
}
- throw new ArgumentOutOfRangeException(nameof(judgedObject), "Invalid result type");
- }
-
- private bool useStrongExplosion(DrawableHitObject judgedObject)
- {
- if (!(judgedObject.HitObject is Hit))
- return false;
-
- if (!(judgedObject.NestedHitObjects.SingleOrDefault() is DrawableStrongNestedHit nestedHit))
- return false;
-
- return judgedObject.Result.Type == nestedHit.Result.Type;
+ throw new ArgumentOutOfRangeException(nameof(result), $"Invalid result type: {result}");
}
///
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 5966b24b34..1ca1be1bdf 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -163,16 +163,14 @@ namespace osu.Game.Rulesets.Taiko.UI
target = centreHit;
back = centre;
- if (gameplayClock?.IsSeeking != true)
- drumSample.Centre?.Play();
+ drumSample.Centre?.Play();
}
else if (action == RimAction)
{
target = rimHit;
back = rim;
- if (gameplayClock?.IsSeeking != true)
- drumSample.Rim?.Play();
+ drumSample.Rim?.Play();
}
if (target != null)
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 7b3fbb1faf..120cf264c3 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -215,16 +215,12 @@ namespace osu.Game.Rulesets.Taiko.UI
private void addDrumRollHit(DrawableDrumRollTick drawableTick) =>
drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick));
- ///
- /// As legacy skins have different explosions for singular and double strong hits,
- /// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame.
- ///
- private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) => Schedule(() =>
+ private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type)
{
hitExplosionContainer.Add(new HitExplosion(drawableObject, result));
if (drawableObject.HitObject.Kiai)
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
- });
+ }
private class ProxyContainer : LifetimeManagementContainer
{
diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
index ff2c9fb1a9..b7a41ffd1c 100644
--- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
+++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
@@ -12,6 +12,14 @@ namespace osu.Game.Tests.Editing
[TestFixture]
public class EditorChangeHandlerTest
{
+ private int stateChangedFired;
+
+ [SetUp]
+ public void SetUp()
+ {
+ stateChangedFired = 0;
+ }
+
[Test]
public void TestSaveRestoreState()
{
@@ -23,6 +31,8 @@ namespace osu.Game.Tests.Editing
addArbitraryChange(beatmap);
handler.SaveState();
+ Assert.That(stateChangedFired, Is.EqualTo(1));
+
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
@@ -30,6 +40,8 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
+
+ Assert.That(stateChangedFired, Is.EqualTo(2));
}
[Test]
@@ -45,6 +57,7 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
+ Assert.That(stateChangedFired, Is.EqualTo(1));
string hash = handler.CurrentStateHash;
@@ -52,6 +65,7 @@ namespace osu.Game.Tests.Editing
handler.SaveState();
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
+ Assert.That(stateChangedFired, Is.EqualTo(1));
handler.RestoreState(-1);
@@ -60,6 +74,7 @@ namespace osu.Game.Tests.Editing
// we should only be able to restore once even though we saved twice.
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
+ Assert.That(stateChangedFired, Is.EqualTo(2));
}
[Test]
@@ -71,6 +86,8 @@ namespace osu.Game.Tests.Editing
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
{
+ Assert.That(stateChangedFired, Is.EqualTo(i));
+
addArbitraryChange(beatmap);
handler.SaveState();
}
@@ -114,7 +131,10 @@ namespace osu.Game.Tests.Editing
{
var beatmap = new EditorBeatmap(new Beatmap());
- return (new EditorChangeHandler(beatmap), beatmap);
+ var changeHandler = new EditorChangeHandler(beatmap);
+
+ changeHandler.OnStateChange += () => stateChangedFired++;
+ return (changeHandler, beatmap);
}
private void addArbitraryChange(EditorBeatmap beatmap)
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
index 4876d051aa..7264083338 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
@@ -170,7 +170,7 @@ namespace osu.Game.Tests.Gameplay
beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 0 });
for (double time = 0; time < 5000; time += 100)
- beatmap.HitObjects.Add(new JudgeableHitObject(false) { StartTime = time });
+ beatmap.HitObjects.Add(new JudgeableHitObject(HitResult.LargeBonus) { StartTime = time });
beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 5000 });
createProcessor(beatmap);
@@ -236,23 +236,23 @@ namespace osu.Game.Tests.Gameplay
private class JudgeableHitObject : HitObject
{
- private readonly bool affectsCombo;
+ private readonly HitResult maxResult;
- public JudgeableHitObject(bool affectsCombo = true)
+ public JudgeableHitObject(HitResult maxResult = HitResult.Perfect)
{
- this.affectsCombo = affectsCombo;
+ this.maxResult = maxResult;
}
- public override Judgement CreateJudgement() => new TestJudgement(affectsCombo);
+ public override Judgement CreateJudgement() => new TestJudgement(maxResult);
protected override HitWindows CreateHitWindows() => new HitWindows();
private class TestJudgement : Judgement
{
- public override bool AffectsCombo { get; }
+ public override HitResult MaxResult { get; }
- public TestJudgement(bool affectsCombo)
+ public TestJudgement(HitResult maxResult)
{
- AffectsCombo = affectsCombo;
+ MaxResult = maxResult;
}
}
}
@@ -263,7 +263,7 @@ namespace osu.Game.Tests.Gameplay
public double Duration { get; set; } = 5000;
public JudgeableLongHitObject()
- : base(false)
+ : base(HitResult.LargeBonus)
{
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
index c9ab4fa489..432e3df95e 100644
--- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
@@ -17,13 +17,13 @@ namespace osu.Game.Tests.Gameplay
[Test]
public void TestNoScoreIncreaseFromMiss()
{
- var beatmap = new Beatmap { HitObjects = { new TestHitObject() } };
+ var beatmap = new Beatmap { HitObjects = { new HitObject() } };
var scoreProcessor = new ScoreProcessor();
scoreProcessor.ApplyBeatmap(beatmap);
// Apply a miss judgement
- scoreProcessor.ApplyResult(new JudgementResult(new TestHitObject(), new TestJudgement()) { Type = HitResult.Miss });
+ scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement()) { Type = HitResult.Miss });
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0));
}
@@ -31,37 +31,25 @@ namespace osu.Game.Tests.Gameplay
[Test]
public void TestOnlyBonusScore()
{
- var beatmap = new Beatmap { HitObjects = { new TestBonusHitObject() } };
+ var beatmap = new Beatmap { HitObjects = { new HitObject() } };
var scoreProcessor = new ScoreProcessor();
scoreProcessor.ApplyBeatmap(beatmap);
// Apply a judgement
- scoreProcessor.ApplyResult(new JudgementResult(new TestBonusHitObject(), new TestBonusJudgement()) { Type = HitResult.Perfect });
+ scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement(HitResult.LargeBonus)) { Type = HitResult.LargeBonus });
- Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(100));
- }
-
- private class TestHitObject : HitObject
- {
- public override Judgement CreateJudgement() => new TestJudgement();
+ Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE));
}
private class TestJudgement : Judgement
{
- protected override int NumericResultFor(HitResult result) => 100;
- }
+ public override HitResult MaxResult { get; }
- private class TestBonusHitObject : HitObject
- {
- public override Judgement CreateJudgement() => new TestBonusJudgement();
- }
-
- private class TestBonusJudgement : Judgement
- {
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result) => 100;
+ public TestJudgement(HitResult maxResult = HitResult.Perfect)
+ {
+ MaxResult = maxResult;
+ }
}
}
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index a690eb3b59..d46769a7c0 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -4,10 +4,12 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
+using osu.Framework.Graphics.Audio;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Audio;
@@ -106,9 +108,14 @@ namespace osu.Game.Tests.Gameplay
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
SelectedMods.Value = new[] { testedMod };
- Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0));
+ var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
- gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
+ Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0)
+ {
+ Child = beatmapSkinSourceContainer
+ });
+
+ beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
{
Clock = gameplayContainer.GameplayClock
});
@@ -116,7 +123,7 @@ namespace osu.Game.Tests.Gameplay
AddStep("start", () => gameplayContainer.Start());
- AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate);
+ AddAssert("sample playback rate matches mod rates", () => sample.ChildrenOfType().First().AggregateFrequency.Value == expectedRate);
}
private class TestSkin : LegacySkin
@@ -168,8 +175,6 @@ namespace osu.Game.Tests.Gameplay
: base(sampleInfo)
{
}
-
- public new SampleChannel Channel => base.Channel;
}
}
}
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index 830e4bc603..90a487c0ac 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -139,6 +139,22 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.Groups.Count, Is.EqualTo(0));
}
+ [Test]
+ public void TestRemoveGroupAlsoRemovedControlPoints()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+
+ group.Add(new SampleControlPoint());
+
+ Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1));
+
+ cpi.RemoveGroup(group);
+
+ Assert.That(cpi.SamplePoints.Count, Is.EqualTo(0));
+ }
+
[Test]
public void TestAddControlPointToGroup()
{
diff --git a/osu.Game.Tests/NonVisual/GameplayClockTest.cs b/osu.Game.Tests/NonVisual/GameplayClockTest.cs
new file mode 100644
index 0000000000..3fd7c364b7
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/GameplayClockTest.cs
@@ -0,0 +1,39 @@
+// 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.Bindables;
+using osu.Framework.Timing;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Tests.NonVisual
+{
+ [TestFixture]
+ public class GameplayClockTest
+ {
+ [TestCase(0)]
+ [TestCase(1)]
+ public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate)
+ {
+ var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate });
+ var gameplayClock = new TestGameplayClock(framedClock);
+
+ gameplayClock.MutableNonGameplayAdjustments.Add(new BindableDouble());
+
+ Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0));
+ }
+
+ private class TestGameplayClock : GameplayClock
+ {
+ public List> MutableNonGameplayAdjustments { get; } = new List>();
+
+ public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments;
+
+ public TestGameplayClock(IFrameBasedClock underlyingClock)
+ : base(underlyingClock)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
index 64d1024efb..ace57aad1d 100644
--- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -35,10 +35,10 @@ namespace osu.Game.Tests.Rulesets.Scoring
}
[TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)]
- [TestCase(ScoringMode.Standardised, HitResult.Good, 800_000)]
+ [TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)]
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
[TestCase(ScoringMode.Classic, HitResult.Meh, 50)]
- [TestCase(ScoringMode.Classic, HitResult.Good, 100)]
+ [TestCase(ScoringMode.Classic, HitResult.Ok, 100)]
[TestCase(ScoringMode.Classic, HitResult.Great, 300)]
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index 720cf51f2c..13a3195824 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -5,6 +5,8 @@ using System;
using System.IO;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
@@ -22,6 +24,9 @@ namespace osu.Game.Tests.Visual.Editing
protected override bool EditorComponentsReady => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true;
+ [Resolved]
+ private BeatmapManager beatmapManager { get; set; }
+
public override void SetUpSteps()
{
AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null));
@@ -38,6 +43,15 @@ namespace osu.Game.Tests.Visual.Editing
{
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0);
+ AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false);
+ }
+
+ [Test]
+ public void TestExitWithoutSave()
+ {
+ AddStep("exit without save", () => Editor.Exit());
+ AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
+ AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs
new file mode 100644
index 0000000000..f182023c0e
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs
@@ -0,0 +1,50 @@
+// 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 NUnit.Framework;
+using osu.Framework.Graphics.Audio;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneEditorSamplePlayback : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ [Test]
+ public void TestSlidingSampleStopsOnSeek()
+ {
+ DrawableSlider slider = null;
+ DrawableSample[] loopingSamples = null;
+ DrawableSample[] onceOffSamples = null;
+
+ AddStep("get first slider", () =>
+ {
+ slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First();
+ onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray();
+ loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray();
+ });
+
+ AddStep("start playback", () => EditorClock.Start());
+
+ AddUntilStep("wait for slider sliding then seek", () =>
+ {
+ if (!slider.Tracking.Value)
+ return false;
+
+ if (!loopingSamples.Any(s => s.Playing))
+ return false;
+
+ EditorClock.Seek(20000);
+ return true;
+ });
+
+ AddAssert("non-looping samples are playing", () => onceOffSamples.Length == 4 && loopingSamples.All(s => s.Played || s.Playing));
+ AddAssert("looping samples are not playing", () => loopingSamples.Length == 1 && loopingSamples.All(s => s.Played && !s.Playing));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
index 09f5ac2224..b82e776164 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
@@ -4,7 +4,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
@@ -17,16 +17,26 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
+ protected override bool ScrollUsingMouseWheel => false;
+
public TestSceneTimingScreen()
{
- editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
}
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
+ Beatmap.Disabled = true;
+
Child = new TimingScreen();
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ Beatmap.Disabled = false;
+ base.Dispose(isDisposing);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
new file mode 100644
index 0000000000..3ab4df20df
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
@@ -0,0 +1,61 @@
+// 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 NUnit.Framework;
+using osu.Framework.Graphics.Audio;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
+using osu.Game.Skinning;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneGameplaySamplePlayback : PlayerTestScene
+ {
+ [Test]
+ public void TestAllSamplesStopDuringSeek()
+ {
+ DrawableSlider slider = null;
+ DrawableSample[] samples = null;
+ ISamplePlaybackDisabler gameplayClock = null;
+
+ AddStep("get variables", () =>
+ {
+ gameplayClock = Player.ChildrenOfType().First().GameplayClock;
+ slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First();
+ samples = slider.ChildrenOfType().ToArray();
+ });
+
+ AddUntilStep("wait for slider sliding then seek", () =>
+ {
+ if (!slider.Tracking.Value)
+ return false;
+
+ if (!samples.Any(s => s.Playing))
+ return false;
+
+ Player.ChildrenOfType().First().Seek(40000);
+ return true;
+ });
+
+ AddAssert("sample playback disabled", () => gameplayClock.SamplePlaybackDisabled.Value);
+
+ // because we are in frame stable context, it's quite likely that not all samples are "played" at this point.
+ // the important thing is that at least one started, and that sample has since stopped.
+ AddAssert("no samples are playing", () => Player.ChildrenOfType().All(s => !s.IsPlaying));
+
+ AddAssert("sample playback still disabled", () => gameplayClock.SamplePlaybackDisabled.Value);
+
+ AddUntilStep("seek finished, sample playback enabled", () => !gameplayClock.SamplePlaybackDisabled.Value);
+ AddUntilStep("any sample is playing", () => Player.ChildrenOfType().Any(s => s.IsPlaying));
+ }
+
+ protected override bool Autoplay => true;
+
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
index 253b8d9c55..377f305d63 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Children = new[]
{
new OsuSpriteText { Text = $@"Great: {hitWindows?.WindowFor(HitResult.Great)}" },
- new OsuSpriteText { Text = $@"Good: {hitWindows?.WindowFor(HitResult.Good)}" },
+ new OsuSpriteText { Text = $@"Good: {hitWindows?.WindowFor(HitResult.Ok)}" },
new OsuSpriteText { Text = $@"Meh: {hitWindows?.WindowFor(HitResult.Meh)}" },
}
});
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 420bf29429..ac0e8eb0d4 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -158,7 +158,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestQuickRetryFromFailedGameplay()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
- AddStep("quick retry", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke());
+ AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke());
confirmExited();
}
@@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestQuickExitFromFailedGameplay()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
- AddStep("quick exit", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke());
+ AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke());
confirmExited();
}
@@ -183,7 +183,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestQuickExitFromGameplay()
{
- AddStep("quick exit", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke());
+ AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke());
confirmExited();
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index ed75d83151..8f2011e5dd 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -22,11 +22,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableSound : OsuTestScene
{
- [Cached]
+ [Cached(typeof(ISamplePlaybackDisabler))]
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
private TestSkinSourceContainer skinSource;
- private SkinnableSound skinnableSound;
+ private PausableSkinnableSound skinnableSound;
[SetUp]
public void SetUp() => Schedule(() =>
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Clock = gameplayClock,
RelativeSizeAxes = Axes.Both,
- Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide"))
+ Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
},
};
});
diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs
index aee5241e35..a1b5ac38ea 100644
--- a/osu.Game.Tournament/Components/DateTextBox.cs
+++ b/osu.Game.Tournament/Components/DateTextBox.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Components
{
base.Bindable = new Bindable();
- ((OsuTextBox)Control).OnCommit = (sender, newText) =>
+ ((OsuTextBox)Control).OnCommit += (sender, newText) =>
{
try
{
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index e7788b75f3..22314f28c7 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -158,6 +158,9 @@ namespace osu.Game.Beatmaps.ControlPoints
public void RemoveGroup(ControlPointGroup group)
{
+ foreach (var item in group.ControlPoints.ToArray())
+ group.Remove(item);
+
group.ItemAdded -= groupItemAdded;
group.ItemRemoved -= groupItemRemoved;
diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs
index 27027202ce..5b0fa44444 100644
--- a/osu.Game/Graphics/Backgrounds/Triangles.cs
+++ b/osu.Game/Graphics/Backgrounds/Triangles.cs
@@ -86,13 +86,24 @@ namespace osu.Game.Graphics.Backgrounds
///
public float Velocity = 1;
+ private readonly Random stableRandom;
+
+ private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle());
+
private readonly SortedList parts = new SortedList(Comparer.Default);
private IShader shader;
private readonly Texture texture;
- public Triangles()
+ ///
+ /// Construct a new triangle visualisation.
+ ///
+ /// An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time.
+ public Triangles(int? seed = null)
{
+ if (seed != null)
+ stableRandom = new Random(seed.Value);
+
texture = Texture.WhitePixel;
}
@@ -175,8 +186,8 @@ namespace osu.Game.Graphics.Backgrounds
{
TriangleParticle particle = CreateTriangle();
- particle.Position = new Vector2(RNG.NextSingle(), randomY ? RNG.NextSingle() : 1);
- particle.ColourShade = RNG.NextSingle();
+ particle.Position = new Vector2(nextRandom(), randomY ? nextRandom() : 1);
+ particle.ColourShade = nextRandom();
particle.Colour = CreateTriangleShade(particle.ColourShade);
return particle;
@@ -191,8 +202,8 @@ namespace osu.Game.Graphics.Backgrounds
const float std_dev = 0.16f;
const float mean = 0.5f;
- float u1 = 1 - RNG.NextSingle(); //uniform(0,1] random floats
- float u2 = 1 - RNG.NextSingle();
+ float u1 = 1 - nextRandom(); //uniform(0,1] random floats
+ float u2 = 1 - nextRandom();
float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1)
var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2)
diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs
index b941cd8973..3d3c07a5ad 100644
--- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs
+++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs
@@ -38,6 +38,7 @@ namespace osu.Game.Online.API.Requests.Responses
Rank = Rank,
Ruleset = ruleset,
Mods = mods,
+ IsLegacyScore = true
};
if (Statistics != null)
diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
index f8810c778f..8b0caddbc6 100644
--- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs
+++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
@@ -59,12 +59,13 @@ namespace osu.Game.Online.Chat
RelativeSizeAxes = Axes.X,
Height = textbox_height,
PlaceholderText = "type your message",
- OnCommit = postMessage,
ReleaseFocusOnCommit = false,
HoldFocus = true,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
});
+
+ textbox.OnCommit += postMessage;
}
Channel.BindValueChanged(channelChanged);
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index 56866765b6..968355c377 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -11,9 +11,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Leaderboards;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Users.Drawables;
+using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@@ -55,6 +57,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
highAccuracyColour = colours.GreenLight;
}
+ ///
+ /// The statistics that appear in the table, in order of appearance.
+ ///
+ private readonly List statisticResultTypes = new List();
+
private bool showPerformancePoints;
public void DisplayScores(IReadOnlyList scores, bool showPerformanceColumn)
@@ -65,11 +72,12 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
return;
showPerformancePoints = showPerformanceColumn;
+ statisticResultTypes.Clear();
for (int i = 0; i < scores.Count; i++)
backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height));
- Columns = createHeaders(scores.FirstOrDefault());
+ Columns = createHeaders(scores);
Content = scores.Select((s, i) => createContent(i, s)).ToArray().ToRectangular();
}
@@ -79,7 +87,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
backgroundFlow.Clear();
}
- private TableColumn[] createHeaders(ScoreInfo score)
+ private TableColumn[] createHeaders(IReadOnlyList scores)
{
var columns = new List
{
@@ -92,10 +100,17 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
new TableColumn("max combo", Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 70, maxSize: 120))
};
- foreach (var statistic in score.SortedStatistics.Take(score.SortedStatistics.Count() - 1))
- columns.Add(new TableColumn(statistic.Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
+ // All statistics across all scores, unordered.
+ var allScoreStatistics = scores.SelectMany(s => s.GetStatisticsForDisplay().Select(stat => stat.result)).ToHashSet();
- columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95)));
+ foreach (var result in OrderAttributeUtils.GetValuesInOrder())
+ {
+ if (!allScoreStatistics.Contains(result))
+ continue;
+
+ columns.Add(new TableColumn(result.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
+ statisticResultTypes.Add(result);
+ }
if (showPerformancePoints)
columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30)));
@@ -148,13 +163,18 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
};
- foreach (var kvp in score.SortedStatistics)
+ var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.result);
+
+ foreach (var result in statisticResultTypes)
{
+ if (!availableStatistics.TryGetValue(result, out var stat))
+ stat = (result, 0, null);
+
content.Add(new OsuSpriteText
{
- Text = $"{kvp.Value}",
+ Text = stat.maxCount == null ? $"{stat.count}" : $"{stat.count}/{stat.maxCount}",
Font = OsuFont.GetFont(size: text_size),
- Colour = kvp.Value == 0 ? Color4.Gray : Color4.White
+ Colour = stat.count == 0 ? Color4.Gray : Color4.White
});
}
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
index 3a842d0a43..05789e1fc0 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
@@ -117,7 +117,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0;
ppColumn.Text = $@"{value.PP:N0}";
- statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value));
+ statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(s => createStatisticsColumn(s.result, s.count, s.maxCount));
modsColumn.Mods = value.Mods;
if (scoreManager != null)
@@ -125,9 +125,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
}
- private TextColumn createStatisticsColumn(HitResult hitResult, int count) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width)
+ private TextColumn createStatisticsColumn(HitResult hitResult, int count, int? maxCount) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width)
{
- Text = count.ToString()
+ Text = maxCount == null ? $"{count}" : $"{count}/{maxCount}"
};
private class InfoColumn : CompositeDrawable
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index c53eccf78b..8bc7e21047 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -146,7 +146,6 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Height = 1,
PlaceholderText = "type your message",
- OnCommit = postMessage,
ReleaseFocusOnCommit = false,
HoldFocus = true,
}
@@ -186,6 +185,8 @@ namespace osu.Game.Overlays
},
};
+ textbox.OnCommit += postMessage;
+
ChannelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue;
ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
ChannelSelectionOverlay.State.ValueChanged += state =>
diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs
index 050e687dfb..b8d04eab4e 100644
--- a/osu.Game/Overlays/Music/PlaylistOverlay.cs
+++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs
@@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Music
},
};
- filter.Search.OnCommit = (sender, newText) =>
+ filter.Search.OnCommit += (sender, newText) =>
{
BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault();
diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
index 34e5da4ef4..f96e204f62 100644
--- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
@@ -236,7 +236,6 @@ namespace osu.Game.Overlays.Settings.Sections.General
PlaceholderText = "password",
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
- OnCommit = (sender, newText) => performLogin()
},
new SettingsCheckbox
{
@@ -276,6 +275,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
}
}
};
+
+ password.OnCommit += (sender, newText) => performLogin();
}
public override bool AcceptsFocus => true;
diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs
index 1070b8cbd2..8ed7885101 100644
--- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs
+++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs
@@ -40,17 +40,28 @@ namespace osu.Game.Rulesets.Edit
Playfield.DisplayJudgements.Value = false;
}
+ [Resolved(canBeNull: true)]
+ private IEditorChangeHandler changeHandler { get; set; }
+
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.HitObjectAdded += addHitObject;
- beatmap.HitObjectUpdated += updateReplay;
beatmap.HitObjectRemoved += removeHitObject;
+
+ if (changeHandler != null)
+ {
+ // for now only regenerate replay on a finalised state change, not HitObjectUpdated.
+ changeHandler.OnStateChange += updateReplay;
+ }
+ else
+ {
+ beatmap.HitObjectUpdated += _ => updateReplay();
+ }
}
- private void updateReplay(HitObject obj = null) =>
- drawableRuleset.RegenerateAutoplay();
+ private void updateReplay() => drawableRuleset.RegenerateAutoplay();
private void addHitObject(HitObject hitObject)
{
@@ -58,8 +69,6 @@ namespace osu.Game.Rulesets.Edit
drawableRuleset.Playfield.Add(drawableObject);
drawableRuleset.Playfield.PostProcess();
-
- updateReplay();
}
private void removeHitObject(HitObject hitObject)
@@ -68,8 +77,6 @@ namespace osu.Game.Rulesets.Edit
drawableRuleset.Playfield.Remove(drawableObject);
drawableRuleset.Playfield.PostProcess();
-
- drawableRuleset.RegenerateAutoplay();
}
public override bool PropagatePositionalInputSubTree => false;
diff --git a/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs b/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs
index 1871249c94..d2a434058d 100644
--- a/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs
+++ b/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs
@@ -7,10 +7,6 @@ namespace osu.Game.Rulesets.Judgements
{
public class IgnoreJudgement : Judgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result) => 0;
-
- protected override double HealthIncreaseFor(HitResult result) => 0;
+ public override HitResult MaxResult => HitResult.IgnoreHit;
}
}
diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs
index 9105b920ca..89a3a2b855 100644
--- a/osu.Game/Rulesets/Judgements/Judgement.cs
+++ b/osu.Game/Rulesets/Judgements/Judgement.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 osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
@@ -11,31 +12,69 @@ namespace osu.Game.Rulesets.Judgements
///
public class Judgement
{
+ ///
+ /// The score awarded for a small bonus.
+ ///
+ public const int SMALL_BONUS_SCORE = 10;
+
+ ///
+ /// The score awarded for a large bonus.
+ ///
+ public const int LARGE_BONUS_SCORE = 50;
+
///
/// 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;
+ ///
+ /// Whether this should affect the current combo.
+ ///
+ [Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328
+ public virtual bool AffectsCombo => true;
+
+ ///
+ /// Whether this should be counted as base (combo) or bonus score.
+ ///
+ [Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328
+ public virtual bool IsBonus => !AffectsCombo;
+
///
/// The maximum that can be achieved.
///
public virtual HitResult MaxResult => HitResult.Perfect;
///
- /// Whether this should affect the current combo.
+ /// The minimum that can be achieved - the inverse of .
///
- public virtual bool AffectsCombo => true;
+ public HitResult MinResult
+ {
+ get
+ {
+ switch (MaxResult)
+ {
+ case HitResult.SmallBonus:
+ case HitResult.LargeBonus:
+ case HitResult.IgnoreHit:
+ return HitResult.IgnoreMiss;
- ///
- /// Whether this should be counted as base (combo) or bonus score.
- ///
- public virtual bool IsBonus => !AffectsCombo;
+ case HitResult.SmallTickHit:
+ return HitResult.SmallTickMiss;
+
+ case HitResult.LargeTickHit:
+ return HitResult.LargeTickMiss;
+
+ default:
+ return HitResult.Miss;
+ }
+ }
+ }
///
/// The numeric score representation for the maximum achievable result.
///
- public int MaxNumericResult => NumericResultFor(MaxResult);
+ public int MaxNumericResult => ToNumericResult(MaxResult);
///
/// The health increase for the maximum achievable result.
@@ -47,14 +86,15 @@ namespace osu.Game.Rulesets.Judgements
///
/// The to find the numeric score representation for.
/// The numeric score representation of .
- protected virtual int NumericResultFor(HitResult result) => result > HitResult.Miss ? 1 : 0;
+ [Obsolete("Has no effect. Use ToNumericResult(HitResult) (standardised across all rulesets).")] // Can be made non-virtual 20210328
+ protected virtual int NumericResultFor(HitResult result) => ToNumericResult(result);
///
/// Retrieves the numeric score representation of a .
///
/// The to find the numeric score representation for.
/// The numeric score representation of .
- public int NumericResultFor(JudgementResult result) => NumericResultFor(result.Type);
+ public int NumericResultFor(JudgementResult result) => ToNumericResult(result.Type);
///
/// Retrieves the numeric health increase of a .
@@ -65,6 +105,21 @@ namespace osu.Game.Rulesets.Judgements
{
switch (result)
{
+ default:
+ return 0;
+
+ case HitResult.SmallTickHit:
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
+
+ case HitResult.SmallTickMiss:
+ return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
+
+ case HitResult.LargeTickHit:
+ return DEFAULT_MAX_HEALTH_INCREASE;
+
+ case HitResult.LargeTickMiss:
+ return -DEFAULT_MAX_HEALTH_INCREASE;
+
case HitResult.Miss:
return -DEFAULT_MAX_HEALTH_INCREASE;
@@ -72,10 +127,10 @@ namespace osu.Game.Rulesets.Judgements
return -DEFAULT_MAX_HEALTH_INCREASE * 0.05;
case HitResult.Ok:
- return -DEFAULT_MAX_HEALTH_INCREASE * 0.01;
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
case HitResult.Good:
- return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
case HitResult.Great:
return DEFAULT_MAX_HEALTH_INCREASE;
@@ -83,8 +138,11 @@ namespace osu.Game.Rulesets.Judgements
case HitResult.Perfect:
return DEFAULT_MAX_HEALTH_INCREASE * 1.05;
- default:
- return 0;
+ case HitResult.SmallBonus:
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
+
+ case HitResult.LargeBonus:
+ return DEFAULT_MAX_HEALTH_INCREASE;
}
}
@@ -95,6 +153,42 @@ namespace osu.Game.Rulesets.Judgements
/// The numeric health increase of .
public double HealthIncreaseFor(JudgementResult result) => HealthIncreaseFor(result.Type);
- public override string ToString() => $"AffectsCombo:{AffectsCombo} MaxResult:{MaxResult} MaxScore:{MaxNumericResult}";
+ public override string ToString() => $"MaxResult:{MaxResult} MaxScore:{MaxNumericResult}";
+
+ public static int ToNumericResult(HitResult result)
+ {
+ switch (result)
+ {
+ default:
+ return 0;
+
+ case HitResult.SmallTickHit:
+ return 10;
+
+ case HitResult.LargeTickHit:
+ return 30;
+
+ case HitResult.Meh:
+ return 50;
+
+ case HitResult.Ok:
+ return 100;
+
+ case HitResult.Good:
+ return 200;
+
+ case HitResult.Great:
+ return 300;
+
+ case HitResult.Perfect:
+ return 350;
+
+ case HitResult.SmallBonus:
+ return SMALL_BONUS_SCORE;
+
+ case HitResult.LargeBonus:
+ return LARGE_BONUS_SCORE;
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs
index 59a7917e55..3a35fd4433 100644
--- a/osu.Game/Rulesets/Judgements/JudgementResult.cs
+++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Judgements
///
/// Whether a successful hit occurred.
///
- public bool IsHit => Type > HitResult.Miss;
+ public bool IsHit => Type.IsHit();
///
/// Creates a new .
diff --git a/osu.Game/Rulesets/Mods/IApplicableToSample.cs b/osu.Game/Rulesets/Mods/IApplicableToSample.cs
index 559d127cfc..50a6d501b6 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToSample.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToSample.cs
@@ -1,7 +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 osu.Framework.Audio.Sample;
+using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods
{
@@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods
///
public interface IApplicableToSample : IApplicableMod
{
- void ApplyToSample(SampleChannel sample);
+ void ApplyToSample(DrawableSample sample);
}
}
diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs
index 4004953cd1..282de3a8e1 100644
--- a/osu.Game/Rulesets/Mods/ModNightcore.cs
+++ b/osu.Game/Rulesets/Mods/ModNightcore.cs
@@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mods
public class NightcoreBeatContainer : BeatSyncedContainer
{
- private SkinnableSound hatSample;
- private SkinnableSound clapSample;
- private SkinnableSound kickSample;
- private SkinnableSound finishSample;
+ private PausableSkinnableSound hatSample;
+ private PausableSkinnableSound clapSample;
+ private PausableSkinnableSound kickSample;
+ private PausableSkinnableSound finishSample;
private int? firstBeat;
@@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Mods
{
InternalChildren = new Drawable[]
{
- hatSample = new SkinnableSound(new SampleInfo("nightcore-hat")),
- clapSample = new SkinnableSound(new SampleInfo("nightcore-clap")),
- kickSample = new SkinnableSound(new SampleInfo("nightcore-kick")),
- finishSample = new SkinnableSound(new SampleInfo("nightcore-finish")),
+ hatSample = new PausableSkinnableSound(new SampleInfo("nightcore-hat")),
+ clapSample = new PausableSkinnableSound(new SampleInfo("nightcore-clap")),
+ kickSample = new PausableSkinnableSound(new SampleInfo("nightcore-kick")),
+ finishSample = new PausableSkinnableSound(new SampleInfo("nightcore-finish")),
};
}
diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs
index 65f1a972ed..df0fc9c4b6 100644
--- a/osu.Game/Rulesets/Mods/ModPerfect.cs
+++ b/osu.Game/Rulesets/Mods/ModPerfect.cs
@@ -16,8 +16,7 @@ namespace osu.Game.Rulesets.Mods
public override string Description => "SS or quit.";
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
- => !(result.Judgement is IgnoreJudgement)
- && result.Judgement.AffectsCombo
+ => result.Type.AffectsAccuracy()
&& result.Type != result.Judgement.MaxResult;
}
}
diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs
index fec21764b0..2150b0fb68 100644
--- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
-using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
+using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods
{
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
}
- public virtual void ApplyToSample(SampleChannel sample)
+ public virtual void ApplyToSample(DrawableSample sample)
{
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
index df10262845..ae71041a64 100644
--- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
+++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Mods
healthProcessor.FailConditions += FailCondition;
}
- protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => !result.IsHit && result.Judgement.AffectsCombo;
+ protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
+ => result.Type.AffectsCombo()
+ && !result.IsHit;
}
}
diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
index 20c8d0f3e7..4d43ae73d3 100644
--- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs
+++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
@@ -6,11 +6,11 @@ using System.Linq;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
+using osu.Framework.Graphics.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
-using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Objects;
-using osu.Framework.Audio.Sample;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods
{
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.TriggerChange();
}
- public void ApplyToSample(SampleChannel sample)
+ public void ApplyToSample(DrawableSample sample)
{
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 7c05bc9aa7..8012b4d95c 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
+using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
@@ -17,7 +18,6 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Configuration;
-using osu.Game.Screens.Play;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Drawables
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
public readonly Bindable AccentColour = new Bindable(Color4.Gray);
- protected SkinnableSound Samples { get; private set; }
+ protected PausableSkinnableSound Samples { get; private set; }
public virtual IEnumerable GetSamples() => HitObject.Samples;
@@ -51,12 +51,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
public override bool PropagateNonPositionalInputSubTree => HandleUserInput;
///
- /// Invoked when a has been applied by this or a nested .
+ /// Invoked by this or a nested after a has been applied.
///
public event Action OnNewResult;
///
- /// Invoked when a is being reverted by this or a nested .
+ /// Invoked by this or a nested prior to a being reverted.
///
public event Action OnRevertResult;
@@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
- Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)));
+ Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)));
AddInternal(Samples);
}
@@ -236,7 +236,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
#region State / Transform Management
///
- /// Bind to apply a custom state which can override the default implementation.
+ /// Invoked by this or a nested to apply a custom state that can override the default implementation.
///
public event Action ApplyCustomUpdateState;
@@ -359,9 +359,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
}
- [Resolved(canBeNull: true)]
- private GameplayClock gameplayClock { get; set; }
-
///
/// Calculate the position to be used for sample playback at a specified X position (0..1).
///
@@ -374,24 +371,29 @@ namespace osu.Game.Rulesets.Objects.Drawables
return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0);
}
- ///
- /// Whether samples should currently be playing. Will be false during seek operations.
- ///
- protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true;
-
///
/// Plays all the hit sounds for this .
/// This is invoked automatically when this is hit.
///
public virtual void PlaySamples()
{
- if (Samples != null && ShouldPlaySamples)
+ if (Samples != null)
{
Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition);
Samples.Play();
}
}
+ ///
+ /// Stops playback of all relevant samples. Generally only looping samples should be stopped by this, and the rest let to play out.
+ /// Automatically called when 's lifetime has been exceeded.
+ ///
+ public virtual void StopAllSamples()
+ {
+ if (Samples?.Looping == true)
+ Samples.Stop();
+ }
+
protected override void Update()
{
base.Update();
@@ -460,6 +462,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
foreach (var nested in NestedHitObjects)
nested.OnKilled();
+ // failsafe to ensure looping samples don't get stuck in a playing state.
+ // this could occur in a non-frame-stable context where DrawableHitObjects get killed before a SkinnableSound has the chance to be stopped.
+ StopAllSamples();
+
UpdateResult(false);
}
@@ -470,29 +476,45 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// The callback that applies changes to the .
protected void ApplyResult(Action application)
{
+ if (Result.HasResult)
+ throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result.");
+
application?.Invoke(Result);
if (!Result.HasResult)
throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}.");
+ // Some (especially older) rulesets use scorable judgements instead of the newer ignorehit/ignoremiss judgements.
+ // Can be removed 20210328
+ if (Result.Judgement.MaxResult == HitResult.IgnoreHit)
+ {
+ HitResult originalType = Result.Type;
+
+ if (Result.Type == HitResult.Miss)
+ Result.Type = HitResult.IgnoreMiss;
+ else if (Result.Type >= HitResult.Meh && Result.Type <= HitResult.Perfect)
+ Result.Type = HitResult.IgnoreHit;
+
+ if (Result.Type != originalType)
+ {
+ Logger.Log($"{GetType().ReadableName()} applied an invalid hit result ({originalType}) when {nameof(HitResult.IgnoreMiss)} or {nameof(HitResult.IgnoreHit)} is expected.\n"
+ + $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2020-03-28 onwards.", level: LogLevel.Important);
+ }
+ }
+
+ if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult))
+ {
+ throw new InvalidOperationException(
+ $"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}]).");
+ }
+
// Ensure that the judgement is given a valid time offset, because this may not get set by the caller
var endTime = HitObject.GetEndTime();
Result.TimeOffset = Math.Min(HitObject.HitWindows.WindowFor(HitResult.Miss), Time.Current - endTime);
- switch (Result.Type)
- {
- case HitResult.None:
- break;
-
- case HitResult.Miss:
- updateState(ArmedState.Miss);
- break;
-
- default:
- updateState(ArmedState.Hit);
- break;
- }
+ if (Result.HasResult)
+ updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);
OnNewResult?.Invoke(this, Result);
}
diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
index ba9bbb055f..cae41e22f4 100644
--- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
@@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Scoring
{
base.ApplyResultInternal(result);
- if (!result.Judgement.IsBonus)
+ if (!result.Type.IsBonus())
healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result)));
}
diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs
index b057af2a50..6a3a034fc1 100644
--- a/osu.Game/Rulesets/Scoring/HitResult.cs
+++ b/osu.Game/Rulesets/Scoring/HitResult.cs
@@ -2,15 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
+using System.Diagnostics;
+using osu.Game.Utils;
namespace osu.Game.Rulesets.Scoring
{
+ [HasOrderedElements]
public enum HitResult
{
///
/// Indicates that the object has not been judged yet.
///
[Description(@"")]
+ [Order(14)]
None,
///
@@ -21,47 +25,169 @@ namespace osu.Game.Rulesets.Scoring
/// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time).
///
[Description(@"Miss")]
+ [Order(5)]
Miss,
[Description(@"Meh")]
+ [Order(4)]
Meh,
- ///
- /// Optional judgement.
- ///
[Description(@"OK")]
+ [Order(3)]
Ok,
[Description(@"Good")]
+ [Order(2)]
Good,
[Description(@"Great")]
+ [Order(1)]
Great,
- ///
- /// Optional judgement.
- ///
[Description(@"Perfect")]
+ [Order(0)]
Perfect,
///
/// Indicates small tick miss.
///
+ [Order(11)]
SmallTickMiss,
///
/// Indicates a small tick hit.
///
+ [Description(@"S Tick")]
+ [Order(7)]
SmallTickHit,
///
/// Indicates a large tick miss.
///
+ [Order(10)]
LargeTickMiss,
///
/// Indicates a large tick hit.
///
- LargeTickHit
+ [Description(@"L Tick")]
+ [Order(6)]
+ LargeTickHit,
+
+ ///
+ /// Indicates a small bonus.
+ ///
+ [Description("S Bonus")]
+ [Order(9)]
+ SmallBonus,
+
+ ///
+ /// Indicates a large bonus.
+ ///
+ [Description("L Bonus")]
+ [Order(8)]
+ LargeBonus,
+
+ ///
+ /// Indicates a miss that should be ignored for scoring purposes.
+ ///
+ [Order(13)]
+ IgnoreMiss,
+
+ ///
+ /// Indicates a hit that should be ignored for scoring purposes.
+ ///
+ [Order(12)]
+ IgnoreHit,
+ }
+
+ public static class HitResultExtensions
+ {
+ ///
+ /// Whether a increases/decreases the combo, and affects the combo portion of the score.
+ ///
+ public static bool AffectsCombo(this HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Miss:
+ case HitResult.Meh:
+ case HitResult.Ok:
+ case HitResult.Good:
+ case HitResult.Great:
+ case HitResult.Perfect:
+ case HitResult.LargeTickHit:
+ case HitResult.LargeTickMiss:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Whether a affects the accuracy portion of the score.
+ ///
+ public static bool AffectsAccuracy(this HitResult result)
+ => IsScorable(result) && !IsBonus(result);
+
+ ///
+ /// Whether a should be counted as bonus score.
+ ///
+ public static bool IsBonus(this HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.SmallBonus:
+ case HitResult.LargeBonus:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Whether a represents a successful hit.
+ ///
+ public static bool IsHit(this HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.None:
+ case HitResult.IgnoreMiss:
+ case HitResult.Miss:
+ case HitResult.SmallTickMiss:
+ case HitResult.LargeTickMiss:
+ return false;
+
+ default:
+ return true;
+ }
+ }
+
+ ///
+ /// Whether a is scorable.
+ ///
+ public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss;
+
+ ///
+ /// Whether a is valid within a given range.
+ ///
+ /// The to check.
+ /// The minimum .
+ /// The maximum .
+ /// Whether falls between and .
+ public static bool IsValidHitResult(this HitResult result, HitResult minResult, HitResult maxResult)
+ {
+ if (result == HitResult.None)
+ return false;
+
+ if (result == minResult || result == maxResult)
+ return true;
+
+ Debug.Assert(minResult <= maxResult);
+ return result > minResult && result < maxResult;
+ }
}
}
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 6fa5a87c8e..7a5b707357 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -71,7 +71,6 @@ namespace osu.Game.Rulesets.Scoring
private double maxBaseScore;
private double rollingMaxBaseScore;
private double baseScore;
- private double bonusScore;
private readonly List hitEvents = new List();
private HitObject lastHitObject;
@@ -116,14 +115,15 @@ namespace osu.Game.Rulesets.Scoring
if (result.FailedAtJudgement)
return;
- if (result.Judgement.AffectsCombo)
+ if (!result.Type.IsScorable())
+ return;
+
+ if (result.Type.AffectsCombo())
{
switch (result.Type)
{
- case HitResult.None:
- break;
-
case HitResult.Miss:
+ case HitResult.LargeTickMiss:
Combo.Value = 0;
break;
@@ -133,22 +133,16 @@ namespace osu.Game.Rulesets.Scoring
}
}
- double scoreIncrease = result.Type == HitResult.Miss ? 0 : result.Judgement.NumericResultFor(result);
+ double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
- if (result.Judgement.IsBonus)
+ if (!result.Type.IsBonus())
{
- if (result.IsHit)
- bonusScore += scoreIncrease;
- }
- else
- {
- if (result.HasResult)
- scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1;
-
baseScore += scoreIncrease;
rollingMaxBaseScore += result.Judgement.MaxNumericResult;
}
+ scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1;
+
hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject;
@@ -171,22 +165,19 @@ namespace osu.Game.Rulesets.Scoring
if (result.FailedAtJudgement)
return;
- double scoreIncrease = result.Type == HitResult.Miss ? 0 : result.Judgement.NumericResultFor(result);
+ if (!result.Type.IsScorable())
+ return;
- if (result.Judgement.IsBonus)
- {
- if (result.IsHit)
- bonusScore -= scoreIncrease;
- }
- else
- {
- if (result.HasResult)
- scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1;
+ double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
+ if (!result.Type.IsBonus())
+ {
baseScore -= scoreIncrease;
rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
}
+ scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1;
+
Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject;
hitEvents.RemoveAt(hitEvents.Count - 1);
@@ -207,7 +198,7 @@ namespace osu.Game.Rulesets.Scoring
return GetScore(mode, maxHighestCombo,
maxBaseScore > 0 ? baseScore / maxBaseScore : 0,
maxHighestCombo > 0 ? (double)HighestCombo.Value / maxHighestCombo : 0,
- bonusScore);
+ scoreResultCounts);
}
///
@@ -217,9 +208,9 @@ namespace osu.Game.Rulesets.Scoring
/// The maximum combo achievable in the beatmap.
/// The accuracy percentage achieved by the player.
/// The proportion of achieved by the player.
- /// Any bonus score to be added.
+ /// Any statistics to be factored in.
/// The total score.
- public double GetScore(ScoringMode mode, int maxCombo, double accuracyRatio, double comboRatio, double bonusScore)
+ public double GetScore(ScoringMode mode, int maxCombo, double accuracyRatio, double comboRatio, Dictionary statistics)
{
switch (mode)
{
@@ -228,14 +219,18 @@ namespace osu.Game.Rulesets.Scoring
double accuracyScore = accuracyPortion * accuracyRatio;
double comboScore = comboPortion * comboRatio;
- return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier;
+ return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier;
case ScoringMode.Classic:
// should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1)
- return bonusScore + (accuracyRatio * maxCombo * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25);
+ return getBonusScore(statistics) + (accuracyRatio * maxCombo * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25);
}
}
+ private double getBonusScore(Dictionary statistics)
+ => statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
+ + statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
+
private ScoreRank rankFrom(double acc)
{
if (acc == 1)
@@ -282,7 +277,6 @@ namespace osu.Game.Rulesets.Scoring
baseScore = 0;
rollingMaxBaseScore = 0;
- bonusScore = 0;
TotalScore.Value = 0;
Accuracy.Value = 1;
@@ -309,9 +303,7 @@ namespace osu.Game.Rulesets.Scoring
score.Rank = Rank.Value;
score.Date = DateTimeOffset.Now;
- var hitWindows = CreateHitWindows();
-
- foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
+ foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.IsScorable()))
score.Statistics[result] = GetStatistic(result);
score.HitEvents = hitEvents;
@@ -320,6 +312,7 @@ namespace osu.Game.Rulesets.Scoring
///
/// Create a for this processor.
///
+ [Obsolete("Method is now unused.")] // Can be removed 20210328
public virtual HitWindows CreateHitWindows() => new HitWindows();
}
diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index a4af92749f..70b3d0c7d4 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -2,7 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
@@ -32,6 +35,7 @@ namespace osu.Game.Rulesets.UI
public GameplayClock GameplayClock => stabilityGameplayClock;
[Cached(typeof(GameplayClock))]
+ [Cached(typeof(ISamplePlaybackDisabler))]
private readonly StabilityGameplayClock stabilityGameplayClock;
public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
@@ -55,13 +59,16 @@ namespace osu.Game.Rulesets.UI
private int direction;
[BackgroundDependencyLoader(true)]
- private void load(GameplayClock clock)
+ private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler)
{
if (clock != null)
{
- stabilityGameplayClock.ParentGameplayClock = parentGameplayClock = clock;
+ parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock;
GameplayClock.IsPaused.BindTo(clock.IsPaused);
}
+
+ // this is a bit temporary. should really be done inside of GameplayClock (but requires large structural changes).
+ stabilityGameplayClock.ParentSampleDisabler = sampleDisabler;
}
protected override void LoadComplete()
@@ -204,25 +211,37 @@ namespace osu.Game.Rulesets.UI
private void setClock()
{
- // in case a parent gameplay clock isn't available, just use the parent clock.
- parentGameplayClock ??= Clock;
-
- Clock = GameplayClock;
- ProcessCustomClock = false;
+ if (parentGameplayClock == null)
+ {
+ // in case a parent gameplay clock isn't available, just use the parent clock.
+ parentGameplayClock ??= Clock;
+ }
+ else
+ {
+ Clock = GameplayClock;
+ }
}
public ReplayInputHandler ReplayInputHandler { get; set; }
private class StabilityGameplayClock : GameplayClock
{
- public IFrameBasedClock ParentGameplayClock;
+ public GameplayClock ParentGameplayClock;
+
+ public ISamplePlaybackDisabler ParentSampleDisabler;
+
+ public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>();
public StabilityGameplayClock(FramedClock underlyingClock)
: base(underlyingClock)
{
}
- public override bool IsSeeking => ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200;
+ protected override bool ShouldDisableSamplePlayback =>
+ // handle the case where playback is catching up to real-time.
+ base.ShouldDisableSamplePlayback
+ || ParentSampleDisabler?.SamplePlaybackDisabled.Value == true
+ || (ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200);
}
}
}
diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
index 6f73a284a2..b58f65800d 100644
--- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
+++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
@@ -28,37 +28,9 @@ namespace osu.Game.Scoring.Legacy
}
}
- public static int? GetCount300(this ScoreInfo scoreInfo)
- {
- switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
- {
- case 0:
- case 1:
- case 3:
- return getCount(scoreInfo, HitResult.Great);
+ public static int? GetCount300(this ScoreInfo scoreInfo) => getCount(scoreInfo, HitResult.Great);
- case 2:
- return getCount(scoreInfo, HitResult.Perfect);
- }
-
- return null;
- }
-
- public static void SetCount300(this ScoreInfo scoreInfo, int value)
- {
- switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
- {
- case 0:
- case 1:
- case 3:
- scoreInfo.Statistics[HitResult.Great] = value;
- break;
-
- case 2:
- scoreInfo.Statistics[HitResult.Perfect] = value;
- break;
- }
- }
+ public static void SetCount300(this ScoreInfo scoreInfo, int value) => scoreInfo.Statistics[HitResult.Great] = value;
public static int? GetCountKatu(this ScoreInfo scoreInfo)
{
@@ -94,8 +66,6 @@ namespace osu.Game.Scoring.Legacy
{
case 0:
case 1:
- return getCount(scoreInfo, HitResult.Good);
-
case 3:
return getCount(scoreInfo, HitResult.Ok);
@@ -112,9 +82,6 @@ namespace osu.Game.Scoring.Legacy
{
case 0:
case 1:
- scoreInfo.Statistics[HitResult.Good] = value;
- break;
-
case 3:
scoreInfo.Statistics[HitResult.Ok] = value;
break;
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index efcf1737c9..0206989231 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
+using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
@@ -147,8 +148,6 @@ namespace osu.Game.Scoring
[JsonProperty("statistics")]
public Dictionary Statistics = new Dictionary();
- public IOrderedEnumerable> SortedStatistics => Statistics.OrderByDescending(pair => pair.Key);
-
[JsonIgnore]
[Column("Statistics")]
public string StatisticsJson
@@ -186,6 +185,76 @@ namespace osu.Game.Scoring
[JsonProperty("position")]
public int? Position { get; set; }
+ private bool isLegacyScore;
+
+ ///
+ /// Whether this represents a legacy (osu!stable) score.
+ ///
+ [JsonIgnore]
+ [NotMapped]
+ public bool IsLegacyScore
+ {
+ get
+ {
+ if (isLegacyScore)
+ return true;
+
+ // The above check will catch legacy online scores that have an appropriate UserString + UserId.
+ // For non-online scores such as those imported in, a heuristic is used based on the following table:
+ //
+ // Mode | UserString | UserId
+ // --------------- | ---------- | ---------
+ // stable | | 1
+ // lazer | |
+ // lazer (offline) | Guest | 1
+
+ return ID > 0 && UserID == 1 && UserString != "Guest";
+ }
+ set => isLegacyScore = value;
+ }
+
+ public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay()
+ {
+ foreach (var key in OrderAttributeUtils.GetValuesInOrder())
+ {
+ if (key.IsBonus())
+ continue;
+
+ int value = Statistics.GetOrDefault(key);
+
+ switch (key)
+ {
+ case HitResult.SmallTickHit:
+ {
+ int total = value + Statistics.GetOrDefault(HitResult.SmallTickMiss);
+ if (total > 0)
+ yield return (key, value, total);
+
+ break;
+ }
+
+ case HitResult.LargeTickHit:
+ {
+ int total = value + Statistics.GetOrDefault(HitResult.LargeTickMiss);
+ if (total > 0)
+ yield return (key, value, total);
+
+ break;
+ }
+
+ case HitResult.SmallTickMiss:
+ case HitResult.LargeTickMiss:
+ break;
+
+ default:
+ if (value > 0 || key == HitResult.Miss)
+ yield return (key, value, null);
+
+ break;
+ }
+ }
+ }
+
[Serializable]
protected class DeserializedMod : IMod
{
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 619ca76598..8e8147ff39 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -10,6 +10,7 @@ using System.Threading;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
@@ -149,23 +150,38 @@ namespace osu.Game.Scoring
return;
}
- int? beatmapMaxCombo = score.Beatmap.MaxCombo;
+ int beatmapMaxCombo;
- if (beatmapMaxCombo == null)
+ if (score.IsLegacyScore)
{
- if (score.Beatmap.ID == 0 || difficulties == null)
+ // This score is guaranteed to be an osu!stable score.
+ // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
+ if (score.Beatmap.MaxCombo == null)
{
- // We don't have enough information (max combo) to compute the score, so let's use the provided score.
- Value = score.TotalScore;
+ if (score.Beatmap.ID == 0 || difficulties == null)
+ {
+ // We don't have enough information (max combo) to compute the score, so use the provided score.
+ Value = score.TotalScore;
+ return;
+ }
+
+ // We can compute the max combo locally after the async beatmap difficulty computation.
+ difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
+ difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true);
+
return;
}
- // We can compute the max combo locally after the async beatmap difficulty computation.
- difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
- difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true);
+ beatmapMaxCombo = score.Beatmap.MaxCombo.Value;
}
else
- updateScore(beatmapMaxCombo.Value);
+ {
+ // This score is guaranteed to be an osu!lazer score.
+ // The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
+ beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetOrDefault(r)).Sum();
+ }
+
+ updateScore(beatmapMaxCombo);
}
private void updateScore(int beatmapMaxCombo)
@@ -181,7 +197,7 @@ namespace osu.Game.Scoring
scoreProcessor.Mods.Value = score.Mods;
- Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo, 0));
+ Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics));
}
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs
index 102955657e..8c0e31c04c 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs
@@ -1,70 +1,50 @@
// 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.Specialized;
using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics;
-using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
///
/// The part of the timeline that displays the control points.
///
- public class ControlPointPart : TimelinePart
+ public class ControlPointPart : TimelinePart
{
+ private IBindableList controlPointGroups;
+
protected override void LoadBeatmap(WorkingBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
- ControlPointInfo cpi = beatmap.Beatmap.ControlPointInfo;
-
- cpi.TimingPoints.ForEach(addTimingPoint);
-
- // Consider all non-timing points as the same type
- cpi.SamplePoints.Select(c => (ControlPoint)c)
- .Concat(cpi.EffectPoints)
- .Concat(cpi.DifficultyPoints)
- .Distinct()
- // Non-timing points should not be added where there are timing points
- .Where(c => cpi.TimingPointAt(c.Time).Time != c.Time)
- .ForEach(addNonTimingPoint);
- }
-
- private void addTimingPoint(ControlPoint controlPoint) => Add(new TimingPointVisualisation(controlPoint));
- private void addNonTimingPoint(ControlPoint controlPoint) => Add(new NonTimingPointVisualisation(controlPoint));
-
- private class TimingPointVisualisation : ControlPointVisualisation
- {
- public TimingPointVisualisation(ControlPoint controlPoint)
- : base(controlPoint)
+ controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
+ controlPointGroups.BindCollectionChanged((sender, args) =>
{
- }
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Reset:
+ Clear();
+ break;
- [BackgroundDependencyLoader]
- private void load(OsuColour colours) => Colour = colours.YellowDark;
- }
+ case NotifyCollectionChangedAction.Add:
+ foreach (var group in args.NewItems.OfType())
+ Add(new GroupVisualisation(group));
+ break;
- private class NonTimingPointVisualisation : ControlPointVisualisation
- {
- public NonTimingPointVisualisation(ControlPoint controlPoint)
- : base(controlPoint)
- {
- }
+ case NotifyCollectionChangedAction.Remove:
+ foreach (var group in args.OldItems.OfType())
+ {
+ var matching = Children.SingleOrDefault(gv => gv.Group == group);
- [BackgroundDependencyLoader]
- private void load(OsuColour colours) => Colour = colours.Green;
- }
+ matching?.Expire();
+ }
- private abstract class ControlPointVisualisation : PointVisualisation
- {
- protected ControlPointVisualisation(ControlPoint controlPoint)
- : base(controlPoint.Time)
- {
- }
+ break;
+ }
+ }, true);
}
}
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs
new file mode 100644
index 0000000000..b9eb53b697
--- /dev/null
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.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.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
+{
+ public class GroupVisualisation : PointVisualisation
+ {
+ public readonly ControlPointGroup Group;
+
+ private BindableList controlPoints;
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ public GroupVisualisation(ControlPointGroup group)
+ : base(group.Time)
+ {
+ Group = group;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy();
+ controlPoints.BindCollectionChanged((_, __) =>
+ {
+ if (controlPoints.Count == 0)
+ {
+ Colour = Color4.Transparent;
+ return;
+ }
+
+ Colour = controlPoints.Any(c => c is TimingControlPoint) ? colours.YellowDark : colours.Green;
+ }, true);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index ed3d328330..3e54813a14 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -8,10 +8,12 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
+using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
@@ -21,6 +23,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
{
public readonly Bindable WaveformVisible = new Bindable();
+
+ public readonly Bindable ControlPointsVisible = new Bindable();
+
+ public readonly Bindable TicksVisible = new Bindable();
+
public readonly IBindable Beatmap = new Bindable();
[Resolved]
@@ -56,24 +63,43 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
private WaveformGraph waveform;
+ private ControlPointPart controlPoints;
+ private TimelineTickDisplay ticks;
[BackgroundDependencyLoader]
private void load(IBindable beatmap, OsuColour colours)
{
- Add(waveform = new WaveformGraph
+ AddRange(new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Colour = colours.Blue.Opacity(0.2f),
- LowColour = colours.BlueLighter,
- MidColour = colours.BlueDark,
- HighColour = colours.BlueDarker,
- Depth = float.MaxValue
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = float.MaxValue,
+ Children = new Drawable[]
+ {
+ waveform = new WaveformGraph
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colours.Blue.Opacity(0.2f),
+ LowColour = colours.BlueLighter,
+ MidColour = colours.BlueDark,
+ HighColour = colours.BlueDarker,
+ },
+ controlPoints = new ControlPointPart
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ ticks = new TimelineTickDisplay(),
+ }
+ },
});
// We don't want the centre marker to scroll
AddInternal(new CentreMarker { Depth = float.MaxValue });
WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
+ ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
+ TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
Beatmap.BindTo(beatmap);
Beatmap.BindValueChanged(b =>
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
index b99a053859..0ec48e04c6 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
@@ -14,9 +14,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineArea : Container
{
- private readonly Timeline timeline = new Timeline { RelativeSizeAxes = Axes.Both };
+ public readonly Timeline Timeline = new Timeline { RelativeSizeAxes = Axes.Both };
- protected override Container Content => timeline;
+ protected override Container Content => Timeline;
[BackgroundDependencyLoader]
private void load()
@@ -25,6 +25,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
CornerRadius = 5;
OsuCheckbox waveformCheckbox;
+ OsuCheckbox controlPointsCheckbox;
+ OsuCheckbox ticksCheckbox;
InternalChildren = new Drawable[]
{
@@ -57,12 +59,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Y,
Width = 160,
- Padding = new MarginPadding { Horizontal = 15 },
+ Padding = new MarginPadding { Horizontal = 10 },
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4),
Children = new[]
{
- waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" }
+ waveformCheckbox = new OsuCheckbox
+ {
+ LabelText = "Waveform",
+ Current = { Value = true },
+ },
+ controlPointsCheckbox = new OsuCheckbox
+ {
+ LabelText = "Control Points",
+ Current = { Value = true },
+ },
+ ticksCheckbox = new OsuCheckbox
+ {
+ LabelText = "Ticks",
+ Current = { Value = true },
+ }
}
}
}
@@ -107,7 +123,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
},
- timeline
+ Timeline
},
},
ColumnDimensions = new[]
@@ -119,11 +135,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
};
- waveformCheckbox.Current.Value = true;
-
- timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
+ Timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
+ Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current);
+ Timeline.TicksVisible.BindTo(ticksCheckbox.Current);
}
- private void changeZoom(float change) => timeline.Zoom += change;
+ private void changeZoom(float change) => Timeline.Zoom += change;
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index b95b3842b3..bc2ccfc605 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -3,18 +3,23 @@
using System;
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.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
@@ -34,11 +39,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly List shadowComponents = new List();
+ private DrawableHitObject drawableHitObject;
+
+ private Bindable comboColour;
+
+ private readonly Container mainComponents;
+
+ private readonly OsuSpriteText comboIndexText;
+
+ private Bindable comboIndex;
+
private const float thickness = 5;
private const float shadow_radius = 5;
- private const float circle_size = 16;
+ private const float circle_size = 24;
public TimelineHitObjectBlueprint(HitObject hitObject)
: base(hitObject)
@@ -54,14 +69,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
+ AddRangeInternal(new Drawable[]
+ {
+ mainComponents = new Container
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ comboIndexText = new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Numeric.With(size: circle_size / 2, weight: FontWeight.Black),
+ },
+ });
+
circle = new Circle
{
Size = new Vector2(circle_size),
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
- RelativePositionAxes = Axes.X,
- AlwaysPresent = true,
- Colour = Color4.White,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
@@ -77,7 +106,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
DragBar dragBarUnderlay;
Container extensionBar;
- AddRangeInternal(new Drawable[]
+ mainComponents.AddRange(new Drawable[]
{
extensionBar = new Container
{
@@ -117,12 +146,54 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
else
{
- AddInternal(circle);
+ mainComponents.Add(circle);
}
updateShadows();
}
+ [BackgroundDependencyLoader(true)]
+ private void load(HitObjectComposer composer)
+ {
+ if (composer != null)
+ {
+ // best effort to get the drawable representation for grabbing colour and what not.
+ drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject);
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (HitObject is IHasComboInformation comboInfo)
+ {
+ comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
+ comboIndex.BindValueChanged(combo =>
+ {
+ comboIndexText.Text = (combo.NewValue + 1).ToString();
+ }, true);
+ }
+
+ if (drawableHitObject != null)
+ {
+ comboColour = drawableHitObject.AccentColour.GetBoundCopy();
+ comboColour.BindValueChanged(colour =>
+ {
+ if (HitObject is IHasDuration)
+ mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White);
+ else
+ mainComponents.Colour = drawableHitObject.AccentColour.Value;
+
+ var col = mainComponents.Colour.TopLeft.Linear;
+ float brightness = col.R + col.G + col.B;
+
+ // decide the combo index colour based on brightness?
+ comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White;
+ }, true);
+ }
+ }
+
protected override void Update()
{
base.Update();
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index d7a4661fa0..5282b4d998 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -1,8 +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.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Skinning;
@@ -18,11 +22,23 @@ namespace osu.Game.Screens.Edit.Compose
{
}
- protected override Drawable CreateMainContent()
+ private Ruleset ruleset;
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
- var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance();
+ var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
+ ruleset = parent.Get>().Value.BeatmapInfo.Ruleset?.CreateInstance();
composer = ruleset?.CreateHitObjectComposer();
+ // make the composer available to the timeline and other components in this screen.
+ dependencies.CacheAs(composer);
+
+ return dependencies;
+ }
+
+ protected override Drawable CreateMainContent()
+ {
if (ruleset == null || composer == null)
return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer");
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index fd090e0959..875ab25003 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -84,6 +84,8 @@ namespace osu.Game.Screens.Edit
private DependencyContainer dependencies;
+ private bool isNewBeatmap;
+
protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo);
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@@ -107,13 +109,12 @@ namespace osu.Game.Screens.Edit
UpdateClockSource();
dependencies.CacheAs(clock);
+ dependencies.CacheAs(clock);
AddInternal(clock);
// todo: remove caching of this and consume via editorBeatmap?
dependencies.Cache(beatDivisor);
- bool isNewBeatmap = false;
-
if (Beatmap.Value is DummyWorkingBeatmap)
{
isNewBeatmap = true;
@@ -286,6 +287,9 @@ namespace osu.Game.Screens.Edit
protected void Save()
{
+ // no longer new after first user-triggered save.
+ isNewBeatmap = false;
+
// apply any set-level metadata changes.
beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet);
@@ -434,10 +438,20 @@ namespace osu.Game.Screens.Edit
public override bool OnExiting(IScreen next)
{
- if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges && !(dialogOverlay.CurrentDialog is PromptForSaveDialog))
+ if (!exitConfirmed)
{
- dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
- return true;
+ // if the confirm dialog is already showing (or we can't show it, ie. in tests) exit without save.
+ if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog)
+ {
+ confirmExit();
+ return true;
+ }
+
+ if (isNewBeatmap || HasUnsavedChanges)
+ {
+ dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
+ return true;
+ }
}
Background.FadeColour(Color4.White, 500);
@@ -455,6 +469,12 @@ namespace osu.Game.Screens.Edit
private void confirmExit()
{
+ if (isNewBeatmap)
+ {
+ // confirming exit without save means we should delete the new beatmap completely.
+ beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet);
+ }
+
exitConfirmed = true;
this.Exit();
}
diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs
index 617c436ee0..b69e9c4c51 100644
--- a/osu.Game/Screens/Edit/EditorChangeHandler.cs
+++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit
public readonly Bindable CanUndo = new Bindable();
public readonly Bindable CanRedo = new Bindable();
+ public event Action OnStateChange;
+
private readonly LegacyEditorBeatmapPatcher patcher;
private readonly List savedStates = new List();
@@ -79,9 +81,6 @@ namespace osu.Game.Screens.Edit
SaveState();
}
- ///
- /// Saves the current state.
- ///
public void SaveState()
{
if (bulkChangesStarted > 0)
@@ -109,6 +108,8 @@ namespace osu.Game.Screens.Edit
savedStates.Add(newState);
currentState = savedStates.Count - 1;
+
+ OnStateChange?.Invoke();
updateBindables();
}
}
@@ -136,6 +137,7 @@ namespace osu.Game.Screens.Edit
isRestoring = false;
+ OnStateChange?.Invoke();
updateBindables();
}
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index ec203df064..4b7cd82637 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -7,17 +7,18 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
-using osu.Framework.Utils;
using osu.Framework.Timing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Screens.Play;
namespace osu.Game.Screens.Edit
{
///
/// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
///
- public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
+ public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISamplePlaybackDisabler
{
public IBindable
public interface IEditorChangeHandler
{
+ ///
+ /// Fired whenever a state change occurs.
+ ///
+ event Action OnStateChange;
+
///
/// Begins a bulk state change event. should be invoked soon after.
///
@@ -29,5 +35,11 @@ namespace osu.Game.Screens.Edit
/// This should be invoked as soon as possible after to cause a state change.
///
void EndChange();
+
+ ///
+ /// Immediately saves the current state.
+ /// Note that this will be a no-op if there is a change in progress via .
+ ///
+ void SaveState();
}
}
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
index e1182d9fa4..c40061b97c 100644
--- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
+++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
@@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit.Timing
private IReadOnlyList createSections() => new Drawable[]
{
+ new GroupSection(),
new TimingSection(),
new DifficultySection(),
new SampleSection(),
diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
index 58a7f97e5f..b55d74e3b4 100644
--- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs
+++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
@@ -2,27 +2,23 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class DifficultySection : Section
{
- private SettingsSlider multiplier;
+ private SliderWithTextBoxInput multiplierSlider;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new[]
{
- multiplier = new SettingsSlider
+ multiplierSlider = new SliderWithTextBoxInput("Speed Multiplier")
{
- LabelText = "Speed Multiplier",
- Bindable = new DifficultyControlPoint().SpeedMultiplierBindable,
- RelativeSizeAxes = Axes.X,
+ Current = new DifficultyControlPoint().SpeedMultiplierBindable
}
});
}
@@ -31,7 +27,8 @@ namespace osu.Game.Screens.Edit.Timing
{
if (point.NewValue != null)
{
- multiplier.Bindable = point.NewValue.SpeedMultiplierBindable;
+ multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable;
+ multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}
diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs
index 71e7f42713..2f143108a9 100644
--- a/osu.Game/Screens/Edit/Timing/EffectSection.cs
+++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs
@@ -28,7 +28,10 @@ namespace osu.Game.Screens.Edit.Timing
if (point.NewValue != null)
{
kiai.Current = point.NewValue.KiaiModeBindable;
+ kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
+
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
+ omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}
diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs
new file mode 100644
index 0000000000..c77d48ef0a
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs
@@ -0,0 +1,119 @@
+// 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.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osuTK;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class GroupSection : CompositeDrawable
+ {
+ private LabelledTextBox textBox;
+
+ private TriangleButton button;
+
+ [Resolved]
+ protected Bindable SelectedGroup { get; private set; }
+
+ [Resolved]
+ protected IBindable Beatmap { get; private set; }
+
+ [Resolved]
+ private EditorClock clock { get; set; }
+
+ [Resolved(canBeNull: true)]
+ private IEditorChangeHandler changeHandler { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ Padding = new MarginPadding(10);
+
+ InternalChildren = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(10),
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ textBox = new LabelledTextBox
+ {
+ Label = "Time"
+ },
+ button = new TriangleButton
+ {
+ Text = "Use current time",
+ RelativeSizeAxes = Axes.X,
+ Action = () => changeSelectedGroupTime(clock.CurrentTime)
+ }
+ }
+ },
+ };
+
+ textBox.OnCommit += (sender, isNew) =>
+ {
+ if (!isNew)
+ return;
+
+ if (double.TryParse(sender.Text, out var newTime))
+ {
+ changeSelectedGroupTime(newTime);
+ }
+ else
+ {
+ SelectedGroup.TriggerChange();
+ }
+ };
+
+ SelectedGroup.BindValueChanged(group =>
+ {
+ if (group.NewValue == null)
+ {
+ textBox.Text = string.Empty;
+
+ textBox.Current.Disabled = true;
+ button.Enabled.Value = false;
+ return;
+ }
+
+ textBox.Current.Disabled = false;
+ button.Enabled.Value = true;
+
+ textBox.Text = $"{group.NewValue.Time:n0}";
+ }, true);
+ }
+
+ private void changeSelectedGroupTime(in double time)
+ {
+ if (SelectedGroup.Value == null || time == SelectedGroup.Value.Time)
+ return;
+
+ changeHandler?.BeginChange();
+
+ var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray();
+
+ Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value);
+
+ foreach (var cp in currentGroupItems)
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp);
+
+ SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time);
+
+ changeHandler?.EndChange();
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs
index 4665c77991..280e19c99a 100644
--- a/osu.Game/Screens/Edit/Timing/SampleSection.cs
+++ b/osu.Game/Screens/Edit/Timing/SampleSection.cs
@@ -2,18 +2,17 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Graphics;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
-using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class SampleSection : Section
{
private LabelledTextBox bank;
- private SettingsSlider volume;
+ private SliderWithTextBoxInput volume;
[BackgroundDependencyLoader]
private void load()
@@ -24,10 +23,9 @@ namespace osu.Game.Screens.Edit.Timing
{
Label = "Bank Name",
},
- volume = new SettingsSlider
+ volume = new SliderWithTextBoxInput("Volume")
{
- Bindable = new SampleControlPoint().SampleVolumeBindable,
- LabelText = "Volume",
+ Current = new SampleControlPoint().SampleVolumeBindable,
}
});
}
@@ -37,7 +35,10 @@ namespace osu.Game.Screens.Edit.Timing
if (point.NewValue != null)
{
bank.Current = point.NewValue.SampleBankBindable;
- volume.Bindable = point.NewValue.SampleVolumeBindable;
+ bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
+
+ volume.Current = point.NewValue.SampleVolumeBindable;
+ volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}
diff --git a/osu.Game/Screens/Edit/Timing/Section.cs b/osu.Game/Screens/Edit/Timing/Section.cs
index 603fb77f31..7a81eeb1a4 100644
--- a/osu.Game/Screens/Edit/Timing/Section.cs
+++ b/osu.Game/Screens/Edit/Timing/Section.cs
@@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
protected Bindable SelectedGroup { get; private set; }
+ [Resolved(canBeNull: true)]
+ protected IEditorChangeHandler ChangeHandler { get; private set; }
+
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
new file mode 100644
index 0000000000..d5afc8978d
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
@@ -0,0 +1,78 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue
+ where T : struct, IEquatable, IComparable, IConvertible
+ {
+ private readonly SettingsSlider slider;
+
+ public SliderWithTextBoxInput(string labelText)
+ {
+ LabelledTextBox textbox;
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ InternalChildren = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ textbox = new LabelledTextBox
+ {
+ Label = labelText,
+ },
+ slider = new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ RelativeSizeAxes = Axes.X,
+ }
+ }
+ },
+ };
+
+ textbox.OnCommit += (t, isNew) =>
+ {
+ if (!isNew) return;
+
+ try
+ {
+ slider.Bindable.Parse(t.Text);
+ }
+ catch
+ {
+ // TriggerChange below will restore the previous text value on failure.
+ }
+
+ // This is run regardless of parsing success as the parsed number may not actually trigger a change
+ // due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
+ Current.TriggerChange();
+ };
+
+ Current.BindValueChanged(val =>
+ {
+ textbox.Text = val.NewValue.ToString();
+ }, true);
+ }
+
+ public Bindable Current
+ {
+ get => slider.Bindable;
+ set => slider.Bindable = value;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
index d7da29218f..0796097186 100644
--- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs
+++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
@@ -58,6 +59,12 @@ namespace osu.Game.Screens.Edit.Timing
});
}
+ protected override void OnTimelineLoaded(TimelineArea timelineArea)
+ {
+ base.OnTimelineLoaded(timelineArea);
+ timelineArea.Timeline.Zoom = timelineArea.Timeline.MinZoom;
+ }
+
public class ControlPointList : CompositeDrawable
{
private OsuButton deleteButton;
@@ -74,6 +81,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private Bindable selectedGroup { get; set; }
+ [Resolved(canBeNull: true)]
+ private IEditorChangeHandler changeHandler { get; set; }
+
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -129,11 +139,13 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true);
controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
- controlGroups.CollectionChanged += (sender, args) => createContent();
- createContent();
- }
- private void createContent() => table.ControlGroups = controlGroups;
+ controlGroups.BindCollectionChanged((sender, args) =>
+ {
+ table.ControlGroups = controlGroups;
+ changeHandler.SaveState();
+ }, true);
+ }
private void delete()
{
diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs
index 879363ba08..2ab8703cc4 100644
--- a/osu.Game/Screens/Edit/Timing/TimingSection.cs
+++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs
@@ -37,8 +37,13 @@ namespace osu.Game.Screens.Edit.Timing
if (point.NewValue != null)
{
bpmSlider.Bindable = point.NewValue.BeatLengthBindable;
+ bpmSlider.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState());
+
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
+ // no need to hook change handler here as it's the same bindable as above
+
timeSignature.Bindable = point.NewValue.TimeSignatureBindable;
+ timeSignature.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}
@@ -65,18 +70,19 @@ namespace osu.Game.Screens.Edit.Timing
{
if (!isNew) return;
- if (double.TryParse(Current.Value, out double doubleVal))
+ try
{
- try
- {
+ if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0)
beatLengthBindable.Value = beatLengthToBpm(doubleVal);
- }
- catch
- {
- // will restore the previous text value on failure.
- beatLengthBindable.TriggerChange();
- }
}
+ catch
+ {
+ // TriggerChange below will restore the previous text value on failure.
+ }
+
+ // This is run regardless of parsing success as the parsed number may not actually trigger a change
+ // due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
+ beatLengthBindable.TriggerChange();
};
beatLengthBindable.BindValueChanged(val =>
@@ -103,14 +109,21 @@ namespace osu.Game.Screens.Edit.Timing
private const double sane_maximum = 240;
private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable;
- private readonly BindableDouble bpmBindable = new BindableDouble();
+
+ private readonly BindableDouble bpmBindable = new BindableDouble(60000 / TimingControlPoint.DEFAULT_BEAT_LENGTH)
+ {
+ MinValue = sane_minimum,
+ MaxValue = sane_maximum,
+ };
public BPMSlider()
{
beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true);
- bpmBindable.BindValueChanged(bpm => bpmBindable.Default = beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
+ bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
base.Bindable = bpmBindable;
+
+ TransferValueOnCommit = true;
}
public override Bindable Bindable
diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs
index 4f2cf5005c..eeea6777c6 100644
--- a/osu.Game/Screens/Play/GameplayClock.cs
+++ b/osu.Game/Screens/Play/GameplayClock.cs
@@ -1,8 +1,11 @@
// 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 System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Timing;
+using osu.Framework.Utils;
namespace osu.Game.Screens.Play
{
@@ -14,12 +17,19 @@ namespace osu.Game.Screens.Play
/// , as this should only be done once to ensure accuracy.
///
///
- public class GameplayClock : IFrameBasedClock
+ public class GameplayClock : IFrameBasedClock, ISamplePlaybackDisabler
{
private readonly IFrameBasedClock underlyingClock;
public readonly BindableBool IsPaused = new BindableBool();
+ ///
+ /// All adjustments applied to this clock which don't come from gameplay or mods.
+ ///
+ public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>();
+
+ private readonly Bindable samplePlaybackDisabled = new Bindable();
+
public GameplayClock(IFrameBasedClock underlyingClock)
{
this.underlyingClock = underlyingClock;
@@ -29,16 +39,40 @@ namespace osu.Game.Screens.Play
public double Rate => underlyingClock.Rate;
+ ///
+ /// The rate of gameplay when playback is at 100%.
+ /// This excludes any seeking / user adjustments.
+ ///
+ public double TrueGameplayRate
+ {
+ get
+ {
+ double baseRate = Rate;
+
+ foreach (var adjustment in NonGameplayAdjustments)
+ {
+ if (Precision.AlmostEquals(adjustment.Value, 0))
+ return 0;
+
+ baseRate /= adjustment.Value;
+ }
+
+ return baseRate;
+ }
+ }
+
public bool IsRunning => underlyingClock.IsRunning;
///
- /// Whether an ongoing seek operation is active.
+ /// Whether nested samples supporting the interface should be paused.
///
- public virtual bool IsSeeking => false;
+ protected virtual bool ShouldDisableSamplePlayback => IsPaused.Value;
public void ProcessFrame()
{
- // we do not want to process the underlying clock.
+ // intentionally not updating the underlying clock (handled externally).
+
+ samplePlaybackDisabled.Value = ShouldDisableSamplePlayback;
}
public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;
@@ -48,5 +82,7 @@ namespace osu.Game.Screens.Play
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public IClock Source => underlyingClock;
+
+ IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
}
}
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
index 7a9cb3dddd..9f8e55f577 100644
--- a/osu.Game/Screens/Play/GameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
@@ -50,8 +51,11 @@ namespace osu.Game.Screens.Play
///
/// The final clock which is exposed to underlying components.
///
- [Cached]
- public readonly GameplayClock GameplayClock;
+ public GameplayClock GameplayClock => localGameplayClock;
+
+ [Cached(typeof(GameplayClock))]
+ [Cached(typeof(ISamplePlaybackDisabler))]
+ private readonly LocalGameplayClock localGameplayClock;
private Bindable userAudioOffset;
@@ -79,7 +83,7 @@ namespace osu.Game.Screens.Play
userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock);
// the clock to be exposed via DI to children.
- GameplayClock = new GameplayClock(userOffsetClock);
+ localGameplayClock = new LocalGameplayClock(userOffsetClock);
GameplayClock.IsPaused.BindTo(IsPaused);
}
@@ -200,7 +204,9 @@ namespace osu.Game.Screens.Play
protected override void Update()
{
if (!IsPaused.Value)
+ {
userOffsetClock.ProcessFrame();
+ }
base.Update();
}
@@ -215,6 +221,9 @@ namespace osu.Game.Screens.Play
track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
+ localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust);
+ localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate);
+
speedAdjustmentsApplied = true;
}
@@ -231,9 +240,24 @@ namespace osu.Game.Screens.Play
track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
+ localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust);
+ localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate);
+
speedAdjustmentsApplied = false;
}
+ private class LocalGameplayClock : GameplayClock
+ {
+ public readonly List> MutableNonGameplayAdjustments = new List>();
+
+ public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments;
+
+ public LocalGameplayClock(FramedOffsetClock underlyingClock)
+ : base(underlyingClock)
+ {
+ }
+ }
+
private class HardwareCorrectionOffsetClock : FramedOffsetClock
{
// we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this.
diff --git a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs
index 7736541c92..fc4a1a5d83 100644
--- a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs
@@ -13,7 +13,6 @@ using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD
{
@@ -106,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD
public void Flash(JudgementResult result)
{
- if (result.Type == HitResult.Miss)
+ if (!result.IsHit)
return;
Scheduler.AddOnce(flash);
diff --git a/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs b/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs
new file mode 100644
index 0000000000..6b37021fe6
--- /dev/null
+++ b/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs
@@ -0,0 +1,20 @@
+// 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.Bindables;
+using osu.Game.Skinning;
+
+namespace osu.Game.Screens.Play
+{
+ ///
+ /// Allows a component to disable sample playback dynamically as required.
+ /// Handled by .
+ ///
+ public interface ISamplePlaybackDisabler
+ {
+ ///
+ /// Whether sample playback should be disabled (or paused for looping samples).
+ ///
+ IBindable SamplePlaybackDisabled { get; }
+ }
+}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 8e2ed583f2..175722c44e 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -191,9 +191,25 @@ namespace osu.Game.Screens.Play
dependencies.CacheAs(gameplayBeatmap);
- addUnderlayComponents(GameplayClockContainer);
- addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap);
- addOverlayComponents(GameplayClockContainer, Beatmap.Value);
+ var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
+
+ // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
+ // full access to all skin sources.
+ var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
+
+ // load the skinning hierarchy first.
+ // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
+ GameplayClockContainer.Add(beatmapSkinProvider.WithChild(rulesetSkinProvider));
+
+ rulesetSkinProvider.AddRange(new[]
+ {
+ // underlay and gameplay should have access the to skinning sources.
+ createUnderlayComponents(),
+ createGameplayComponents(Beatmap.Value, playableBeatmap)
+ });
+
+ // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
+ GameplayClockContainer.Add(createOverlayComponents(Beatmap.Value));
if (!DrawableRuleset.AllowGameplayOverlays)
{
@@ -238,45 +254,31 @@ namespace osu.Game.Screens.Play
breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
}
- private void addUnderlayComponents(Container target)
+ private Drawable createUnderlayComponents() =>
+ DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both };
+
+ private Drawable createGameplayComponents(WorkingBeatmap working, IBeatmap playableBeatmap) => new ScalingContainer(ScalingMode.Gameplay)
{
- target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both });
- }
-
- private void addGameplayComponents(Container target, WorkingBeatmap working, IBeatmap playableBeatmap)
- {
- var beatmapSkinProvider = new BeatmapSkinProvidingContainer(working.Skin);
-
- // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
- // full access to all skin sources.
- var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
-
- // load the skinning hierarchy first.
- // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
- target.Add(new ScalingContainer(ScalingMode.Gameplay)
- .WithChild(beatmapSkinProvider
- .WithChild(target = rulesetSkinProvider)));
-
- target.AddRange(new Drawable[]
+ Children = new Drawable[]
{
- DrawableRuleset,
+ DrawableRuleset.With(r =>
+ r.FrameStableComponents.Children = new Drawable[]
+ {
+ ScoreProcessor,
+ HealthProcessor,
+ breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
+ {
+ Breaks = working.Beatmap.Breaks
+ }
+ }),
new ComboEffects(ScoreProcessor)
- });
+ }
+ };
- DrawableRuleset.FrameStableComponents.AddRange(new Drawable[]
- {
- ScoreProcessor,
- HealthProcessor,
- breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
- {
- Breaks = working.Beatmap.Breaks
- }
- });
- }
-
- private void addOverlayComponents(Container target, WorkingBeatmap working)
+ private Drawable createOverlayComponents(WorkingBeatmap working) => new Container
{
- target.AddRange(new[]
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
{
DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
@@ -342,8 +344,8 @@ namespace osu.Game.Screens.Play
},
},
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
- });
- }
+ }
+ };
private void onBreakTimeChanged(ValueChangedEvent isBreakTime)
{
diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
index 0b85eeafa8..95ece1a9fb 100644
--- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Leaderboards;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
@@ -116,7 +117,7 @@ namespace osu.Game.Screens.Ranking.Contracted
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
- ChildrenEnumerable = score.SortedStatistics.Select(s => createStatistic(s.Key.GetDescription(), s.Value.ToString()))
+ ChildrenEnumerable = score.GetStatisticsForDisplay().Select(s => createStatistic(s.result, s.count, s.maxCount))
},
new FillFlowContainer
{
@@ -198,6 +199,9 @@ namespace osu.Game.Screens.Ranking.Contracted
};
}
+ private Drawable createStatistic(HitResult result, int count, int? maxCount)
+ => createStatistic(result.GetDescription(), maxCount == null ? $"{count}" : $"{count}/{maxCount}");
+
private Drawable createStatistic(string key, string value) => new Container
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 0033cd1f43..ebab8c88f6 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -65,8 +65,9 @@ namespace osu.Game.Screens.Ranking.Expanded
};
var bottomStatistics = new List();
- foreach (var stat in score.SortedStatistics)
- bottomStatistics.Add(new HitResultStatistic(stat.Key, stat.Value));
+
+ foreach (var (key, value, maxCount) in score.GetStatisticsForDisplay())
+ bottomStatistics.Add(new HitResultStatistic(key, value, maxCount));
statisticDisplays.AddRange(topStatistics);
statisticDisplays.AddRange(bottomStatistics);
diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs
index e820831809..fc01f5e9c4 100644
--- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -16,6 +17,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
public class CounterStatistic : StatisticDisplay
{
private readonly int count;
+ private readonly int? maxCount;
private RollingCounter counter;
@@ -24,10 +26,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
///
/// The name of the statistic.
/// The value to display.
- public CounterStatistic(string header, int count)
+ /// The maximum value of . Not displayed if null.
+ public CounterStatistic(string header, int count, int? maxCount = null)
: base(header)
{
this.count = count;
+ this.maxCount = maxCount;
}
public override void Appear()
@@ -36,7 +40,33 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
counter.Current.Value = count;
}
- protected override Drawable CreateContent() => counter = new Counter();
+ protected override Drawable CreateContent()
+ {
+ var container = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Child = counter = new Counter
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre
+ }
+ };
+
+ if (maxCount != null)
+ {
+ container.Add(new OsuSpriteText
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Font = OsuFont.Torus.With(size: 12, fixedWidth: true),
+ Spacing = new Vector2(-2, 0),
+ Text = $"/{maxCount}"
+ });
+ }
+
+ return container;
+ }
private class Counter : RollingCounter
{
diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs
index faa4a6a96c..a86033713f 100644
--- a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs
@@ -12,8 +12,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
{
private readonly HitResult result;
- public HitResultStatistic(HitResult result, int count)
- : base(result.GetDescription(), count)
+ public HitResultStatistic(HitResult result, int count, int? maxCount = null)
+ : base(result.GetDescription(), count, maxCount)
{
this.result = result;
}
diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
index 45fdc3ff33..aa2a83774e 100644
--- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
+++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Screens.Ranking.Statistics
/// The s to display the timing distribution of.
public HitEventTimingDistributionGraph(IReadOnlyList hitEvents)
{
- this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss).ToList();
+ this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList();
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs
index 18a2238784..055db143d1 100644
--- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs
+++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Screens.Ranking.Statistics
public UnstableRate(IEnumerable hitEvents)
: base("Unstable Rate")
{
- var timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss)
+ var timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit())
.Select(ev => ev.TimeOffset).ToArray();
Value = 10 * standardDeviation(timeOffsets);
}
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 5caf07b554..e38913b13a 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -336,7 +336,7 @@ namespace osu.Game.Skinning
case HitResult.Meh:
return this.GetAnimation("hit50", true, false);
- case HitResult.Good:
+ case HitResult.Ok:
return this.GetAnimation("hit100", true, false);
case HitResult.Great:
diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs
index 1d5412d93f..828804b9cb 100644
--- a/osu.Game/Skinning/LegacySkinConfiguration.cs
+++ b/osu.Game/Skinning/LegacySkinConfiguration.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Skinning
ComboPrefix,
ComboOverlap,
AnimationFramerate,
- LayeredHitSounds,
+ LayeredHitSounds
}
}
}
diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs
new file mode 100644
index 0000000000..d340f67575
--- /dev/null
+++ b/osu.Game/Skinning/PausableSkinnableSound.cs
@@ -0,0 +1,73 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Audio;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Skinning
+{
+ public class PausableSkinnableSound : SkinnableSound
+ {
+ protected bool RequestedPlaying { get; private set; }
+
+ public PausableSkinnableSound(ISampleInfo hitSamples)
+ : base(hitSamples)
+ {
+ }
+
+ public PausableSkinnableSound(IEnumerable hitSamples)
+ : base(hitSamples)
+ {
+ }
+
+ private readonly IBindable samplePlaybackDisabled = new Bindable();
+
+ [BackgroundDependencyLoader(true)]
+ private void load(ISamplePlaybackDisabler samplePlaybackDisabler)
+ {
+ // if in a gameplay context, pause sample playback when gameplay is paused.
+ if (samplePlaybackDisabler != null)
+ {
+ samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
+ samplePlaybackDisabled.BindValueChanged(disabled =>
+ {
+ if (!RequestedPlaying) return;
+
+ // let non-looping samples that have already been started play out to completion (sounds better than abruptly cutting off).
+ if (!Looping) return;
+
+ if (disabled.NewValue)
+ base.Stop();
+ else
+ {
+ // schedule so we don't start playing a sample which is no longer alive.
+ Schedule(() =>
+ {
+ if (RequestedPlaying)
+ base.Play();
+ });
+ }
+ });
+ }
+ }
+
+ public override void Play()
+ {
+ RequestedPlaying = true;
+
+ if (samplePlaybackDisabled.Value)
+ return;
+
+ base.Play();
+ }
+
+ public override void Stop()
+ {
+ RequestedPlaying = false;
+ base.Stop();
+ }
+ }
+}
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index ba14049b41..f6e91811dd 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -11,7 +11,6 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
-using osu.Game.Screens.Play;
namespace osu.Game.Skinning
{
@@ -22,8 +21,6 @@ namespace osu.Game.Skinning
[Resolved]
private ISampleStore samples { get; set; }
- private bool requestedPlaying;
-
public override bool RemoveWhenNotAlive => false;
public override bool RemoveCompletedTransforms => false;
@@ -37,7 +34,7 @@ namespace osu.Game.Skinning
///
protected bool PlayWhenZeroVolume => Looping;
- private readonly AudioContainer samplesContainer;
+ protected readonly AudioContainer SamplesContainer;
public SkinnableSound(ISampleInfo hitSamples)
: this(new[] { hitSamples })
@@ -47,28 +44,7 @@ namespace osu.Game.Skinning
public SkinnableSound(IEnumerable hitSamples)
{
this.hitSamples = hitSamples.ToArray();
- InternalChild = samplesContainer = new AudioContainer();
- }
-
- private Bindable gameplayClockPaused;
-
- [BackgroundDependencyLoader(true)]
- private void load(GameplayClock gameplayClock)
- {
- // if in a gameplay context, pause sample playback when gameplay is paused.
- gameplayClockPaused = gameplayClock?.IsPaused.GetBoundCopy();
- gameplayClockPaused?.BindValueChanged(paused =>
- {
- if (requestedPlaying)
- {
- if (paused.NewValue)
- stop();
- // it's not easy to know if a sample has finished playing (to end).
- // to keep things simple only resume playing looping samples.
- else if (Looping)
- play();
- }
- });
+ InternalChild = SamplesContainer = new AudioContainer();
}
private bool looping;
@@ -82,34 +58,22 @@ namespace osu.Game.Skinning
looping = value;
- samplesContainer.ForEach(c => c.Looping = looping);
+ SamplesContainer.ForEach(c => c.Looping = looping);
}
}
- public void Play()
+ public virtual void Play()
{
- requestedPlaying = true;
- play();
- }
-
- private void play()
- {
- samplesContainer.ForEach(c =>
+ SamplesContainer.ForEach(c =>
{
if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0)
c.Play();
});
}
- public void Stop()
+ public virtual void Stop()
{
- requestedPlaying = false;
- stop();
- }
-
- private void stop()
- {
- samplesContainer.ForEach(c => c.Stop());
+ SamplesContainer.ForEach(c => c.Stop());
}
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
@@ -138,33 +102,33 @@ namespace osu.Game.Skinning
return ch;
}).Where(c => c != null);
- samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c));
+ SamplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c));
// Start playback internally for the new samples if the previous ones were playing beforehand.
if (wasPlaying)
- play();
+ Play();
}
#region Re-expose AudioContainer
- public BindableNumber Volume => samplesContainer.Volume;
+ public BindableNumber Volume => SamplesContainer.Volume;
- public BindableNumber Balance => samplesContainer.Balance;
+ public BindableNumber Balance => SamplesContainer.Balance;
- public BindableNumber Frequency => samplesContainer.Frequency;
+ public BindableNumber Frequency => SamplesContainer.Frequency;
- public BindableNumber Tempo => samplesContainer.Tempo;
+ public BindableNumber Tempo => SamplesContainer.Tempo;
public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable)
- => samplesContainer.AddAdjustment(type, adjustBindable);
+ => SamplesContainer.AddAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable)
- => samplesContainer.RemoveAdjustment(type, adjustBindable);
+ => SamplesContainer.RemoveAdjustment(type, adjustBindable);
public void RemoveAllAdjustments(AdjustableProperty type)
- => samplesContainer.RemoveAllAdjustments(type);
+ => SamplesContainer.RemoveAllAdjustments(type);
- public bool IsPlaying => samplesContainer.Any(s => s.Playing);
+ public bool IsPlaying => SamplesContainer.Any(s => s.Playing);
#endregion
}
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs
index 119c48836b..08811b9b8c 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs
@@ -4,15 +4,13 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
+using osu.Game.Skinning;
namespace osu.Game.Storyboards.Drawables
{
- public class DrawableStoryboardSample : Component
+ public class DrawableStoryboardSample : PausableSkinnableSound
{
///
/// The amount of time allowable beyond the start time of the sample, for the sample to start.
@@ -21,38 +19,37 @@ namespace osu.Game.Storyboards.Drawables
private readonly StoryboardSampleInfo sampleInfo;
- protected SampleChannel Channel { get; private set; }
-
public override bool RemoveWhenNotAlive => false;
public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo)
+ : base(sampleInfo)
{
this.sampleInfo = sampleInfo;
LifetimeStart = sampleInfo.StartTime;
}
- [BackgroundDependencyLoader]
- private void load(IBindable beatmap, IBindable> mods)
- {
- Channel = beatmap.Value.Skin.GetSample(sampleInfo);
- if (Channel == null)
- return;
+ [Resolved]
+ private IBindable> mods { get; set; }
- Channel.Volume.Value = sampleInfo.Volume / 100.0;
+ protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ base.SkinChanged(skin, allowFallback);
foreach (var mod in mods.Value.OfType())
- mod.ApplyToSample(Channel);
+ {
+ foreach (var sample in SamplesContainer)
+ mod.ApplyToSample(sample);
+ }
}
protected override void Update()
{
base.Update();
- // TODO: this logic will need to be consolidated with other game samples like hit sounds.
if (Time.Current < sampleInfo.StartTime)
{
// We've rewound before the start time of the sample
- Channel?.Stop();
+ Stop();
// In the case that the user fast-forwards to a point far beyond the start time of the sample,
// we want to be able to fall into the if-conditional below (therefore we must not have a life time end)
@@ -63,8 +60,8 @@ namespace osu.Game.Storyboards.Drawables
{
// We've passed the start time of the sample. We only play the sample if we're within an allowable range
// from the sample's start, to reduce layering if we've been fast-forwarded far into the future
- if (Time.Current - sampleInfo.StartTime < allowable_late_start)
- Channel?.Play();
+ if (!RequestedPlaying && Time.Current - sampleInfo.StartTime < allowable_late_start)
+ Play();
// In the case that the user rewinds to a point far behind the start time of the sample,
// we want to be able to fall into the if-conditional above (therefore we must not have a life time start)
@@ -72,13 +69,5 @@ namespace osu.Game.Storyboards.Drawables
LifetimeEnd = sampleInfo.StartTime;
}
}
-
- protected override void Dispose(bool isDisposing)
- {
- Channel?.Stop();
- Channel = null;
-
- base.Dispose(isDisposing);
- }
}
}
diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs
index 31cced6ce4..9090a12d3f 100644
--- a/osu.Game/Tests/TestScoreInfo.cs
+++ b/osu.Game/Tests/TestScoreInfo.cs
@@ -35,8 +35,16 @@ namespace osu.Game.Tests
Statistics[HitResult.Miss] = 1;
Statistics[HitResult.Meh] = 50;
- Statistics[HitResult.Good] = 100;
+ Statistics[HitResult.Ok] = 100;
+ Statistics[HitResult.Good] = 200;
Statistics[HitResult.Great] = 300;
+ Statistics[HitResult.Perfect] = 320;
+ Statistics[HitResult.SmallTickHit] = 50;
+ Statistics[HitResult.SmallTickMiss] = 25;
+ Statistics[HitResult.LargeTickHit] = 100;
+ Statistics[HitResult.LargeTickMiss] = 50;
+ Statistics[HitResult.SmallBonus] = 10;
+ Statistics[HitResult.SmallBonus] = 50;
Position = 1;
}
diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs
index f0ec638fc9..693c9cb792 100644
--- a/osu.Game/Tests/Visual/EditorClockTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Tests.Visual
protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor();
protected new readonly EditorClock Clock;
+ protected virtual bool ScrollUsingMouseWheel => true;
+
protected EditorClockTestScene()
{
Clock = new EditorClock(new ControlPointInfo(), 5000, BeatDivisor) { IsCoupled = false };
@@ -57,6 +59,9 @@ namespace osu.Game.Tests.Visual
protected override bool OnScroll(ScrollEvent e)
{
+ if (!ScrollUsingMouseWheel)
+ return false;
+
if (e.ScrollDelta.Y > 0)
Clock.SeekBackward(true);
else
diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs
index 9c95eff739..cc4fca9b94 100644
--- a/osu.Game/Users/UserListPanel.cs
+++ b/osu.Game/Users/UserListPanel.cs
@@ -26,6 +26,8 @@ namespace osu.Game.Users
private void load()
{
Background.Width = 0.5f;
+ Background.Origin = Anchor.CentreRight;
+ Background.Anchor = Anchor.CentreRight;
Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.3f));
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 6412f707d0..fa2135580d 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index f1e13169a5..20a51e5feb 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -80,11 +80,11 @@
-
+
-
+