diff --git a/appveyor.yml b/appveyor.yml index 9048428590..9cf68803a2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,17 +9,17 @@ cache: - inspectcode -> appveyor.yml - packages -> **\packages.config install: - - cmd: git submodule update --init --recursive + - cmd: git submodule update --init --recursive --depth=5 - cmd: choco install resharper-clt -y - cmd: choco install nvika -y - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.3/CodeFileSanity.exe before_build: - cmd: CodeFileSanity.exe - - cmd: nuget restore + - cmd: nuget restore -verbosity quiet build: project: osu.sln parallel: true verbosity: minimal after_build: - - cmd: inspectcode /o="inspectcodereport.xml" /caches-home="inspectcode" osu.sln + - cmd: inspectcode --o="inspectcodereport.xml" --projects:osu.Game* --caches-home="inspectcode" osu.sln > NUL - cmd: NVika parsereport "inspectcodereport.xml" --treatwarningsaserrors \ No newline at end of file diff --git a/osu-framework b/osu-framework index d87dab204b..4fc866eee3 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit d87dab204b3df50f62e6070b1970c135ea647d78 +Subproject commit 4fc866eee3803f88b155150e32e021b9c21e647f diff --git a/osu-resources b/osu-resources index 1750ab8f67..4287ee8043 160000 --- a/osu-resources +++ b/osu-resources @@ -1 +1 @@ -Subproject commit 1750ab8f6761ab35592fd46da71fbe0c141bfd93 +Subproject commit 4287ee8043fb1419017359bc3a5db5dc06bc643f diff --git a/osu.Desktop.Deploy/App.config b/osu.Desktop.Deploy/App.config index 2fae7a5e1c..2fbea810f6 100644 --- a/osu.Desktop.Deploy/App.config +++ b/osu.Desktop.Deploy/App.config @@ -13,7 +13,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste - + diff --git a/osu.Desktop.Deploy/Program.cs b/osu.Desktop.Deploy/Program.cs index 54fb50d0f8..e90fb1e567 100644 --- a/osu.Desktop.Deploy/Program.cs +++ b/osu.Desktop.Deploy/Program.cs @@ -145,6 +145,8 @@ namespace osu.Desktop.Deploy /// private static void checkReleaseFiles() { + if (!canGitHub) return; + var releaseLines = getReleaseLines(); //ensure we have all files necessary @@ -157,6 +159,8 @@ namespace osu.Desktop.Deploy private static void pruneReleases() { + if (!canGitHub) return; + write("Pruning RELEASES..."); var releaseLines = getReleaseLines().ToList(); @@ -190,7 +194,7 @@ namespace osu.Desktop.Deploy private static void uploadBuild(string version) { - if (string.IsNullOrEmpty(GitHubAccessToken) || string.IsNullOrEmpty(codeSigningCertPath)) + if (!canGitHub || string.IsNullOrEmpty(CodeSigningCertificate)) return; write("Publishing to GitHub..."); @@ -228,8 +232,12 @@ namespace osu.Desktop.Deploy private static void openGitHubReleasePage() => Process.Start(GitHubReleasePage); + private static bool canGitHub => !string.IsNullOrEmpty(GitHubAccessToken); + private static void checkGitHubReleases() { + if (!canGitHub) return; + write("Checking GitHub releases..."); var req = new JsonWebRequest>($"{GitHubApiEndpoint}"); req.AuthenticatedBlockingPerform(); diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 0e4935aa7a..6b9ec8b9a4 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -11,14 +11,14 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Catch.Beatmaps { - internal class CatchBeatmapConverter : BeatmapConverter + internal class CatchBeatmapConverter : BeatmapConverter { protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; - protected override IEnumerable ConvertHitObject(HitObject obj, Beatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject obj, Beatmap beatmap) { var curveData = obj as IHasCurve; - var positionData = obj as IHasPosition; + var positionData = obj as IHasXPosition; var comboData = obj as IHasCombo; if (positionData == null) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 7fac19d135..b2f7fdabfc 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -6,9 +6,9 @@ using osu.Game.Rulesets.Catch.Objects; namespace osu.Game.Rulesets.Catch.Beatmaps { - internal class CatchBeatmapProcessor : BeatmapProcessor + internal class CatchBeatmapProcessor : BeatmapProcessor { - public override void PostProcess(Beatmap beatmap) + public override void PostProcess(Beatmap beatmap) { if (beatmap.ComboColors.Count == 0) return; @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int comboIndex = 0; int colourIndex = 0; - CatchBaseHit lastObj = null; + CatchHitObject lastObj = null; foreach (var obj in beatmap.HitObjects) { diff --git a/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs index b77be9d1f0..e9524a867d 100644 --- a/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs @@ -8,14 +8,14 @@ using System.Collections.Generic; namespace osu.Game.Rulesets.Catch { - public class CatchDifficultyCalculator : DifficultyCalculator + public class CatchDifficultyCalculator : DifficultyCalculator { public CatchDifficultyCalculator(Beatmap beatmap) : base(beatmap) { } - public override double Calculate(Dictionary categoryDifficulty = null) => 0; + public override double Calculate(Dictionary categoryDifficulty = null) => 0; - protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new CatchBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new CatchBeatmapConverter(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs similarity index 55% rename from osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs rename to osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 2f33cf1093..cb4e6453ce 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -1,14 +1,18 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects { - public abstract class CatchBaseHit : HitObject, IHasXPosition, IHasCombo + public abstract class CatchHitObject : HitObject, IHasXPosition, IHasCombo { + public const double OBJECT_RADIUS = 44; + public float X { get; set; } public Color4 ComboColour { get; set; } = Color4.Gray; @@ -20,5 +24,14 @@ namespace osu.Game.Rulesets.Catch.Objects /// The next fruit starts a new combo. Used for explodey. /// public virtual bool LastInCombo { get; set; } + + public float Scale { get; set; } = 1; + + public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaults(controlPointInfo, difficulty); + + Scale = 1.0f - 0.7f * (difficulty.CircleSize - 5) / 5; + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index e057bf3d8e..b90a06b94e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -5,11 +5,12 @@ using System; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; namespace osu.Game.Rulesets.Catch.Objects.Drawable { public abstract class DrawableCatchHitObject : DrawableCatchHitObject - where TObject : CatchBaseHit + where TObject : CatchHitObject { public new TObject HitObject; @@ -17,12 +18,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable : base(hitObject) { HitObject = hitObject; + + Scale = new Vector2(HitObject.Scale); } } - public abstract class DrawableCatchHitObject : DrawableScrollingHitObject + public abstract class DrawableCatchHitObject : DrawableScrollingHitObject { - protected DrawableCatchHitObject(CatchBaseHit hitObject) + protected DrawableCatchHitObject(CatchHitObject hitObject) : base(hitObject) { RelativePositionAxes = Axes.Both; @@ -30,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable Y = (float)HitObject.StartTime; } - public Func CheckPosition; + public Func CheckPosition; protected override void CheckForJudgements(bool userTriggered, double timeOffset) { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs index afda91d0b4..bfb674d1b4 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable RelativeChildSize = new Vector2(1, (float)HitObject.Duration) }; - foreach (CatchBaseHit tick in s.Ticks) + foreach (CatchHitObject tick in s.Ticks) { TinyDroplet tiny = tick as TinyDroplet; if (tiny != null) @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable } } - protected override void AddNested(DrawableHitObject h) + protected override void AddNested(DrawableHitObject h) { ((DrawableCatchHitObject)h).CheckPosition = o => CheckPosition?.Invoke(o) ?? false; dropletContainer.Add(h); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs index 00ddd365e3..2de266b3f0 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces { public class Pulp : Circle, IHasAccentColour { - public const float PULP_SIZE = 20; + public const float PULP_SIZE = (float)CatchHitObject.OBJECT_RADIUS / 2.2f; public Pulp() { diff --git a/osu.Game.Rulesets.Catch/Objects/Droplet.cs b/osu.Game.Rulesets.Catch/Objects/Droplet.cs index b1206e0d75..a2bdf830e5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Droplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Droplet.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Catch.Objects { - public class Droplet : CatchBaseHit + public class Droplet : CatchHitObject { } } diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs index fc55f83969..5f1060fb51 100644 --- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Catch.Objects { - public class Fruit : CatchBaseHit + public class Fruit : CatchHitObject { } } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 6462f6f6a8..bf9f0bd44b 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -15,7 +15,7 @@ using osu.Framework.Lists; namespace osu.Game.Rulesets.Catch.Objects { - public class JuiceStream : CatchBaseHit, IHasCurve + public class JuiceStream : CatchHitObject, IHasCurve { /// /// Positional distance that results in a duration of one second, before any speed adjustments. @@ -42,11 +42,11 @@ namespace osu.Game.Rulesets.Catch.Objects TickDistance = scoringDistance / difficulty.SliderTickRate; } - public IEnumerable Ticks + public IEnumerable Ticks { get { - SortedList ticks = new SortedList((a, b) => a.StartTime.CompareTo(b.StartTime)); + SortedList ticks = new SortedList((a, b) => a.StartTime.CompareTo(b.StartTime)); if (TickDistance == 0) return ticks; diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 66a5636b74..0806c4b29d 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -10,14 +10,14 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch.Scoring { - internal class CatchScoreProcessor : ScoreProcessor + internal class CatchScoreProcessor : ScoreProcessor { - public CatchScoreProcessor(RulesetContainer rulesetContainer) + public CatchScoreProcessor(RulesetContainer rulesetContainer) : base(rulesetContainer) { } - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void SimulateAutoplay(Beatmap beatmap) { foreach (var obj in beatmap.HitObjects) { diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs index a890a8a386..586de17f15 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs @@ -11,16 +11,26 @@ namespace osu.Game.Rulesets.Catch.Tests [Ignore("getting CI working")] public class TestCaseCatchStacker : Game.Tests.Visual.TestCasePlayer { - public TestCaseCatchStacker() : base(typeof(CatchRuleset)) + public TestCaseCatchStacker() + : base(typeof(CatchRuleset)) { } protected override Beatmap CreateBeatmap() { - var beatmap = new Beatmap(); + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 6, + } + } + }; - for (int i = 0; i < 256; i++) - beatmap.HitObjects.Add(new Fruit { X = 0.5f, StartTime = i * 100, NewCombo = i % 8 == 0 }); + for (int i = 0; i < 512; i++) + beatmap.HitObjects.Add(new Fruit { X = 0.5f + i / 2048f * (i % 10 - 5), StartTime = i * 100, NewCombo = i % 8 == 0 }); return beatmap; } diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatcher.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcher.cs deleted file mode 100644 index 341612b760..0000000000 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseCatcher.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.UI; -using osu.Game.Tests.Visual; -using OpenTK; - -namespace osu.Game.Rulesets.Catch.Tests -{ - [TestFixture] - [Ignore("getting CI working")] - internal class TestCaseCatcher : OsuTestCase - { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Catcher), - }; - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - Children = new Drawable[] - { - new CatchInputManager(rulesets.GetRuleset(2)) - { - RelativeSizeAxes = Axes.Both, - Child = new Catcher - { - RelativePositionAxes = Axes.Both, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Size = new Vector2(1, 0.2f), - } - }, - }; - } - } -} diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs new file mode 100644 index 0000000000..538f6930ed --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + [Ignore("getting CI working")] + internal class TestCaseCatcherArea : OsuTestCase + { + private RulesetInfo catchRuleset; + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(CatcherArea), + }; + + public TestCaseCatcherArea() + { + AddSliderStep("CircleSize", 0, 8, 5, createCatcher); + } + + private void createCatcher(float size) + { + Child = new CatchInputManager(catchRuleset) + { + RelativeSizeAxes = Axes.Both, + Child = new CatcherArea(new BeatmapDifficulty { CircleSize = size }) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.BottomLeft + }, + }; + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + catchRuleset = rulesets.GetRuleset(2); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..0d2dc14160 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [Ignore("getting CI working")] + public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints + { + public TestCasePerformancePoints() + : base(new CatchRuleset(new RulesetInfo())) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 987eef5e45..6fd0793500 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -1,11 +1,11 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using osu.Framework.Graphics; using osu.Game.Rulesets.UI; using OpenTK; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawable; using osu.Game.Rulesets.Judgements; @@ -20,10 +20,9 @@ namespace osu.Game.Rulesets.Catch.UI protected override Container Content => content; private readonly Container content; - private readonly Container catcherContainer; - private readonly Catcher catcher; + private readonly CatcherArea catcherArea; - public CatchPlayfield() + public CatchPlayfield(BeatmapDifficulty difficulty) : base(Axes.Y) { Container explodingFruitContainer; @@ -43,30 +42,16 @@ namespace osu.Game.Rulesets.Catch.UI { RelativeSizeAxes = Axes.Both, }, - catcherContainer = new Container + catcherArea = new CatcherArea(difficulty) { - RelativeSizeAxes = Axes.X, + ExplodingFruitTarget = explodingFruitContainer, Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, - Height = 180, - Child = catcher = new Catcher - { - ExplodingFruitTarget = explodingFruitContainer, - RelativePositionAxes = Axes.Both, - Origin = Anchor.TopCentre, - X = 0.5f, - } } }; } - protected override void Update() - { - base.Update(); - catcher.Size = new Vector2(catcherContainer.DrawSize.Y); - } - - public bool CheckIfWeCanCatch(CatchBaseHit obj) => Math.Abs(catcher.Position.X - obj.X) < catcher.DrawSize.X / DrawSize.X / 2; + public bool CheckIfWeCanCatch(CatchHitObject obj) => catcherArea.CanCatch(obj); public override void Add(DrawableHitObject h) { @@ -88,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.UI (judgedObject.Parent as Container)?.Remove(judgedObject); (judgedObject.Parent as Container)?.Remove(judgedObject); - catcher.Add(judgedObject, screenPosition); + catcherArea.Add(judgedObject, screenPosition); } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 92912eb177..3ed9090098 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch.UI { - public class CatchRulesetContainer : ScrollingRulesetContainer + public class CatchRulesetContainer : ScrollingRulesetContainer { public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) : base(ruleset, beatmap, isForCurrentRuleset) @@ -22,15 +22,15 @@ namespace osu.Game.Rulesets.Catch.UI public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this); - protected override BeatmapProcessor CreateBeatmapProcessor() => new CatchBeatmapProcessor(); + protected override BeatmapProcessor CreateBeatmapProcessor() => new CatchBeatmapProcessor(); - protected override BeatmapConverter CreateBeatmapConverter() => new CatchBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new CatchBeatmapConverter(); - protected override Playfield CreatePlayfield() => new CatchPlayfield(); + protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo); - protected override DrawableHitObject GetVisualRepresentation(CatchBaseHit h) + protected override DrawableHitObject GetVisualRepresentation(CatchHitObject h) { var fruit = h as Fruit; if (fruit != null) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs deleted file mode 100644 index 87fe95ed2f..0000000000 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input.Bindings; -using osu.Framework.MathUtils; -using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Objects.Drawables; -using OpenTK; - -namespace osu.Game.Rulesets.Catch.UI -{ - public class Catcher : Container, IKeyBindingHandler - { - private Texture texture; - - private Container caughtFruit; - - public Container ExplodingFruitTarget; - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - texture = textures.Get(@"Play/Catch/fruit-catcher-idle"); - - Children = new Drawable[] - { - createCatcherSprite(), - caughtFruit = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - } - }; - } - - private int currentDirection; - - private bool dashing; - - protected bool Dashing - { - get { return dashing; } - set - { - if (value == dashing) return; - - dashing = value; - - if (dashing) - Schedule(addAdditiveSprite); - } - } - - private void addAdditiveSprite() - { - if (!dashing) return; - - var additive = createCatcherSprite(); - - additive.RelativePositionAxes = Axes.Both; - additive.Blending = BlendingMode.Additive; - additive.Position = Position; - additive.Scale = Scale; - - ((Container)Parent).Add(additive); - - additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint).Expire(); - - Scheduler.AddDelayed(addAdditiveSprite, 50); - } - - private Sprite createCatcherSprite() => new Sprite - { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, - Texture = texture, - OriginPosition = new Vector2(DrawWidth / 2, 10) //temporary until the sprite is aligned correctly. - }; - - public bool OnPressed(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection--; - return true; - case CatchAction.MoveRight: - currentDirection++; - return true; - case CatchAction.Dash: - Dashing = true; - return true; - } - - return false; - } - - public bool OnReleased(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection++; - return true; - case CatchAction.MoveRight: - currentDirection--; - return true; - case CatchAction.Dash: - Dashing = false; - return true; - } - - return false; - } - - /// - /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. - /// - private const double base_speed = 1.0 / 512; - - protected override void Update() - { - base.Update(); - - if (currentDirection == 0) return; - - double dashModifier = Dashing ? 1 : 0.5; - - Scale = new Vector2(Math.Sign(currentDirection), 1); - X = (float)MathHelper.Clamp(X + Math.Sign(currentDirection) * Clock.ElapsedFrameTime * base_speed * dashModifier, 0, 1); - } - - public void Add(DrawableHitObject fruit, Vector2 absolutePosition) - { - fruit.RelativePositionAxes = Axes.None; - fruit.Position = new Vector2(ToLocalSpace(absolutePosition).X - DrawSize.X / 2, 0); - - fruit.Anchor = Anchor.TopCentre; - fruit.Origin = Anchor.BottomCentre; - fruit.Scale *= 0.7f; - fruit.LifetimeEnd = double.MaxValue; - - float distance = fruit.DrawSize.X / 2 * fruit.Scale.X; - - while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.DistanceSquared(f.Position, fruit.Position) < distance * distance)) - { - fruit.X += RNG.Next(-5, 5); - fruit.Y -= RNG.Next(0, 5); - } - - caughtFruit.Add(fruit); - - if (((CatchBaseHit)fruit.HitObject).LastInCombo) - explode(); - } - - private void explode() - { - var fruit = caughtFruit.ToArray(); - - foreach (var f in fruit) - { - var originalX = f.X * Scale.X; - - if (ExplodingFruitTarget != null) - { - f.Anchor = Anchor.TopLeft; - f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget); - - caughtFruit.Remove(f); - - ExplodingFruitTarget.Add(f); - } - - f.MoveToY(f.Y - 50, 250, Easing.OutSine) - .Then() - .MoveToY(f.Y + 50, 500, Easing.InSine); - - f.MoveToX(f.X + originalX * 6, 1000); - f.FadeOut(750); - - f.Expire(); - } - } - } -} diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs new file mode 100644 index 0000000000..203db1bb8c --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -0,0 +1,240 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Bindings; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class CatcherArea : Container + { + public const float CATCHER_SIZE = 172; + + private readonly Catcher catcher; + + public Container ExplodingFruitTarget + { + set { catcher.ExplodingFruitTarget = value; } + } + + public CatcherArea(BeatmapDifficulty difficulty = null) + { + RelativeSizeAxes = Axes.X; + Height = CATCHER_SIZE; + Child = catcher = new Catcher(difficulty) + { + AdditiveTarget = this, + }; + } + + public void Add(DrawableHitObject fruit, Vector2 absolutePosition) + { + fruit.RelativePositionAxes = Axes.None; + fruit.Position = new Vector2(catcher.ToLocalSpace(absolutePosition).X - catcher.DrawSize.X / 2, 0); + + fruit.Anchor = Anchor.TopCentre; + fruit.Origin = Anchor.BottomCentre; + fruit.Scale *= 0.7f; + fruit.LifetimeEnd = double.MaxValue; + + catcher.Add(fruit); + } + + public bool CanCatch(CatchHitObject obj) => Math.Abs(catcher.Position.X - obj.X) < catcher.DrawSize.X * Math.Abs(catcher.Scale.X) / DrawSize.X / 2; + + public class Catcher : Container, IKeyBindingHandler + { + private Texture texture; + + private Container caughtFruit; + + public Container ExplodingFruitTarget; + + public Container AdditiveTarget; + + public Catcher(BeatmapDifficulty difficulty = null) + { + RelativePositionAxes = Axes.X; + X = 0.5f; + + Origin = Anchor.TopCentre; + Anchor = Anchor.TopLeft; + + Size = new Vector2(CATCHER_SIZE); + if (difficulty != null) + Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + texture = textures.Get(@"Play/Catch/fruit-catcher-idle"); + + Children = new Drawable[] + { + createCatcherSprite(), + caughtFruit = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + } + }; + } + + private int currentDirection; + + private bool dashing; + + protected bool Dashing + { + get { return dashing; } + set + { + if (value == dashing) return; + + dashing = value; + + if (dashing) + Schedule(addAdditiveSprite); + } + } + + private void addAdditiveSprite() + { + if (!dashing || AdditiveTarget == null) return; + + var additive = createCatcherSprite(); + + additive.Anchor = Anchor; + additive.OriginPosition = additive.OriginPosition + new Vector2(DrawWidth / 2, 0); // also temporary to align sprite correctly. + additive.Position = Position; + additive.Scale = Scale; + additive.RelativePositionAxes = RelativePositionAxes; + additive.Blending = BlendingMode.Additive; + + AdditiveTarget.Add(additive); + + additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint).Expire(); + + Scheduler.AddDelayed(addAdditiveSprite, 50); + } + + private Sprite createCatcherSprite() => new Sprite + { + Size = new Vector2(CATCHER_SIZE), + FillMode = FillMode.Fill, + Texture = texture, + OriginPosition = new Vector2(-3, 10) // temporary until the sprite is aligned correctly. + }; + + public void Add(DrawableHitObject fruit) + { + float distance = fruit.DrawSize.X / 2 * fruit.Scale.X; + + while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.DistanceSquared(f.Position, fruit.Position) < distance * distance)) + { + fruit.X += RNG.Next(-5, 5); + fruit.Y -= RNG.Next(0, 5); + } + + caughtFruit.Add(fruit); + + if (((CatchHitObject)fruit.HitObject).LastInCombo) + explode(); + } + + public bool OnPressed(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection--; + return true; + case CatchAction.MoveRight: + currentDirection++; + return true; + case CatchAction.Dash: + Dashing = true; + return true; + } + + return false; + } + + public bool OnReleased(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection++; + return true; + case CatchAction.MoveRight: + currentDirection--; + return true; + case CatchAction.Dash: + Dashing = false; + return true; + } + + return false; + } + + /// + /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. + /// + public const double BASE_SPEED = 1.0 / 512; + + protected override void Update() + { + base.Update(); + + if (currentDirection == 0) return; + + double dashModifier = Dashing ? 1 : 0.5; + + Scale = new Vector2(Math.Abs(Scale.X) * Math.Sign(currentDirection), Scale.Y); + X = (float)MathHelper.Clamp(X + Math.Sign(currentDirection) * Clock.ElapsedFrameTime * BASE_SPEED * dashModifier, 0, 1); + } + + private void explode() + { + var fruit = caughtFruit.ToArray(); + + foreach (var f in fruit) + { + var originalX = f.X * Scale.X; + + if (ExplodingFruitTarget != null) + { + f.Anchor = Anchor.TopLeft; + f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget); + + caughtFruit.Remove(f); + + ExplodingFruitTarget.Add(f); + } + + f.MoveToY(f.Y - 50, 250, Easing.OutSine) + .Then() + .MoveToY(f.Y + 50, 500, Easing.InSine); + + f.MoveToX(f.X + originalX * 6, 1000); + f.FadeOut(750); + + f.Expire(); + } + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj index a666984b95..b7916f674e 100644 --- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj +++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj @@ -57,16 +57,17 @@ - + - + + - + diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index f6d30ad3fa..d5a799b4ed 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -138,8 +138,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Pattern newPattern = conversion.Generate(); lastPattern = newPattern; - var stairPatternGenerator = (HitObjectPatternGenerator)conversion; - lastStair = stairPatternGenerator.StairType; + var stairPatternGenerator = conversion as HitObjectPatternGenerator; + lastStair = stairPatternGenerator?.StairType ?? lastStair; return newPattern.HitObjects; } diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index 67bc347535..e0763284a6 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania { } - public override double Calculate(Dictionary categoryDifficulty = null) => 0; + public override double Calculate(Dictionary categoryDifficulty = null) => 0; protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new ManiaBeatmapConverter(true, (int)Math.Max(1, Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize))); } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs index 164309c227..dfc9993bde 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs @@ -176,22 +176,10 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModAutoplay : ModAutoplay { - private int availableColumns; - - public override void ApplyToRulesetContainer(RulesetContainer rulesetContainer) - { - // Todo: This shouldn't be done, we should be getting a ManiaBeatmap which should store AvailableColumns - // But this is dependent on a _lot_ of refactoring - var maniaRulesetContainer = (ManiaRulesetContainer)rulesetContainer; - availableColumns = maniaRulesetContainer.AvailableColumns; - - base.ApplyToRulesetContainer(rulesetContainer); - } - protected override Score CreateReplayScore(Beatmap beatmap) => new Score { User = new User { Username = "osu!topus!" }, - Replay = new ManiaAutoGenerator(beatmap, availableColumns).Generate(), + Replay = new ManiaAutoGenerator(beatmap).Generate(), }; } } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 64982532a7..153fee3ab6 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -1,7 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; +using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; @@ -13,15 +13,11 @@ namespace osu.Game.Rulesets.Mania.Replays { internal class ManiaAutoGenerator : AutoGenerator { - private const double release_delay = 20; + public const double RELEASE_DELAY = 20; - private readonly int availableColumns; - - public ManiaAutoGenerator(Beatmap beatmap, int availableColumns) + public ManiaAutoGenerator(Beatmap beatmap) : base(beatmap) { - this.availableColumns = availableColumns; - Replay = new Replay { User = new User { Username = @"Autoplay" } }; } @@ -30,104 +26,52 @@ namespace osu.Game.Rulesets.Mania.Replays public override Replay Generate() { // Todo: Realistically this shouldn't be needed, but the first frame is skipped with the way replays are currently handled - Replay.Frames.Add(new ReplayFrame(-100000, null, null, ReplayButtonState.None)); + Replay.Frames.Add(new ManiaReplayFrame(-100000, 0)); - double[] holdEndTimes = new double[availableColumns]; - for (int i = 0; i < availableColumns; i++) - holdEndTimes[i] = double.NegativeInfinity; + var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); - // Notes are handled row-by-row - foreach (var objGroup in Beatmap.HitObjects.GroupBy(h => h.StartTime)) + int activeColumns = 0; + foreach (var group in pointGroups) { - double groupTime = objGroup.Key; - - int activeColumns = 0; - - // Get the previously held-down active columns - for (int i = 0; i < availableColumns; i++) + foreach (var point in group) { - if (holdEndTimes[i] > groupTime) - activeColumns |= 1 << i; + if (point is HitPoint) + activeColumns |= 1 << point.Column; + if (point is ReleasePoint) + activeColumns ^= 1 << point.Column; } - // Add on the group columns, keeping track of the held notes for the next rows - foreach (var obj in objGroup) - { - var holdNote = obj as HoldNote; - if (holdNote != null) - holdEndTimes[obj.Column] = Math.Max(holdEndTimes[obj.Column], holdNote.EndTime); - - activeColumns |= 1 << obj.Column; - } - - Replay.Frames.Add(new ReplayFrame(groupTime, activeColumns, null, ReplayButtonState.None)); - - // Add the release frames. We can't do this with the loop above because we need activeColumns to be fully populated - foreach (var obj in objGroup.GroupBy(h => (h as IHasEndTime)?.EndTime ?? h.StartTime + release_delay).OrderBy(h => h.Key)) - { - var groupEndTime = obj.Key; - - int activeColumnsAtEnd = 0; - for (int i = 0; i < availableColumns; i++) - { - if (holdEndTimes[i] > groupEndTime) - activeColumnsAtEnd |= 1 << i; - } - - Replay.Frames.Add(new ReplayFrame(groupEndTime, activeColumnsAtEnd, 0, ReplayButtonState.None)); - } + Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, activeColumns)); } - Replay.Frames = Replay.Frames - // Pick the maximum activeColumns for all frames at the same time - .GroupBy(f => f.Time) - .Select(g => new ReplayFrame(g.First().Time, maxMouseX(g), 0, ReplayButtonState.None)) - // The addition of release frames above maybe result in unordered frames, but we need them ordered - .OrderBy(f => f.Time) - .ToList(); - return Replay; } - /// - /// Finds the maximum by count of bits from a grouping of s. - /// - /// The grouping to search. - /// The maximum by count of bits. - private float maxMouseX(IGrouping group) + private IEnumerable generateActionPoints() { - int currentCount = -1; - int currentMax = 0; - - foreach (var val in group) + foreach (var obj in Beatmap.HitObjects) { - int newCount = countBits((int)(val.MouseX ?? 0)); - if (newCount > currentCount) - { - currentCount = newCount; - currentMax = (int)(val.MouseX ?? 0); - } + yield return new HitPoint { Time = obj.StartTime, Column = obj.Column }; + yield return new ReleasePoint { Time = ((obj as IHasEndTime)?.EndTime ?? obj.StartTime) + RELEASE_DELAY, Column = obj.Column }; } - - return currentMax; } - /// - /// Counts the number of bits set in a value. - /// - /// The value to count. - /// The number of set bits. - private int countBits(int value) + private interface IActionPoint { - int count = 0; - while (value > 0) - { - if ((value & 1) > 0) - count++; - value >>= 1; - } + double Time { get; set; } + int Column { get; set; } + } - return count; + private struct HitPoint : IActionPoint + { + public double Time { get; set; } + public int Column { get; set; } + } + + private struct ReleasePoint : IActionPoint + { + public double Time { get; set; } + public int Column { get; set; } } } } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs index e352997f2c..12534d6eb4 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs @@ -2,29 +2,37 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.Linq; using osu.Framework.Input; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Mania.Replays { internal class ManiaFramedReplayInputHandler : FramedReplayInputHandler { - public ManiaFramedReplayInputHandler(Replay replay) + private readonly ManiaRulesetContainer container; + + public ManiaFramedReplayInputHandler(Replay replay, ManiaRulesetContainer container) : base(replay) { + this.container = container; } + private ManiaPlayfield playfield; public override List GetPendingStates() { var actions = new List(); - int activeColumns = (int)(CurrentFrame.MouseX ?? 0); + if (playfield == null) + playfield = (ManiaPlayfield)container.Playfield; + int activeColumns = (int)(CurrentFrame.MouseX ?? 0); int counter = 0; while (activeColumns > 0) { if ((activeColumns & 1) > 0) - actions.Add(ManiaAction.Key1 + counter); + actions.Add(playfield.Columns.ElementAt(counter).Action); counter++; activeColumns >>= 1; } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs new file mode 100644 index 0000000000..d1bc7da911 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.Mania.Replays +{ + public class ManiaReplayFrame : ReplayFrame + { + public override bool IsImportant => MouseX > 0; + + public ManiaReplayFrame(double time, int activeColumns) + : base(time, activeColumns, null, ReplayButtonState.None) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs b/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs new file mode 100644 index 0000000000..805553eafc --- /dev/null +++ b/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs @@ -0,0 +1,173 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [Ignore("getting CI working")] + public class TestCaseAutoGeneration : OsuTestCase + { + [Test] + public void TestSingleNote() + { + // | | + // | - | + // | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new Note { StartTime = 1000 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); + Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); + Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 0 has not been pressed"); + Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 0 has not been released"); + } + + [Test] + public void TestSingleHoldNote() + { + // | | + // | * | + // | * | + // | * | + // | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); + Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 0 has not been pressed"); + Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 0 has not been released"); + } + + [Test] + public void TestSingleNoteChord() + { + // | | | + // | - | - | + // | | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new Note { StartTime = 1000 }); + beatmap.HitObjects.Add(new Note { StartTime = 1000, Column = 1 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); + Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); + Assert.AreEqual(3, generated.Frames[1].MouseX, "Keys 1 and 2 have not been pressed"); + Assert.AreEqual(0, generated.Frames[2].MouseX, "Keys 1 and 2 have not been released"); + } + + [Test] + public void TestHoldNoteChord() + { + // | | | + // | * | * | + // | * | * | + // | * | * | + // | | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000, Column = 1 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); + Assert.AreEqual(3, generated.Frames[1].MouseX, "Keys 1 and 2 have not been pressed"); + Assert.AreEqual(0, generated.Frames[2].MouseX, "Keys 1 and 2 have not been released"); + } + + [Test] + public void TestSingleNoteStair() + { + // | | | + // | | - | + // | - | | + // | | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new Note { StartTime = 1000 }); + beatmap.HitObjects.Add(new Note { StartTime = 2000, Column = 1 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); + Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect first note release time"); + Assert.AreEqual(2000, generated.Frames[3].Time, "Incorrect second note hit time"); + Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time"); + Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed"); + Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 1 has not been released"); + Assert.AreEqual(2, generated.Frames[3].MouseX, "Key 2 has not been pressed"); + Assert.AreEqual(0, generated.Frames[4].MouseX, "Key 2 has not been released"); + } + + [Test] + public void TestHoldNoteStair() + { + // | | | + // | | * | + // | * | * | + // | * | * | + // | * | | + // | | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); + beatmap.HitObjects.Add(new HoldNote { StartTime = 2000, Duration = 2000, Column = 1 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect first note release time"); + Assert.AreEqual(2000, generated.Frames[2].Time, "Incorrect second note hit time"); + Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time"); + Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed"); + Assert.AreEqual(3, generated.Frames[2].MouseX, "Keys 1 and 2 have not been pressed"); + Assert.AreEqual(2, generated.Frames[3].MouseX, "Key 1 has not been released"); + Assert.AreEqual(0, generated.Frames[4].MouseX, "Key 2 has not been released"); + } + + [Test] + public void TestHoldNoteWithReleasePress() + { + // | | | + // | * | - | + // | * | | + // | * | | + // | | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 - ManiaAutoGenerator.RELEASE_DELAY }); + beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 4, "Replay must have 4 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); + Assert.AreEqual(3000, generated.Frames[2].Time, "Incorrect second note press time + first note release time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect second note release time"); + Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed"); + Assert.AreEqual(2, generated.Frames[2].MouseX, "Key 1 has not been released or key 2 has not been pressed"); + Assert.AreEqual(0, generated.Frames[3].MouseX, "Keys 1 and 2 have not been released"); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..8aa8c6b799 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [Ignore("getting CI working")] + public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints + { + public TestCasePerformancePoints() + : base(new ManiaRuleset(new RulesetInfo())) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 08acd46c57..cbbcb84b31 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -124,6 +124,6 @@ namespace osu.Game.Rulesets.Mania.UI protected override SpeedAdjustmentContainer CreateSpeedAdjustmentContainer(MultiplierControlPoint controlPoint) => new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Basic); - protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); + protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay, this); } } diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 6f45a64d92..19832d733e 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -72,6 +72,7 @@ + @@ -80,8 +81,10 @@ + + diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 112fcb1a30..39ec753fe1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -45,6 +45,18 @@ namespace osu.Game.Rulesets.Osu.Objects set { Curve.Distance = value; } } + /// + /// The position of the cursor at the point of completion of this if it was hit + /// with as few movements as possible. This is set and used by difficulty calculation. + /// + internal Vector2? LazyEndPosition; + + /// + /// The distance travelled by the cursor upon completion of this if it was hit + /// with as few movements as possible. This is set and used by difficulty calculation. + /// + internal float LazyTravelDistance; + public List RepeatSamples { get; set; } = new List(); public int RepeatCount { get; set; } = 1; diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs index 537874f643..3d185ab694 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs @@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty (h as Slider)?.Curve?.Calculate(); } - public override double Calculate(Dictionary categoryDifficulty = null) + public override double Calculate(Dictionary categoryDifficulty = null) { - OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Beatmap.HitObjects); + OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Beatmap.HitObjects, TimeRate); Skill[] skills = { new Aim(), @@ -67,8 +67,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty if (categoryDifficulty != null) { - categoryDifficulty.Add("Aim", aimRating.ToString("0.00")); - categoryDifficulty.Add("Speed", speedRating.ToString("0.00")); + categoryDifficulty.Add("Aim", aimRating); + categoryDifficulty.Add("Speed", speedRating); } return starRating; diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs index c6ecc3a506..f8e9423e29 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs @@ -20,12 +20,12 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing /// Creates an enumerator, which preprocesses a list of s recieved as input, wrapping them as /// which contains extra data required for difficulty calculation. /// - public OsuDifficultyBeatmap(List objects) + public OsuDifficultyBeatmap(List objects, double timeRate) { // Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases. // This should probably happen before the objects reach the difficulty calculator. objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime)); - difficultyObjects = createDifficultyObjectEnumerator(objects); + difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate); } /// @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - private IEnumerator createDifficultyObjectEnumerator(List objects) + private IEnumerator createDifficultyObjectEnumerator(List objects, double timeRate) { // We will process OsuHitObjects in groups of three to form a triangle, so we can calculate an angle for each object. OsuHitObject[] triangle = new OsuHitObject[3]; @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing triangle[1] = triangle[0]; triangle[0] = objects[i]; - yield return new OsuDifficultyHitObject(triangle); + yield return new OsuDifficultyHitObject(triangle, timeRate); } } } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs index bdeb62df3e..972677a6f1 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Linq; +using OpenTK; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing @@ -33,13 +35,17 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing private const int normalized_radius = 52; + private readonly double timeRate; + private readonly OsuHitObject[] t; /// /// Initializes the object calculating extra data required for difficulty calculation. /// - public OsuDifficultyHitObject(OsuHitObject[] triangle) + public OsuDifficultyHitObject(OsuHitObject[] triangle, double timeRate) { + this.timeRate = timeRate; + t = triangle; BaseObject = t[0]; setDistances(); @@ -57,14 +63,53 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing scalingFactor *= 1 + smallCircleBonus; } - Distance = (t[0].StackedPosition - t[1].StackedPosition).Length * scalingFactor; + Vector2 lastCursorPosition = t[1].StackedPosition; + float lastTravelDistance = 0; + + var lastSlider = t[1] as Slider; + if (lastSlider != null) + { + computeSliderCursorPosition(lastSlider); + lastCursorPosition = lastSlider.LazyEndPosition ?? lastCursorPosition; + lastTravelDistance = lastSlider.LazyTravelDistance; + } + + Distance = (lastTravelDistance + (BaseObject.StackedPosition - lastCursorPosition).Length) * scalingFactor; } private void setTimingValues() { // Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure. - DeltaTime = Math.Max(40, t[0].StartTime - t[1].StartTime); + DeltaTime = Math.Max(40, (t[0].StartTime - t[1].StartTime) / timeRate); TimeUntilHit = 450; // BaseObject.PreEmpt; } + + private void computeSliderCursorPosition(Slider slider) + { + if (slider.LazyEndPosition != null) + return; + slider.LazyEndPosition = slider.StackedPosition; + + float approxFollowCircleRadius = (float)(slider.Radius * 3); + var computeVertex = new Action(t => + { + var diff = slider.PositionAt(t) - slider.LazyEndPosition.Value; + float dist = diff.Length; + + if (dist > approxFollowCircleRadius) + { + // The cursor would be outside the follow circle, we need to move it + diff.Normalize(); // Obtain direction of diff + dist -= approxFollowCircleRadius; + slider.LazyEndPosition += diff * dist; + slider.LazyTravelDistance += dist; + } + }); + + var scoringTimes = slider.Ticks.Select(t => t.StartTime).Concat(slider.RepeatPoints.Select(r => r.StartTime)).OrderBy(t => t); + foreach (var time in scoringTimes) + computeVertex(time); + computeVertex(slider.EndTime); + } } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index c87328d87c..fdf2a458b7 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -14,6 +14,8 @@ using System.Linq; using osu.Framework.Graphics; using osu.Game.Overlays.Settings; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Osu.Scoring; namespace osu.Game.Rulesets.Osu { @@ -114,6 +116,8 @@ namespace osu.Game.Rulesets.Osu public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods); + public override PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score); + public override string Description => "osu!"; public override SettingsSubsection CreateSettings() => new OsuSettings(); diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs new file mode 100644 index 0000000000..cd6b6c5e27 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs @@ -0,0 +1,199 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public class OsuPerformanceCalculator : PerformanceCalculator + { + private readonly int countHitCircles; + private readonly int beatmapMaxCombo; + + private Mod[] mods; + private double realApproachRate; + private double accuracy; + private int scoreMaxCombo; + private int count300; + private int count100; + private int count50; + private int countMiss; + + public OsuPerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score) + : base(ruleset, beatmap, score) + { + countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle); + + beatmapMaxCombo = Beatmap.HitObjects.Count; + beatmapMaxCombo += Beatmap.HitObjects.OfType().Sum(s => s.RepeatCount + s.Ticks.Count()); + } + + public override double Calculate(Dictionary categoryRatings = null) + { + mods = Score.Mods; + accuracy = Score.Accuracy; + scoreMaxCombo = Score.MaxCombo; + count300 = Convert.ToInt32(Score.Statistics["300"]); + count100 = Convert.ToInt32(Score.Statistics["100"]); + count50 = Convert.ToInt32(Score.Statistics["50"]); + countMiss = Convert.ToInt32(Score.Statistics["x"]); + + // Don't count scores made with supposedly unranked mods + if (mods.Any(m => !m.Ranked)) + return 0; + + // Todo: In the future we should apply changes to PreEmpt/AR at an OsuHitObject/BaseDifficulty level, but this is done + // locally for now as doing so would modify animations and other things unexpectedly + // DO NOT MODIFY THIS + double ar = Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate; + if (mods.Any(m => m is OsuModHardRock)) + ar = Math.Min(10, ar * 1.4); + if (mods.Any(m => m is OsuModEasy)) + ar = Math.Max(0, ar / 2); + double preEmpt = BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450); + realApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5; + + // Custom multipliers for NoFail and SpunOut. + double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things + + if (mods.Any(m => m is OsuModNoFail)) + multiplier *= 0.90f; + + if (mods.Any(m => m is OsuModSpunOut)) + multiplier *= 0.95f; + + double aimValue = computeAimValue(); + double speedValue = computeSpeedValue(); + double accuracyValue = computeAccuracyValue(); + double totalValue = + Math.Pow( + Math.Pow(aimValue, 1.1f) + + Math.Pow(speedValue, 1.1f) + + Math.Pow(accuracyValue, 1.1f), 1.0f / 1.1f + ) * multiplier; + + if (categoryRatings != null) + { + categoryRatings.Add("Aim", aimValue); + categoryRatings.Add("Speed", speedValue); + categoryRatings.Add("Accuracy", accuracyValue); + } + + return totalValue; + } + + private double computeAimValue() + { + double aimValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes["Aim"] / 0.0675f) - 4.0f, 3.0f) / 100000.0f; + + // Longer maps are worth more + double lengthBonus = 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) + + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f); + + aimValue *= lengthBonus; + + // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available + aimValue *= Math.Pow(0.97f, countMiss); + + // Combo scaling + if (beatmapMaxCombo > 0) + aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f); + + double approachRateFactor = 1.0f; + if (realApproachRate > 10.33f) + approachRateFactor += 0.45f * (realApproachRate - 10.33f); + else if (realApproachRate < 8.0f) + { + // HD is worth more with lower ar! + if (mods.Any(h => h is OsuModHidden)) + approachRateFactor += 0.02f * (8.0f - realApproachRate); + else + approachRateFactor += 0.01f * (8.0f - realApproachRate); + } + + aimValue *= approachRateFactor; + + if (mods.Any(h => h is OsuModHidden)) + aimValue *= 1.18f; + + if (mods.Any(h => h is OsuModFlashlight)) + { + // Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps. + aimValue *= 1.45f * lengthBonus; + } + + // Scale the aim value with accuracy _slightly_ + aimValue *= 0.5f + accuracy / 2.0f; + // It is important to also consider accuracy difficulty when doing that + aimValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500; + + return aimValue; + } + + private double computeSpeedValue() + { + double speedValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes["Speed"] / 0.0675f) - 4.0f, 3.0f) / 100000.0f; + + // Longer maps are worth more + speedValue *= 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) + + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f); + + // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available + speedValue *= Math.Pow(0.97f, countMiss); + + // Combo scaling + if (beatmapMaxCombo > 0) + speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f); + + // Scale the speed value with accuracy _slightly_ + speedValue *= 0.5f + accuracy / 2.0f; + // It is important to also consider accuracy difficulty when doing that + speedValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500; + + return speedValue; + } + + private double computeAccuracyValue() + { + // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window + double betterAccuracyPercentage; + int amountHitObjectsWithAccuracy = countHitCircles; + + if (amountHitObjectsWithAccuracy > 0) + betterAccuracyPercentage = ((count300 - (totalHits - amountHitObjectsWithAccuracy)) * 6 + count100 * 2 + count50) / (amountHitObjectsWithAccuracy * 6); + else + betterAccuracyPercentage = 0; + + // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points + if (betterAccuracyPercentage < 0) + betterAccuracyPercentage = 0; + + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution + double accuracyValue = Math.Pow(1.52163f, Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f; + + // Bonus for many hitcircles - it's harder to keep good accuracy up for longer + accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f)); + + if (mods.Any(m => m is OsuModHidden)) + accuracyValue *= 1.02f; + if (mods.Any(m => m is OsuModFlashlight)) + accuracyValue *= 1.02f; + + return accuracyValue; + } + + private double totalHits => count300 + count100 + count50 + countMiss; + private double totalSuccessfulHits => count300 + count100 + count50; + + protected override BeatmapConverter CreateBeatmapConverter() => new OsuBeatmapConverter(); + } +} diff --git a/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..25a6110459 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [Ignore("getting CI working")] + public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints + { + public TestCasePerformancePoints() + : base(new OsuRuleset(new RulesetInfo())) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 3c90749777..2be057de40 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -85,9 +85,11 @@ + + diff --git a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs index e881942fbf..e74c12fa5d 100644 --- a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs @@ -5,7 +5,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; using System.Collections.Generic; -using System.Globalization; using System; namespace osu.Game.Rulesets.Taiko @@ -36,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko { } - public override double Calculate(Dictionary categoryDifficulty = null) + public override double Calculate(Dictionary categoryDifficulty = null) { // Fill our custom DifficultyHitObject class, that carries additional information difficultyHitObjects.Clear(); @@ -53,8 +52,8 @@ namespace osu.Game.Rulesets.Taiko if (categoryDifficulty != null) { - categoryDifficulty.Add("Strain", starRating.ToString("0.00", CultureInfo.InvariantCulture)); - categoryDifficulty.Add("Hit window 300", (35 /*HitObjectManager.HitWindow300*/ / TimeRate).ToString("0.00", CultureInfo.InvariantCulture)); + categoryDifficulty.Add("Strain", starRating); + categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate); } return starRating; diff --git a/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..96d5b20b6e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [Ignore("getting CI working")] + public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints + { + public TestCasePerformancePoints() + : base(new TaikoRuleset(new RulesetInfo())) + { + } + } +} diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index bf627d205a..0b4e6e43f2 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -83,6 +83,7 @@ + diff --git a/osu.Game.Tests/Visual/TestCaseButtonSystem.cs b/osu.Game.Tests/Visual/TestCaseButtonSystem.cs new file mode 100644 index 0000000000..d260de69f1 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseButtonSystem.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Shapes; +using osu.Game.Screens.Menu; +using OpenTK.Graphics; + +namespace osu.Game.Tests.Visual +{ + internal class TestCaseButtonSystem : OsuTestCase + { + public TestCaseButtonSystem() + { + OsuLogo logo; + ButtonSystem buttons; + + Children = new Drawable[] + { + new Box + { + Colour = ColourInfo.GradientVertical(Color4.Gray, Color4.WhiteSmoke), + RelativeSizeAxes = Axes.Both, + }, + buttons = new ButtonSystem(), + logo = new OsuLogo() + }; + + buttons.SetOsuLogo(logo); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMenuButtonSystem.cs b/osu.Game.Tests/Visual/TestCaseMenuButtonSystem.cs deleted file mode 100644 index b5310f0fb0..0000000000 --- a/osu.Game.Tests/Visual/TestCaseMenuButtonSystem.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.ComponentModel; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Shapes; -using osu.Game.Screens.Menu; -using OpenTK.Graphics; - -namespace osu.Game.Tests.Visual -{ - [Description("main menu")] - internal class TestCaseMenuButtonSystem : OsuTestCase - { - public TestCaseMenuButtonSystem() - { - Add(new Box - { - Colour = ColourInfo.GradientVertical(Color4.Gray, Color4.WhiteSmoke), - RelativeSizeAxes = Framework.Graphics.Axes.Both, - }); - Add(new ButtonSystem()); - } - } -} diff --git a/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs b/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs index 22a2d717e4..badb98e6b7 100644 --- a/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual Add(container = new ExampleContainer()); - AddStep(@"Add button", () => container.Add(new OsuButton + AddStep(@"Add button", () => container.Add(new TriangleButton { RelativeSizeAxes = Axes.X, Text = @"Button", diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 9bba09b1a7..312a564f71 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -116,7 +116,7 @@ - + diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 962c790fb2..e087eebbfe 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -80,6 +80,7 @@ namespace osu.Game.Beatmaps /// /// Performs the conversion of a hit object. + /// This method is generally executed sequentially for all objects in a beatmap. /// /// The hit object to convert. /// The un-converted Beatmap. diff --git a/osu.Game/Beatmaps/BeatmapMetrics.cs b/osu.Game/Beatmaps/BeatmapMetrics.cs index 730cf635da..e0cd5f10e7 100644 --- a/osu.Game/Beatmaps/BeatmapMetrics.cs +++ b/osu.Game/Beatmaps/BeatmapMetrics.cs @@ -12,7 +12,7 @@ namespace osu.Game.Beatmaps public class BeatmapMetrics { /// - /// Total vote counts of user ratings on a scale of 0..length. + /// Total vote counts of user ratings on a scale of 0..10 where 0 is unused (probably will be fixed at API?). /// public IEnumerable Ratings { get; set; } diff --git a/osu.Game/Beatmaps/DifficultyCalculator.cs b/osu.Game/Beatmaps/DifficultyCalculator.cs index f58f433cb2..687e1b2177 100644 --- a/osu.Game/Beatmaps/DifficultyCalculator.cs +++ b/osu.Game/Beatmaps/DifficultyCalculator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Beatmaps { protected double TimeRate = 1; - public abstract double Calculate(Dictionary categoryDifficulty = null); + public abstract double Calculate(Dictionary categoryDifficulty = null); } public abstract class DifficultyCalculator : DifficultyCalculator where T : HitObject diff --git a/osu.Game/Beatmaps/Drawables/Panel.cs b/osu.Game/Beatmaps/Drawables/Panel.cs index d6ed306b39..c990a0ea46 100644 --- a/osu.Game/Beatmaps/Drawables/Panel.cs +++ b/osu.Game/Beatmaps/Drawables/Panel.cs @@ -3,12 +3,18 @@ using System; using osu.Framework; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; using OpenTK; using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.MathUtils; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; namespace osu.Game.Beatmaps.Drawables { @@ -22,6 +28,10 @@ namespace osu.Game.Beatmaps.Drawables private readonly Container nestedContainer; + private readonly Container borderContainer; + + private readonly Box hoverLayer; + protected override Container Content => nestedContainer; protected Panel() @@ -29,20 +39,56 @@ namespace osu.Game.Beatmaps.Drawables Height = MAX_HEIGHT; RelativeSizeAxes = Axes.X; - AddInternal(nestedContainer = new Container + AddInternal(borderContainer = new Container { RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, BorderColour = new Color4(221, 255, 255, 255), + Children = new Drawable[] + { + nestedContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + hoverLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Blending = BlendingMode.Additive, + }, + } }); Alpha = 0; } + private SampleChannel sampleHover; + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + sampleHover = audio.Sample.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}"); + hoverLayer.Colour = colours.Blue.Opacity(0.1f); + } + + protected override bool OnHover(InputState state) + { + sampleHover?.Play(); + + hoverLayer.FadeIn(100, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + hoverLayer.FadeOut(1000, Easing.OutQuint); + base.OnHoverLost(state); + } + public void SetMultiplicativeAlpha(float alpha) { - nestedContainer.Alpha = alpha; + borderContainer.Alpha = alpha; } protected override void LoadComplete() @@ -94,8 +140,8 @@ namespace osu.Game.Beatmaps.Drawables protected virtual void Selected() { - nestedContainer.BorderThickness = 2.5f; - nestedContainer.EdgeEffect = new EdgeEffectParameters + borderContainer.BorderThickness = 2.5f; + borderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = new Color4(130, 204, 255, 150), @@ -106,8 +152,8 @@ namespace osu.Game.Beatmaps.Drawables protected virtual void Deselected() { - nestedContainer.BorderThickness = 0; - nestedContainer.EdgeEffect = new EdgeEffectParameters + borderContainer.BorderThickness = 0; + borderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Offset = new Vector2(1), diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index b3e99fce06..1a7d29e907 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -16,12 +16,13 @@ namespace osu.Game.Configuration Set(OsuSetting.Ruleset, 0, 0, int.MaxValue); Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details); + Set(OsuSetting.ShowConvertedBeatmaps, true); Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); Set(OsuSetting.DisplayStarsMaximum, 10.0, 0, 10, 0.1); Set(OsuSetting.SelectionRandomType, SelectionRandomType.RandomPermutation); - Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2, 1, 0.01); + Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2, 1); // Online settings Set(OsuSetting.Username, string.Empty); @@ -112,6 +113,7 @@ namespace osu.Game.Configuration SnakingOutSliders, ShowFpsDisplay, ChatDisplayHeight, - Version + Version, + ShowConvertedBeatmaps } } diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index bc1b7132eb..d8c3ce6694 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -35,6 +35,7 @@ namespace osu.Game.Database var id = obj.ID; obj = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find(id); + context.Entry(obj).Reload(); } /// diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs index 11c049ed3e..8df533ad6e 100644 --- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs +++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs @@ -2,34 +2,39 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.Containers { public class OsuClickableContainer : ClickableContainer { - protected SampleChannel SampleClick, SampleHover; + private readonly HoverSampleSet sampleSet; + + private readonly Container content = new Container { RelativeSizeAxes = Axes.Both }; + + protected override Container Content => content; + + public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Normal) + { + this.sampleSet = sampleSet; + } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { - SampleHover = audio.Sample.Get(@"UI/generic-hover"); - SampleClick = audio.Sample.Get(@"UI/generic-click"); - } + if (AutoSizeAxes != Axes.None) + { + content.RelativeSizeAxes = RelativeSizeAxes; + content.AutoSizeAxes = AutoSizeAxes; + } - protected override bool OnHover(InputState state) - { - SampleHover?.Play(); - return base.OnHover(state); - } - - protected override bool OnClick(InputState state) - { - SampleClick?.Play(); - return base.OnClick(state); + InternalChildren = new Drawable[] + { + content, + new HoverClickSounds(sampleSet) + }; } } } diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 4ea4f4cdc3..c788df3066 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -16,8 +16,8 @@ namespace osu.Game.Graphics.Containers [BackgroundDependencyLoader] private void load(AudioManager audio) { - samplePopIn = audio.Sample.Get(@"UI/melodic-5"); - samplePopOut = audio.Sample.Get(@"UI/melodic-4"); + samplePopIn = audio.Sample.Get(@"UI/overlay-pop-in"); + samplePopOut = audio.Sample.Get(@"UI/overlay-pop-out"); StateChanged += onStateChanged; } diff --git a/osu.Game/Graphics/SpriteIcon.cs b/osu.Game/Graphics/SpriteIcon.cs index ca108bfa7a..e752b1d91a 100644 --- a/osu.Game/Graphics/SpriteIcon.cs +++ b/osu.Game/Graphics/SpriteIcon.cs @@ -57,19 +57,31 @@ namespace osu.Game.Graphics private void load(FontStore store) { this.store = store; - updateTexture(); } + protected override void LoadComplete() + { + base.LoadComplete(); + updateTexture(); + } + + private FontAwesome loadedIcon; private void updateTexture() { - var texture = store?.Get(((char)icon).ToString()); + var loadableIcon = icon; + + if (loadableIcon == loadedIcon) return; + + var texture = store?.Get(((char)loadableIcon).ToString()); spriteMain.Texture = texture; spriteShadow.Texture = texture; if (Size == Vector2.Zero) Size = new Vector2(texture?.DisplayWidth ?? 0, texture?.DisplayHeight ?? 0); + + loadedIcon = loadableIcon; } public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) diff --git a/osu.Game/Graphics/UserInterface/Bar.cs b/osu.Game/Graphics/UserInterface/Bar.cs index 20df553142..c25a9bf5e9 100644 --- a/osu.Game/Graphics/UserInterface/Bar.cs +++ b/osu.Game/Graphics/UserInterface/Bar.cs @@ -20,6 +20,7 @@ namespace osu.Game.Graphics.UserInterface private const Easing easing = Easing.InOutCubic; private float length; + /// /// Length of the bar, ranges from 0 to 1 /// @@ -134,4 +135,4 @@ namespace osu.Game.Graphics.UserInterface Vertical = TopToBottom | BottomToTop, Horizontal = LeftToRight | RightToLeft, } -} \ No newline at end of file +} diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs new file mode 100644 index 0000000000..0fac1c8092 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions; +using osu.Framework.Input; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Adds hover and click sounds to a drawable. + /// Does not draw anything. + /// + public class HoverClickSounds : HoverSounds + { + private SampleChannel sampleClick; + + public HoverClickSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal) : base(sampleSet) + { + } + + protected override bool OnClick(InputState state) + { + sampleClick?.Play(); + return base.OnClick(state); + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleClick = audio.Sample.Get($@"UI/generic-select{SampleSet.GetDescription()}"); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs new file mode 100644 index 0000000000..24dbe37567 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Adds hover sounds to a drawable. + /// Does not draw anything. + /// + public class HoverSounds : CompositeDrawable + { + private SampleChannel sampleHover; + + protected readonly HoverSampleSet SampleSet; + + public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal) + { + SampleSet = sampleSet; + RelativeSizeAxes = Axes.Both; + } + + protected override bool OnHover(InputState state) + { + sampleHover?.Play(); + return base.OnHover(state); + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleHover = audio.Sample.Get($@"UI/generic-hover{SampleSet.GetDescription()}"); + } + } + + public enum HoverSampleSet + { + [Description("")] + Loud, + [Description("-soft")] + Normal, + [Description("-softer")] + Soft + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index ccc23e3ff6..081e59d3a7 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -1,126 +1,18 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; -using OpenTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.UserInterface { - public class OsuButton : Button, IFilterable + /// + /// A button with added default sound effects. + /// + public class OsuButton : Button { - private Box hover; - - private SampleChannel sampleClick; - private SampleChannel sampleHover; - - protected Triangles Triangles; - public OsuButton() { - Height = 40; - } - - protected override SpriteText CreateText() => new OsuSpriteText - { - Depth = -1, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Font = @"Exo2.0-Bold", - }; - - public override bool HandleInput => Action != null; - - [BackgroundDependencyLoader] - private void load(OsuColour colours, AudioManager audio) - { - if (Action == null) - Colour = OsuColour.Gray(0.5f); - - BackgroundColour = colours.BlueDark; - - Content.Masking = true; - Content.CornerRadius = 5; - - AddRange(new Drawable[] - { - Triangles = new Triangles - { - RelativeSizeAxes = Axes.Both, - ColourDark = colours.BlueDarker, - ColourLight = colours.Blue, - }, - hover = new Box - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, - Colour = Color4.White.Opacity(0.1f), - Alpha = 0, - }, - }); - - sampleClick = audio.Sample.Get(@"UI/generic-click"); - sampleHover = audio.Sample.Get(@"UI/generic-hover"); - - Enabled.ValueChanged += enabled_ValueChanged; - Enabled.TriggerChange(); - } - - private void enabled_ValueChanged(bool enabled) - { - this.FadeColour(enabled ? Color4.White : Color4.Gray, 200, Easing.OutQuint); - } - - protected override bool OnClick(InputState state) - { - sampleClick?.Play(); - return base.OnClick(state); - } - - protected override bool OnHover(InputState state) - { - sampleHover?.Play(); - hover.FadeIn(200); - return base.OnHover(state); - } - - protected override void OnHoverLost(InputState state) - { - hover.FadeOut(200); - base.OnHoverLost(state); - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - Content.ScaleTo(0.9f, 4000, Easing.OutQuint); - return base.OnMouseDown(state, args); - } - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) - { - Content.ScaleTo(1, 1000, Easing.OutElastic); - return base.OnMouseUp(state, args); - } - - public IEnumerable FilterTerms => new[] { Text }; - - public bool MatchingFilter - { - set - { - this.FadeTo(value ? 1 : 0); - } + Add(new HoverClickSounds(HoverSampleSet.Loud)); } } } diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 68ff99e593..40ff1243dc 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -70,7 +70,8 @@ namespace osu.Game.Graphics.UserInterface Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding { Right = 5 }, - } + }, + new HoverClickSounds() }; Nub.Current.BindTo(Current); diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index f605804aaa..4401b509fd 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -33,7 +33,6 @@ namespace osu.Game.Graphics.UserInterface if (accentColour == default(Color4)) accentColour = colours.PinkDarker; updateAccentColour(); - } private void updateAccentColour() @@ -137,6 +136,8 @@ namespace osu.Game.Graphics.UserInterface nonAccentHoverColour = colours.PinkDarker; nonAccentSelectedColour = Color4.Black.Opacity(0.5f); updateColours(); + + AddInternal(new HoverClickSounds(HoverSampleSet.Soft)); } protected override void UpdateForegroundColour() @@ -183,7 +184,7 @@ namespace osu.Game.Graphics.UserInterface { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - } + }, }; } } @@ -237,8 +238,10 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.CentreRight, Margin = new MarginPadding { Right = 4 }, Size = new Vector2(20), - } + }, }; + + AddInternal(new HoverClickSounds()); } [BackgroundDependencyLoader] diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 3fd5481152..7f16d73613 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -72,7 +72,7 @@ namespace osu.Game.Graphics.UserInterface private void load(AudioManager audio) { sampleHover = audio.Sample.Get(@"UI/generic-hover"); - sampleClick = audio.Sample.Get(@"UI/generic-click"); + sampleClick = audio.Sample.Get(@"UI/generic-select"); BackgroundColour = Color4.Transparent; BackgroundColourHover = OsuColour.FromHex(@"172023"); diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 3dd3596c30..fd75269610 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -88,7 +88,8 @@ namespace osu.Game.Graphics.UserInterface { Origin = Anchor.TopCentre, Expanded = true, - } + }, + new HoverClickSounds() }; Current.DisabledChanged += disabled => diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index b053195030..decbf57ad1 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -131,7 +131,8 @@ namespace osu.Game.Graphics.UserInterface Colour = Color4.White, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - } + }, + new HoverClickSounds() }; } diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index 6b97e54ecd..c69857a5c4 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -57,6 +57,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, }, + new HoverClickSounds() }; } diff --git a/osu.Game/Graphics/UserInterface/TriangleButton.cs b/osu.Game/Graphics/UserInterface/TriangleButton.cs new file mode 100644 index 0000000000..61e9705064 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/TriangleButton.cs @@ -0,0 +1,108 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// A button with moving triangles in the background. + /// + public class TriangleButton : OsuButton, IFilterable + { + private Box hover; + + protected Triangles Triangles; + + public TriangleButton() + { + Height = 40; + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Font = @"Exo2.0-Bold", + }; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.BlueDark; + + Content.Masking = true; + Content.CornerRadius = 5; + + AddRange(new Drawable[] + { + Triangles = new Triangles + { + RelativeSizeAxes = Axes.Both, + ColourDark = colours.BlueDarker, + ColourLight = colours.Blue, + }, + hover = new Box + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingMode.Additive, + Colour = Color4.White.Opacity(0.1f), + Alpha = 0, + }, + }); + + Enabled.ValueChanged += enabled_ValueChanged; + Enabled.TriggerChange(); + } + + private void enabled_ValueChanged(bool enabled) + { + this.FadeColour(enabled ? Color4.White : Color4.Gray, 200, Easing.OutQuint); + } + + protected override bool OnHover(InputState state) + { + hover.FadeIn(200); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + hover.FadeOut(200); + base.OnHoverLost(state); + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + Content.ScaleTo(0.9f, 4000, Easing.OutQuint); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + Content.ScaleTo(1, 1000, Easing.OutElastic); + return base.OnMouseUp(state, args); + } + + public IEnumerable FilterTerms => new[] { Text }; + + public bool MatchingFilter + { + set + { + this.FadeTo(value ? 1 : 0); + } + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 50639e3427..8eaa20f781 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Reflection; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Configuration; using osu.Framework.Development; using osu.Framework.Graphics; @@ -137,6 +138,10 @@ namespace osu.Game Beatmap = new NonNullableBindable(defaultBeatmap); BeatmapManager.DefaultBeatmap = defaultBeatmap; + // tracks play so loud our samples can't keep up. + // this adds a global reduction of track volume for the time being. + Audio.Track.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8)); + Beatmap.ValueChanged += b => { var trackLoaded = lastBeatmap?.TrackLoaded ?? false; diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index af59b21713..4135aef268 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -234,7 +234,7 @@ namespace osu.Game.Overlays.BeatmapSet private void handleBeatmapAdd(BeatmapSetInfo beatmap) { - if (beatmap.OnlineBeatmapSetID == BeatmapSet.OnlineBeatmapSetID) + if (beatmap.OnlineBeatmapSetID == BeatmapSet?.OnlineBeatmapSetID) downloadButtonsContainer.FadeOut(transition_duration); } diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 4db6bdf5e4..32f933ff42 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -222,7 +222,7 @@ namespace osu.Game.Overlays.Chat } - private class MessageSender : ClickableContainer, IHasContextMenu + private class MessageSender : OsuClickableContainer, IHasContextMenu { private readonly User sender; diff --git a/osu.Game/Overlays/Chat/ChatTabControl.cs b/osu.Game/Overlays/Chat/ChatTabControl.cs index 9f1028c168..f58ee8f819 100644 --- a/osu.Game/Overlays/Chat/ChatTabControl.cs +++ b/osu.Game/Overlays/Chat/ChatTabControl.cs @@ -17,6 +17,7 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Configuration; using System; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Chat { @@ -259,7 +260,7 @@ namespace osu.Game.Overlays.Chat }; } - public class CloseButton : ClickableContainer + public class CloseButton : OsuClickableContainer { private readonly SpriteIcon icon; diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 6f7fabb910..0b7a30797d 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays { if (beatmapSets?.Equals(value) ?? false) return; - beatmapSets = value; + beatmapSets = value?.ToList(); if (beatmapSets == null) return; @@ -65,8 +65,6 @@ namespace osu.Game.Overlays } ResultAmounts = new ResultCounts(distinctCount(artists), distinctCount(songs), distinctCount(tags)); - - recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); } } @@ -222,7 +220,11 @@ namespace osu.Game.Overlays switch (displayStyle) { case PanelDisplayStyle.Grid: - return new DirectGridPanel(b); + return new DirectGridPanel(b) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }; default: return new DirectListPanel(b); } @@ -282,7 +284,11 @@ namespace osu.Game.Overlays var sets = response.Select(r => r.ToBeatmapSet(rulesets)).Where(b => !presentOnlineIds.Contains(b.OnlineBeatmapSetID)).ToList(); // may not need scheduling; loads async internally. - Schedule(() => BeatmapSets = sets); + Schedule(() => + { + BeatmapSets = sets; + recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); + }); }); }; diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index 30ff0ab026..c670cc0153 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.KeyBinding } } - public class ResetButton : OsuButton + public class ResetButton : TriangleButton { [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 0ead4ea019..77b7c3add2 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI; using System; using System.Linq; using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Mods { @@ -148,7 +149,7 @@ namespace osu.Game.Overlays.Mods // the mods from Mod, only multiple if Mod is a MultiMod - public override Mod SelectedMod => Mods.ElementAtOrDefault(SelectedIndex); + public virtual Mod SelectedMod => Mods.ElementAtOrDefault(SelectedIndex); [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -253,6 +254,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopCentre, TextSize = 18, }, + new HoverClickSounds() }; Mod = mod; diff --git a/osu.Game/Overlays/Mods/ModButtonEmpty.cs b/osu.Game/Overlays/Mods/ModButtonEmpty.cs index 638c2a0e47..f776c174d2 100644 --- a/osu.Game/Overlays/Mods/ModButtonEmpty.cs +++ b/osu.Game/Overlays/Mods/ModButtonEmpty.cs @@ -3,7 +3,6 @@ using OpenTK; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { @@ -12,8 +11,6 @@ namespace osu.Game.Overlays.Mods /// public class ModButtonEmpty : Container { - public virtual Mod SelectedMod => null; - public ModButtonEmpty() { Size = new Vector2(100f); diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs index 07a8e7464a..9875ee8004 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs @@ -17,6 +17,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { Children = new Drawable[] { + new SettingsCheckbox + { + LabelText = "Show converted beatmaps", + Bindable = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), + }, new SettingsSlider { LabelText = "Display beatmaps from", @@ -33,7 +38,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { LabelText = "Random beatmap selection", Bindable = config.GetBindable(OsuSetting.SelectionRandomType), - }, + } }; } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 4c82a9ae4b..4f4f381ae1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -12,9 +12,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { public class GeneralSettings : SettingsSubsection { - private OsuButton importButton; - private OsuButton deleteButton; - private OsuButton restoreButton; + private TriangleButton importButton; + private TriangleButton deleteButton; + private TriangleButton restoreButton; protected override string Header => "General"; diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index 5320cef850..19493f6c70 100644 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -6,7 +6,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { - public class SettingsButton : OsuButton + public class SettingsButton : TriangleButton { public SettingsButton() { diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index b39c8ab7cf..4b8366f0fc 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -12,14 +12,14 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { - public class SidebarButton : Container + public class SidebarButton : OsuButton { private readonly SpriteIcon drawableIcon; private readonly SpriteText headerText; - private readonly Box backgroundBox; private readonly Box selectionIndicator; private readonly Container text; public Action Action; @@ -61,17 +61,14 @@ namespace osu.Game.Overlays.Settings public SidebarButton() { + BackgroundColour = OsuColour.Gray(60); + Background.Alpha = 0; + Height = Sidebar.DEFAULT_WIDTH; RelativeSizeAxes = Axes.X; - Children = new Drawable[] + + AddRange(new Drawable[] { - backgroundBox = new Box - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, - Colour = OsuColour.Gray(60), - Alpha = 0, - }, text = new Container { Width = Sidebar.DEFAULT_WIDTH, @@ -101,7 +98,7 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, } - }; + }); } [BackgroundDependencyLoader] @@ -113,20 +110,19 @@ namespace osu.Game.Overlays.Settings protected override bool OnClick(InputState state) { Action?.Invoke(section); - backgroundBox.FlashColour(Color4.White, 400); - return true; + return base.OnClick(state); } protected override bool OnHover(InputState state) { - backgroundBox.FadeTo(0.4f, 200); + Background.FadeTo(0.4f, 200); return base.OnHover(state); } protected override void OnHoverLost(InputState state) { - backgroundBox.FadeTo(0, 200); + Background.FadeTo(0, 200); base.OnHoverLost(state); } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index cb17216679..c039f9d311 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -13,6 +13,7 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Toolbar { @@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Toolbar private readonly SpriteText tooltip2; protected FillFlowContainer Flow; - public ToolbarButton() + public ToolbarButton() : base(HoverSampleSet.Loud) { Width = WIDTH; RelativeSizeAxes = Axes.Y; @@ -195,4 +196,4 @@ namespace osu.Game.Overlays.Toolbar }; } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Replays/ReplayFrame.cs b/osu.Game/Rulesets/Replays/ReplayFrame.cs index b0f62e5271..02c969f648 100644 --- a/osu.Game/Rulesets/Replays/ReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/ReplayFrame.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Replays { public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); - public bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight); + public virtual bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight); public float? MouseX; public float? MouseY; @@ -68,4 +68,4 @@ namespace osu.Game.Rulesets.Replays return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}"; } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index ed2fdf4157..d787da6a0a 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets @@ -49,6 +50,8 @@ namespace osu.Game.Rulesets public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null); + public virtual PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => null; + public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_question_circle }; public abstract string Description { get; } diff --git a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs new file mode 100644 index 0000000000..4f603049db --- /dev/null +++ b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Scoring +{ + public abstract class PerformanceCalculator + { + public abstract double Calculate(Dictionary categoryDifficulty = null); + } + + public abstract class PerformanceCalculator : PerformanceCalculator + where TObject : HitObject + { + private readonly Dictionary attributes = new Dictionary(); + protected IDictionary Attributes => attributes; + + protected readonly Beatmap Beatmap; + protected readonly Score Score; + + protected PerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score) + { + Beatmap = CreateBeatmapConverter().Convert(beatmap); + Score = score; + + var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods); + diffCalc.Calculate(attributes); + } + + protected abstract BeatmapConverter CreateBeatmapConverter(); + } +} diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs index ccd61643ce..5e55166c19 100644 --- a/osu.Game/Screens/Menu/Button.cs +++ b/osu.Game/Screens/Menu/Button.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleHover = audio.Sample.Get(@"Menu/hover"); + sampleHover = audio.Sample.Get(@"Menu/button-hover"); if (!string.IsNullOrEmpty(sampleName)) sampleClick = audio.Sample.Get($@"Menu/{sampleName}"); } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 5a4a5f07b5..ce7856c5a9 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -117,13 +117,13 @@ namespace osu.Game.Screens.Menu }, }; - buttonsPlay.Add(new Button(@"solo", @"select-6", FontAwesome.fa_user, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new Button(@"multi", @"select-5", FontAwesome.fa_users, new Color4(94, 63, 186, 255), () => OnMulti?.Invoke(), 0, Key.M)); - buttonsPlay.Add(new Button(@"chart", @"select-5", FontAwesome.fa_osu_charts, new Color4(80, 53, 160, 255), () => OnChart?.Invoke())); + buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.fa_user, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); + buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.fa_users, new Color4(94, 63, 186, 255), () => OnMulti?.Invoke(), 0, Key.M)); + buttonsPlay.Add(new Button(@"chart", @"button-generic-select", FontAwesome.fa_osu_charts, new Color4(80, 53, 160, 255), () => OnChart?.Invoke())); - buttonsTopLevel.Add(new Button(@"play", @"select-1", FontAwesome.fa_osu_logo, new Color4(102, 68, 204, 255), onPlay, WEDGE_WIDTH, Key.P)); - buttonsTopLevel.Add(new Button(@"osu!editor", @"select-5", FontAwesome.fa_osu_edit_o, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); - buttonsTopLevel.Add(new Button(@"osu!direct", string.Empty, FontAwesome.fa_osu_chevron_down_o, new Color4(165, 204, 0, 255), () => OnDirect?.Invoke(), 0, Key.D)); + buttonsTopLevel.Add(new Button(@"play", @"button-play-select", FontAwesome.fa_osu_logo, new Color4(102, 68, 204, 255), onPlay, WEDGE_WIDTH, Key.P)); + buttonsTopLevel.Add(new Button(@"osu!editor", @"button-generic-select", FontAwesome.fa_osu_edit_o, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); + buttonsTopLevel.Add(new Button(@"osu!direct", @"button-direct-select", FontAwesome.fa_osu_chevron_down_o, new Color4(165, 204, 0, 255), () => OnDirect?.Invoke(), 0, Key.D)); buttonsTopLevel.Add(new Button(@"exit", string.Empty, FontAwesome.fa_osu_cross_o, new Color4(238, 51, 153, 255), onExit, 0, Key.Q)); buttonFlow.AddRange(buttonsPlay); @@ -134,7 +134,7 @@ namespace osu.Game.Screens.Menu private void load(AudioManager audio, OsuGame game = null) { toolbar = game?.Toolbar; - sampleBack = audio.Sample.Get(@"Menu/select-4"); + sampleBack = audio.Sample.Get(@"Menu/button-back-select"); } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) @@ -180,19 +180,21 @@ namespace osu.Game.Screens.Menu State = MenuState.TopLevel; } - private void onOsuLogo() + private bool onOsuLogo() { switch (state) { + default: + return true; case MenuState.Initial: State = MenuState.TopLevel; - return; + return true; case MenuState.TopLevel: buttonsTopLevel.First().TriggerOnClick(); - return; + return false; case MenuState.Play: buttonsPlay.First().TriggerOnClick(); - return; + return false; } } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 252f2d37b5..9ca12702e5 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -48,7 +48,10 @@ namespace osu.Game.Screens.Menu private readonly Triangles triangles; - public Action Action; + /// + /// Return value decides whether the logo should play its own sample for the click action. + /// + public Func Action; public float SizeForFlow => logo == null ? 0 : logo.DrawSize.X * logo.Scale.X * logoBounceContainer.Scale.X * logoHoverContainer.Scale.X * 0.74f; @@ -248,8 +251,8 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(TextureStore textures, AudioManager audio) { - sampleClick = audio.Sample.Get(@"Menu/select-2"); - sampleBeat = audio.Sample.Get(@"Menu/heartbeat"); + sampleClick = audio.Sample.Get(@"Menu/osu-logo-select"); + sampleBeat = audio.Sample.Get(@"Menu/osu-logo-heartbeat"); logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); @@ -354,13 +357,12 @@ namespace osu.Game.Screens.Menu { if (!interactive) return false; - sampleClick.Play(); + if (Action?.Invoke() ?? true) + sampleClick.Play(); flashLayer.ClearTransforms(); flashLayer.Alpha = 0.4f; flashLayer.FadeOut(1500, Easing.OutExpo); - - Action?.Invoke(); return true; } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index f5ff9ea036..76ee4a607e 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens if (osuGame != null) Ruleset.BindTo(osuGame.Ruleset); - sampleExit = audio.Sample.Get(@"UI/melodic-1"); + sampleExit = audio.Sample.Get(@"UI/screen-back"); } protected override void OnResuming(Screen last) diff --git a/osu.Game/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PauseContainer.cs index eed5cd1c20..5f5eeb63a0 100644 --- a/osu.Game/Screens/Play/PauseContainer.cs +++ b/osu.Game/Screens/Play/PauseContainer.cs @@ -22,8 +22,6 @@ namespace osu.Game.Screens.Play { public bool IsPaused { get; private set; } - public bool AllowExit => IsPaused && pauseOverlay.Alpha == 1; - public Func CheckCanPause; private const double pause_cooldown = 1000; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 1e1b7bac93..dc746b305c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -46,6 +46,8 @@ namespace osu.Game.Screens.Play public bool HasFailed { get; private set; } + public bool AllowPause { get; set; } = true; + public int RestartCount; private IAdjustableClock adjustableSourceClock; @@ -158,7 +160,7 @@ namespace osu.Game.Screens.Play FramedClock = offsetClock, OnRetry = Restart, OnQuit = Exit, - CheckCanPause = () => ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded, + CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded, Retries = RestartCount, OnPause = () => { hudOverlay.KeyCounter.IsCounting = pauseContainer.IsPaused; @@ -308,7 +310,7 @@ namespace osu.Game.Screens.Play if (!loadedSuccessfully) return; - (Background as BackgroundScreenBeatmap)?.BlurTo(Vector2.Zero, 1500, Easing.OutQuint); + (Background as BackgroundScreenBeatmap)?.BlurTo(Vector2.Zero, 1000, Easing.OutQuint); dimLevel.ValueChanged += dimLevel_ValueChanged; showStoryboard.ValueChanged += showStoryboard_ValueChanged; @@ -355,7 +357,7 @@ namespace osu.Game.Screens.Play protected override bool OnExiting(Screen next) { - if (HasFailed || !ValidForResume || pauseContainer?.AllowExit != false || RulesetContainer?.HasReplayLoaded != false) + if (!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false) { // In the case of replays, we may have changed the playback rate. applyRateFromMods(); diff --git a/osu.Game/Screens/Select/Details/UserRatings.cs b/osu.Game/Screens/Select/Details/UserRatings.cs index 2153eb150c..19bcad367e 100644 --- a/osu.Game/Screens/Select/Details/UserRatings.cs +++ b/osu.Game/Screens/Select/Details/UserRatings.cs @@ -29,11 +29,17 @@ namespace osu.Game.Screens.Select.Details if (value == metrics) return; metrics = value; - var ratings = Metrics.Ratings.ToList(); - negativeRatings.Text = ratings.GetRange(0, ratings.Count / 2 + 1).Sum().ToString(); - positiveRatings.Text = ratings.GetRange(ratings.Count / 2 + 1, ratings.Count / 2).Sum().ToString(); - ratingsBar.Length = (float)ratings.GetRange(0, ratings.Count / 2 + 1).Sum() / ratings.Sum(); - graph.Values = Metrics.Ratings.Select(r => (float)r); + const int rating_range = 10; + + var ratings = Metrics.Ratings.Skip(1).Take(rating_range); // adjust for API returning weird empty data at 0. + + var negativeCount = ratings.Take(rating_range / 2).Sum(); + var totalCount = ratings.Sum(); + + negativeRatings.Text = negativeCount.ToString(); + positiveRatings.Text = (totalCount - negativeCount).ToString(); + ratingsBar.Length = totalCount == 0 ? 0 : (float)negativeCount / totalCount; + graph.Values = ratings.Take(rating_range).Select(r => (float)r); } } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index e83613125b..1b86cec613 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -15,6 +15,7 @@ using osu.Game.Screens.Select.Filter; using Container = osu.Framework.Graphics.Containers.Container; using osu.Framework.Input; using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; using osu.Game.Rulesets; namespace osu.Game.Screens.Select @@ -60,6 +61,7 @@ namespace osu.Game.Screens.Select Group = group, Sort = sort, SearchText = searchTextBox.Text, + AllowConvertedBeatmaps = showConverted, Ruleset = ruleset }; @@ -163,17 +165,24 @@ namespace osu.Game.Screens.Select private readonly Bindable ruleset = new Bindable(); + private Bindable showConverted; + [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuColour colours, OsuGame osu) + private void load(OsuColour colours, OsuGame osu, OsuConfigManager config) { sortTabs.AccentColour = colours.GreenLight; + showConverted = config.GetBindable(OsuSetting.ShowConvertedBeatmaps); + showConverted.ValueChanged += val => updateCriteria(); + if (osu != null) ruleset.BindTo(osu.Ruleset); - ruleset.ValueChanged += val => FilterChanged?.Invoke(CreateCriteria()); + ruleset.ValueChanged += val => updateCriteria(); ruleset.TriggerChange(); } + private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; protected override bool OnMouseMove(InputState state) => true; @@ -182,4 +191,4 @@ namespace osu.Game.Screens.Select protected override bool OnDragStart(InputState state) => true; } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 6c1fb1703d..c1355bfa63 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -16,6 +16,7 @@ namespace osu.Game.Screens.Select public SortMode Sort; public string SearchText; public RulesetInfo Ruleset; + public bool AllowConvertedBeatmaps; public void Filter(List groups) { @@ -23,7 +24,7 @@ namespace osu.Game.Screens.Select { var set = g.BeatmapSet; - bool hasCurrentMode = set.Beatmaps.Any(bm => bm.RulesetID == (Ruleset?.ID ?? 0)); + bool hasCurrentMode = AllowConvertedBeatmaps || set.Beatmaps.Any(bm => bm.RulesetID == (Ruleset?.ID ?? 0)); bool match = hasCurrentMode; diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index e0a3693371..bba6ddf577 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -4,6 +4,8 @@ using System.Linq; using OpenTK.Input; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -42,9 +44,13 @@ namespace osu.Game.Screens.Select beatmapDetails.Leaderboard.ScoreSelected += s => Push(new Results(s)); } + private SampleChannel sampleConfirm; + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { + sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); + Footer.AddButton(@"mods", colours.Yellow, modSelect, Key.F1, float.MaxValue); BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.fa_times_circle_o, colours.Purple, null, Key.Number1); @@ -128,6 +134,8 @@ namespace osu.Game.Screens.Select Beatmap.Value.Track.Looping = false; Beatmap.Disabled = true; + sampleConfirm?.Play(); + LoadComponentAsync(player = new PlayerLoader(new Player()), l => Push(player)); } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 5500d06136..a0b788d777 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -203,8 +203,8 @@ namespace osu.Game.Screens.Select Push(new Editor()); } - private void onBeatmapRestored(BeatmapInfo b) => carousel.UpdateBeatmap(b); - private void onBeatmapHidden(BeatmapInfo b) => carousel.UpdateBeatmap(b); + private void onBeatmapRestored(BeatmapInfo b) => Schedule(() => carousel.UpdateBeatmap(b)); + private void onBeatmapHidden(BeatmapInfo b) => Schedule(() => carousel.UpdateBeatmap(b)); private void carouselBeatmapsLoaded() { @@ -332,7 +332,11 @@ namespace osu.Game.Screens.Select logo.FadeIn(logo_transition, Easing.OutQuint); logo.ScaleTo(0.4f, logo_transition, Easing.OutQuint); - logo.Action = () => carouselRaisedStart(); + logo.Action = () => + { + carouselRaisedStart(); + return false; + }; } protected override void LogoExiting(OsuLogo logo) @@ -413,7 +417,7 @@ namespace osu.Game.Screens.Select if (backgroundModeBeatmap != null) { backgroundModeBeatmap.Beatmap = beatmap; - backgroundModeBeatmap.BlurTo(background_blur, 1000); + backgroundModeBeatmap.BlurTo(background_blur, 750, Easing.OutQuint); backgroundModeBeatmap.FadeTo(1, 250); } diff --git a/osu.Game/Screens/Tournament/Drawings.cs b/osu.Game/Screens/Tournament/Drawings.cs index e540782fc1..3e7ab56c99 100644 --- a/osu.Game/Screens/Tournament/Drawings.cs +++ b/osu.Game/Screens/Tournament/Drawings.cs @@ -193,21 +193,21 @@ namespace osu.Game.Screens.Tournament Children = new Drawable[] { - new OsuButton + new TriangleButton { RelativeSizeAxes = Axes.X, Text = "Begin random", Action = teamsContainer.StartScrolling, }, - new OsuButton + new TriangleButton { RelativeSizeAxes = Axes.X, Text = "Stop random", Action = teamsContainer.StopScrolling, }, - new OsuButton + new TriangleButton { RelativeSizeAxes = Axes.X, @@ -232,7 +232,7 @@ namespace osu.Game.Screens.Tournament Children = new Drawable[] { - new OsuButton + new TriangleButton { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Tests/Visual/ScreenTestCase.cs b/osu.Game/Tests/Visual/ScreenTestCase.cs new file mode 100644 index 0000000000..2f0831d84a --- /dev/null +++ b/osu.Game/Tests/Visual/ScreenTestCase.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Screens; +using osu.Game.Screens; + +namespace osu.Game.Tests.Visual +{ + /// + /// A test case which can be used to test a screen (that relies on OnEntering being called to execute startup instructions). + /// + public abstract class ScreenTestCase : OsuTestCase + { + private readonly TestOsuScreen baseScreen; + + protected ScreenTestCase() + { + Add(baseScreen = new TestOsuScreen()); + } + + protected void LoadScreen(OsuScreen screen) => baseScreen.LoadScreen(screen); + + public class TestOsuScreen : OsuScreen + { + private OsuScreen nextScreen; + + public void LoadScreen(OsuScreen screen) => Schedule(() => + { + nextScreen = screen; + + if (IsCurrentScreen) + { + Push(screen); + nextScreen = null; + } + else + MakeCurrent(); + }); + + protected override void OnResuming(Screen last) + { + base.OnResuming(last); + if (nextScreen != null) + LoadScreen(nextScreen); + } + } + } +} diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..6da14e9b12 --- /dev/null +++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs @@ -0,0 +1,395 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Caching; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Tests.Visual +{ + public abstract class TestCasePerformancePoints : OsuTestCase + { + protected TestCasePerformancePoints(Ruleset ruleset) + { + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new BeatmapList(ruleset) + } + } + }, + null, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StarRatingGrid() + } + } + }, + null, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new PerformanceList() + } + } + }, + } + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 20), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 20) + } + }; + } + + private class BeatmapList : CompositeDrawable + { + private readonly Container beatmapDisplays; + private readonly Ruleset ruleset; + + public BeatmapList(Ruleset ruleset) + { + this.ruleset = ruleset; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = beatmapDisplays = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 4) + }; + } + + [BackgroundDependencyLoader] + private void load(BeatmapManager beatmaps) + { + var sets = beatmaps.GetAllUsableBeatmapSets(); + var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID < 0 || b.RulesetID == ruleset.LegacyID); + + allBeatmaps.ForEach(b => beatmapDisplays.Add(new BeatmapDisplay(b))); + } + + private class BeatmapDisplay : CompositeDrawable, IHasTooltip + { + private readonly OsuSpriteText text; + private readonly BeatmapInfo beatmap; + + private BeatmapManager beatmaps; + private OsuGameBase osuGame; + + private bool isSelected; + + public string TooltipText => text.Text; + + public BeatmapDisplay(BeatmapInfo beatmap) + { + this.beatmap = beatmap; + + AutoSizeAxes = Axes.Both; + InternalChild = text = new OsuSpriteText(); + } + + protected override bool OnClick(InputState state) + { + if (osuGame.Beatmap.Value.BeatmapInfo.ID == beatmap.ID) + return false; + + osuGame.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); + isSelected = true; + return true; + } + + protected override bool OnHover(InputState state) + { + if (isSelected) + return false; + this.FadeColour(Color4.Yellow, 100); + return true; + } + + protected override void OnHoverLost(InputState state) + { + if (isSelected) + return; + this.FadeColour(Color4.White, 100); + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame, BeatmapManager beatmaps) + { + this.osuGame = osuGame; + this.beatmaps = beatmaps; + + var working = beatmaps.GetWorkingBeatmap(beatmap); + text.Text = $"{working.Metadata.Artist} - {working.Metadata.Title} ({working.Metadata.AuthorString}) [{working.BeatmapInfo.Version}]"; + + osuGame.Beatmap.ValueChanged += beatmapChanged; + } + + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + if (isSelected) + this.FadeColour(Color4.White, 100); + isSelected = false; + } + } + } + + private class PerformanceList : CompositeDrawable + { + private readonly FillFlowContainer scores; + private APIAccess api; + + public PerformanceList() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = scores = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 4) + }; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame, APIAccess api) + { + this.api = api; + + if (!api.IsLoggedIn) + { + InternalChild = new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Please login to see online scores", + }; + } + + osuGame.Beatmap.ValueChanged += beatmapChanged; + } + + private GetScoresRequest lastRequest; + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + lastRequest?.Cancel(); + scores.Clear(); + + if (!api.IsLoggedIn) + return; + + lastRequest = new GetScoresRequest(newBeatmap.BeatmapInfo); + lastRequest.Success += res => res.Scores.ForEach(s => scores.Add(new PerformanceDisplay(s, newBeatmap.Beatmap))); + api.Queue(lastRequest); + } + + private class PerformanceDisplay : CompositeDrawable + { + private readonly OsuSpriteText text; + + private readonly Score score; + private readonly Beatmap beatmap; + + public PerformanceDisplay(Score score, Beatmap beatmap) + { + this.score = score; + this.beatmap = beatmap; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = text = new OsuSpriteText(); + } + + [BackgroundDependencyLoader] + private void load() + { + var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); + var calculator = ruleset.CreatePerformanceCalculator(beatmap, score); + if (calculator == null) + return; + + var attributes = new Dictionary(); + double performance = calculator.Calculate(attributes); + + text.Text = $"{score.User.Username} -> online: {score.PP:n2}pp | local: {performance:n2}pp"; + } + } + } + + private class StarRatingGrid : CompositeDrawable + { + private readonly FillFlowContainer modFlow; + private readonly OsuSpriteText totalText; + private readonly FillFlowContainer categoryTexts; + + public StarRatingGrid() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + modFlow = new FillFlowContainer + { + Name = "Checkbox flow", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(4, 4) + }, + new FillFlowContainer + { + Name = "Information display", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 4), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + totalText = new OsuSpriteText { TextSize = 24 }, + categoryTexts = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame) + { + osuGame.Beatmap.ValueChanged += beatmapChanged; + } + + private Cached informationCache = new Cached(); + + private Ruleset ruleset; + private WorkingBeatmap beatmap; + + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + beatmap = newBeatmap; + + modFlow.Clear(); + + ruleset = newBeatmap.BeatmapInfo.Ruleset.CreateInstance(); + foreach (var mod in ruleset.GetAllMods()) + { + var checkBox = new OsuCheckbox + { + RelativeSizeAxes = Axes.None, + Width = 50, + LabelText = mod.ShortenedName + }; + + checkBox.Current.ValueChanged += v => informationCache.Invalidate(); + modFlow.Add(checkBox); + } + + informationCache.Invalidate(); + } + + protected override void Update() + { + base.Update(); + + if (ruleset == null) + return; + + if (!informationCache.IsValid) + { + totalText.Text = string.Empty; + categoryTexts.Clear(); + + var allMods = ruleset.GetAllMods().ToList(); + Mod[] activeMods = modFlow.Where(c => c.Current.Value).Select(c => allMods.First(m => m.ShortenedName == c.LabelText)).ToArray(); + + var diffCalc = ruleset.CreateDifficultyCalculator(beatmap.Beatmap, activeMods); + if (diffCalc != null) + { + var categories = new Dictionary(); + double totalSr = diffCalc.Calculate(categories); + + totalText.Text = $"Star rating: {totalSr:n2}"; + foreach (var kvp in categories) + categoryTexts.Add(new OsuSpriteText { Text = $"{kvp.Key}: {kvp.Value:n2}" }); + } + + informationCache.Validate(); + } + } + } + } +} diff --git a/osu.Game/Tests/Visual/TestCasePlayer.cs b/osu.Game/Tests/Visual/TestCasePlayer.cs index cef85b65f1..d9951e002b 100644 --- a/osu.Game/Tests/Visual/TestCasePlayer.cs +++ b/osu.Game/Tests/Visual/TestCasePlayer.cs @@ -17,7 +17,7 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { - public abstract class TestCasePlayer : OsuTestCase + public abstract class TestCasePlayer : ScreenTestCase { private readonly Type ruleset; @@ -44,12 +44,17 @@ namespace osu.Game.Tests.Visual { RelativeSizeAxes = Framework.Graphics.Axes.Both, Colour = Color4.Black, + Depth = int.MaxValue }); string instantiation = ruleset?.AssemblyQualifiedName; foreach (var r in rulesets.AvailableRulesets.Where(rs => instantiation == null || rs.InstantiationInfo == instantiation)) - AddStep(r.Name, () => loadPlayerFor(r)); + { + Player p = null; + AddStep(r.Name, () => p = loadPlayerFor(r)); + AddUntilStep(() => p.IsLoaded); + } } protected virtual Beatmap CreateBeatmap() @@ -63,7 +68,7 @@ namespace osu.Game.Tests.Visual return beatmap; } - private void loadPlayerFor(RulesetInfo r) + private Player loadPlayerFor(RulesetInfo r) { var beatmap = CreateBeatmap(); @@ -77,19 +82,21 @@ namespace osu.Game.Tests.Visual if (Player != null) Remove(Player); - Add(Player = CreatePlayer(working, instance)); + var player = CreatePlayer(working, instance); + + LoadComponentAsync(player, LoadScreen); + + return player; } - protected virtual Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) + protected virtual Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) => new Player { - return new Player - { - InitialBeatmap = beatmap - }; - } + InitialBeatmap = beatmap, + AllowPause = false + }; private const string test_beatmap_data = -@"osu file format v14 + @"osu file format v14 [General] AudioLeadIn: 500 diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 1f235e3893..d056afcf54 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -17,10 +17,11 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; namespace osu.Game.Users { - public class UserPanel : ClickableContainer, IHasContextMenu + public class UserPanel : OsuClickableContainer, IHasContextMenu { private readonly User user; private const float height = 100; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 87c8275512..ccd1bd03dc 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -271,6 +271,9 @@ + + + 20171019041408_InitialCreate.cs @@ -366,7 +369,7 @@ - + @@ -629,6 +632,7 @@ + @@ -784,6 +788,8 @@ + +