diff --git a/osu-framework b/osu-framework index 777996fb97..0f3db5da09 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 777996fb9731ba1895a5ab1323cbbc97259ff741 +Subproject commit 0f3db5da09d0e7c4d2ef3057030e018f34ba536e diff --git a/osu.Desktop.VisualTests/Tests/TestCaseBreadcrumbs.cs b/osu.Desktop.VisualTests/Tests/TestCaseBreadcrumbs.cs new file mode 100644 index 0000000000..658d2f92b1 --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseBreadcrumbs.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Framework.Graphics; + +namespace osu.Desktop.VisualTests.Tests +{ + internal class TestCaseBreadcrumbs : TestCase + { + public override string Description => @"breadcrumb > control"; + + public override void Reset() + { + base.Reset(); + + BreadcrumbControl c; + Add(c = new BreadcrumbControl + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Width = 0.5f, + }); + + AddStep(@"first", () => c.Current.Value = BreadcrumbTab.Click); + AddStep(@"second", () => c.Current.Value = BreadcrumbTab.The); + AddStep(@"third", () => c.Current.Value = BreadcrumbTab.Circles); + } + + private enum BreadcrumbTab + { + Click, + The, + Circles, + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseDrawableRoom.cs b/osu.Desktop.VisualTests/Tests/TestCaseDrawableRoom.cs new file mode 100644 index 0000000000..de58323abe --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseDrawableRoom.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Screens.Multiplayer; +using osu.Game.Online.Multiplayer; +using osu.Game.Users; +using osu.Game.Database; + +namespace osu.Desktop.VisualTests.Tests +{ + internal class TestCaseDrawableRoom : TestCase + { + public override string Description => @"Select your favourite room"; + + public override void Reset() + { + base.Reset(); + + DrawableRoom first; + DrawableRoom second; + Add(new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Width = 500f, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + first = new DrawableRoom(new Room()), + second = new DrawableRoom(new Room()), + } + }); + + first.Room.Name.Value = @"Great Room Right Here"; + first.Room.Host.Value = new User { Username = @"Naeferith", Id = 9492835, Country = new Country { FlagName = @"FR" }}; + first.Room.Status.Value = new RoomStatusOpen(); + first.Room.Beatmap.Value = new BeatmapMetadata { Title = @"Seiryu", Artist = @"Critical Crystal" }; + + second.Room.Name.Value = @"Relax It's The Weekend"; + second.Room.Host.Value = new User { Username = @"peppy", Id = 2, Country = new Country { FlagName = @"AU" }}; + second.Room.Status.Value = new RoomStatusPlaying(); + second.Room.Beatmap.Value = new BeatmapMetadata { Title = @"ZAQ", Artist = @"Serendipity" }; + + AddStep(@"change state", () => + { + first.Room.Status.Value = new RoomStatusPlaying(); + }); + + AddStep(@"change name", () => + { + first.Room.Name.Value = @"I Changed Name"; + }); + + AddStep(@"change host", () => + { + first.Room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } }; + }); + + AddStep(@"change beatmap", () => + { + first.Room.Beatmap.Value = null; + }); + + AddStep(@"change state", () => + { + first.Room.Status.Value = new RoomStatusOpen(); + }); + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs index 9dcba02849..95287c3199 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs @@ -11,6 +11,9 @@ using OpenTK; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Timing; +using osu.Framework.Configuration; +using OpenTK.Input; +using osu.Framework.Timing; namespace osu.Desktop.VisualTests.Tests { @@ -59,6 +62,51 @@ namespace osu.Desktop.VisualTests.Tests } }; + Action createPlayfieldWithNotesAcceptingInput = () => + { + Clear(); + + var rateAdjustClock = new StopwatchClock(true) { Rate = 0.5 }; + + ManiaPlayfield playField; + Add(playField = new ManiaPlayfield(4, new List { new TimingChange { BeatLength = 200 } }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1, -1), + Clock = new FramedClock(rateAdjustClock) + }); + + for (int t = 1000; t <= 2000; t += 100) + { + playField.Add(new DrawableNote(new Note + { + StartTime = t, + Column = 0 + }, new Bindable(Key.D))); + + playField.Add(new DrawableNote(new Note + { + StartTime = t, + Column = 3 + }, new Bindable(Key.K))); + } + + playField.Add(new DrawableHoldNote(new HoldNote + { + StartTime = 1000, + Duration = 1000, + Column = 1 + }, new Bindable(Key.F))); + + playField.Add(new DrawableHoldNote(new HoldNote + { + StartTime = 1000, + Duration = 1000, + Column = 2 + }, new Bindable(Key.J))); + }; + AddStep("1 column", () => createPlayfield(1, SpecialColumnPosition.Normal)); AddStep("4 columns", () => createPlayfield(4, SpecialColumnPosition.Normal)); AddStep("Left special style", () => createPlayfield(4, SpecialColumnPosition.Left)); @@ -76,11 +124,13 @@ namespace osu.Desktop.VisualTests.Tests AddWaitStep(10); AddStep("Right special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Right)); AddWaitStep(10); + + AddStep("Notes with input", () => createPlayfieldWithNotesAcceptingInput()); } private void triggerKeyDown(Column column) { - column.TriggerKeyDown(new InputState(), new KeyDownEventArgs + column.TriggerOnKeyDown(new InputState(), new KeyDownEventArgs { Key = column.Key, Repeat = false @@ -89,7 +139,7 @@ namespace osu.Desktop.VisualTests.Tests private void triggerKeyUp(Column column) { - column.TriggerKeyUp(new InputState(), new KeyUpEventArgs + column.TriggerOnKeyUp(new InputState(), new KeyUpEventArgs { Key = column.Key }); diff --git a/osu.Desktop.VisualTests/Tests/TestCaseUserPanel.cs b/osu.Desktop.VisualTests/Tests/TestCaseUserPanel.cs index 513bf24e0d..92d58c10c9 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseUserPanel.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseUserPanel.cs @@ -32,14 +32,14 @@ namespace osu.Desktop.VisualTests.Tests Username = @"flyte", Id = 3103765, Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/3103765/5b012e13611d5761caa7e24fecb3d3a16e1cf48fc2a3032cfd43dd444af83d82.jpeg" + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" }) { Width = 300 }, peppy = new UserPanel(new User { Username = @"peppy", Id = 2, Country = new Country { FlagName = @"AU" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/2/08cad88747c235a64fca5f1b770e100f120827ded1ffe3b66bfcd19c940afa65.jpeg" + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg" }) { Width = 300 }, }, }); diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index 68dc8bb7b8..5a532d7234 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -220,8 +220,10 @@ + + diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 125d8cdded..0ed2a0ba6f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Database; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; using OpenTK; +using osu.Game.Audio; namespace osu.Game.Rulesets.Mania.Beatmaps { @@ -161,9 +162,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps pattern.Add(new HoldNote { StartTime = HitObject.StartTime, - Samples = HitObject.Samples, Duration = endTimeData.Duration, Column = column, + Head = { Samples = sampleInfoListAt(HitObject.StartTime) }, + Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) }, }); } else if (positionData != null) @@ -178,6 +180,24 @@ namespace osu.Game.Rulesets.Mania.Beatmaps return pattern; } + + /// + /// Retrieves the sample info list at a point in time. + /// + /// The time to retrieve the sample info list from. + /// + private SampleInfoList sampleInfoListAt(double time) + { + var curveData = HitObject as IHasCurve; + + if (curveData == null) + return HitObject.Samples; + + double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.RepeatCount; + + int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime); + return curveData.RepeatSamples[index]; + } } } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 718e0967da..2d1f75e196 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -471,14 +471,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } else { - newObject = new HoldNote + var holdNote = new HoldNote { StartTime = startTime, - Samples = sampleInfoListAt(startTime), - EndSamples = sampleInfoListAt(endTime), Column = column, - Duration = endTime - startTime + Duration = endTime - startTime, + Head = { Samples = sampleInfoListAt(startTime) }, + Tail = { Samples = sampleInfoListAt(endTime) } }; + + + newObject = holdNote; } pattern.Add(newObject); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 8f438f9ff4..6ad7489e0f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -69,18 +69,21 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (holdNote) { - newObject = new HoldNote + var hold = new HoldNote { StartTime = HitObject.StartTime, - EndSamples = HitObject.Samples, Column = column, Duration = endTime - HitObject.StartTime }; - newObject.Samples.Add(new SampleInfo + hold.Head.Samples.Add(new SampleInfo { Name = SampleInfo.HIT_NORMAL }); + + hold.Tail.Samples = HitObject.Samples; + + newObject = hold; } else { diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs new file mode 100644 index 0000000000..d5cf57a5da --- /dev/null +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Mania.Judgements +{ + public class HoldNoteTailJudgement : ManiaJudgement + { + /// + /// Whether the hold note has been released too early and shouldn't give full score for the release. + /// + public bool HasBroken; + + public override int NumericResultForScore(ManiaHitResult result) + { + switch (result) + { + default: + return base.NumericResultForScore(result); + case ManiaHitResult.Great: + case ManiaHitResult.Perfect: + return base.NumericResultForScore(HasBroken ? ManiaHitResult.Good : result); + } + } + + public override int NumericResultForAccuracy(ManiaHitResult result) + { + switch (result) + { + default: + return base.NumericResultForAccuracy(result); + case ManiaHitResult.Great: + case ManiaHitResult.Perfect: + return base.NumericResultForAccuracy(HasBroken ? ManiaHitResult.Good : result); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs new file mode 100644 index 0000000000..852f97b3f2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Mania.Judgements +{ + public class HoldNoteTickJudgement : ManiaJudgement + { + public override bool AffectsCombo => false; + + public override int NumericResultForScore(ManiaHitResult result) => 20; + public override int NumericResultForAccuracy(ManiaHitResult result) => 0; // Don't count ticks into accuracy + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index 6e69da3da7..33083ca0f5 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -2,11 +2,37 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.Judgements { public class ManiaJudgement : Judgement { + /// + /// The maximum possible hit result. + /// + public const ManiaHitResult MAX_HIT_RESULT = ManiaHitResult.Perfect; + + /// + /// The result value for the combo portion of the score. + /// + public int ResultValueForScore => Result == HitResult.Miss ? 0 : NumericResultForScore(ManiaResult); + + /// + /// The result value for the accuracy portion of the score. + /// + public int ResultValueForAccuracy => Result == HitResult.Miss ? 0 : NumericResultForAccuracy(ManiaResult); + + /// + /// The maximum result value for the combo portion of the score. + /// + public int MaxResultValueForScore => NumericResultForScore(MAX_HIT_RESULT); + + /// + /// The maximum result value for the accuracy portion of the score. + /// + public int MaxResultValueForAccuracy => NumericResultForAccuracy(MAX_HIT_RESULT); + public override string ResultString => string.Empty; public override string MaxResultString => string.Empty; @@ -15,5 +41,42 @@ namespace osu.Game.Rulesets.Mania.Judgements /// The hit result. /// public ManiaHitResult ManiaResult; + + public virtual int NumericResultForScore(ManiaHitResult result) + { + switch (result) + { + default: + return 0; + case ManiaHitResult.Bad: + return 50; + case ManiaHitResult.Ok: + return 100; + case ManiaHitResult.Good: + return 200; + case ManiaHitResult.Great: + case ManiaHitResult.Perfect: + return 300; + } + } + + public virtual int NumericResultForAccuracy(ManiaHitResult result) + { + switch (result) + { + default: + return 0; + case ManiaHitResult.Bad: + return 50; + case ManiaHitResult.Ok: + return 100; + case ManiaHitResult.Good: + return 200; + case ManiaHitResult.Great: + return 300; + case ManiaHitResult.Perfect: + return 305; + } + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/BarLine.cs b/osu.Game.Rulesets.Mania/Objects/BarLine.cs new file mode 100644 index 0000000000..76a3d3920d --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/BarLine.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps.ControlPoints; + +namespace osu.Game.Rulesets.Mania.Objects +{ + public class BarLine : ManiaHitObject + { + /// + /// The control point which this bar line is part of. + /// + public TimingControlPoint ControlPoint; + + /// + /// The index of the beat which this bar line represents within the control point. + /// This is a "major" bar line if % == 0. + /// + public int BeatIndex; + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs new file mode 100644 index 0000000000..0b4d8b2d4e --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + /// + /// Visualises a . Although this derives DrawableManiaHitObject, + /// this does not handle input/sound like a normal hit object. + /// + public class DrawableBarLine : DrawableManiaHitObject + { + /// + /// Height of major bar line triangles. + /// + private const float triangle_height = 12; + + /// + /// Offset of the major bar line triangles from the sides of the bar line. + /// + private const float triangle_offset = 9; + + public DrawableBarLine(BarLine barLine) + : base(barLine) + { + RelativeSizeAxes = Axes.X; + Height = 1; + + Add(new Box + { + Name = "Bar line", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + }); + + bool isMajor = barLine.BeatIndex % (int)barLine.ControlPoint.TimeSignature == 0; + + if (isMajor) + { + Add(new EquilateralTriangle + { + Name = "Left triangle", + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopCentre, + Size = new Vector2(triangle_height), + X = -triangle_offset, + Rotation = 90 + }); + + Add(new EquilateralTriangle + { + Name = "Right triangle", + Anchor = Anchor.BottomRight, + Origin = Anchor.TopCentre, + Size = new Vector2(triangle_height), + X = triangle_offset, + Rotation = -90 + }); + } + + if (!isMajor && barLine.BeatIndex % 2 == 1) + Alpha = 0.2f; + } + + protected override void UpdateState(ArmedState state) + { + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index f9d027e7ce..5d7f3314cd 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -7,14 +7,34 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using OpenTK.Graphics; using osu.Framework.Configuration; using OpenTK.Input; +using osu.Framework.Input; +using OpenTK; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.Judgements; +using osu.Framework.Extensions.IEnumerableExtensions; namespace osu.Game.Rulesets.Mania.Objects.Drawables { + /// + /// Visualises a hit object. + /// public class DrawableHoldNote : DrawableManiaHitObject { - private readonly NotePiece headPiece; + private readonly DrawableNote head; + private readonly DrawableNote tail; + private readonly BodyPiece bodyPiece; - private readonly NotePiece tailPiece; + private readonly Container tickContainer; + + /// + /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. + /// + private double? holdStartTime; + + /// + /// Whether the hold note has been released too early and shouldn't give full score for the release. + /// + private bool hasBroken; public DrawableHoldNote(HoldNote hitObject, Bindable key = null) : base(hitObject, key) @@ -32,17 +52,39 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, - headPiece = new NotePiece + tickContainer = new Container + { + RelativeSizeAxes = Axes.Both, + RelativeCoordinateSpace = new Vector2(1, (float)HitObject.Duration) + }, + head = new DrawableHeadNote(this, key) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, - tailPiece = new NotePiece + tail = new DrawableTailNote(this, key) { Anchor = Anchor.BottomCentre, Origin = Anchor.TopCentre } }); + + foreach (var tick in HitObject.Ticks) + { + var drawableTick = new DrawableHoldNoteTick(tick) + { + HoldStartTime = () => holdStartTime + }; + + // To make the ticks relative to ourselves we need to offset them backwards + drawableTick.Y -= (float)HitObject.StartTime; + + tickContainer.Add(drawableTick); + AddNested(drawableTick); + } + + AddNested(head); + AddNested(tail); } public override Color4 AccentColour @@ -54,9 +96,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return; base.AccentColour = value; - headPiece.AccentColour = value; + tickContainer.Children.ForEach(t => t.AccentColour = value); + bodyPiece.AccentColour = value; - tailPiece.AccentColour = value; + head.AccentColour = value; + tail.AccentColour = value; } } @@ -64,14 +108,132 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { } - protected override void Update() + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - if (Time.Current > HitObject.StartTime) - headPiece.Colour = Color4.Green; - if (Time.Current > HitObject.EndTime) + // Make sure the keypress happened within the body of the hold note + if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime) + return false; + + if (args.Key != Key) + return false; + + if (args.Repeat) + return false; + + // The user has pressed during the body of the hold note, after the head note and its hit windows have passed + // and within the limited range of the above if-statement. This state will be managed by the head note if the + // user has pressed during the hit windows of the head note. + holdStartTime = Time.Current; + + return true; + } + + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + { + // Make sure that the user started holding the key during the hold note + if (!holdStartTime.HasValue) + return false; + + if (args.Key != Key) + return false; + + holdStartTime = null; + + // If the key has been released too early, the user should not receive full score for the release + if (!tail.Judged) + hasBroken = true; + + return true; + } + + /// + /// The head note of a hold. + /// + private class DrawableHeadNote : DrawableNote + { + private readonly DrawableHoldNote holdNote; + + public DrawableHeadNote(DrawableHoldNote holdNote, Bindable key = null) + : base(holdNote.HitObject.Head, key) { - bodyPiece.Colour = Color4.Green; - tailPiece.Colour = Color4.Green; + this.holdNote = holdNote; + + RelativePositionAxes = Axes.None; + Y = 0; + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (!base.OnKeyDown(state, args)) + return false; + + // We only want to trigger a holding state from the head if the head has received a judgement + if (!Judged) + return false; + + // If the key has been released too early, the user should not receive full score for the release + if (Judgement.Result == HitResult.Miss) + holdNote.hasBroken = true; + + // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held + // The body doesn't handle these early early hits, so we have to explicitly set the holding state here + holdNote.holdStartTime = Time.Current; + + return true; + } + } + + /// + /// The tail note of a hold. + /// + private class DrawableTailNote : DrawableNote + { + private readonly DrawableHoldNote holdNote; + + public DrawableTailNote(DrawableHoldNote holdNote, Bindable key = null) + : base(holdNote.HitObject.Tail, key) + { + this.holdNote = holdNote; + + RelativePositionAxes = Axes.None; + Y = 0; + } + + protected override ManiaJudgement CreateJudgement() => new HoldNoteTailJudgement(); + + protected override void CheckJudgement(bool userTriggered) + { + base.CheckJudgement(userTriggered); + + var tailJudgement = Judgement as HoldNoteTailJudgement; + if (tailJudgement == null) + return; + + tailJudgement.HasBroken = holdNote.hasBroken; + } + + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + { + // Make sure that the user started holding the key during the hold note + if (!holdNote.holdStartTime.HasValue) + return false; + + if (Judgement.Result != HitResult.None) + return false; + + if (args.Key != Key) + return false; + + UpdateJudgement(true); + + // Handled by the hold note, which will set holding = false + return false; + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + // Tail doesn't handle key down + return false; } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs new file mode 100644 index 0000000000..9ecc77d3fc --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -0,0 +1,121 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Mania.Judgements; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + /// + /// Visualises a hit object. + /// + public class DrawableHoldNoteTick : DrawableManiaHitObject + { + /// + /// References the time at which the user started holding the hold note. + /// + public Func HoldStartTime; + + /// + /// References whether the user is currently holding the hold note. + /// + public Func IsHolding; + + private readonly Container glowContainer; + + public DrawableHoldNoteTick(HoldNoteTick hitObject) + : base(hitObject) + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + + RelativeSizeAxes = Axes.X; + Size = new Vector2(1); + + Children = new[] + { + glowContainer = new CircularContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + }; + + // Set the default glow + AccentColour = Color4.White; + } + + public override Color4 AccentColour + { + get { return base.AccentColour; } + set + { + base.AccentColour = value; + + glowContainer.EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Glow, + Radius = 2f, + Roundness = 15f, + Colour = value.Opacity(0.3f) + }; + } + } + + protected override ManiaJudgement CreateJudgement() => new HoldNoteTickJudgement(); + + protected override void CheckJudgement(bool userTriggered) + { + if (!userTriggered) + return; + + if (Time.Current < HitObject.StartTime) + return; + + if (HoldStartTime?.Invoke() > HitObject.StartTime) + return; + + Judgement.ManiaResult = ManiaHitResult.Perfect; + Judgement.Result = HitResult.Hit; + } + + protected override void UpdateState(ArmedState state) + { + switch (State) + { + case ArmedState.Hit: + AccentColour = Color4.Green; + break; + } + } + + protected override void Update() + { + if (Judgement.Result != HitResult.None) + return; + + if (IsHolding?.Invoke() != true) + return; + + UpdateJudgement(true); + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 42bb371975..658d409bb8 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -13,6 +13,9 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.Objects.Drawables { + /// + /// Visualises a hit object. + /// public class DrawableNote : DrawableManiaHitObject { private readonly NotePiece headPiece; diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 30e71aeb5d..fa32d46a88 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -1,10 +1,9 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . +// Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Audio; +using System.Collections.Generic; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Database; -using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Mania.Objects @@ -12,32 +11,99 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// Represents a hit object which requires pressing, holding, and releasing a key. /// - public class HoldNote : Note, IHasEndTime + public class HoldNote : ManiaHitObject, IHasEndTime { - /// - /// Lenience of release hit windows. This is to make cases where the hold note release - /// is timed alongside presses of other hit objects less awkward. - /// - private const double release_window_lenience = 1.5; - - public double Duration { get; set; } public double EndTime => StartTime + Duration; - /// - /// The samples to be played when this hold note is released. - /// - public SampleInfoList EndSamples = new SampleInfoList(); + private double duration; + public double Duration + { + get { return duration; } + set + { + duration = value; + Tail.StartTime = EndTime; + } + } + + public override double StartTime + { + get { return base.StartTime; } + set + { + base.StartTime = value; + Head.StartTime = value; + Tail.StartTime = EndTime; + } + } /// - /// The key-release hit windows for this hold note. + /// The head note of the hold. /// - public HitWindows ReleaseHitWindows { get; protected set; } = new HitWindows(); + public readonly Note Head = new Note(); + + /// + /// The tail note of the hold. + /// + public readonly Note Tail = new TailNote(); + + /// + /// The time between ticks of this hold. + /// + private double tickSpacing = 50; public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaults(controlPointInfo, difficulty); - ReleaseHitWindows = HitWindows * release_window_lenience; + TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); + tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate; + + Head.ApplyDefaults(controlPointInfo, difficulty); + Tail.ApplyDefaults(controlPointInfo, difficulty); + } + + /// + /// The scoring scoring ticks of the hold note. + /// + public IEnumerable Ticks => ticks ?? (ticks = createTicks()); + private List ticks; + + private List createTicks() + { + var ret = new List(); + + if (tickSpacing == 0) + return ret; + + for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing) + { + ret.Add(new HoldNoteTick + { + StartTime = t + }); + } + + return ret; + } + + /// + /// The tail of the hold note. + /// + private class TailNote : Note + { + /// + /// Lenience of release hit windows. This is to make cases where the hold note release + /// is timed alongside presses of other hit objects less awkward. + /// + private const double release_window_lenience = 1.5; + + public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaults(controlPointInfo, difficulty); + + HitWindows *= release_window_lenience; + } } } } diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs new file mode 100644 index 0000000000..6c4cf127f3 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Mania.Objects +{ + /// + /// A scoring tick of a hold note. + /// + public class HoldNoteTick : ManiaHitObject + { + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 7a9572a0c7..798d4b8c5b 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -1,8 +1,13 @@ // 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.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -10,6 +15,143 @@ namespace osu.Game.Rulesets.Mania.Scoring { internal class ManiaScoreProcessor : ScoreProcessor { + /// + /// The maximum score achievable. + /// Does _not_ include bonus score - for bonus score see . + /// + private const int max_score = 1000000; + + /// + /// The amount of the score attributed to combo. + /// + private const double combo_portion_max = max_score * 0.2; + + /// + /// The amount of the score attributed to accuracy. + /// + private const double accuracy_portion_max = max_score * 0.8; + + /// + /// The factor used to determine relevance of combos. + /// + private const double combo_base = 4; + + /// + /// The combo value at which hit objects result in the max score possible. + /// + private const int combo_relevance_cap = 400; + + /// + /// The hit HP multiplier at OD = 0. + /// + private const double hp_multiplier_min = 0.75; + + /// + /// The hit HP multiplier at OD = 0. + /// + private const double hp_multiplier_mid = 0.85; + + /// + /// The hit HP multiplier at OD = 0. + /// + private const double hp_multiplier_max = 1; + + /// + /// The default BAD hit HP increase. + /// + private const double hp_increase_bad = 0.005; + + /// + /// The default OK hit HP increase. + /// + private const double hp_increase_ok = 0.010; + + /// + /// The default GOOD hit HP increase. + /// + private const double hp_increase_good = 0.035; + + /// + /// The default tick hit HP increase. + /// + private const double hp_increase_tick = 0.040; + + /// + /// The default GREAT hit HP increase. + /// + private const double hp_increase_great = 0.055; + + /// + /// The default PERFECT hit HP increase. + /// + private const double hp_increase_perfect = 0.065; + + /// + /// The MISS HP multiplier at OD = 0. + /// + private const double hp_multiplier_miss_min = 0.5; + + /// + /// The MISS HP multiplier at OD = 5. + /// + private const double hp_multiplier_miss_mid = 0.75; + + /// + /// The MISS HP multiplier at OD = 10. + /// + private const double hp_multiplier_miss_max = 1; + + /// + /// The default MISS HP increase. + /// + private const double hp_increase_miss = -0.125; + + /// + /// The MISS HP multiplier. This is multiplied to the miss hp increase. + /// + private double hpMissMultiplier = 1; + + /// + /// The HIT HP multiplier. This is multiplied to hit hp increases. + /// + private double hpMultiplier = 1; + + /// + /// The cumulative combo portion of the score. + /// + private double comboScore => combo_portion_max * comboPortion / maxComboPortion; + + /// + /// The cumulative accuracy portion of the score. + /// + private double accuracyScore => accuracy_portion_max * Math.Pow(Accuracy, 4) * totalHits / maxTotalHits; + + /// + /// The cumulative bonus score. + /// This is added on top of , thus the total score can exceed . + /// + private double bonusScore; + + /// + /// The achieved by a perfect playthrough. + /// + private double maxComboPortion; + + /// + /// The portion of the score dedicated to combo. + /// + private double comboPortion; + + /// + /// The achieved by a perfect playthrough. + /// + private int maxTotalHits; + + /// + /// The total hits. + /// + private int totalHits; + public ManiaScoreProcessor() { } @@ -19,8 +161,124 @@ namespace osu.Game.Rulesets.Mania.Scoring { } + protected override void ComputeTargets(Beatmap beatmap) + { + BeatmapDifficulty difficulty = beatmap.BeatmapInfo.Difficulty; + hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max); + hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max); + + while (true) + { + foreach (var obj in beatmap.HitObjects) + { + var holdNote = obj as HoldNote; + + if (obj is Note) + { + AddJudgement(new ManiaJudgement + { + Result = HitResult.Hit, + ManiaResult = ManiaHitResult.Perfect + }); + } + else if (holdNote != null) + { + // Head + AddJudgement(new ManiaJudgement + { + Result = HitResult.Hit, + ManiaResult = ManiaJudgement.MAX_HIT_RESULT + }); + + // Ticks + int tickCount = holdNote.Ticks.Count(); + for (int i = 0; i < tickCount; i++) + { + AddJudgement(new HoldNoteTickJudgement + { + Result = HitResult.Hit, + ManiaResult = ManiaJudgement.MAX_HIT_RESULT, + }); + } + + AddJudgement(new HoldNoteTailJudgement + { + Result = HitResult.Hit, + ManiaResult = ManiaJudgement.MAX_HIT_RESULT + }); + } + } + + if (!HasFailed) + break; + + hpMultiplier *= 1.01; + hpMissMultiplier *= 0.98; + + Reset(); + } + + maxTotalHits = totalHits; + maxComboPortion = comboPortion; + } + protected override void OnNewJudgement(ManiaJudgement judgement) { + bool isTick = judgement is HoldNoteTickJudgement; + + if (!isTick) + totalHits++; + + switch (judgement.Result) + { + case HitResult.Miss: + Health.Value += hpMissMultiplier * hp_increase_miss; + break; + case HitResult.Hit: + if (isTick) + { + Health.Value += hpMultiplier * hp_increase_tick; + bonusScore += judgement.ResultValueForScore; + } + else + { + switch (judgement.ManiaResult) + { + case ManiaHitResult.Bad: + Health.Value += hpMultiplier * hp_increase_bad; + break; + case ManiaHitResult.Ok: + Health.Value += hpMultiplier * hp_increase_ok; + break; + case ManiaHitResult.Good: + Health.Value += hpMultiplier * hp_increase_good; + break; + case ManiaHitResult.Great: + Health.Value += hpMultiplier * hp_increase_great; + break; + case ManiaHitResult.Perfect: + Health.Value += hpMultiplier * hp_increase_perfect; + break; + } + + // A factor that is applied to make higher combos more relevant + double comboRelevance = Math.Min(Math.Max(0.5, Math.Log(Combo.Value, combo_base)), Math.Log(combo_relevance_cap, combo_base)); + comboPortion += judgement.ResultValueForScore * comboRelevance; + } + break; + } + + int scoreForAccuracy = 0; + int maxScoreForAccuracy = 0; + + foreach (var j in Judgements) + { + scoreForAccuracy += j.ResultValueForAccuracy; + maxScoreForAccuracy += j.MaxResultValueForAccuracy; + } + + Accuracy.Value = (double)scoreForAccuracy / maxScoreForAccuracy; + TotalScore.Value = comboScore + accuracyScore + bonusScore; } protected override void Reset() @@ -28,6 +286,10 @@ namespace osu.Game.Rulesets.Mania.Scoring base.Reset(); Health.Value = 1; + + bonusScore = 0; + comboPortion = 0; + totalHits = 0; } } } diff --git a/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs index cc8897840e..0a8bc2d44a 100644 --- a/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs +++ b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using OpenTK; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Timing { @@ -128,6 +129,8 @@ namespace osu.Game.Rulesets.Mania.Timing /// private class AutoTimeRelativeContainer : Container { + protected override IComparer DepthComparer => new HitObjectReverseStartTimeComparer(); + public override void InvalidateFromChild(Invalidation invalidation) { // We only want to re-compute our size when a child's size or position has changed diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index c8cb5f6387..6dfd5000d4 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - using OpenTK; using OpenTK.Graphics; using OpenTK.Input; @@ -188,7 +187,11 @@ namespace osu.Game.Rulesets.Mania.UI } } - public void Add(DrawableHitObject hitObject) => ControlPointContainer.Add(hitObject); + public void Add(DrawableHitObject hitObject) + { + hitObject.AccentColour = AccentColour; + ControlPointContainer.Add(hitObject); + } private bool onKeyDown(InputState state, KeyDownEventArgs args) { diff --git a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs index 95b7979e43..57477147d5 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs @@ -6,9 +6,11 @@ using System.Collections.Generic; using System.Linq; using OpenTK; using OpenTK.Input; +using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Lists; +using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Beatmaps; @@ -85,6 +87,34 @@ namespace osu.Game.Rulesets.Mania.UI }; } + [BackgroundDependencyLoader] + private void load() + { + var maniaPlayfield = (ManiaPlayfield)Playfield; + + double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; + + SortedList timingPoints = Beatmap.ControlPointInfo.TimingPoints; + for (int i = 0; i < timingPoints.Count; i++) + { + TimingControlPoint point = timingPoints[i]; + + // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object + double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - point.BeatLength : lastObjectTime + point.BeatLength * (int)point.TimeSignature; + + int index = 0; + for (double t = timingPoints[i].Time; Precision.DefinitelyBigger(endTime, t); t += point.BeatLength, index++) + { + maniaPlayfield.Add(new DrawableBarLine(new BarLine + { + StartTime = t, + ControlPoint = point, + BeatIndex = index + })); + } + } + } + public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index ff763f87c4..2e6b63579e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Mania.Timing; using osu.Framework.Input; using osu.Framework.Graphics.Transforms; using osu.Framework.MathUtils; +using osu.Game.Rulesets.Mania.Objects.Drawables; namespace osu.Game.Rulesets.Mania.UI { @@ -57,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly FlowContainer columns; public IEnumerable Columns => columns.Children; - private readonly ControlPointContainer barlineContainer; + private readonly ControlPointContainer barLineContainer; private List normalColumnColours = new List(); private Color4 specialColumnColour; @@ -77,35 +78,51 @@ namespace osu.Game.Rulesets.Mania.UI { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Masking = true, Children = new Drawable[] { - new Box + new Container { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black - }, - columns = new FillFlowContainer - { - Name = "Columns", + Name = "Masked elements", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Left = 1, Right = 1 }, - Spacing = new Vector2(1, 0) + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + columns = new FillFlowContainer + { + Name = "Columns", + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = 1, Right = 1 }, + Spacing = new Vector2(1, 0) + } + } }, new Container { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = HIT_TARGET_POSITION }, Children = new[] { - barlineContainer = new ControlPointContainer(timingChanges) + barLineContainer = new ControlPointContainer(timingChanges) { Name = "Bar lines", - RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y + // Width is set in the Update method } } } @@ -190,6 +207,7 @@ namespace osu.Game.Rulesets.Mania.UI } public override void Add(DrawableHitObject h) => Columns.ElementAt(h.HitObject.Column).Add(h); + public void Add(DrawableBarLine barline) => barLineContainer.Add(barline); protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { @@ -224,7 +242,7 @@ namespace osu.Game.Rulesets.Mania.UI timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max); - barlineContainer.TimeSpan = value; + barLineContainer.TimeSpan = value; Columns.ForEach(c => c.ControlPointContainer.TimeSpan = value); } } @@ -234,6 +252,13 @@ namespace osu.Game.Rulesets.Mania.UI TransformTo(() => TimeSpan, newTimeSpan, duration, easing, new TransformTimeSpan()); } + protected override void Update() + { + // Due to masking differences, it is not possible to get the width of the columns container automatically + // While masking on effectively only the Y-axis, so we need to set the width of the bar line container manually + barLineContainer.Width = columns.Width; + } + private class TransformTimeSpan : Transform { public override double CurrentValue diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 9442d7cf8f..3d5614bd90 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -57,17 +57,23 @@ + + + + + + diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index eff60ba935..6ba499739a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects base.ApplyDefaults(controlPointInfo, difficulty); SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5)); + + // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. + SpinsRequired = (int)(SpinsRequired * 0.6); } } } diff --git a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs index cb1d9d2fcd..c6aac3bb71 100644 --- a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs @@ -428,6 +428,9 @@ namespace osu.Game.Beatmaps.Formats break; } } + + foreach (var hitObject in beatmap.HitObjects) + hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.Difficulty); } internal enum LegacySampleBank diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 5cca57be8a..7a2345a80c 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -1,20 +1,36 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.MathUtils; using OpenTK; using OpenTK.Graphics; using System; +using osu.Framework.Graphics.OpenGL; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Allocation; +using System.Collections.Generic; +using osu.Framework.Graphics.Batches; +using osu.Framework.Lists; namespace osu.Game.Graphics.Backgrounds { - public class Triangles : Container + public class Triangles : Drawable { + private const float triangle_size = 100; + private const float base_velocity = 50; + + /// + /// How many screen-space pixels are smoothed over. + /// Same behavior as Sprite's EdgeSmoothness. + /// + private const float edge_smoothness = 1; + public override bool HandleInput => false; public Color4 ColourLight = Color4.White; @@ -49,6 +65,28 @@ namespace osu.Game.Graphics.Backgrounds /// public float Velocity = 1; + private readonly SortedList parts = new SortedList(Comparer.Default); + + private Shader shader; + private readonly Texture texture; + + public Triangles() + { + texture = Texture.WhitePixel; + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + shader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + addTriangles(true); + } + public float TriangleScale { get { return triangleScale; } @@ -57,42 +95,74 @@ namespace osu.Game.Graphics.Backgrounds float change = value / triangleScale; triangleScale = value; - if (change != 1) - Children.ForEach(t => t.Scale *= change); + for (int i = 0; i < parts.Count; i++) + { + TriangleParticle newParticle = parts[i]; + newParticle.Scale *= change; + parts[i] = newParticle; + } } } - protected override void LoadComplete() - { - base.LoadComplete(); - - addTriangles(true); - } - - private int aimTriangleCount => (int)(DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio); - protected override void Update() { base.Update(); + Invalidate(Invalidation.DrawNode, shallPropagate: false); + + if (CreateNewTriangles) + addTriangles(false); + float adjustedAlpha = HideAlphaDiscrepancies ? // Cubically scale alpha to make it drop off more sharply. (float)Math.Pow(DrawInfo.Colour.AverageColour.Linear.A, 3) : 1; - foreach (var t in Children) - { - t.Alpha = adjustedAlpha; - t.Position -= new Vector2(0, (float)(t.Scale.X * (50 / DrawHeight) * (Time.Elapsed / 950)) / triangleScale * Velocity); - if (ExpireOffScreenTriangles && t.DrawPosition.Y + t.DrawSize.Y * t.Scale.Y < 0) - t.Expire(); - } + float elapsedSeconds = (float)Time.Elapsed / 1000; + // Since position is relative, the velocity needs to scale inversely with DrawHeight. + // Since we will later multiply by the scale of individual triangles we normalize by + // dividing by triangleScale. + float movedDistance = -elapsedSeconds * Velocity * base_velocity / (DrawHeight * triangleScale); - if (CreateNewTriangles) - addTriangles(false); + for (int i = 0; i < parts.Count; i++) + { + TriangleParticle newParticle = parts[i]; + + // Scale moved distance by the size of the triangle. Smaller triangles should move more slowly. + newParticle.Position.Y += parts[i].Scale * movedDistance; + newParticle.Colour.A = adjustedAlpha; + + parts[i] = newParticle; + + float bottomPos = parts[i].Position.Y + triangle_size * parts[i].Scale * 0.866f / DrawHeight; + if (bottomPos < 0) + parts.RemoveAt(i); + } } - protected virtual Triangle CreateTriangle() + private void addTriangles(bool randomY) + { + int aimTriangleCount = (int)(DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio); + + for (int i = 0; i < aimTriangleCount - parts.Count; i++) + parts.Add(createTriangle(randomY)); + } + + private TriangleParticle createTriangle(bool randomY) + { + TriangleParticle particle = CreateTriangle(); + + particle.Position = new Vector2(RNG.NextSingle(), randomY ? RNG.NextSingle() : 1); + particle.Colour = CreateTriangleShade(); + + return particle; + } + + /// + /// Creates a triangle particle with a random scale. + /// + /// The triangle particle. + protected virtual TriangleParticle CreateTriangle() { const float std_dev = 0.16f; const float mean = 0.5f; @@ -102,32 +172,108 @@ namespace osu.Game.Graphics.Backgrounds float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); //random normal(0,1) var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); //random normal(mean,stdDev^2) - const float size = 100; - - return new EquilateralTriangle - { - Origin = Anchor.TopCentre, - RelativePositionAxes = Axes.Both, - Size = new Vector2(size), - Scale = new Vector2(scale), - EdgeSmoothness = new Vector2(1), - Colour = GetTriangleShade(), - Depth = scale, - }; + return new TriangleParticle { Scale = scale }; } - protected virtual Color4 GetTriangleShade() => Interpolation.ValueAt(RNG.NextSingle(), ColourDark, ColourLight, 0, 1); + /// + /// Creates a shade of colour for the triangles. + /// + /// The colour. + protected virtual Color4 CreateTriangleShade() => Interpolation.ValueAt(RNG.NextSingle(), ColourDark, ColourLight, 0, 1); - private void addTriangles(bool randomY) + protected override DrawNode CreateDrawNode() => new TrianglesDrawNode(); + + private readonly TrianglesDrawNodeSharedData sharedData = new TrianglesDrawNodeSharedData(); + protected override void ApplyDrawNode(DrawNode node) { - int addCount = aimTriangleCount - Children.Count(); - for (int i = 0; i < addCount; i++) + base.ApplyDrawNode(node); + + var trianglesNode = (TrianglesDrawNode)node; + + trianglesNode.Shader = shader; + trianglesNode.Texture = texture; + trianglesNode.Size = DrawSize; + trianglesNode.Shared = sharedData; + + trianglesNode.Parts.Clear(); + trianglesNode.Parts.AddRange(parts); + } + + private class TrianglesDrawNodeSharedData + { + public readonly LinearBatch VertexBatch = new LinearBatch(100 * 3, 10, PrimitiveType.Triangles); + } + + private class TrianglesDrawNode : DrawNode + { + public Shader Shader; + public Texture Texture; + + public TrianglesDrawNodeSharedData Shared; + + public readonly List Parts = new List(); + public Vector2 Size; + + public override void Draw(Action vertexAction) { - var sprite = CreateTriangle(); - float triangleHeight = sprite.DrawHeight / DrawHeight; - sprite.Position = new Vector2(RNG.NextSingle(), randomY ? RNG.NextSingle() * (1 + triangleHeight) - triangleHeight : 1); - Add(sprite); + base.Draw(vertexAction); + + Shader.Bind(); + Texture.TextureGL.Bind(); + + Vector2 localInflationAmount = edge_smoothness * DrawInfo.MatrixInverse.ExtractScale().Xy; + + foreach (TriangleParticle particle in Parts) + { + var offset = triangle_size * new Vector2(particle.Scale * 0.5f, particle.Scale * 0.866f); + var size = new Vector2(2 * offset.X, offset.Y); + + var triangle = new Triangle( + particle.Position * Size * DrawInfo.Matrix, + (particle.Position * Size + offset) * DrawInfo.Matrix, + (particle.Position * Size + new Vector2(-offset.X, offset.Y)) * DrawInfo.Matrix + ); + + ColourInfo colourInfo = DrawInfo.Colour; + colourInfo.ApplyChild(particle.Colour); + + Texture.DrawTriangle( + triangle, + colourInfo, + null, + Shared.VertexBatch.Add, + Vector2.Divide(localInflationAmount, size)); + } + + Shader.Unbind(); } } + + protected struct TriangleParticle : IComparable + { + /// + /// The position of the top vertex of the triangle. + /// + public Vector2 Position; + + /// + /// The colour of the triangle. + /// + public Color4 Colour; + + /// + /// The scale of the triangle. + /// + public float Scale; + + /// + /// Compares two s. This is a reverse comparer because when the + /// triangles are added to the particles list, they should be drawn from largest to smallest + /// such that the smaller triangles appear on top. + /// + /// + /// + public int CompareTo(TriangleParticle other) => other.Scale.CompareTo(Scale); + } } } diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs new file mode 100644 index 0000000000..8d113f4918 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using System.Linq; + +namespace osu.Game.Graphics.UserInterface +{ + public class BreadcrumbControl : OsuTabControl + { + private const float padding = 10; + + protected override TabItem CreateTabItem(T value) => new BreadcrumbTabItem(value); + + public BreadcrumbControl() + { + Height = 26; + TabContainer.Spacing = new Vector2(padding, 0f); + Current.ValueChanged += tab => + { + foreach (var t in TabContainer.Children.OfType()) + { + var tIndex = TabContainer.IndexOf(t); + var tabIndex = TabContainer.IndexOf(TabMap[tab]); + + t.State = tIndex < tabIndex ? Visibility.Hidden : Visibility.Visible; + t.Chevron.FadeTo(tIndex <= tabIndex ? 0f : 1f, 500, EasingTypes.OutQuint); + } + }; + } + + private class BreadcrumbTabItem : OsuTabItem, IStateful + { + public readonly TextAwesome Chevron; + + //don't allow clicking between transitions and don't make the chevron clickable + protected override bool InternalContains(Vector2 screenSpacePos) => Alpha == 1f && Text.Contains(screenSpacePos); + public override bool HandleInput => State == Visibility.Visible; + + private Visibility state; + public Visibility State + { + get { return state; } + set + { + if (value == state) return; + state = value; + + const float transition_duration = 500; + + if (State == Visibility.Visible) + { + FadeIn(transition_duration, EasingTypes.OutQuint); + ScaleTo(new Vector2(1f), transition_duration, EasingTypes.OutQuint); + } + else + { + FadeOut(transition_duration, EasingTypes.OutQuint); + ScaleTo(new Vector2(0.8f, 1f), transition_duration, EasingTypes.OutQuint); + } + } + } + + public BreadcrumbTabItem(T value) : base(value) + { + Text.TextSize = 16; + Padding = new MarginPadding { Right = padding + 8 }; //padding + chevron width + Add(Chevron = new TextAwesome + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreLeft, + TextSize = 12, + Icon = FontAwesome.fa_chevron_right, + Margin = new MarginPadding { Left = padding }, + Alpha = 0f, + }); + } + } + } +} diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 2f53d00c7e..fe1d255bba 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -3,6 +3,7 @@ using OpenTK.Graphics; using OpenTK.Input; +using osu.Framework.Allocation; using osu.Framework.Input; using System; using System.Linq; @@ -23,11 +24,19 @@ namespace osu.Game.Graphics.UserInterface set { focus = value; - if (!focus) - TriggerFocusLost(); + if (!focus && HasFocus) + inputManager.ChangeFocus(null); } } + private InputManager inputManager; + + [BackgroundDependencyLoader] + private void load(UserInputManager inputManager) + { + this.inputManager = inputManager; + } + protected override bool OnFocus(InputState state) { var result = base.OnFocus(state); diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 9c1799c04c..14483f3bfb 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -42,7 +42,7 @@ namespace osu.Game.Graphics.UserInterface protected override DropdownMenuItem CreateMenuItem(string text, T value) => new OsuDropdownMenuItem(text, value) { AccentColour = AccentColour }; - private class OsuDropdownMenuItem : DropdownMenuItem + public class OsuDropdownMenuItem : DropdownMenuItem { public OsuDropdownMenuItem(string text, T current) : base(text, current) { @@ -60,7 +60,7 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Y, Children = new Drawable[] { - chevron = new TextAwesome + Chevron = new TextAwesome { AlwaysPresent = true, Icon = FontAwesome.fa_chevron_right, @@ -84,12 +84,12 @@ namespace osu.Game.Graphics.UserInterface private Color4? accentColour; - private readonly TextAwesome chevron; + protected readonly TextAwesome Chevron; protected override void FormatForeground(bool hover = false) { base.FormatForeground(hover); - chevron.Alpha = hover ? 1 : 0; + Chevron.Alpha = hover ? 1 : 0; } public Color4 AccentColour @@ -115,11 +115,11 @@ namespace osu.Game.Graphics.UserInterface public class OsuDropdownHeader : DropdownHeader { - private readonly SpriteText label; + protected readonly SpriteText Text; protected override string Label { - get { return label.Text; } - set { label.Text = value; } + get { return Text.Text; } + set { Text.Text = value; } } protected readonly TextAwesome Icon; @@ -146,7 +146,7 @@ namespace osu.Game.Graphics.UserInterface Foreground.Children = new Drawable[] { - label = new OsuSpriteText + Text = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index 237aaa44a6..ebaef661c4 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -5,18 +5,21 @@ using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using OpenTK; using OpenTK.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Extensions.Color4Extensions; +using osu.Game.Graphics.Containers; +using osu.Game.Beatmaps.ControlPoints; +using osu.Framework.Audio.Track; +using System; namespace osu.Game.Graphics.UserInterface { public class TwoLayerButton : ClickableContainer { - private readonly TextAwesome icon; + private readonly BouncingIcon bouncingIcon; public Box IconLayer; public Box TextLayer; @@ -95,11 +98,10 @@ namespace osu.Game.Graphics.UserInterface }, } }, - icon = new TextAwesome + bouncingIcon = new BouncingIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - TextSize = 25, }, } }, @@ -146,7 +148,7 @@ namespace osu.Game.Graphics.UserInterface { set { - icon.Icon = value; + bouncingIcon.Icon = value; } } @@ -162,58 +164,20 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(InputState state) { - icon.ClearTransforms(); - ResizeTo(SIZE_EXTENDED, transform_time, EasingTypes.OutElastic); - - int duration = 0; //(int)(Game.Audio.BeatLength / 2); - if (duration == 0) duration = pulse_length; - IconLayer.FadeColour(HoverColour, transform_time, EasingTypes.OutElastic); - const double offset = 0; //(1 - Game.Audio.SyncBeatProgress) * duration; - double startTime = Time.Current + offset; - - // basic pulse - icon.Transforms.Add(new TransformScale - { - StartValue = new Vector2(1.1f), - EndValue = Vector2.One, - StartTime = startTime, - EndTime = startTime + duration, - Easing = EasingTypes.Out, - LoopCount = -1, - LoopDelay = duration - }); + bouncingIcon.ScaleTo(1.1f, transform_time, EasingTypes.OutElastic); return true; } protected override void OnHoverLost(InputState state) { - icon.ClearTransforms(); - ResizeTo(SIZE_RETRACTED, transform_time, EasingTypes.OutElastic); - IconLayer.FadeColour(TextLayer.Colour, transform_time, EasingTypes.OutElastic); - int duration = 0; //(int)(Game.Audio.BeatLength); - if (duration == 0) duration = pulse_length * 2; - - const double offset = 0; //(1 - Game.Audio.SyncBeatProgress) * duration; - double startTime = Time.Current + offset; - - // slow pulse - icon.Transforms.Add(new TransformScale - { - StartValue = new Vector2(1.1f), - EndValue = Vector2.One, - StartTime = startTime, - EndTime = startTime + duration, - Easing = EasingTypes.Out, - LoopCount = -1, - LoopDelay = duration - }); + bouncingIcon.ScaleTo(1, transform_time, EasingTypes.OutElastic); } protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) @@ -239,5 +203,45 @@ namespace osu.Game.Graphics.UserInterface return base.OnClick(state); } + + private class BouncingIcon : BeatSyncedContainer + { + private const double beat_in_time = 60; + + private readonly TextAwesome icon; + + public FontAwesome Icon { set { icon.Icon = value; } } + + public BouncingIcon() + { + EarlyActivationMilliseconds = beat_in_time; + AutoSizeAxes = Axes.Both; + + Children = new Drawable[] + { + icon = new TextAwesome + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + TextSize = 25 + } + }; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + var beatLength = timingPoint.BeatLength; + + float amplitudeAdjust = Math.Min(1, 0.4f + amplitudes.Maximum); + + if (beatIndex < 0) return; + + icon.ScaleTo(1 - 0.1f * amplitudeAdjust, beat_in_time, EasingTypes.Out); + using (icon.BeginDelayedSequence(beat_in_time)) + icon.ScaleTo(1, beatLength * 2, EasingTypes.OutQuint); + } + } } } \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs index f2ae47354e..a758d5fdef 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs +++ b/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs @@ -74,7 +74,7 @@ namespace osu.Game.Graphics.UserInterface.Volume return; } - volumeMeterMaster.TriggerWheel(state); + volumeMeterMaster.TriggerOnWheel(state); } [BackgroundDependencyLoader] diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs new file mode 100644 index 0000000000..c82025f902 --- /dev/null +++ b/osu.Game/Online/Multiplayer/Room.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.Framework.Configuration; +using osu.Game.Database; +using osu.Game.Users; + +namespace osu.Game.Online.Multiplayer +{ + public class Room + { + public Bindable Name = new Bindable(); + public Bindable Host = new Bindable(); + public Bindable Status = new Bindable(); + public Bindable Beatmap = new Bindable(); + } +} diff --git a/osu.Game/Online/Multiplayer/RoomStatus.cs b/osu.Game/Online/Multiplayer/RoomStatus.cs new file mode 100644 index 0000000000..4f943596a7 --- /dev/null +++ b/osu.Game/Online/Multiplayer/RoomStatus.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK.Graphics; +using osu.Game.Graphics; + +namespace osu.Game.Online.Multiplayer +{ + public abstract class RoomStatus + { + public abstract string Message { get; } + public abstract Color4 GetAppropriateColour(OsuColour colours); + } + + public class RoomStatusOpen : RoomStatus + { + public override string Message => @"Welcoming Players"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight; + } + + public class RoomStatusPlaying : RoomStatus + { + public override string Message => @"Now Playing"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple; + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 886ff4f8d1..2c952ee514 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -255,6 +255,9 @@ namespace osu.Game settings.ToggleVisibility(); return true; case Key.D: + if (state.Keyboard.ShiftPressed || state.Keyboard.AltPressed) + return false; + direct.ToggleVisibility(); return true; } diff --git a/osu.Game/Overlays/Chat/ChatTabControl.cs b/osu.Game/Overlays/Chat/ChatTabControl.cs index 57447f1913..a281cff7db 100644 --- a/osu.Game/Overlays/Chat/ChatTabControl.cs +++ b/osu.Game/Overlays/Chat/ChatTabControl.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Chat; using OpenTK; using OpenTK.Graphics; +using osu.Framework.Configuration; namespace osu.Game.Overlays.Chat { @@ -23,6 +24,8 @@ namespace osu.Game.Overlays.Chat private const float shear_width = 10; + public readonly Bindable ChannelSelectorActive = new Bindable(); + public ChatTabControl() { TabContainer.Margin = new MarginPadding { Left = 50 }; @@ -37,6 +40,8 @@ namespace osu.Game.Overlays.Chat TextSize = 20, Padding = new MarginPadding(10), }); + + AddTabItem(new ChannelTabItem.ChannelSelectorTabItem(new Channel { Name = "+" }, ChannelSelectorActive)); } private class ChannelTabItem : TabItem @@ -49,6 +54,7 @@ namespace osu.Game.Overlays.Chat private readonly SpriteText textBold; private readonly Box box; private readonly Box highlightBox; + private readonly TextAwesome icon; public override bool Active { @@ -114,6 +120,11 @@ namespace osu.Game.Overlays.Chat backgroundHover = colours.Gray7; highlightBox.Colour = colours.Yellow; + } + + protected override void LoadComplete() + { + base.LoadComplete(); updateState(); } @@ -159,7 +170,7 @@ namespace osu.Game.Overlays.Chat RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new TextAwesome + icon = new TextAwesome { Icon = FontAwesome.fa_hashtag, Anchor = Anchor.CentreLeft, @@ -191,6 +202,40 @@ namespace osu.Game.Overlays.Chat } }; } + + public class ChannelSelectorTabItem : ChannelTabItem + { + public override bool Active + { + get { return base.Active; } + set + { + activeBindable.Value = value; + base.Active = value; + } + } + + private readonly Bindable activeBindable; + + public ChannelSelectorTabItem(Channel value, Bindable active) : base(value) + { + activeBindable = active; + Depth = float.MaxValue; + Width = 45; + + icon.Alpha = 0; + + text.TextSize = 45; + textBold.TextSize = 45; + } + + [BackgroundDependencyLoader] + private new void load(OsuColour colour) + { + backgroundInactive = colour.Gray2; + backgroundActive = colour.Gray3; + } + } } } } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 686a1d513a..5b2c01151c 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -135,17 +135,22 @@ namespace osu.Game.Overlays channelTabs.Current.ValueChanged += newChannel => CurrentChannel = newChannel; } + private double startDragChatHeight; + protected override bool OnDragStart(InputState state) { - if (channelTabs.Hovering) - return true; + if (!channelTabs.Hovering) + return base.OnDragStart(state); - return base.OnDragStart(state); + startDragChatHeight = chatHeight.Value; + return true; } protected override bool OnDrag(InputState state) { - chatHeight.Value = Height - state.Mouse.Delta.Y / Parent.DrawSize.Y; + Trace.Assert(state.Mouse.PositionMouseDown != null); + + chatHeight.Value = startDragChatHeight - (state.Mouse.Position.Y - state.Mouse.PositionMouseDown.Value.Y) / Parent.DrawSize.Y; return base.OnDrag(state); } @@ -165,7 +170,7 @@ namespace osu.Game.Overlays protected override bool OnFocus(InputState state) { //this is necessary as inputTextBox is masked away and therefore can't get focus :( - inputTextBox.TriggerFocus(); + InputManager.ChangeFocus(inputTextBox); return false; } @@ -255,6 +260,8 @@ namespace osu.Game.Overlays { if (currentChannel == value) return; + if (channelTabs.ChannelSelectorActive) return; + if (currentChannel != null) currentChannelContainer.Clear(false); diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 47674de817..42819f7f87 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Dialog private void pressButtonAtIndex(int index) { if (index < Buttons.Count()) - Buttons.Skip(index).First().TriggerClick(); + Buttons.Skip(index).First().TriggerOnClick(); } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Dialog if (args.Key == Key.Enter) { - Buttons.OfType().FirstOrDefault()?.TriggerClick(); + Buttons.OfType().FirstOrDefault()?.TriggerOnClick(); return true; } diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 0930c825b6..b1bc7e0c04 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -189,7 +189,7 @@ namespace osu.Game.Overlays protected override bool OnFocus(InputState state) { - filter.Search.TriggerFocus(); + InputManager.ChangeFocus(filter.Search); return false; } diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index e555600028..c3f41270ce 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -66,7 +66,7 @@ namespace osu.Game.Overlays settingsSection.Bounding = true; FadeIn(transition_time, EasingTypes.OutQuint); - settingsSection.TriggerFocus(); + InputManager.ChangeFocus(settingsSection); } protected override void PopOut() diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 831b9082bd..f7df67b66d 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -27,6 +27,7 @@ namespace osu.Game.Overlays.Mods public class ModButton : ModButtonEmpty, IHasTooltip { private ModIcon foregroundIcon; + private ModIcon backgroundIcon; private readonly SpriteText text; private readonly Container iconsContainer; private SampleChannel sampleOn, sampleOff; @@ -35,38 +36,67 @@ namespace osu.Game.Overlays.Mods public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty; - private int _selectedIndex = -1; - private int selectedIndex + private const EasingTypes mod_switch_easing = EasingTypes.InOutSine; + private const double mod_switch_duration = 120; + + // A selected index of -1 means not selected. + private int selectedIndex = -1; + + protected int SelectedIndex { get { - return _selectedIndex; + return selectedIndex; } set { - if (value == _selectedIndex) return; - _selectedIndex = value; + if (value == selectedIndex) return; + + int direction = value < selectedIndex ? -1 : 1; + bool beforeSelected = Selected; + + Mod modBefore = SelectedMod ?? Mods[0]; if (value >= Mods.Length) + selectedIndex = -1; + else if (value < -1) + selectedIndex = Mods.Length - 1; + else + selectedIndex = value; + + Mod modAfter = SelectedMod ?? Mods[0]; + + if (beforeSelected != Selected) { - _selectedIndex = -1; - } - else if (value <= -2) - { - _selectedIndex = Mods.Length - 1; + iconsContainer.RotateTo(Selected ? 5f : 0f, 300, EasingTypes.OutElastic); + iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, EasingTypes.OutElastic); + } + + if (modBefore != modAfter) + { + const float rotate_angle = 16; + + foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing); + backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing); + + backgroundIcon.Icon = modAfter.Icon; + using (iconsContainer.BeginDelayedSequence(mod_switch_duration, true)) + { + foregroundIcon.RotateTo(-rotate_angle * direction); + foregroundIcon.RotateTo(0f, mod_switch_duration, mod_switch_easing); + + backgroundIcon.RotateTo(rotate_angle * direction); + backgroundIcon.RotateTo(0f, mod_switch_duration, mod_switch_easing); + + iconsContainer.Schedule(() => displayMod(modAfter)); + } } - iconsContainer.RotateTo(Selected ? 5f : 0f, 300, EasingTypes.OutElastic); - iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, EasingTypes.OutElastic); foregroundIcon.Highlighted = Selected; - - if (mod != null) - displayMod(SelectedMod ?? Mods[0]); } } - public bool Selected => selectedIndex != -1; - + public bool Selected => SelectedIndex != -1; private Color4 selectedColour; public Color4 SelectedColour @@ -117,7 +147,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 override Mod SelectedMod => Mods.ElementAtOrDefault(SelectedIndex); [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -142,23 +172,25 @@ namespace osu.Game.Overlays.Mods public void SelectNext() { - (++selectedIndex == -1 ? sampleOff : sampleOn).Play(); + (++SelectedIndex == -1 ? sampleOff : sampleOn).Play(); Action?.Invoke(SelectedMod); } public void SelectPrevious() { - (--selectedIndex == -1 ? sampleOff : sampleOn).Play(); + (--SelectedIndex == -1 ? sampleOff : sampleOn).Play(); Action?.Invoke(SelectedMod); } public void Deselect() { - selectedIndex = -1; + SelectedIndex = -1; } private void displayMod(Mod mod) { + if (backgroundIcon != null) + backgroundIcon.Icon = foregroundIcon.Icon; foregroundIcon.Icon = mod.Icon; text.Text = mod.Name; } @@ -170,17 +202,17 @@ namespace osu.Game.Overlays.Mods { iconsContainer.Add(new[] { - new ModIcon(Mods[0]) + backgroundIcon = new ModIcon(Mods[1]) { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.Both, Position = new Vector2(1.5f), }, foregroundIcon = new ModIcon(Mods[0]) { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.Both, Position = new Vector2(-1.5f), }, diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 5e433aa414..82596252b3 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics; using OpenTK; using OpenTK.Graphics; using osu.Framework.Extensions; +using osu.Framework.Input; namespace osu.Game.Overlays.Music { @@ -35,10 +36,12 @@ namespace osu.Game.Overlays.Music private readonly Bindable beatmapBacking = new Bindable(); public IEnumerable BeatmapSets; + private InputManager inputManager; [BackgroundDependencyLoader] - private void load(OsuGameBase game, BeatmapDatabase beatmaps, OsuColour colours) + private void load(OsuGameBase game, BeatmapDatabase beatmaps, OsuColour colours, UserInputManager inputManager) { + this.inputManager = inputManager; this.beatmaps = beatmaps; trackManager = game.Audio.Track; @@ -100,7 +103,7 @@ namespace osu.Game.Overlays.Music protected override void PopIn() { filter.Search.HoldFocus = true; - Schedule(() => filter.Search.TriggerFocus()); + Schedule(() => inputManager.ChangeFocus(filter.Search)); ResizeTo(new Vector2(1, playlist_height), transition_duration, EasingTypes.OutQuint); FadeIn(transition_duration, EasingTypes.OutQuint); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 4faa339bed..f1be55801f 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -266,24 +266,27 @@ namespace osu.Game.Overlays { progressBar.IsEnabled = beatmap != null; - bool audioEquals = beatmapBacking.Value?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) ?? false; + TransformDirection direction = TransformDirection.None; - TransformDirection direction; - - if (audioEquals) - direction = TransformDirection.None; - else if (queuedDirection.HasValue) + if (current != null) { - direction = queuedDirection.Value; - queuedDirection = null; - } - else - { - //figure out the best direction based on order in playlist. - var last = current == null ? -1 : playlist.BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo.ID).Count(); - var next = beatmapBacking.Value == null ? -1 : playlist.BeatmapSets.TakeWhile(b => b.ID != beatmapBacking.Value.BeatmapSetInfo.ID).Count(); + bool audioEquals = beatmapBacking.Value?.BeatmapInfo?.AudioEquals(current.BeatmapInfo) ?? false; - direction = last > next ? TransformDirection.Prev : TransformDirection.Next; + if (audioEquals) + direction = TransformDirection.None; + else if (queuedDirection.HasValue) + { + direction = queuedDirection.Value; + queuedDirection = null; + } + else + { + //figure out the best direction based on order in playlist. + var last = playlist.BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo.ID).Count(); + var next = beatmapBacking.Value == null ? -1 : playlist.BeatmapSets.TakeWhile(b => b.ID != beatmapBacking.Value.BeatmapSetInfo.ID).Count(); + + direction = last > next ? TransformDirection.Prev : TransformDirection.Next; + } } current = beatmapBacking.Value; diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index d94388ed87..00ca50927e 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -13,15 +13,23 @@ using osu.Game.Online.API; using OpenTK; using osu.Framework.Input; using osu.Game.Users; +using System.ComponentModel; +using osu.Game.Graphics; +using OpenTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; + +using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Overlays.Settings.Sections.General { - public class LoginSettings : SettingsSubsection, IOnlineComponent + public class LoginSettings : FillFlowContainer, IOnlineComponent { private bool bounding = true; private LoginForm form; + private OsuColour colours; - protected override string Header => "Account"; + private UserPanel panel; + private UserDropdown dropdown; public override RectangleF BoundingBox => bounding ? base.BoundingBox : RectangleF.Empty; @@ -35,9 +43,21 @@ namespace osu.Game.Overlays.Settings.Sections.General } } - [BackgroundDependencyLoader(permitNulls: true)] - private void load(APIAccess api) + public LoginSettings() { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + Spacing = new Vector2(0f, 5f); + } + + private InputManager inputManager; + + [BackgroundDependencyLoader(permitNulls: true)] + private void load(OsuColour colours, APIAccess api, UserInputManager inputManager) + { + this.inputManager = inputManager; + this.colours = colours; api?.Register(this); } @@ -50,6 +70,12 @@ namespace osu.Game.Overlays.Settings.Sections.General case APIState.Offline: Children = new Drawable[] { + new OsuSpriteText + { + Text = "ACCOUNT", + Margin = new MarginPadding { Bottom = 5 }, + Font = @"Exo2.0-Black", + }, form = new LoginForm() }; break; @@ -67,33 +93,82 @@ namespace osu.Game.Overlays.Settings.Sections.General { new OsuSpriteText { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Text = "Connecting...", + Margin = new MarginPadding { Top = 10, Bottom = 10 }, }, }; break; case APIState.Online: Children = new Drawable[] { - new UserPanel(api.LocalUser.Value) + new FillFlowContainer { RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = 20, Right = 20 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Signed in", + TextSize = 18, + Font = @"Exo2.0-Bold", + Margin = new MarginPadding { Top = 5, Bottom = 5 }, + }, + }, + }, + panel = new UserPanel(api.LocalUser.Value) { RelativeSizeAxes = Axes.X }, + dropdown = new UserDropdown { RelativeSizeAxes = Axes.X }, + }, }, - new OsuButton + }; + + panel.Status.BindTo(api.LocalUser.Value.Status); + + dropdown.Current.ValueChanged += newValue => + { + switch (newValue) { - RelativeSizeAxes = Axes.X, - Text = "Sign out", - Action = api.Logout + case UserAction.Online: + api.LocalUser.Value.Status.Value = new UserStatusOnline(); + dropdown.StatusColour = colours.Green; + break; + case UserAction.DoNotDisturb: + api.LocalUser.Value.Status.Value = new UserStatusDoNotDisturb(); + dropdown.StatusColour = colours.Red; + break; + case UserAction.AppearOffline: + api.LocalUser.Value.Status.Value = new UserStatusOffline(); + dropdown.StatusColour = colours.Gray7; + break; + case UserAction.SignOut: + api.Logout(); + break; } }; + dropdown.Current.TriggerChange(); + break; } - form?.TriggerFocus(); + if (form != null) inputManager.ChangeFocus(form); } protected override bool OnFocus(InputState state) { - form?.TriggerFocus(); + if (form != null) inputManager.ChangeFocus(form); return base.OnFocus(state); } @@ -102,6 +177,7 @@ namespace osu.Game.Overlays.Settings.Sections.General private TextBox username; private TextBox password; private APIAccess api; + private InputManager inputManager; private void performLogin() { @@ -110,8 +186,9 @@ namespace osu.Game.Overlays.Settings.Sections.General } [BackgroundDependencyLoader(permitNulls: true)] - private void load(APIAccess api, OsuConfigManager config) + private void load(APIAccess api, OsuConfigManager config, UserInputManager inputManager) { + this.inputManager = inputManager; this.api = api; Direction = FillDirection.Vertical; Spacing = new Vector2(0, 5); @@ -160,16 +237,123 @@ namespace osu.Game.Overlays.Settings.Sections.General protected override bool OnFocus(InputState state) { - Schedule(() => - { - if (string.IsNullOrEmpty(username.Text)) - username.TriggerFocus(); - else - password.TriggerFocus(); - }); - + Schedule(() => { inputManager.ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); return base.OnFocus(state); } } + + private class UserDropdown : OsuEnumDropdown + { + protected override DropdownHeader CreateHeader() => new UserDropdownHeader { AccentColour = AccentColour }; + protected override Menu CreateMenu() => new UserDropdownMenu(); + protected override DropdownMenuItem CreateMenuItem(string text, UserAction value) => new UserDropdownMenuItem(text, value) { AccentColour = AccentColour }; + + public Color4 StatusColour + { + set + { + var h = Header as UserDropdownHeader; + if (h == null) return; + h.StatusColour = value; + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.Gray5; + } + + private class UserDropdownHeader : OsuDropdownHeader + { + public const float LABEL_LEFT_MARGIN = 20; + + private readonly TextAwesome statusIcon; + public Color4 StatusColour + { + set + { + statusIcon.FadeColour(value, 500, EasingTypes.OutQuint); + } + } + + public UserDropdownHeader() + { + Foreground.Padding = new MarginPadding { Left = 10, Right = 10 }; + Margin = new MarginPadding { Bottom = 5 }; + Masking = true; + CornerRadius = 5; + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4, + }; + + Icon.TextSize = 14; + Icon.Margin = new MarginPadding(0); + + Foreground.Add(statusIcon = new TextAwesome + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.fa_circle_o, + TextSize = 14, + }); + + Text.Margin = new MarginPadding { Left = LABEL_LEFT_MARGIN }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Gray3; + } + } + + private class UserDropdownMenu : OsuMenu + { + public UserDropdownMenu() + { + Margin = new MarginPadding { Bottom = 5 }; + CornerRadius = 5; + ItemsContainer.Padding = new MarginPadding(0); + Masking = true; + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Background.Colour = colours.Gray3; + } + } + + private class UserDropdownMenuItem : OsuDropdownMenuItem + { + public UserDropdownMenuItem(string text, UserAction current) : base(text, current) + { + Foreground.Padding = new MarginPadding { Top = 5, Bottom = 5, Left = UserDropdownHeader.LABEL_LEFT_MARGIN, Right = 5 }; + Chevron.Margin = new MarginPadding { Left = 2, Right = 3 }; + CornerRadius = 5; + } + } + } + + private enum UserAction + { + Online, + [Description(@"Do not disturb")] + DoNotDisturb, + [Description(@"Appear offline")] + AppearOffline, + [Description(@"Sign out")] + SignOut, + } } } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 943545e858..474631fd1e 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -134,12 +134,13 @@ namespace osu.Game.Overlays FadeTo(0, TRANSITION_LENGTH / 2); searchTextBox.HoldFocus = false; - searchTextBox.TriggerFocusLost(); + if (searchTextBox.HasFocus) + InputManager.ChangeFocus(null); } protected override bool OnFocus(InputState state) { - searchTextBox.TriggerFocus(state); + InputManager.ChangeFocus(searchTextBox); return false; } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index c5dbc27fd3..fcdcf672d5 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -15,28 +15,51 @@ using System.Linq; namespace osu.Game.Rulesets.Objects.Drawables { - public abstract class DrawableHitObject : Container - where TObject : HitObject - where TJudgement : Judgement + public abstract class DrawableHitObject : Container { - public event Action> OnJudgement; - - public TObject HitObject; + public readonly HitObject HitObject; /// /// The colour used for various elements of this DrawableHitObject. /// public virtual Color4 AccentColour { get; set; } + protected DrawableHitObject(HitObject hitObject) + { + HitObject = hitObject; + } + } + + public abstract class DrawableHitObject : DrawableHitObject + where TObject : HitObject + { + public new readonly TObject HitObject; + + protected DrawableHitObject(TObject hitObject) + : base(hitObject) + { + HitObject = hitObject; + } + } + + public abstract class DrawableHitObject : DrawableHitObject + where TObject : HitObject + where TJudgement : Judgement + { + public event Action> OnJudgement; + public override bool HandleInput => Interactive; public bool Interactive = true; public TJudgement Judgement; - protected abstract TJudgement CreateJudgement(); + protected List Samples = new List(); - protected abstract void UpdateState(ArmedState state); + protected DrawableHitObject(TObject hitObject) + : base(hitObject) + { + } private ArmedState state; public ArmedState State @@ -59,8 +82,6 @@ namespace osu.Game.Rulesets.Objects.Drawables } } - protected List Samples = new List(); - protected void PlaySamples() { Samples.ForEach(s => s?.Play()); @@ -79,11 +100,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public bool Judged => (Judgement?.Result ?? HitResult.None) != HitResult.None && (NestedHitObjects?.All(h => h.Judged) ?? true); - protected DrawableHitObject(TObject hitObject) - { - HitObject = hitObject; - } - /// /// Process a hit of this hitobject. Carries out judgement. /// @@ -176,5 +192,8 @@ namespace osu.Game.Rulesets.Objects.Drawables h.OnJudgement += d => OnJudgement?.Invoke(d); nestedHitObjects.Add(h); } + + protected abstract TJudgement CreateJudgement(); + protected abstract void UpdateState(ArmedState state); } } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 5592681cab..b282965db8 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Objects /// /// The time at which the HitObject starts. /// - public double StartTime; + public virtual double StartTime { get; set; } /// /// The samples to be played when this hit object is hit. diff --git a/osu.Game/Rulesets/Objects/HitObjectStartTimeComparer.cs b/osu.Game/Rulesets/Objects/HitObjectStartTimeComparer.cs new file mode 100644 index 0000000000..b089856dcb --- /dev/null +++ b/osu.Game/Rulesets/Objects/HitObjectStartTimeComparer.cs @@ -0,0 +1,55 @@ +// 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.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Objects +{ + /// + /// Compares two hit objects by their start time, falling back to creation order if their start time is equal. + /// + public class HitObjectStartTimeComparer : Drawable.CreationOrderDepthComparer + { + public override int Compare(Drawable x, Drawable y) + { + var hitObjectX = x as DrawableHitObject; + var hitObjectY = y as DrawableHitObject; + + // If either of the two drawables are not hit objects, fall back to the base comparer + if (hitObjectX?.HitObject == null || hitObjectY?.HitObject == null) + return base.Compare(x, y); + + // Compare by start time + int i = hitObjectX.HitObject.StartTime.CompareTo(hitObjectY.HitObject.StartTime); + if (i != 0) + return i; + + return base.Compare(x, y); + } + } + + /// + /// Compares two hit objects by their start time, falling back to creation order if their start time is equal. + /// This will compare the two hit objects in reverse order. + /// + public class HitObjectReverseStartTimeComparer : Drawable.ReverseCreationOrderDepthComparer + { + public override int Compare(Drawable x, Drawable y) + { + var hitObjectX = x as DrawableHitObject; + var hitObjectY = y as DrawableHitObject; + + // If either of the two drawables are not hit objects, fall back to the base comparer + if (hitObjectX?.HitObject == null || hitObjectY?.HitObject == null) + return base.Compare(x, y); + + // Compare by start time + int i = hitObjectY.HitObject.StartTime.CompareTo(hitObjectX.HitObject.StartTime); + if (i != 0) + return i; + + return base.Compare(x, y); + } + } +} \ No newline at end of file diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 7580404e81..8c2aead5ff 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -6,20 +6,30 @@ using System; using System.Collections.Generic; using OpenTK; using osu.Game.Audio; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Database; namespace osu.Game.Rulesets.Objects.Legacy { internal abstract class ConvertSlider : HitObject, IHasCurve { + /// + /// Scoring distance with a speed-adjusted beat length of 1 second. + /// + private const float base_scoring_distance = 100; + public List ControlPoints { get; set; } public CurveType CurveType { get; set; } + public double Distance { get; set; } public List RepeatSamples { get; set; } public int RepeatCount { get; set; } = 1; - public double EndTime { get; set; } - public double Duration { get; set; } + public double EndTime => StartTime + RepeatCount * Distance / Velocity; + public double Duration => EndTime - StartTime; + + public double Velocity = 1; public Vector2 PositionAt(double progress) { @@ -35,5 +45,17 @@ namespace osu.Game.Rulesets.Objects.Legacy { throw new NotImplementedException(); } + + public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaults(controlPointInfo, difficulty); + + TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); + DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); + + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier / difficultyPoint.SpeedMultiplier; + + Velocity = scoringDistance / timingPoint.BeatLength; + } } } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 72db445052..52039a8417 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Menu switch (args.Key) { case Key.Space: - osuLogo.TriggerClick(state); + osuLogo.TriggerOnClick(state); return true; case Key.Escape: switch (State) @@ -144,7 +144,7 @@ namespace osu.Game.Screens.Menu State = MenuState.Initial; return true; case MenuState.Play: - backButton.TriggerClick(); + backButton.TriggerOnClick(); return true; } @@ -178,10 +178,10 @@ namespace osu.Game.Screens.Menu State = MenuState.TopLevel; return; case MenuState.TopLevel: - buttonsTopLevel.First().TriggerClick(); + buttonsTopLevel.First().TriggerOnClick(); return; case MenuState.Play: - buttonsPlay.First().TriggerClick(); + buttonsPlay.First().TriggerOnClick(); return; } } diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index 0cf1fa54fa..77239726e8 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -36,6 +36,8 @@ namespace osu.Game.Screens.Menu public MenuSideFlashes() { + EarlyActivationMilliseconds = box_fade_in_time; + RelativeSizeAxes = Axes.Both; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game/Screens/Multiplayer/DrawableRoom.cs b/osu.Game/Screens/Multiplayer/DrawableRoom.cs new file mode 100644 index 0000000000..7365963085 --- /dev/null +++ b/osu.Game/Screens/Multiplayer/DrawableRoom.cs @@ -0,0 +1,259 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +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.Sprites; +using osu.Framework.Localisation; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Screens.Multiplayer +{ + public class DrawableRoom : ClickableContainer + { + private const float content_padding = 5; + private const float height = 90; + + private readonly Box sideStrip; + private readonly UpdateableAvatar avatar; + private readonly OsuSpriteText name; + private readonly Container flagContainer; + private readonly OsuSpriteText host; + private readonly OsuSpriteText rankBounds; + private readonly OsuSpriteText status; + private readonly FillFlowContainer beatmapInfoFlow; + private readonly OsuSpriteText beatmapTitle; + private readonly OsuSpriteText beatmapDash; + private readonly OsuSpriteText beatmapArtist; + + private OsuColour colours; + private LocalisationEngine localisation; + + public readonly Room Room; + + public DrawableRoom(Room room) + { + Room = room; + + RelativeSizeAxes = Axes.X; + Height = height; + CornerRadius = 5; + Masking = true; + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(40), + Radius = 5, + }; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(34), + }, + sideStrip = new Box + { + RelativeSizeAxes = Axes.Y, + Width = content_padding, + }, + avatar = new UpdateableAvatar + { + Size = new Vector2(Height - content_padding* 2), + Masking = true, + CornerRadius = 5f, + Margin = new MarginPadding { Left = content_padding * 2, Top = content_padding }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = content_padding, Bottom = content_padding, Left = Height + content_padding * 2, Right = content_padding }, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5f), + Children = new Drawable[] + { + name = new OsuSpriteText + { + TextSize = 18, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 20f, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5f, 0f), + Children = new Drawable[] + { + flagContainer = new Container + { + Width = 30f, + RelativeSizeAxes = Axes.Y, + }, + new Container + { + Width = 40f, + RelativeSizeAxes = Axes.Y, + }, + new OsuSpriteText + { + Text = "hosted by", + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + TextSize = 14, + }, + host = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + TextSize = 14, + Font = @"Exo2.0-BoldItalic", + }, + }, + }, + rankBounds = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Text = "#0 - #0", + TextSize = 14, + Margin = new MarginPadding { Right = 10 }, + }, + }, + }, + }, + }, + new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Bottom = content_padding }, + Children = new Drawable[] + { + status = new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-Bold", + }, + beatmapInfoFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Children = new[] + { + beatmapTitle = new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-BoldItalic", + }, + beatmapDash = new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-RegularItalic", + }, + beatmapArtist = new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-RegularItalic", + }, + }, + }, + }, + }, + }, + }, + }; + + Room.Name.ValueChanged += displayName; + Room.Host.ValueChanged += displayUser; + Room.Status.ValueChanged += displayStatus; + Room.Beatmap.ValueChanged += displayBeatmap; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, LocalisationEngine localisation) + { + this.localisation = localisation; + this.colours = colours; + + beatmapInfoFlow.Colour = rankBounds.Colour = colours.Gray9; + host.Colour = colours.Blue; + + displayStatus(Room.Status.Value); + } + + private void displayName(string value) + { + name.Text = value; + } + + private void displayUser(User value) + { + avatar.User = value; + host.Text = value.Username; + flagContainer.Children = new[] { new DrawableFlag(value.Country?.FlagName ?? @"__") { RelativeSizeAxes = Axes.Both } }; + } + + private void displayStatus(RoomStatus value) + { + if (value == null) return; + status.Text = value.Message; + + foreach (Drawable d in new Drawable[] { sideStrip, status }) + d.FadeColour(value.GetAppropriateColour(colours), 100); + } + + private void displayBeatmap(BeatmapMetadata value) + { + if (value != null) + { + beatmapTitle.Current = localisation.GetUnicodePreference(value.TitleUnicode, value.Title); + beatmapDash.Text = @" - "; + beatmapArtist.Current = localisation.GetUnicodePreference(value.ArtistUnicode, value.Artist); + } + else + { + beatmapTitle.Current = null; + beatmapArtist.Current = null; + + beatmapTitle.Text = @"Changing map"; + beatmapDash.Text = string.Empty; + beatmapArtist.Text = string.Empty; + } + } + + protected override void Dispose(bool isDisposing) + { + Room.Name.ValueChanged -= displayName; + Room.Host.ValueChanged -= displayUser; + Room.Status.ValueChanged -= displayStatus; + Room.Beatmap.ValueChanged -= displayBeatmap; + + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Game/Screens/Play/FailOverlay.cs b/osu.Game/Screens/Play/FailOverlay.cs index faff687ddb..3e31da2348 100644 --- a/osu.Game/Screens/Play/FailOverlay.cs +++ b/osu.Game/Screens/Play/FailOverlay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Play { if (!args.Repeat && args.Key == Key.Escape) { - Buttons.Children.Last().TriggerClick(); + Buttons.Children.Last().TriggerOnClick(); return true; } diff --git a/osu.Game/Screens/Play/KeyCounterCollection.cs b/osu.Game/Screens/Play/KeyCounterCollection.cs index 6e1c8a34e5..25cbfc14b7 100644 --- a/osu.Game/Screens/Play/KeyCounterCollection.cs +++ b/osu.Game/Screens/Play/KeyCounterCollection.cs @@ -131,13 +131,13 @@ namespace osu.Game.Screens.Play public override bool HandleInput => true; - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => target.Children.Any(c => c.TriggerKeyDown(state, args)); + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => target.Children.Any(c => c.TriggerOnKeyDown(state, args)); - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => target.Children.Any(c => c.TriggerKeyUp(state, args)); + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => target.Children.Any(c => c.TriggerOnKeyUp(state, args)); - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => target.Children.Any(c => c.TriggerMouseDown(state, args)); + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => target.Children.Any(c => c.TriggerOnMouseDown(state, args)); - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => target.Children.Any(c => c.TriggerMouseUp(state, args)); + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => target.Children.Any(c => c.TriggerOnMouseUp(state, args)); } } } diff --git a/osu.Game/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PauseContainer.cs index 1de62048b7..f052bafd63 100644 --- a/osu.Game/Screens/Play/PauseContainer.cs +++ b/osu.Game/Screens/Play/PauseContainer.cs @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Play { if (!args.Repeat && args.Key == Key.Escape) { - Buttons.Children.First().TriggerClick(); + Buttons.Children.First().TriggerOnClick(); return true; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a39e7dbab2..707d026e2b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -293,7 +293,7 @@ namespace osu.Game.Screens.Play protected override bool OnExiting(Screen next) { - if (HasFailed || !ValidForResume || pauseContainer.AllowExit || HitRenderer.HasReplayLoaded) + if (HasFailed || !ValidForResume || pauseContainer?.AllowExit != false || HitRenderer?.HasReplayLoaded != false) { fadeOut(); return base.OnExiting(next); @@ -310,7 +310,7 @@ namespace osu.Game.Screens.Play HitRenderer?.FadeOut(fade_out_duration); Content.FadeOut(fade_out_duration); - hudOverlay.ScaleTo(0.7f, fade_out_duration * 3, EasingTypes.In); + hudOverlay?.ScaleTo(0.7f, fade_out_duration * 3, EasingTypes.In); Background?.FadeTo(1f, fade_out_duration); } diff --git a/osu.Game/Screens/Play/SkipButton.cs b/osu.Game/Screens/Play/SkipButton.cs index 86bbb26412..ee11fc0ca6 100644 --- a/osu.Game/Screens/Play/SkipButton.cs +++ b/osu.Game/Screens/Play/SkipButton.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Play switch (args.Key) { case Key.Space: - button.TriggerClick(); + button.TriggerOnClick(); return true; } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 7c7863acd1..94bc60a6ca 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -154,7 +154,8 @@ namespace osu.Game.Screens.Select public void Deactivate() { searchTextBox.HoldFocus = false; - searchTextBox.TriggerFocusLost(); + if (searchTextBox.HasFocus) + inputManager.ChangeFocus(searchTextBox); } public void Activate() @@ -164,9 +165,13 @@ namespace osu.Game.Screens.Select private readonly Bindable ruleset = new Bindable(); - [BackgroundDependencyLoader(permitNulls:true)] - private void load(OsuColour colours, OsuGame osu) + private InputManager inputManager; + + [BackgroundDependencyLoader(permitNulls: true)] + private void load(OsuColour colours, OsuGame osu, UserInputManager inputManager) { + this.inputManager = inputManager; + sortTabs.AccentColour = colours.GreenLight; if (osu != null) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 1361eefcff..93933c8fe9 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using Newtonsoft.Json; +using osu.Framework.Configuration; namespace osu.Game.Users { @@ -19,6 +20,8 @@ namespace osu.Game.Users [JsonProperty(@"country")] public Country Country; + public Bindable Status = new Bindable(); + //public Team Team; [JsonProperty(@"profile_colour")] diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index c78a69dac8..bdfe6d1c8e 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -158,14 +158,19 @@ namespace osu.Game.Users }, }, }; - - Status.ValueChanged += displayStatus; } [BackgroundDependencyLoader] private void load(OsuColour colours) { this.colours = colours; + Status.ValueChanged += displayStatus; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Status.TriggerChange(); } private void displayStatus(UserStatus status) diff --git a/osu.Game/Users/UserStatus.cs b/osu.Game/Users/UserStatus.cs index dcb5ccbd8f..461008db0f 100644 --- a/osu.Game/Users/UserStatus.cs +++ b/osu.Game/Users/UserStatus.cs @@ -58,4 +58,10 @@ namespace osu.Game.Users public override string Message => @"Modding a map"; public override Color4 GetAppropriateColour(OsuColour colours) => colours.PurpleDark; } + + public class UserStatusDoNotDisturb : UserStatus + { + public override string Message => @"Do not disturb"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.RedDark; + } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 98fb4b9707..0ec0624978 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -176,6 +176,7 @@ + @@ -432,6 +433,9 @@ + + + @@ -447,6 +451,7 @@ +