diff --git a/osu-framework b/osu-framework index c6f030d6f1..8baad1b948 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit c6f030d6f1ab65a48de9ff1d0c424acb686e0149 +Subproject commit 8baad1b9484b9f35724e2f965c18cfe710907d80 diff --git a/osu.Desktop.VisualTests/Tests/TestCaseDirect.cs b/osu.Desktop.VisualTests/Tests/TestCaseDirect.cs new file mode 100644 index 0000000000..4cda14559f --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseDirect.cs @@ -0,0 +1,199 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Database; +using osu.Game.Overlays; + +namespace osu.Desktop.VisualTests.Tests +{ + public class TestCaseDirect : TestCase + { + public override string Description => @"osu!direct overlay"; + + private DirectOverlay direct; + private RulesetDatabase rulesets; + + public override void Reset() + { + base.Reset(); + + Add(direct = new DirectOverlay()); + newBeatmaps(); + + AddStep(@"toggle", direct.ToggleVisibility); + AddStep(@"result counts", () => direct.ResultAmounts = new DirectOverlay.ResultCounts(1, 4, 13)); + } + + [BackgroundDependencyLoader] + private void load(RulesetDatabase rulesets) + { + this.rulesets = rulesets; + } + + private void newBeatmaps() + { + var ruleset = rulesets.GetRuleset(0); + + direct.BeatmapSets = new[] + { + new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Title = @"OrVid", + Artist = @"An", + Author = @"RLC", + Source = @"", + }, + Beatmaps = new List<BeatmapInfo> + { + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 5.35f, + Metadata = new BeatmapMetadata(), + OnlineInfo = new BeatmapOnlineInfo + { + Covers = new[] { @"https://assets.ppy.sh//beatmaps/578332/covers/cover.jpg?1494591390" }, + Preview = @"https://b.ppy.sh/preview/578332.mp3", + PlayCount = 97, + FavouriteCount = 72, + }, + }, + }, + }, + new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Title = @"tiny lamp", + Artist = @"fhana", + Author = @"Sotarks", + Source = @"ぎんぎつね", + }, + Beatmaps = new List<BeatmapInfo> + { + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 5.81f, + Metadata = new BeatmapMetadata(), + OnlineInfo = new BeatmapOnlineInfo + { + Covers = new[] { @"https://assets.ppy.sh//beatmaps/599627/covers/cover.jpg?1494539318" }, + Preview = @"https//b.ppy.sh/preview/599627.mp3", + PlayCount = 3082, + FavouriteCount = 14, + }, + }, + }, + }, + new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Title = @"At Gwanghwamun", + Artist = @"KYUHYUN", + Author = @"Cerulean Veyron", + Source = @"", + }, + Beatmaps = new List<BeatmapInfo> + { + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 0.9f, + Metadata = new BeatmapMetadata(), + OnlineInfo = new BeatmapOnlineInfo + { + Covers = new[] { @"https://assets.ppy.sh//beatmaps/513268/covers/cover.jpg?1494502863" }, + Preview = @"https//b.ppy.sh/preview/513268.mp3", + PlayCount = 2762, + FavouriteCount = 15, + }, + }, + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 1.1f, + }, + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 2.02f, + }, + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 3.49f, + }, + }, + }, + new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Title = @"RHAPSODY OF BLUE SKY", + Artist = @"fhana", + Author = @"[Kamiya]", + Source = @"小林さんちのメイドラゴン", + }, + Beatmaps = new List<BeatmapInfo> + { + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 1.26f, + Metadata = new BeatmapMetadata(), + OnlineInfo = new BeatmapOnlineInfo + { + Covers = new[] { @"https://assets.ppy.sh//beatmaps/586841/covers/cover.jpg?1494052741" }, + Preview = @"https//b.ppy.sh/preview/586841.mp3", + PlayCount = 62317, + FavouriteCount = 161, + }, + }, + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 2.01f, + }, + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 2.87f, + }, + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 3.76f, + }, + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 3.93f, + }, + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 4.37f, + }, + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 5.13f, + }, + new BeatmapInfo + { + Ruleset = ruleset, + StarDifficulty = 5.42f, + }, + }, + }, + }; + } + } +} 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 <contact@ppy.sh>. +// 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/TestCaseUserPanel.cs b/osu.Desktop.VisualTests/Tests/TestCaseUserPanel.cs new file mode 100644 index 0000000000..513bf24e0d --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseUserPanel.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Testing; +using osu.Framework.Graphics; +using osu.Game.Users; +using osu.Framework.Graphics.Containers; +using OpenTK; + +namespace osu.Desktop.VisualTests.Tests +{ + internal class TestCaseUserPanel : TestCase + { + public override string Description => @"Panels for displaying a user's status"; + + public override void Reset() + { + base.Reset(); + + UserPanel flyte; + UserPanel peppy; + Add(new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f), + Children = new[] + { + flyte = new UserPanel(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/3103765/5b012e13611d5761caa7e24fecb3d3a16e1cf48fc2a3032cfd43dd444af83d82.jpeg" + }) { 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" + }) { Width = 300 }, + }, + }); + + flyte.Status.Value = new UserStatusOnline(); + peppy.Status.Value = new UserStatusSoloGame(); + + AddStep(@"spectating", () => { flyte.Status.Value = new UserStatusSpectating(); }); + AddStep(@"multiplaying", () => { flyte.Status.Value = new UserStatusMultiplayerGame(); }); + AddStep(@"modding", () => { flyte.Status.Value = new UserStatusModding(); }); + AddStep(@"offline", () => { flyte.Status.Value = new UserStatusOffline(); }); + AddStep(@"null status", () => { flyte.Status.Value = null; }); + } + } +} diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index dbb1750b72..7b7997063b 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -220,6 +220,9 @@ <Compile Include="Tests\TestCaseLeaderboard.cs" /> <Compile Include="Beatmaps\TestWorkingBeatmap.cs" /> <Compile Include="Tests\TestCaseBeatmapDetailArea.cs" /> + <Compile Include="Tests\TestCaseDrawableRoom.cs" /> + <Compile Include="Tests\TestCaseUserPanel.cs" /> + <Compile Include="Tests\TestCaseDirect.cs" /> </ItemGroup> <ItemGroup /> <ItemGroup /> 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.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index 9f91488fe3..3ea05b6558 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs @@ -7,6 +7,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Backgrounds; using OpenTK.Graphics; +using osu.Game.Beatmaps.ControlPoints; +using osu.Framework.Audio.Track; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { @@ -22,6 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces public const float SYMBOL_SIZE = TaikoHitObject.DEFAULT_CIRCLE_DIAMETER * 0.45f; public const float SYMBOL_BORDER = 8; public const float SYMBOL_INNER_SIZE = SYMBOL_SIZE - 2 * SYMBOL_BORDER; + private const double pre_beat_transition_time = 80; /// <summary> /// The colour of the inner circle and outer glows. @@ -63,6 +66,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces public CirclePiece(bool isStrong = false) { + EarlyActivationMilliseconds = pre_beat_transition_time; + AddInternal(new Drawable[] { background = new CircularContainer @@ -139,14 +144,31 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces Content.Width = 1 / Content.Scale.X; } + private const float edge_alpha_kiai = 0.5f; + private void resetEdgeEffects() { background.EdgeEffect = new EdgeEffect { Type = EdgeEffectType.Glow, - Colour = AccentColour, - Radius = KiaiMode ? 50 : 8 + Colour = AccentColour.Opacity(KiaiMode ? edge_alpha_kiai : 1f), + Radius = KiaiMode ? 32 : 8 }; } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + if (!effectPoint.KiaiMode) + return; + + if (beatIndex % (int)timingPoint.TimeSignature != 0) + return; + + double duration = timingPoint.BeatLength * 2; + + background.FadeEdgeEffectTo(1, pre_beat_transition_time, EasingTypes.OutQuint); + using (background.BeginDelayedSequence(pre_beat_transition_time)) + background.FadeEdgeEffectTo(edge_alpha_kiai, duration, EasingTypes.OutQuint); + } } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs index 83b2e59e44..5e7e9e6350 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs @@ -1,14 +1,14 @@ // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using OpenTK; using OpenTK.Graphics; +using osu.Game.Graphics.Containers; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { - public class TaikoPiece : Container, IHasAccentColour + public class TaikoPiece : BeatSyncedContainer, IHasAccentColour { private Color4 accentColour; /// <summary> @@ -17,10 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces public virtual Color4 AccentColour { get { return accentColour; } - set - { - accentColour = value; - } + set { accentColour = value; } } private bool kiaiMode; diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 82ceaeed1a..79bb901112 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -13,12 +13,12 @@ namespace osu.Game.Rulesets.Taiko.Objects /// <summary> /// Diameter of a circle relative to the size of the <see cref="TaikoPlayfield"/>. /// </summary> - public const float PLAYFIELD_RELATIVE_DIAMETER = 0.5f; + public const float PLAYFIELD_RELATIVE_DIAMETER = 0.45f; /// <summary> /// Scale multiplier for a strong circle. /// </summary> - public const float STRONG_CIRCLE_DIAMETER_SCALE = 1.5f; + public const float STRONG_CIRCLE_DIAMETER_SCALE = 1.4f; /// <summary> /// Default circle diameter. diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index 2ebdeaa5b0..c0c329c870 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -18,22 +18,23 @@ namespace osu.Game.Rulesets.Taiko.UI /// </summary> internal class HitExplosion : CircularContainer { - /// <summary> - /// The judgement this hit explosion visualises. - /// </summary> public readonly TaikoJudgement Judgement; private readonly Box innerFill; - public HitExplosion(TaikoJudgement judgement) - { - Judgement = judgement; + private readonly bool isRim; - Size = new Vector2(TaikoHitObject.DEFAULT_CIRCLE_DIAMETER); + public HitExplosion(TaikoJudgement judgement, bool isRim) + { + this.isRim = isRim; + + Judgement = judgement; Anchor = Anchor.Centre; Origin = Anchor.Centre; + Size = new Vector2(TaikoPlayfield.HIT_TARGET_OFFSET + TaikoHitObject.DEFAULT_CIRCLE_DIAMETER); + RelativePositionAxes = Axes.Both; BorderColour = Color4.White; @@ -54,22 +55,14 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load(OsuColour colours) { - switch (Judgement.TaikoResult) - { - case TaikoHitResult.Good: - innerFill.Colour = colours.Green; - break; - case TaikoHitResult.Great: - innerFill.Colour = colours.Blue; - break; - } + innerFill.Colour = isRim ? colours.BlueDarker : colours.PinkDarker; } protected override void LoadComplete() { base.LoadComplete(); - ScaleTo(5f, 1000, EasingTypes.OutQuint); + ScaleTo(3f, 1000, EasingTypes.OutQuint); FadeOut(500); Expire(); @@ -80,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.UI /// </summary> public void VisualiseSecondHit() { - ResizeTo(Size * TaikoHitObject.STRONG_CIRCLE_DIAMETER_SCALE, 50); + ResizeTo(new Vector2(TaikoPlayfield.HIT_TARGET_OFFSET + TaikoHitObject.DEFAULT_STRONG_CIRCLE_DIAMETER), 50); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs new file mode 100644 index 0000000000..e0da3ed3db --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -0,0 +1,68 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public class KiaiHitExplosion : CircularContainer + { + public readonly TaikoJudgement Judgement; + + private readonly bool isRim; + + public KiaiHitExplosion(TaikoJudgement judgement, bool isRim) + { + this.isRim = isRim; + + Judgement = judgement; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Y; + Size = new Vector2(TaikoHitObject.DEFAULT_CIRCLE_DIAMETER, 1); + + Masking = true; + Alpha = 0.25f; + + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Glow, + Colour = isRim ? colours.BlueDarker : colours.PinkDarker, + Radius = 60, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScaleTo(new Vector2(1, 3f), 500, EasingTypes.OutQuint); + FadeOut(250); + + Expire(); + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 2278158506..c7bd4a6704 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -24,12 +24,12 @@ namespace osu.Game.Rulesets.Taiko.UI /// <summary> /// The default play field height. /// </summary> - public const float DEFAULT_PLAYFIELD_HEIGHT = 168f; + public const float DEFAULT_PLAYFIELD_HEIGHT = 178f; /// <summary> /// The offset from <see cref="left_area_size"/> which the center of the hit target lies at. /// </summary> - private const float hit_target_offset = TaikoHitObject.DEFAULT_STRONG_CIRCLE_DIAMETER / 2f + 40; + public const float HIT_TARGET_OFFSET = TaikoHitObject.DEFAULT_STRONG_CIRCLE_DIAMETER / 2f + 40; /// <summary> /// The size of the left area of the playfield. This area contains the input drum. @@ -39,15 +39,18 @@ namespace osu.Game.Rulesets.Taiko.UI protected override Container<Drawable> Content => hitObjectContainer; private readonly Container<HitExplosion> hitExplosionContainer; + private readonly Container<KiaiHitExplosion> kiaiExplosionContainer; private readonly Container<DrawableBarLine> barLineContainer; private readonly Container<DrawableTaikoJudgement> judgementContainer; private readonly Container hitObjectContainer; private readonly Container topLevelHitContainer; - private readonly Container leftBackgroundContainer; - private readonly Container rightBackgroundContainer; - private readonly Box leftBackground; - private readonly Box rightBackground; + + private readonly Container overlayBackgroundContainer; + private readonly Container backgroundContainer; + + private readonly Box overlayBackground; + private readonly Box background; public TaikoPlayfield() { @@ -59,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.UI Height = DEFAULT_PLAYFIELD_HEIGHT, Children = new[] { - rightBackgroundContainer = new Container + backgroundContainer = new Container { Name = "Transparent playfield background", RelativeSizeAxes = Axes.Both, @@ -73,7 +76,7 @@ namespace osu.Game.Rulesets.Taiko.UI }, Children = new Drawable[] { - rightBackground = new Box + background = new Box { RelativeSizeAxes = Axes.Both, Alpha = 0.6f @@ -82,24 +85,23 @@ namespace osu.Game.Rulesets.Taiko.UI }, new Container { - Name = "Transparent playfield elements", + Name = "Right area", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = left_area_size }, + Margin = new MarginPadding { Left = left_area_size }, Children = new Drawable[] { new Container { - Name = "Hit target container", - X = hit_target_offset, + Name = "Masked elements", RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, + Masking = true, Children = new Drawable[] { hitExplosionContainer = new Container<HitExplosion> { - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, - BlendingMode = BlendingMode.Additive + BlendingMode = BlendingMode.Additive, }, barLineContainer = new Container<DrawableBarLine> { @@ -114,23 +116,32 @@ namespace osu.Game.Rulesets.Taiko.UI { RelativeSizeAxes = Axes.Both, }, - judgementContainer = new Container<DrawableTaikoJudgement> - { - RelativeSizeAxes = Axes.Y, - BlendingMode = BlendingMode.Additive - }, - }, + } + }, + kiaiExplosionContainer = new Container<KiaiHitExplosion> + { + Name = "Kiai hit explosions", + RelativeSizeAxes = Axes.Y, + Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, + BlendingMode = BlendingMode.Additive + }, + judgementContainer = new Container<DrawableTaikoJudgement> + { + Name = "Judgements", + RelativeSizeAxes = Axes.Y, + Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, + BlendingMode = BlendingMode.Additive }, } }, - leftBackgroundContainer = new Container + overlayBackgroundContainer = new Container { Name = "Left overlay", Size = new Vector2(left_area_size, DEFAULT_PLAYFIELD_HEIGHT), BorderThickness = 1, Children = new Drawable[] { - leftBackground = new Box + overlayBackground = new Box { RelativeSizeAxes = Axes.Both, }, @@ -164,11 +175,11 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load(OsuColour colours) { - leftBackgroundContainer.BorderColour = colours.Gray0; - leftBackground.Colour = colours.Gray1; + overlayBackgroundContainer.BorderColour = colours.Gray0; + overlayBackground.Colour = colours.Gray1; - rightBackgroundContainer.BorderColour = colours.Gray1; - rightBackground.Colour = colours.Gray0; + backgroundContainer.BorderColour = colours.Gray1; + background.Colour = colours.Gray0; } public override void Add(DrawableHitObject<TaikoHitObject, TaikoJudgement> h) @@ -204,6 +215,8 @@ namespace osu.Game.Rulesets.Taiko.UI if (!wasHit) return; + bool isRim = judgedObject.HitObject is RimHit; + if (!secondHit) { if (judgedObject.X >= -0.05f && !(judgedObject is DrawableSwell)) @@ -212,7 +225,11 @@ namespace osu.Game.Rulesets.Taiko.UI topLevelHitContainer.Add(judgedObject.CreateProxy()); } - hitExplosionContainer.Add(new HitExplosion(judgedObject.Judgement)); + hitExplosionContainer.Add(new HitExplosion(judgedObject.Judgement, isRim)); + + if (judgedObject.HitObject.Kiai) + kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject.Judgement, isRim)); + } else hitExplosionContainer.Children.FirstOrDefault(e => e.Judgement == judgedObject.Judgement)?.VisualiseSecondHit(); @@ -221,7 +238,7 @@ namespace osu.Game.Rulesets.Taiko.UI /// <summary> /// This is a very special type of container. It serves a similar purpose to <see cref="FillMode.Fit"/>, however unlike <see cref="FillMode.Fit"/>, /// this will only adjust the scale relative to the height of its parent and will maintain the original width relative to its parent. - /// + /// /// <para> /// By adjusting the scale relative to the height of its parent, the aspect ratio of this container's children is maintained, however this is undesirable /// in the case where the hit object container should not have its width adjusted by scale. To counteract this, another container is nested inside this diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index 983dc72d9e..8d6fcb503c 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -87,6 +87,7 @@ <Compile Include="Scoring\TaikoScoreProcessor.cs" /> <Compile Include="UI\HitTarget.cs" /> <Compile Include="UI\InputDrum.cs" /> + <Compile Include="UI\KiaiHitExplosion.cs" /> <Compile Include="UI\DrawableTaikoJudgement.cs" /> <Compile Include="UI\HitExplosion.cs" /> <Compile Include="UI\TaikoHitRenderer.cs" /> diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 5740c961b1..acf90931ac 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -55,7 +55,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// </summary> /// <param name="time">The time to find the timing control point at.</param> /// <returns>The timing control point.</returns> - public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time); + public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.FirstOrDefault()); /// <summary> /// Finds the maximum BPM represented by any timing control point. @@ -75,14 +75,21 @@ namespace osu.Game.Beatmaps.ControlPoints public double BPMMode => 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength; - private T binarySearch<T>(SortedList<T> list, double time) + /// <summary> + /// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>. + /// </summary> + /// <param name="list">The list to search.</param> + /// <param name="time">The time to find the control point at.</param> + /// <param name="prePoint">The control point to use when <paramref name="time"/> is before any control points. If null, a new control point will be constructed.</param> + /// <returns>The active control point at <paramref name="time"/>.</returns> + private T binarySearch<T>(SortedList<T> list, double time, T prePoint = null) where T : ControlPoint, new() { if (list.Count == 0) return new T(); if (time < list[0].Time) - return new T(); + return prePoint ?? new T(); int index = list.BinarySearch(new T() { Time = time }); @@ -92,8 +99,8 @@ namespace osu.Game.Beatmaps.ControlPoints index = ~index; - if (index == list.Count) - return list[list.Count - 1]; + // BinarySearch will return the index of the first element _greater_ than the search + // This is the inactive point - the active point is the one before it (index - 1) return list[index - 1]; } } diff --git a/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs b/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs index 7c0aa49d2a..e91b52565a 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs @@ -9,7 +9,7 @@ using OpenTK.Graphics; namespace osu.Game.Beatmaps.Drawables { - internal class DifficultyColouredContainer : Container, IHasAccentColour + public class DifficultyColouredContainer : Container, IHasAccentColour { public Color4 AccentColour { get; set; } diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index a8b63c2502..9df1f0f284 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -11,7 +11,7 @@ using OpenTK.Graphics; namespace osu.Game.Beatmaps.Drawables { - internal class DifficultyIcon : DifficultyColouredContainer + public class DifficultyIcon : DifficultyColouredContainer { private readonly BeatmapInfo beatmap; diff --git a/osu.Game/Database/BeatmapInfo.cs b/osu.Game/Database/BeatmapInfo.cs index c2e35d64a8..9f253f6055 100644 --- a/osu.Game/Database/BeatmapInfo.cs +++ b/osu.Game/Database/BeatmapInfo.cs @@ -43,6 +43,9 @@ namespace osu.Game.Database [Ignore] public BeatmapMetrics Metrics { get; set; } + [Ignore] + public BeatmapOnlineInfo OnlineInfo { get; set; } + public string Path { get; set; } [JsonProperty("file_md5")] diff --git a/osu.Game/Database/BeatmapOnlineInfo.cs b/osu.Game/Database/BeatmapOnlineInfo.cs new file mode 100644 index 0000000000..bcb2ef16ac --- /dev/null +++ b/osu.Game/Database/BeatmapOnlineInfo.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace osu.Game.Database +{ + /// <summary> + /// Beatmap info retrieved for previewing locally without having the beatmap downloaded. + /// </summary> + public class BeatmapOnlineInfo + { + /// <summary> + /// The different sizes of cover art for this beatmap: cover, cover@2x, card, card@2x, list, list@2x. + /// </summary> + [JsonProperty(@"covers")] + public IEnumerable<string> Covers { get; set; } + + /// <summary> + /// A small sample clip of this beatmap's song. + /// </summary> + [JsonProperty(@"previewUrl")] + public string Preview { get; set; } + + /// <summary> + /// The amount of plays this beatmap has. + /// </summary> + [JsonProperty(@"play_count")] + public int PlayCount { get; set; } + + /// <summary> + /// The amount of people who have favourited this map. + /// </summary> + [JsonProperty(@"favourite_count")] + public int FavouriteCount { get; set; } + } +} diff --git a/osu.Game/Database/OnlineWorkingBeatmap.cs b/osu.Game/Database/OnlineWorkingBeatmap.cs new file mode 100644 index 0000000000..1465c59434 --- /dev/null +++ b/osu.Game/Database/OnlineWorkingBeatmap.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps; + +namespace osu.Game.Database +{ + internal class OnlineWorkingBeatmap : WorkingBeatmap + { + private readonly TextureStore textures; + private readonly TrackManager tracks; + + public OnlineWorkingBeatmap(BeatmapInfo beatmapInfo, TextureStore textures, TrackManager tracks) : base(beatmapInfo) + { + this.textures = textures; + this.tracks = tracks; + } + + protected override Beatmap GetBeatmap() + { + return new Beatmap(); + } + + protected override Texture GetBackground() + { + return textures.Get(BeatmapInfo.OnlineInfo.Covers.FirstOrDefault()); + } + + protected override Track GetTrack() + { + return tracks.Get(BeatmapInfo.OnlineInfo.Preview); + } + } +} diff --git a/osu.Game/Database/RankStatus.cs b/osu.Game/Database/RankStatus.cs new file mode 100644 index 0000000000..f2a7d67a40 --- /dev/null +++ b/osu.Game/Database/RankStatus.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; + +namespace osu.Game.Database +{ + public enum RankStatus + { + Any = 7, + [Description("Ranked & Approved")] + RankedApproved = 0, + Approved = 1, + Loved = 8, + Favourites = 2, + [Description("Mod Requests")] + ModRequests = 3, + Pending = 4, + Graveyard = 5, + [Description("My Maps")] + MyMaps = 6, + } +} diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 830d0adc97..9a19819af8 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -1,20 +1,29 @@ // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. // 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<Triangle> + public class Triangles : Drawable { + private const float triangle_size = 100; + public override bool HandleInput => false; public Color4 ColourLight = Color4.White; @@ -44,6 +53,33 @@ namespace osu.Game.Graphics.Backgrounds /// </summary> public bool HideAlphaDiscrepancies = true; + /// <summary> + /// The relative velocity of the triangles. Default is 1. + /// </summary> + public float Velocity = 1; + + private readonly SortedList<TriangleParticle> parts = new SortedList<TriangleParticle>(Comparer<TriangleParticle>.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; } @@ -52,42 +88,71 @@ 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(); - float adjustedAlpha = HideAlphaDiscrepancies ? - // Cubically scale alpha to make it drop off more sharply. - (float)Math.Pow(DrawInfo.Colour.AverageColour.Linear.A, 3) : - 1; + Invalidate(Invalidation.DrawNode, shallPropagate: false); - foreach (var t in Children) + for (int i = 0; i < parts.Count; i++) { - t.Alpha = adjustedAlpha; - t.Position -= new Vector2(0, (float)(t.Scale.X * (50 / DrawHeight) * (Time.Elapsed / 950)) / triangleScale); - if (ExpireOffScreenTriangles && t.DrawPosition.Y + t.DrawSize.Y * t.Scale.Y < 0) - t.Expire(); + TriangleParticle newParticle = parts[i]; + + float adjustedAlpha = HideAlphaDiscrepancies ? + // Cubically scale alpha to make it drop off more sharply. + (float)Math.Pow(DrawInfo.Colour.AverageColour.Linear.A, 3) : + 1; + + + newParticle.Position += new Vector2(0, -(parts[i].Scale * (50 / DrawHeight)) / triangleScale * Velocity) * ((float)Time.Elapsed / 950); + newParticle.Colour.A = adjustedAlpha; + + parts[i] = newParticle; + + if (!CreateNewTriangles) + continue; + + float bottomPos = parts[i].Position.Y + triangle_size * parts[i].Scale * 0.866f / DrawHeight; + + if (bottomPos < 0) + parts.RemoveAt(i); } - if (CreateNewTriangles) - addTriangles(false); + addTriangles(false); } - 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; + } + + /// <summary> + /// Creates a triangle particle with a random scale. + /// </summary> + /// <returns>The triangle particle.</returns> + protected virtual TriangleParticle CreateTriangle() { const float std_dev = 0.16f; const float mean = 0.5f; @@ -97,32 +162,100 @@ 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); + /// <summary> + /// Creates a shade of colour for the triangles. + /// </summary> + /// <returns>The colour.</returns> + 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<TexturedVertex2D> VertexBatch = new LinearBatch<TexturedVertex2D>(100 * 3, 10, PrimitiveType.Triangles); + } + + private class TrianglesDrawNode : DrawNode + { + public Shader Shader; + public Texture Texture; + + public TrianglesDrawNodeSharedData Shared; + + public readonly List<TriangleParticle> Parts = new List<TriangleParticle>(); + public Vector2 Size; + + public override void Draw(Action<TexturedVertex2D> 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(); + + foreach (TriangleParticle particle in Parts) + { + var offset = new Vector2(particle.Scale * 0.5f, particle.Scale * 0.866f); + + var triangle = new Triangle( + particle.Position * Size * DrawInfo.Matrix, + (particle.Position * Size + offset * triangle_size) * DrawInfo.Matrix, + (particle.Position * Size + new Vector2(-offset.X, offset.Y) * triangle_size) * DrawInfo.Matrix + ); + + ColourInfo colourInfo = DrawInfo.Colour; + colourInfo.ApplyChild(particle.Colour); + + Texture.DrawTriangle(triangle, colourInfo, null, Shared.VertexBatch.Add); + } + + Shader.Unbind(); } } + + protected struct TriangleParticle : IComparable<TriangleParticle> + { + /// <summary> + /// The position of the top vertex of the triangle. + /// </summary> + public Vector2 Position; + + /// <summary> + /// The colour of the triangle. + /// </summary> + public Color4 Colour; + + /// <summary> + /// The scale of the triangle. + /// </summary> + public float Scale; + + /// <summary> + /// Compares two <see cref="TriangleParticle"/>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. + /// </summary> + /// <param name="other"></param> + /// <returns></returns> + public int CompareTo(TriangleParticle other) => other.Scale.CompareTo(Scale); + } } } diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 5895eabde4..c0defceac0 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -2,59 +2,65 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Configuration; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Timing; namespace osu.Game.Graphics.Containers { public class BeatSyncedContainer : Container { - private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>(); + protected readonly Bindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>(); private int lastBeat; private TimingControlPoint lastTimingPoint; + /// <summary> + /// The amount of time before a beat we should fire <see cref="OnNewBeat(int, TimingControlPoint, EffectControlPoint, TrackAmplitudes)"/>. + /// This allows for adding easing to animations that may be synchronised to the beat. + /// </summary> + protected double EarlyActivationMilliseconds; + protected override void Update() { - if (beatmap.Value?.Track == null) + if (Beatmap.Value?.Track == null) return; - double currentTrackTime = beatmap.Value.Track.CurrentTime; + double currentTrackTime = Beatmap.Value.Track.CurrentTime + EarlyActivationMilliseconds; - TimingControlPoint timingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); - EffectControlPoint effectPoint = beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); + TimingControlPoint timingPoint = Beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); + EffectControlPoint effectPoint = Beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); if (timingPoint.BeatLength == 0) return; - int beat = (int)((currentTrackTime - timingPoint.Time) / timingPoint.BeatLength); + int beatIndex = (int)((currentTrackTime - timingPoint.Time) / timingPoint.BeatLength); // The beats before the start of the first control point are off by 1, this should do the trick if (currentTrackTime < timingPoint.Time) - beat--; + beatIndex--; - if (timingPoint == lastTimingPoint && beat == lastBeat) + if (timingPoint == lastTimingPoint && beatIndex == lastBeat) return; double offsetFromBeat = (timingPoint.Time - currentTrackTime) % timingPoint.BeatLength; using (BeginDelayedSequence(offsetFromBeat, true)) - OnNewBeat(beat, timingPoint.BeatLength, timingPoint.TimeSignature, effectPoint.KiaiMode); + OnNewBeat(beatIndex, timingPoint, effectPoint, Beatmap.Value.Track.CurrentAmplitudes); - lastBeat = beat; + lastBeat = beatIndex; lastTimingPoint = timingPoint; } [BackgroundDependencyLoader] private void load(OsuGameBase game) { - beatmap.BindTo(game.Beatmap); + Beatmap.BindTo(game.Beatmap); } - protected virtual void OnNewBeat(int newBeat, double beatLength, TimeSignatures timeSignature, bool kiai) + protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) { } } diff --git a/osu.Game/Graphics/Containers/ReverseDepthFillFlowContainer.cs b/osu.Game/Graphics/Containers/ReverseDepthFillFlowContainer.cs new file mode 100644 index 0000000000..2b52b06abc --- /dev/null +++ b/osu.Game/Graphics/Containers/ReverseDepthFillFlowContainer.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Graphics.Containers +{ + public class ReverseDepthFillFlowContainer<T> : FillFlowContainer<T> where T : Drawable + { + protected override IComparer<Drawable> DepthComparer => new ReverseCreationOrderDepthComparer(); + protected override IEnumerable<T> FlowingChildren => base.FlowingChildren.Reverse(); + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 4f286ba7b5..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<T> CreateMenuItem(string text, T value) => new OsuDropdownMenuItem(text, value) { AccentColour = AccentColour }; - private class OsuDropdownMenuItem : DropdownMenuItem<T> + public class OsuDropdownMenuItem : DropdownMenuItem<T> { 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 @@ -113,13 +113,13 @@ namespace osu.Game.Graphics.UserInterface } } - protected class OsuDropdownHeader : DropdownHeader + 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/OsuEnumDropdown.cs b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs new file mode 100644 index 0000000000..5de6507bb3 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.ComponentModel; +using System.Reflection; +using System.Collections.Generic; + +namespace osu.Game.Graphics.UserInterface +{ + public class OsuEnumDropdown<T> : OsuDropdown<T> + { + public OsuEnumDropdown() + { + if (!typeof(T).IsEnum) + throw new InvalidOperationException("OsuEnumDropdown only supports enums as the generic type argument"); + + List<KeyValuePair<string, T>> items = new List<KeyValuePair<string, T>>(); + foreach(var val in (T[])Enum.GetValues(typeof(T))) + { + var field = typeof(T).GetField(Enum.GetName(typeof(T), val)); + items.Add( + new KeyValuePair<string, T>( + field.GetCustomAttribute<DescriptionAttribute>()?.Description ?? Enum.GetName(typeof(T), val), + val + ) + ); + } + Items = items; + } + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index a9fad0c739..4bbae4efd1 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -57,9 +57,9 @@ namespace osu.Game.Graphics.UserInterface } } - private class OsuTabItem : TabItem<T> + public class OsuTabItem : TabItem<T> { - private readonly SpriteText text; + protected readonly SpriteText Text; private readonly Box box; private Color4? accentColour; @@ -70,7 +70,7 @@ namespace osu.Game.Graphics.UserInterface { accentColour = value; if (!Active) - text.Colour = value; + Text.Colour = value; } } @@ -94,13 +94,13 @@ namespace osu.Game.Graphics.UserInterface private void fadeActive() { box.FadeIn(transition_length, EasingTypes.OutQuint); - text.FadeColour(Color4.White, transition_length, EasingTypes.OutQuint); + Text.FadeColour(Color4.White, transition_length, EasingTypes.OutQuint); } private void fadeInactive() { box.FadeOut(transition_length, EasingTypes.OutQuint); - text.FadeColour(AccentColour, transition_length, EasingTypes.OutQuint); + Text.FadeColour(AccentColour, transition_length, EasingTypes.OutQuint); } protected override bool OnHover(InputState state) @@ -130,7 +130,7 @@ namespace osu.Game.Graphics.UserInterface Children = new Drawable[] { - text = new OsuSpriteText + Text = new OsuSpriteText { Margin = new MarginPadding { Top = 5, Bottom = 5 }, Origin = Anchor.BottomLeft, 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 <contact@ppy.sh>. +// 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<string> Name = new Bindable<string>(); + public Bindable<User> Host = new Bindable<User>(); + public Bindable<RoomStatus> Status = new Bindable<RoomStatus>(); + public Bindable<BeatmapMetadata> Beatmap = new Bindable<BeatmapMetadata>(); + } +} 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 <contact@ppy.sh>. +// 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 f13043a745..886ff4f8d1 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -41,6 +41,8 @@ namespace osu.Game private DialogOverlay dialogOverlay; + private DirectOverlay direct; + private Intro intro { get @@ -70,6 +72,8 @@ namespace osu.Game public void ToggleSettings() => settings.ToggleVisibility(); + public void ToggleDirect() => direct.ToggleVisibility(); + [BackgroundDependencyLoader] private void load() { @@ -160,6 +164,7 @@ namespace osu.Game }); //overlay elements + LoadComponentAsync(direct = new DirectOverlay { Depth = -1 }, mainContent.Add); LoadComponentAsync(chat = new ChatOverlay { Depth = -1 }, mainContent.Add); LoadComponentAsync(settings = new SettingsOverlay { Depth = -1 }, overlayContent.Add); LoadComponentAsync(musicController = new MusicController @@ -249,6 +254,9 @@ namespace osu.Game case Key.O: settings.ToggleVisibility(); return true; + case Key.D: + direct.ToggleVisibility(); + return true; } } @@ -280,6 +288,7 @@ namespace osu.Game Toolbar.State = Visibility.Hidden; musicController.State = Visibility.Hidden; chat.State = Visibility.Hidden; + direct.State = Visibility.Hidden; } else { diff --git a/osu.Game/Overlays/Direct/DirectGridPanel.cs b/osu.Game/Overlays/Direct/DirectGridPanel.cs new file mode 100644 index 0000000000..4be68157d7 --- /dev/null +++ b/osu.Game/Overlays/Direct/DirectGridPanel.cs @@ -0,0 +1,193 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +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.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Overlays.Direct +{ + public class DirectGridPanel : DirectPanel + { + private const float horizontal_padding = 10; + private const float vertical_padding = 5; + + private FillFlowContainer bottomPanel; + + public DirectGridPanel(BeatmapSetInfo beatmap) : base(beatmap) + { + Height = 140 + vertical_padding; //full height of all the elements plus vertical padding (autosize uses the image) + CornerRadius = 4; + Masking = true; + + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 1f), + Radius = 3f, + Colour = Color4.Black.Opacity(0.25f), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + FadeInFromZero(200, EasingTypes.Out); + bottomPanel.LayoutDuration = 200; + bottomPanel.LayoutEasing = EasingTypes.Out; + bottomPanel.Origin = Anchor.BottomLeft; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, LocalisationEngine localisation, TextureStore textures) + { + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + GetBackground(textures), + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f), + }, + bottomPanel = new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopLeft, + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, vertical_padding), + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = horizontal_padding, Right = horizontal_padding }, + Direction = FillDirection.Vertical, + Children = new[] + { + new OsuSpriteText + { + Text = localisation.GetUnicodePreference(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), + TextSize = 18, + Font = @"Exo2.0-BoldItalic", + }, + new OsuSpriteText + { + Text = localisation.GetUnicodePreference(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), + Font = @"Exo2.0-BoldItalic", + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding + { + Top = vertical_padding, + Bottom = vertical_padding, + Left = horizontal_padding, + Right = horizontal_padding, + }, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new[] + { + new OsuSpriteText + { + Text = "mapped by ", + TextSize = 14, + Shadow = false, + Colour = colours.Gray5, + }, + new OsuSpriteText + { + Text = SetInfo.Metadata.Author, + TextSize = 14, + Font = @"Exo2.0-SemiBoldItalic", + Shadow = false, + Colour = colours.BlueDark, + }, + }, + }, + new Container + { + AutoSizeAxes = Axes.X, + Height = 14, + Children = new[] + { + new OsuSpriteText + { + Text = $"from {SetInfo.Metadata.Source}", + TextSize = 14, + Shadow = false, + Colour = colours.Gray5, + Alpha = string.IsNullOrEmpty(SetInfo.Metadata.Source) ? 0f : 1f, + }, + }, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.X, + Height = 20, + Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding }, + Children = GetDifficultyIcons(), + }, + }, + }, + }, + }, + }, + }, + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Top = vertical_padding, Right = vertical_padding }, + Children = new[] + { + new Statistic(FontAwesome.fa_play_circle, SetInfo.Beatmaps.FirstOrDefault()?.OnlineInfo.PlayCount ?? 0) + { + Margin = new MarginPadding { Right = 1 }, + }, + new Statistic(FontAwesome.fa_heart, SetInfo.Beatmaps.FirstOrDefault()?.OnlineInfo.FavouriteCount ?? 0), + }, + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs new file mode 100644 index 0000000000..48636a5228 --- /dev/null +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -0,0 +1,197 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +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.Framework.Graphics.Colour; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Database; +using osu.Framework.Allocation; +using osu.Framework.Localisation; +using osu.Framework.Graphics.Textures; +using System.Linq; +using osu.Framework.Input; + +namespace osu.Game.Overlays.Direct +{ + public class DirectListPanel : DirectPanel + { + private const float horizontal_padding = 10; + private const float vertical_padding = 5; + private const float height = 70; + + public DirectListPanel(BeatmapSetInfo beatmap) : base(beatmap) + { + RelativeSizeAxes = Axes.X; + Height = height; + CornerRadius = 5; + Masking = true; + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 1f), + Radius = 3f, + Colour = Color4.Black.Opacity(0.25f), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + FadeInFromZero(200, EasingTypes.Out); + } + + [BackgroundDependencyLoader] + private void load(LocalisationEngine localisation, TextureStore textures) + { + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + GetBackground(textures), + new Box + { + RelativeSizeAxes = Axes.Both, + ColourInfo = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.25f), Color4.Black.Opacity(0.75f)), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding, Left = horizontal_padding, Right = vertical_padding }, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Current = localisation.GetUnicodePreference(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), + TextSize = 18, + Font = @"Exo2.0-BoldItalic", + }, + new OsuSpriteText + { + Current = localisation.GetUnicodePreference(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), + Font = @"Exo2.0-BoldItalic", + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.X, + Height = 20, + Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding }, + Children = GetDifficultyIcons(), + }, + }, + }, + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Right = height - vertical_padding * 2 + vertical_padding }, + Children = new Drawable[] + { + new Statistic(FontAwesome.fa_play_circle, SetInfo.Beatmaps.FirstOrDefault()?.OnlineInfo.PlayCount ?? 0) + { + Margin = new MarginPadding { Right = 1 }, + }, + new Statistic(FontAwesome.fa_heart, SetInfo.Beatmaps.FirstOrDefault()?.OnlineInfo.FavouriteCount ?? 0), + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new[] + { + new OsuSpriteText + { + Text = "mapped by ", + TextSize = 14, + }, + new OsuSpriteText + { + Text = SetInfo.Metadata.Author, + TextSize = 14, + Font = @"Exo2.0-SemiBoldItalic", + }, + }, + }, + new OsuSpriteText + { + Text = $"from {SetInfo.Metadata.Source}", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + TextSize = 14, + Alpha = string.IsNullOrEmpty(SetInfo.Metadata.Source) ? 0f : 1f, + }, + }, + }, + new DownloadButton + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Size = new Vector2(height - vertical_padding * 2), + }, + }, + }, + }; + } + + private class DownloadButton : ClickableContainer + { + private readonly TextAwesome icon; + + public DownloadButton() + { + Children = new Drawable[] + { + icon = new TextAwesome + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + TextSize = 30, + Icon = FontAwesome.fa_osu_chevron_down_o, + }, + }; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + icon.ScaleTo(0.9f, 1000, EasingTypes.Out); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + icon.ScaleTo(1f, 500, EasingTypes.OutElastic); + return base.OnMouseUp(state, args); + } + + protected override bool OnHover(InputState state) + { + icon.ScaleTo(1.1f, 500, EasingTypes.OutElastic); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + icon.ScaleTo(1f, 500, EasingTypes.OutElastic); + } + } + } +} diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs new file mode 100644 index 0000000000..8a56cf392e --- /dev/null +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -0,0 +1,88 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Overlays.Direct +{ + public abstract class DirectPanel : Container + { + protected readonly BeatmapSetInfo SetInfo; + + protected DirectPanel(BeatmapSetInfo setInfo) + { + SetInfo = setInfo; + } + + protected IEnumerable<DifficultyIcon> GetDifficultyIcons() + { + var icons = new List<DifficultyIcon>(); + + foreach (var b in SetInfo.Beatmaps) + icons.Add(new DifficultyIcon(b)); + + return icons; + } + + protected Drawable GetBackground(TextureStore textures) + { + return new AsyncLoadWrapper(new BeatmapBackgroundSprite(new OnlineWorkingBeatmap(SetInfo.Beatmaps.FirstOrDefault(), textures, null)) + { + FillMode = FillMode.Fill, + OnLoadComplete = d => d.FadeInFromZero(400, EasingTypes.Out), + }) { RelativeSizeAxes = Axes.Both }; + } + + public class Statistic : FillFlowContainer + { + private readonly SpriteText text; + + private int value; + public int Value + { + get { return value; } + set + { + this.value = value; + text.Text = Value.ToString(@"N0"); + } + } + + public Statistic(FontAwesome icon, int value = 0) + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Horizontal; + Spacing = new Vector2(5f, 0f); + + Children = new Drawable[] + { + text = new OsuSpriteText + { + Font = @"Exo2.0-SemiBoldItalic", + }, + new TextAwesome + { + Icon = icon, + Shadow = true, + TextSize = 14, + Margin = new MarginPadding { Top = 1 }, + }, + }; + + Value = value; + } + } + } +} diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs new file mode 100644 index 0000000000..735e14b8c1 --- /dev/null +++ b/osu.Game/Overlays/Direct/FilterControl.cs @@ -0,0 +1,233 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// 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.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Direct +{ + public class FilterControl : Container + { + public static readonly float HEIGHT = 35 + 32 + 30 + padding * 2; // search + mode toggle buttons + sort tabs + padding + + private const float padding = 10; + + private readonly Box tabStrip; + private readonly FillFlowContainer<RulesetToggleButton> modeButtons; + + public readonly SearchTextBox Search; + public readonly SortTabControl SortTabs; + public readonly OsuEnumDropdown<RankStatus> RankStatusDropdown; + public readonly Bindable<DirectOverlay.PanelDisplayStyle> DisplayStyle = new Bindable<DirectOverlay.PanelDisplayStyle>(); + + protected override bool InternalContains(Vector2 screenSpacePos) => base.InternalContains(screenSpacePos) || RankStatusDropdown.Contains(screenSpacePos); + + public FilterControl() + { + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + DisplayStyle.Value = DirectOverlay.PanelDisplayStyle.Grid; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"384552"), + Alpha = 0.9f, + }, + tabStrip = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopLeft, + RelativeSizeAxes = Axes.X, + Height = 1, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = DirectOverlay.WIDTH_PADDING, Right = DirectOverlay.WIDTH_PADDING }, + Children = new Drawable[] + { + Search = new DirectSearchTextBox + { + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = padding }, + }, + modeButtons = new FillFlowContainer<RulesetToggleButton> + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(padding, 0f), + Margin = new MarginPadding { Top = padding }, + }, + SortTabs = new SortTabControl + { + RelativeSizeAxes = Axes.X, + }, + }, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Spacing = new Vector2(10f, 0f), + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Top = HEIGHT - SlimEnumDropdown<DirectTab>.HEIGHT - padding, Right = DirectOverlay.WIDTH_PADDING }, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5f, 0f), + Direction = FillDirection.Horizontal, + Children = new[] + { + new DisplayStyleToggleButton(FontAwesome.fa_th_large, DirectOverlay.PanelDisplayStyle.Grid, DisplayStyle), + new DisplayStyleToggleButton(FontAwesome.fa_list_ul, DirectOverlay.PanelDisplayStyle.List, DisplayStyle), + }, + }, + RankStatusDropdown = new SlimEnumDropdown<RankStatus> + { + RelativeSizeAxes = Axes.None, + Width = 160f, + }, + }, + }, + }; + + RankStatusDropdown.Current.Value = RankStatus.RankedApproved; + SortTabs.Current.Value = SortCriteria.Title; + SortTabs.Current.TriggerChange(); + } + + [BackgroundDependencyLoader(true)] + private void load(OsuGame game, RulesetDatabase rulesets, OsuColour colours) + { + tabStrip.Colour = colours.Yellow; + RankStatusDropdown.AccentColour = colours.BlueDark; + + var b = new Bindable<RulesetInfo>(); //backup bindable incase the game is null + foreach (var r in rulesets.AllRulesets) + { + modeButtons.Add(new RulesetToggleButton(game?.Ruleset ?? b, r)); + } + } + + private class DirectSearchTextBox : SearchTextBox + { + protected override Color4 BackgroundUnfocused => backgroundColour; + protected override Color4 BackgroundFocused => backgroundColour; + + private Color4 backgroundColour; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + backgroundColour = colours.Gray2.Opacity(0.9f); + } + } + + private class RulesetToggleButton : ClickableContainer + { + private readonly TextAwesome icon; + + private RulesetInfo ruleset; + public RulesetInfo Ruleset + { + get { return ruleset; } + set + { + ruleset = value; + icon.Icon = Ruleset.CreateInstance().Icon; + } + } + + private readonly Bindable<RulesetInfo> bindable; + + private void Bindable_ValueChanged(RulesetInfo obj) + { + icon.FadeTo(Ruleset.ID == obj?.ID ? 1f : 0.5f, 100); + } + + public RulesetToggleButton(Bindable<RulesetInfo> bindable, RulesetInfo ruleset) + { + this.bindable = bindable; + AutoSizeAxes = Axes.Both; + + Children = new[] + { + icon = new TextAwesome + { + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + TextSize = 32, + } + }; + + Ruleset = ruleset; + bindable.ValueChanged += Bindable_ValueChanged; + Bindable_ValueChanged(bindable.Value); + Action = () => bindable.Value = Ruleset; + } + + protected override void Dispose(bool isDisposing) + { + if (bindable != null) + bindable.ValueChanged -= Bindable_ValueChanged; + base.Dispose(isDisposing); + } + } + + private class DisplayStyleToggleButton : ClickableContainer + { + private readonly TextAwesome icon; + private readonly DirectOverlay.PanelDisplayStyle style; + private readonly Bindable<DirectOverlay.PanelDisplayStyle> bindable; + + public DisplayStyleToggleButton(FontAwesome icon, DirectOverlay.PanelDisplayStyle style, Bindable<DirectOverlay.PanelDisplayStyle> bindable) + { + this.bindable = bindable; + this.style = style; + Size = new Vector2(SlimEnumDropdown<DirectTab>.HEIGHT); + + Children = new Drawable[] + { + this.icon = new TextAwesome + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = icon, + TextSize = 18, + UseFullGlyphHeight = false, + Alpha = 0.5f, + }, + }; + + bindable.ValueChanged += Bindable_ValueChanged; + Bindable_ValueChanged(bindable.Value); + Action = () => bindable.Value = this.style; + } + + private void Bindable_ValueChanged(DirectOverlay.PanelDisplayStyle style) + { + icon.FadeTo(style == this.style ? 1.0f : 0.5f, 100); + } + + protected override void Dispose(bool isDisposing) + { + bindable.ValueChanged -= Bindable_ValueChanged; + } + } + } +} diff --git a/osu.Game/Overlays/Direct/Header.cs b/osu.Game/Overlays/Direct/Header.cs new file mode 100644 index 0000000000..8e4ede48d5 --- /dev/null +++ b/osu.Game/Overlays/Direct/Header.cs @@ -0,0 +1,124 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +using Container = osu.Framework.Graphics.Containers.Container; + +namespace osu.Game.Overlays.Direct +{ + public class Header : Container + { + public static readonly float HEIGHT = 90; + + private readonly Box tabStrip; + + public readonly OsuTabControl<DirectTab> Tabs; + + public Header() + { + Height = HEIGHT; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"252f3a"), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = DirectOverlay.WIDTH_PADDING, Right = DirectOverlay.WIDTH_PADDING }, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.BottomLeft, + Position = new Vector2(-35f, 5f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10f, 0f), + Children = new Drawable[] + { + new TextAwesome + { + TextSize = 25, + Icon = FontAwesome.fa_osu_chevron_down_o, + }, + new OsuSpriteText + { + TextSize = 25, + Text = @"osu!direct", + }, + }, + }, + tabStrip = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Width = 282, //todo: make this actually match the tab control's width instead of hardcoding + Height = 1, + }, + Tabs = new DirectTabControl + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + }, + }, + }, + }; + + Tabs.Current.Value = DirectTab.Search; + Tabs.Current.TriggerChange(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + tabStrip.Colour = colours.Green; + } + + private class DirectTabControl : OsuTabControl<DirectTab> + { + protected override TabItem<DirectTab> CreateTabItem(DirectTab value) => new DirectTabItem(value); + + public DirectTabControl() + { + Height = 25; + AccentColour = Color4.White; + } + + private class DirectTabItem : OsuTabItem + { + public DirectTabItem(DirectTab value) : base(value) + { + Text.TextSize = 15; + } + } + } + } + + public enum DirectTab + { + Search, + [Description("Newest Maps")] + NewestMaps, + [Description("Top Rated")] + TopRated, + [Description("Most Played")] + MostPlayed + } +} diff --git a/osu.Game/Overlays/Direct/SlimEnumDropdown.cs b/osu.Game/Overlays/Direct/SlimEnumDropdown.cs new file mode 100644 index 0000000000..1d12b8477b --- /dev/null +++ b/osu.Game/Overlays/Direct/SlimEnumDropdown.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Direct +{ + public class SlimEnumDropdown<T> : OsuEnumDropdown<T> + { + public const float HEIGHT = 25; + + protected override DropdownHeader CreateHeader() => new SlimDropdownHeader { AccentColour = AccentColour }; + protected override Menu CreateMenu() => new SlimMenu(); + + private class SlimDropdownHeader : OsuDropdownHeader + { + public SlimDropdownHeader() + { + Height = HEIGHT; + Icon.TextSize = 16; + Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + BackgroundColour = Color4.Black.Opacity(0.25f); + } + } + + private class SlimMenu : OsuMenu + { + public SlimMenu() + { + Background.Colour = Color4.Black.Opacity(0.25f); + } + } + } +} diff --git a/osu.Game/Overlays/Direct/SortTabControl.cs b/osu.Game/Overlays/Direct/SortTabControl.cs new file mode 100644 index 0000000000..4d4e02d875 --- /dev/null +++ b/osu.Game/Overlays/Direct/SortTabControl.cs @@ -0,0 +1,117 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Direct +{ + public class SortTabControl : OsuTabControl<SortCriteria> + { + protected override TabItem<SortCriteria> CreateTabItem(SortCriteria value) => new SortTabItem(value); + + public SortTabControl() + { + Height = 30; + } + + private class SortTabItem : TabItem<SortCriteria> + { + private const float transition_duration = 100; + + private readonly Box box; + + public override bool Active + { + get { return base.Active; } + set + { + if (Active == value) return; + + if (value) + slideActive(); + else + slideInactive(); + base.Active = value; + } + } + + public SortTabItem(SortCriteria value) : base(value) + { + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + + Children = new Drawable[] + { + new OsuSpriteText + { + Margin = new MarginPadding { Top = 8, Bottom = 8 }, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Text = (value as Enum).GetDescription() ?? value.ToString(), + TextSize = 14, + Font = @"Exo2.0-Bold", + }, + box = new Box + { + RelativeSizeAxes = Axes.X, + Height = 5, + Scale = new Vector2(1f, 0f), + Colour = Color4.White, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + box.Colour = colours.Yellow; + } + + protected override bool OnHover(InputState state) + { + if (!Active) + slideActive(); + return true; + } + + protected override void OnHoverLost(InputState state) + { + if (!Active) + slideInactive(); + } + + private void slideActive() + { + box.ScaleTo(new Vector2(1f), transition_duration); + } + + private void slideInactive() + { + box.ScaleTo(new Vector2(1f, 0f), transition_duration); + } + } + } + + public enum SortCriteria + { + Title, + Artist, + Creator, + Difficulty, + Ranked, + Rating, + } +} diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs new file mode 100644 index 0000000000..0930c825b6 --- /dev/null +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -0,0 +1,230 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using OpenTK; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays.Direct; + +using Container = osu.Framework.Graphics.Containers.Container; + +namespace osu.Game.Overlays +{ + public class DirectOverlay : WaveOverlayContainer + { + public static readonly int WIDTH_PADDING = 80; + private const float panel_padding = 10f; + + private readonly FilterControl filter; + private readonly FillFlowContainer resultCountsContainer; + private readonly OsuSpriteText resultCountsText; + private readonly FillFlowContainer<DirectPanel> panels; + + private IEnumerable<BeatmapSetInfo> beatmapSets; + public IEnumerable<BeatmapSetInfo> BeatmapSets + { + get { return beatmapSets; } + set + { + if (beatmapSets?.Equals(value) ?? false) return; + beatmapSets = value; + + recreatePanels(filter.DisplayStyle.Value); + } + } + + private ResultCounts resultAmounts; + public ResultCounts ResultAmounts + { + get { return resultAmounts; } + set + { + if (value == ResultAmounts) return; + resultAmounts = value; + + updateResultCounts(); + } + } + + public DirectOverlay() + { + RelativeSizeAxes = Axes.Both; + + // osu!direct colours are not part of the standard palette + + FirstWaveColour = OsuColour.FromHex(@"19b0e2"); + SecondWaveColour = OsuColour.FromHex(@"2280a2"); + ThirdWaveColour = OsuColour.FromHex(@"005774"); + FourthWaveColour = OsuColour.FromHex(@"003a4e"); + + Header header; + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"485e74"), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new[] + { + new Triangles + { + RelativeSizeAxes = Axes.Both, + TriangleScale = 5, + ColourLight = OsuColour.FromHex(@"465b71"), + ColourDark = OsuColour.FromHex(@"3f5265"), + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = Header.HEIGHT + FilterControl.HEIGHT }, + Children = new[] + { + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollDraggerVisible = false, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + resultCountsContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Left = WIDTH_PADDING, Top = 6 }, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Found ", + TextSize = 15, + }, + resultCountsText = new OsuSpriteText + { + TextSize = 15, + Font = @"Exo2.0-Bold", + }, + } + }, + panels = new FillFlowContainer<DirectPanel> + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Top = panel_padding, Bottom = panel_padding, Left = WIDTH_PADDING, Right = WIDTH_PADDING }, + Spacing = new Vector2(panel_padding), + }, + }, + }, + }, + }, + }, + }, + filter = new FilterControl + { + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = Header.HEIGHT }, + }, + header = new Header + { + RelativeSizeAxes = Axes.X, + }, + }; + + header.Tabs.Current.ValueChanged += tab => { if (tab != DirectTab.Search) filter.Search.Current.Value = string.Empty; }; + + filter.Search.Exit = Hide; + filter.Search.Current.ValueChanged += text => { if (text != string.Empty) header.Tabs.Current.Value = DirectTab.Search; }; + filter.DisplayStyle.ValueChanged += recreatePanels; + + updateResultCounts(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + resultCountsContainer.Colour = colours.Yellow; + } + + private void updateResultCounts() + { + resultCountsContainer.FadeTo(ResultAmounts == null ? 0f : 1f, 200, EasingTypes.Out); + if (ResultAmounts == null) return; + + resultCountsText.Text = pluralize("Artist", ResultAmounts.Artists) + ", " + + pluralize("Song", ResultAmounts.Songs) + ", " + + pluralize("Tag", ResultAmounts.Tags); + } + + private string pluralize(string prefix, int value) + { + return $@"{value} {prefix}" + (value == 1 ? string.Empty : @"s"); + } + + private void recreatePanels(PanelDisplayStyle displayStyle) + { + if (BeatmapSets == null) return; + panels.Children = BeatmapSets.Select(b => displayStyle == PanelDisplayStyle.Grid ? (DirectPanel)new DirectGridPanel(b) { Width = 400 } : new DirectListPanel(b)); + } + + protected override bool OnFocus(InputState state) + { + filter.Search.TriggerFocus(); + return false; + } + + protected override void PopIn() + { + base.PopIn(); + + filter.Search.HoldFocus = true; + } + + protected override void PopOut() + { + base.PopOut(); + + filter.Search.HoldFocus = false; + } + + public class ResultCounts + { + public readonly int Artists; + public readonly int Songs; + public readonly int Tags; + + public ResultCounts(int artists, int songs, int tags) + { + Artists = artists; + Songs = songs; + Tags = tags; + } + } + + public enum PanelDisplayStyle + { + Grid, + List, + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 86a47d8a95..561f81d6c3 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -12,15 +12,24 @@ using osu.Game.Graphics.UserInterface; 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; @@ -34,9 +43,18 @@ 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); + } + + [BackgroundDependencyLoader(permitNulls: true)] + private void load(OsuColour colours, APIAccess api) + { + this.colours = colours; api?.Register(this); } @@ -49,6 +67,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; @@ -66,24 +90,73 @@ 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 OsuSpriteText - { - Text = $"Connected as {api.Username}!", - }, - new OsuButton + new FillFlowContainer { RelativeSizeAxes = Axes.X, - Text = "Sign out", - Action = api.Logout + 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 }, + }, + }, + }; + + panel.Status.BindTo(api.LocalUser.Value.Status); + + dropdown.Current.ValueChanged += newValue => + { + switch (newValue) + { + 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; } @@ -170,5 +243,119 @@ namespace osu.Game.Overlays.Settings.Sections.General return base.OnFocus(state); } } + + private class UserDropdown : OsuEnumDropdown<UserAction> + { + protected override DropdownHeader CreateHeader() => new UserDropdownHeader { AccentColour = AccentColour }; + protected override Menu CreateMenu() => new UserDropdownMenu(); + protected override DropdownMenuItem<UserAction> 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/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index a9f0848403..5725ee8439 100644 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs @@ -1,32 +1,17 @@ // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; -using System.Reflection; -using System.ComponentModel; -using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { public class SettingsEnumDropdown<T> : SettingsDropdown<T> { - public SettingsEnumDropdown() + protected override Drawable CreateControl() => new OsuEnumDropdown<T> { - if (!typeof(T).IsEnum) - throw new InvalidOperationException("SettingsDropdown only supports enums as the generic type argument"); - - List<KeyValuePair<string, T>> items = new List<KeyValuePair<string, T>>(); - foreach(var val in (T[])Enum.GetValues(typeof(T))) - { - var field = typeof(T).GetField(Enum.GetName(typeof(T), val)); - items.Add( - new KeyValuePair<string, T>( - field.GetCustomAttribute<DescriptionAttribute>()?.Description ?? Enum.GetName(typeof(T), val), - val - ) - ); - } - Items = items; - } + Margin = new MarginPadding { Top = 5 }, + RelativeSizeAxes = Axes.X, + }; } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index b74ee52f9c..1a7d2d4e37 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -48,7 +48,8 @@ namespace osu.Game.Screens.Menu OnSolo = delegate { Push(consumeSongSelect()); }, OnMulti = delegate { Push(new Lobby()); }, OnExit = delegate { Exit(); }, - } + }, + new MenuSideFlashes(), } } }; @@ -60,6 +61,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(background); buttons.OnSettings = game.ToggleSettings; + buttons.OnDirect = game.ToggleDirect; preloadSongSelect(); } diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs new file mode 100644 index 0000000000..77239726e8 --- /dev/null +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -0,0 +1,98 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using System; + +namespace osu.Game.Screens.Menu +{ + public class MenuSideFlashes : BeatSyncedContainer + { + public override bool HandleInput => false; + + private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>(); + + private readonly Box leftBox; + private readonly Box rightBox; + + private const float amplitude_dead_zone = 0.25f; + private const float alpha_multiplier = (1 - amplitude_dead_zone) / 0.55f; + private const float kiai_multiplier = (1 - amplitude_dead_zone * 0.95f) / 0.8f; + + private const int box_max_alpha = 200; + private const double box_fade_in_time = 65; + private const int box_width = 200; + + public MenuSideFlashes() + { + EarlyActivationMilliseconds = box_fade_in_time; + + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Children = new Drawable[] + { + leftBox = new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + Width = box_width, + Alpha = 0, + BlendingMode = BlendingMode.Additive, + }, + rightBox = new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + Width = box_width, + Alpha = 0, + BlendingMode = BlendingMode.Additive, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase game, OsuColour colours) + { + beatmap.BindTo(game.Beatmap); + + // linear colour looks better in this case, so let's use it for now. + Color4 gradientDark = colours.Blue.Opacity(0).ToLinear(); + Color4 gradientLight = colours.Blue.Opacity(0.3f).ToLinear(); + + leftBox.ColourInfo = ColourInfo.GradientHorizontal(gradientLight, gradientDark); + rightBox.ColourInfo = ColourInfo.GradientHorizontal(gradientDark, gradientLight); + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + if (beatIndex < 0) + return; + + if (effectPoint.KiaiMode ? beatIndex % 2 == 0 : beatIndex % (int)timingPoint.TimeSignature == 0) + flash(leftBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes); + if (effectPoint.KiaiMode ? beatIndex % 2 == 1 : beatIndex % (int)timingPoint.TimeSignature == 0) + flash(rightBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes); + } + + private void flash(Drawable d, double beatLength, bool kiai, TrackAmplitudes amplitudes) + { + d.FadeTo(Math.Max(0, ((d.Equals(leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier)), box_fade_in_time); + using (d.BeginDelayedSequence(box_fade_in_time)) + d.FadeOut(beatLength, EasingTypes.In); + } + } +} diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index e28adeacff..44b7b6bceb 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -5,13 +5,17 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; using OpenTK; using OpenTK.Graphics; @@ -20,19 +24,23 @@ namespace osu.Game.Screens.Menu /// <summary> /// osu! logo and its attachments (pulsing, visualiser etc.) /// </summary> - public class OsuLogo : Container + public class OsuLogo : BeatSyncedContainer { public readonly Color4 OsuPink = OsuColour.FromHex(@"e967a1"); private readonly Sprite logo; private readonly CircularContainer logoContainer; private readonly Container logoBounceContainer; + private readonly Container logoBeatContainer; + private readonly Container logoAmplitudeContainer; private readonly Container logoHoverContainer; private SampleChannel sampleClick; private readonly Container colourAndTriangles; + private readonly Triangles triangles; + public Action Action; public float SizeForFlow => logo == null ? 0 : logo.DrawSize.X * logo.Scale.X * logoBounceContainer.Scale.X * logoHoverContainer.Scale.X * 0.74f; @@ -67,8 +75,12 @@ namespace osu.Game.Screens.Menu private const float default_size = 480; + private const double beat_in_time = 60; + public OsuLogo() { + EarlyActivationMilliseconds = beat_in_time; + Size = new Vector2(default_size); Anchor = Anchor.Centre; @@ -78,67 +90,16 @@ namespace osu.Game.Screens.Menu Children = new Drawable[] { - logoBounceContainer = new Container + logoHoverContainer = new Container { AutoSizeAxes = Axes.Both, Children = new Drawable[] { - logoHoverContainer = new Container + logoBounceContainer = new Container { AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new BufferedContainer - { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - logoContainer = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.88f), - Masking = true, - Children = new Drawable[] - { - colourAndTriangles = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuPink, - }, - new Triangles - { - TriangleScale = 4, - ColourLight = OsuColour.FromHex(@"ff7db7"), - ColourDark = OsuColour.FromHex(@"de5b95"), - RelativeSizeAxes = Axes.Both, - }, - } - }, - flashLayer = new Box - { - RelativeSizeAxes = Axes.Both, - BlendingMode = BlendingMode.Additive, - Colour = Color4.White, - Alpha = 0, - }, - }, - }, - logo = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } - }, rippleContainer = new Container { Anchor = Anchor.Centre, @@ -151,36 +112,101 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, BlendingMode = BlendingMode.Additive, - Alpha = 0.15f + Alpha = 0 } } }, - impactContainer = new CircularContainer + logoAmplitudeContainer = new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - BorderColour = Color4.White, - RelativeSizeAxes = Axes.Both, - BorderThickness = 10, - Masking = true, + AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new Box + logoBeatContainer = new Container { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new BufferedContainer + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + logoContainer = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.88f), + Masking = true, + Children = new Drawable[] + { + colourAndTriangles = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuPink, + }, + triangles = new Triangles + { + TriangleScale = 4, + ColourLight = OsuColour.FromHex(@"ff7db7"), + ColourDark = OsuColour.FromHex(@"de5b95"), + RelativeSizeAxes = Axes.Both, + }, + } + }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + BlendingMode = BlendingMode.Additive, + Colour = Color4.White, + Alpha = 0, + }, + }, + }, + logo = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }, + impactContainer = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + BorderColour = Color4.White, + RelativeSizeAxes = Axes.Both, + BorderThickness = 10, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0, + } + } + }, + new MenuVisualisation + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + BlendingMode = BlendingMode.Additive, + Alpha = 0.2f, + } + } } } - }, - new MenuVisualisation - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - BlendingMode = BlendingMode.Additive, - Alpha = 0.2f, } } } @@ -197,13 +223,56 @@ namespace osu.Game.Screens.Menu ripple.Texture = textures.Get(@"Menu/logo"); } - protected override void LoadComplete() - { - base.LoadComplete(); + private int lastBeatIndex; - ripple.ScaleTo(ripple.Scale * 1.1f, 500); - ripple.FadeOut(500); - ripple.Loop(300); + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + lastBeatIndex = beatIndex; + + var beatLength = timingPoint.BeatLength; + + float amplitudeAdjust = Math.Min(1, 0.4f + amplitudes.Maximum); + + if (beatIndex < 0) return; + + logoBeatContainer.ScaleTo(1 - 0.02f * amplitudeAdjust, beat_in_time, EasingTypes.Out); + using (logoBeatContainer.BeginDelayedSequence(beat_in_time)) + logoBeatContainer.ScaleTo(1, beatLength * 2, EasingTypes.OutQuint); + + ripple.ClearTransforms(); + + ripple.ScaleTo(logoAmplitudeContainer.Scale); + ripple.Alpha = 0.15f * amplitudeAdjust; + + ripple.ScaleTo(logoAmplitudeContainer.Scale * (1 + 0.04f * amplitudeAdjust), beatLength, EasingTypes.OutQuint); + ripple.FadeOut(beatLength, EasingTypes.OutQuint); + + if (effectPoint.KiaiMode && flashLayer.Alpha < 0.4f) + { + flashLayer.ClearTransforms(); + + flashLayer.FadeTo(0.2f * amplitudeAdjust, beat_in_time, EasingTypes.Out); + using (flashLayer.BeginDelayedSequence(beat_in_time)) + flashLayer.FadeOut(beatLength); + } + } + + protected override void Update() + { + base.Update(); + + const float scale_adjust_cutoff = 0.4f; + const float velocity_adjust_cutoff = 0.98f; + + var maxAmplitude = lastBeatIndex >= 0 ? Beatmap.Value?.Track?.CurrentAmplitudes.Maximum ?? 0 : 0; + logoAmplitudeContainer.ScaleTo(1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 75, EasingTypes.OutQuint); + + if (maxAmplitude > velocity_adjust_cutoff) + triangles.Velocity = 1 + Math.Max(0, maxAmplitude - velocity_adjust_cutoff) * 50; + else + triangles.Velocity = (float)Interpolation.Damp(triangles.Velocity, 1, 0.995f, Time.Elapsed); } protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) @@ -254,4 +323,4 @@ namespace osu.Game.Screens.Menu impactContainer.ScaleTo(1.12f, 250); } } -} \ No newline at end of file +} 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 <contact@ppy.sh>. +// 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<OsuSpriteText> 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<OsuSpriteText> + { + 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/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 92c40f6351..4a10438cdb 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using System.Linq; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,6 +11,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using OpenTK; using osu.Framework.Input; +using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Play.HUD { @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play.HUD { Children = new Drawable[] { - iconsContainer = new IconFlow + iconsContainer = new ReverseDepthFillFlowContainer<ModIcon> { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -93,12 +93,5 @@ namespace osu.Game.Screens.Play.HUD contract(); base.OnHoverLost(state); } - - private class IconFlow : FillFlowContainer<ModIcon> - { - // just reverses the depth of flow contents. - protected override IComparer<Drawable> DepthComparer => new ReverseCreationOrderDepthComparer(); - protected override IEnumerable<ModIcon> FlowingChildren => base.FlowingChildren.Reverse(); - } } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 2d6d212130..130642b9c7 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -170,8 +170,8 @@ namespace osu.Game.Screens.Select List<BeatmapGroup> visibleGroups = groups.Where(selectGroup => selectGroup.State != BeatmapGroupState.Hidden).ToList(); if (visibleGroups.Count < 1) return; - BeatmapGroup group = visibleGroups[RNG.Next(visibleGroups.Count)]; + BeatmapGroup group = visibleGroups[RNG.Next(visibleGroups.Count)]; BeatmapPanel panel = group.BeatmapPanels[RNG.Next(group.BeatmapPanels.Count)]; selectGroup(group, panel); diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index c064a0272e..be2f5196ef 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -2,8 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,6 +10,7 @@ using osu.Game.Graphics; using OpenTK; using OpenTK.Graphics; using OpenTK.Input; +using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Select.Options { @@ -71,7 +70,7 @@ namespace osu.Game.Screens.Select.Options Scale = new Vector2(1, 0), Colour = Color4.Black.Opacity(0.5f), }, - buttonsContainer = new ButtonFlow + buttonsContainer = new ReverseDepthFillFlowContainer<BeatmapOptionsButton> { Height = height, RelativePositionAxes = Axes.X, @@ -109,16 +108,5 @@ namespace osu.Game.Screens.Select.Options HotKey = hotkey }); } - - private class ButtonFlow : FillFlowContainer<BeatmapOptionsButton> - { - protected override IComparer<Drawable> DepthComparer => new ReverseCreationOrderDepthComparer(); - protected override IEnumerable<BeatmapOptionsButton> FlowingChildren => base.FlowingChildren.Reverse(); - - public ButtonFlow() - { - Direction = FillDirection.Horizontal; - } - } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index e9ead7c9c0..41fa53e8a3 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -316,11 +316,12 @@ namespace osu.Game.Screens.Select /// </summary> private void selectionChanged(BeatmapInfo beatmap) { - bool beatmapSetChange = false; + selectionChangedDebounce?.Cancel(); if (beatmap.Equals(Beatmap?.BeatmapInfo)) return; + bool beatmapSetChange = false; if (beatmap.BeatmapSetInfoID == selectionChangeNoBounce?.BeatmapSetInfoID) sampleChangeDifficulty.Play(); else @@ -331,7 +332,6 @@ namespace osu.Game.Screens.Select selectionChangeNoBounce = beatmap; - selectionChangedDebounce?.Cancel(); selectionChangedDebounce = Scheduler.AddDelayed(delegate { Beatmap = database.GetWorkingBeatmap(beatmap, Beatmap); 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<UserStatus> Status = new Bindable<UserStatus>(); + //public Team Team; [JsonProperty(@"profile_colour")] diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs new file mode 100644 index 0000000000..bdfe6d1c8e --- /dev/null +++ b/osu.Game/Users/UserPanel.cs @@ -0,0 +1,214 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// 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.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Users +{ + public class UserPanel : Container + { + private const float height = 100; + private const float content_padding = 10; + private const float status_height = 30; + + private OsuColour colours; + + private readonly Container statusBar; + private readonly Box statusBg; + private readonly OsuSpriteText statusMessage; + + public readonly Bindable<UserStatus> Status = new Bindable<UserStatus>(); + + public UserPanel(User user) + { + Height = height - status_height; + Masking = true; + CornerRadius = 5; + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4, + }; + + Children = new Drawable[] + { + new AsyncLoadWrapper(new CoverBackgroundSprite(user) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + OnLoadComplete = d => d.FadeInFromZero(200), + }) { RelativeSizeAxes = Axes.Both }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Top = content_padding, Left = content_padding, Right = content_padding }, + Children = new Drawable[] + { + new UpdateableAvatar + { + Size = new Vector2(height - status_height - content_padding * 2), + User = user, + Masking = true, + CornerRadius = 5, + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = height - status_height - content_padding }, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = user.Username, + TextSize = 18, + Font = @"Exo2.0-SemiBoldItalic", + }, + new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.X, + Height = 20f, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5f, 0f), + Children = new Drawable[] + { + new DrawableFlag(user.Country?.FlagName ?? @"__") + { + Width = 30f, + RelativeSizeAxes = Axes.Y, + }, + new Container + { + Width = 40f, + RelativeSizeAxes = Axes.Y, + }, + new CircularContainer + { + Width = 20f, + RelativeSizeAxes = Axes.Y, + }, + }, + }, + }, + }, + }, + }, + statusBar = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Alpha = 0f, + Children = new Drawable[] + { + statusBg = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.5f, + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5f, 0f), + Children = new[] + { + new TextAwesome + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.fa_circle_o, + Shadow = true, + TextSize = 14, + }, + statusMessage = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = @"Exo2.0-Semibold", + }, + }, + }, + }, + }, + }; + } + + [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) + { + const float transition_duration = 500; + + if (status == null) + { + statusBar.ResizeHeightTo(0f, transition_duration, EasingTypes.OutQuint); + statusBar.FadeOut(transition_duration, EasingTypes.OutQuint); + ResizeHeightTo(height - status_height, transition_duration, EasingTypes.OutQuint); + } + else + { + statusBar.ResizeHeightTo(status_height, transition_duration, EasingTypes.OutQuint); + statusBar.FadeIn(transition_duration, EasingTypes.OutQuint); + ResizeHeightTo(height, transition_duration, EasingTypes.OutQuint); + + statusBg.FadeColour(status.GetAppropriateColour(colours), 500, EasingTypes.OutQuint); + statusMessage.Text = status.Message; + } + } + + private class CoverBackgroundSprite : Sprite + { + private readonly User user; + + public CoverBackgroundSprite(User user) + { + this.user = user; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + if (!string.IsNullOrEmpty(user.CoverUrl)) + Texture = textures.Get(user.CoverUrl); + } + } + } +} diff --git a/osu.Game/Users/UserStatus.cs b/osu.Game/Users/UserStatus.cs new file mode 100644 index 0000000000..461008db0f --- /dev/null +++ b/osu.Game/Users/UserStatus.cs @@ -0,0 +1,67 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK.Graphics; +using osu.Game.Graphics; + +namespace osu.Game.Users +{ + public abstract class UserStatus + { + public abstract string Message { get; } + public abstract Color4 GetAppropriateColour(OsuColour colours); + } + + public abstract class UserStatusAvailable : UserStatus + { + public override Color4 GetAppropriateColour(OsuColour colours) => colours.BlueDarker; + } + + public abstract class UserStatusBusy : UserStatus + { + public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDark; + } + + public class UserStatusOffline : UserStatus + { + public override string Message => @"Offline"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.Gray7; + } + + public class UserStatusOnline : UserStatusAvailable + { + public override string Message => @"Online"; + } + + public class UserStatusSpectating : UserStatusAvailable + { + public override string Message => @"Spectating a game"; + } + + public class UserStatusInLobby : UserStatusAvailable + { + public override string Message => @"in Multiplayer Lobby"; + } + + public class UserStatusSoloGame : UserStatusBusy + { + public override string Message => @"Solo Game"; + } + + public class UserStatusMultiplayerGame: UserStatusBusy + { + public override string Message => @"Multiplaying"; + } + + public class UserStatusModding : UserStatus + { + 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 cde7a51ef4..25b692151f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -189,6 +189,7 @@ <Compile Include="Database\RulesetDatabase.cs" /> <Compile Include="Rulesets\Scoring\Score.cs" /> <Compile Include="Rulesets\Scoring\ScoreProcessor.cs" /> + <Compile Include="Screens\Menu\MenuSideFlashes.cs" /> <Compile Include="Screens\Play\HUD\HealthDisplay.cs" /> <Compile Include="Screens\Play\HUDOverlay.cs" /> <Compile Include="Screens\Play\HUD\StandardHealthDisplay.cs" /> @@ -431,6 +432,24 @@ <Compile Include="Overlays\Music\PlaylistOverlay.cs" /> <Compile Include="Rulesets\Replays\IAutoGenerator.cs" /> <Compile Include="Rulesets\Replays\AutoGenerator.cs" /> + <Compile Include="Screens\Multiplayer\DrawableRoom.cs" /> + <Compile Include="Online\Multiplayer\Room.cs" /> + <Compile Include="Online\Multiplayer\RoomStatus.cs" /> + <Compile Include="Users\UserPanel.cs" /> + <Compile Include="Users\UserStatus.cs" /> + <Compile Include="Overlays\DirectOverlay.cs" /> + <Compile Include="Overlays\Direct\FilterControl.cs" /> + <Compile Include="Overlays\Direct\Header.cs" /> + <Compile Include="Overlays\Direct\SortTabControl.cs" /> + <Compile Include="Graphics\UserInterface\OsuEnumDropdown.cs" /> + <Compile Include="Overlays\Direct\DirectPanel.cs" /> + <Compile Include="Overlays\Direct\DirectGridPanel.cs" /> + <Compile Include="Overlays\Direct\DirectListPanel.cs" /> + <Compile Include="Database\OnlineWorkingBeatmap.cs" /> + <Compile Include="Database\BeatmapOnlineInfo.cs" /> + <Compile Include="Overlays\Direct\SlimEnumDropdown.cs" /> + <Compile Include="Graphics\Containers\ReverseDepthFillFlowContainer.cs" /> + <Compile Include="Database\RankStatus.cs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\osu-framework\osu.Framework\osu.Framework.csproj">