diff --git a/README.md b/README.md index 016bd7d922..8f922f74a7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # osu! -[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) +[![Build status](https://github.com/ppy/osu/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/ppy/osu/actions/workflows/ci.yml) [![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) diff --git a/osu.Android.props b/osu.Android.props index f8cd4e41d4..ec223f98c2 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,8 +51,8 @@ - - + + diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/CatchModMirrorTest.cs b/osu.Game.Rulesets.Catch.Tests/Mods/CatchModMirrorTest.cs new file mode 100644 index 0000000000..fbbfee6b60 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Mods/CatchModMirrorTest.cs @@ -0,0 +1,120 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Tests.Mods +{ + [TestFixture] + public class CatchModMirrorTest + { + [Test] + public void TestModMirror() + { + IBeatmap original = createBeatmap(false); + IBeatmap mirrored = createBeatmap(true); + + assertEffectivePositionsMirrored(original, mirrored); + } + + private static IBeatmap createBeatmap(bool withMirrorMod) + { + var beatmap = createRawBeatmap(); + var mirrorMod = new CatchModMirror(); + + var beatmapProcessor = new CatchBeatmapProcessor(beatmap); + beatmapProcessor.PreProcess(); + + foreach (var hitObject in beatmap.HitObjects) + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + beatmapProcessor.PostProcess(); + + if (withMirrorMod) + mirrorMod.ApplyToBeatmap(beatmap); + + return beatmap; + } + + private static IBeatmap createRawBeatmap() => new Beatmap + { + HitObjects = new List + { + new Fruit + { + OriginalX = 150, + StartTime = 0 + }, + new Fruit + { + OriginalX = 450, + StartTime = 500 + }, + new JuiceStream + { + OriginalX = 250, + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(new Vector2(-100, 1)), + new PathControlPoint(new Vector2(0, 2)), + new PathControlPoint(new Vector2(100, 3)), + new PathControlPoint(new Vector2(0, 4)) + } + }, + StartTime = 1000, + }, + new BananaShower + { + StartTime = 5000, + Duration = 5000 + } + } + }; + + private static void assertEffectivePositionsMirrored(IBeatmap original, IBeatmap mirrored) + { + if (original.HitObjects.Count != mirrored.HitObjects.Count) + Assert.Fail($"Top-level object count mismatch (original: {original.HitObjects.Count}, mirrored: {mirrored.HitObjects.Count})"); + + for (int i = 0; i < original.HitObjects.Count; ++i) + { + var originalObject = (CatchHitObject)original.HitObjects[i]; + var mirroredObject = (CatchHitObject)mirrored.HitObjects[i]; + + // banana showers themselves are exempt, as we only really care about their nested bananas' positions. + if (!effectivePositionMirrored(originalObject, mirroredObject) && !(originalObject is BananaShower)) + Assert.Fail($"{originalObject.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalObject, mirroredObject)})"); + + if (originalObject.NestedHitObjects.Count != mirroredObject.NestedHitObjects.Count) + Assert.Fail($"{originalObject.GetType().Name} nested object count mismatch (original: {originalObject.NestedHitObjects.Count}, mirrored: {mirroredObject.NestedHitObjects.Count})"); + + for (int j = 0; j < originalObject.NestedHitObjects.Count; ++j) + { + var originalNested = (CatchHitObject)originalObject.NestedHitObjects[j]; + var mirroredNested = (CatchHitObject)mirroredObject.NestedHitObjects[j]; + + if (!effectivePositionMirrored(originalNested, mirroredNested)) + Assert.Fail($"{originalObject.GetType().Name}'s nested {originalNested.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalNested, mirroredNested)})"); + } + } + } + + private static string printEffectivePositions(CatchHitObject original, CatchHitObject mirrored) + => $"original X: {original.EffectiveX}, mirrored X is: {mirrored.EffectiveX}, mirrored X should be: {CatchPlayfield.WIDTH - original.EffectiveX}"; + + private static bool effectivePositionMirrored(CatchHitObject original, CatchHitObject mirrored) + => Precision.AlmostEquals(original.EffectiveX, CatchPlayfield.WIDTH - mirrored.EffectiveX); + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index a3307c9224..6abfbdbe21 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Tests { AddSliderStep("circle size", 0, 8, 5, createCatcher); AddToggleStep("hyper dash", t => this.ChildrenOfType().ForEach(area => area.ToggleHyperDash(t))); + AddToggleStep("toggle hit lighting", lighting => config.SetValue(OsuSetting.HitLighting, lighting)); AddStep("catch centered fruit", () => attemptCatch(new Fruit())); AddStep("catch many random fruit", () => diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index eafa1b9b9d..9fee6b2bc1 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -117,6 +117,7 @@ namespace osu.Game.Rulesets.Catch { new CatchModDifficultyAdjust(), new CatchModClassic(), + new CatchModMirror(), }; case ModType.Automation: diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs index e736d68740..371e901c69 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs @@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Catch Banana, Droplet, Catcher, - CatchComboCounter + CatchComboCounter, + HitExplosion } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs b/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs new file mode 100644 index 0000000000..932c8cad85 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Mods +{ + public class CatchModMirror : ModMirror, IApplicableToBeatmap + { + public override string Description => "Fruits are flipped horizontally."; + + /// + /// is used instead of , + /// as applies offsets in . + /// runs after post-processing, while runs before it. + /// + public void ApplyToBeatmap(IBeatmap beatmap) + { + foreach (var hitObject in beatmap.HitObjects) + applyToHitObject(hitObject); + } + + private void applyToHitObject(HitObject hitObject) + { + var catchObject = (CatchHitObject)hitObject; + + switch (catchObject) + { + case Fruit fruit: + mirrorEffectiveX(fruit); + break; + + case JuiceStream juiceStream: + mirrorEffectiveX(juiceStream); + mirrorJuiceStreamPath(juiceStream); + break; + + case BananaShower bananaShower: + mirrorBananaShower(bananaShower); + break; + } + } + + /// + /// Mirrors the effective X position of and its nested hit objects. + /// + private static void mirrorEffectiveX(CatchHitObject catchObject) + { + catchObject.OriginalX = CatchPlayfield.WIDTH - catchObject.OriginalX; + catchObject.XOffset = -catchObject.XOffset; + + foreach (var nested in catchObject.NestedHitObjects.Cast()) + { + nested.OriginalX = CatchPlayfield.WIDTH - nested.OriginalX; + nested.XOffset = -nested.XOffset; + } + } + + /// + /// Mirrors the path of the . + /// + private static void mirrorJuiceStreamPath(JuiceStream juiceStream) + { + var controlPoints = juiceStream.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray(); + foreach (var point in controlPoints) + point.Position.Value = new Vector2(-point.Position.Value.X, point.Position.Value.Y); + + juiceStream.Path = new SliderPath(controlPoints, juiceStream.Path.ExpectedDistance.Value); + } + + /// + /// Mirrors X positions of all bananas in the . + /// + private static void mirrorBananaShower(BananaShower bananaShower) + { + foreach (var banana in bananaShower.NestedHitObjects.OfType()) + banana.XOffset = CatchPlayfield.WIDTH - banana.XOffset; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultHitExplosion.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultHitExplosion.cs new file mode 100644 index 0000000000..e1fad564a3 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultHitExplosion.cs @@ -0,0 +1,129 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Skinning.Default +{ + public class DefaultHitExplosion : CompositeDrawable, IHitExplosion + { + private CircularContainer largeFaint; + private CircularContainer smallFaint; + private CircularContainer directionalGlow1; + private CircularContainer directionalGlow2; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(20); + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + + // scale roughly in-line with visual appearance of notes + const float initial_height = 10; + + InternalChildren = new Drawable[] + { + largeFaint = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Blending = BlendingParameters.Additive, + }, + smallFaint = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Blending = BlendingParameters.Additive, + }, + directionalGlow1 = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Size = new Vector2(0.01f, initial_height), + Blending = BlendingParameters.Additive, + }, + directionalGlow2 = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Size = new Vector2(0.01f, initial_height), + Blending = BlendingParameters.Additive, + } + }; + } + + public void Animate(HitExplosionEntry entry) + { + X = entry.Position; + Scale = new Vector2(entry.HitObject.Scale); + setColour(entry.ObjectColour); + + using (BeginAbsoluteSequence(entry.LifetimeStart)) + applyTransforms(entry.HitObject.RandomSeed); + } + + private void applyTransforms(int randomSeed) + { + const double duration = 400; + + // we want our size to be very small so the glow dominates it. + largeFaint.Size = new Vector2(0.8f); + largeFaint + .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) + .FadeOut(duration * 2); + + const float angle_variangle = 15; // should be less than 45 + directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4); + directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5); + + this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); + } + + private void setColour(Color4 objectColour) + { + const float roundness = 100; + + largeFaint.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), + Roundness = 160, + Radius = 200, + }; + + smallFaint.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), + Roundness = 20, + Radius = 50, + }; + + directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1), + Roundness = roundness, + Radius = 40, + }; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 5e744ec001..10fc4e78b2 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -70,13 +70,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy if (version < 2.3m) { - if (GetTexture(@"fruit-ryuuta") != null || - GetTexture(@"fruit-ryuuta-0") != null) + if (hasOldStyleCatcherSprite()) return new LegacyCatcherOld(); } - if (GetTexture(@"fruit-catcher-idle") != null || - GetTexture(@"fruit-catcher-idle-0") != null) + if (hasNewStyleCatcherSprite()) return new LegacyCatcherNew(); return null; @@ -86,12 +84,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return new LegacyCatchComboCounter(Skin); return null; + + case CatchSkinComponents.HitExplosion: + if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite()) + return new LegacyHitExplosion(); + + return null; } } return base.GetDrawableComponent(component); } + private bool hasOldStyleCatcherSprite() => + GetTexture(@"fruit-ryuuta") != null + || GetTexture(@"fruit-ryuuta-0") != null; + + private bool hasNewStyleCatcherSprite() => + GetTexture(@"fruit-catcher-idle") != null + || GetTexture(@"fruit-catcher-idle-0") != null; + public override IBindable GetConfig(TLookup lookup) { switch (lookup) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs new file mode 100644 index 0000000000..c262b0a4ac --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Skinning.Legacy +{ + public class LegacyHitExplosion : CompositeDrawable, IHitExplosion + { + [Resolved] + private Catcher catcher { get; set; } + + private const float catch_margin = (1 - Catcher.ALLOWED_CATCH_RANGE) / 2; + + private readonly Sprite explosion1; + private readonly Sprite explosion2; + + public LegacyHitExplosion() + { + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + RelativeSizeAxes = Axes.Both; + Scale = new Vector2(0.5f); + + InternalChildren = new[] + { + explosion1 = new Sprite + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.CentreLeft, + Alpha = 0, + Blending = BlendingParameters.Additive, + Rotation = -90 + }, + explosion2 = new Sprite + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.CentreLeft, + Alpha = 0, + Blending = BlendingParameters.Additive, + Rotation = -90 + } + }; + } + + [BackgroundDependencyLoader] + private void load(SkinManager skins) + { + var defaultLegacySkin = skins.DefaultLegacySkin; + + // sprite names intentionally swapped to match stable member naming / ease of cross-referencing + explosion1.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-2"); + explosion2.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-1"); + } + + public void Animate(HitExplosionEntry entry) + { + Colour = entry.ObjectColour; + + using (BeginAbsoluteSequence(entry.LifetimeStart)) + { + float halfCatchWidth = catcher.CatchWidth / 2; + float explosionOffset = Math.Clamp(entry.Position, -halfCatchWidth + catch_margin * 3, halfCatchWidth - catch_margin * 3); + + if (!(entry.HitObject is Droplet)) + { + float scale = Math.Clamp(entry.JudgementResult.ComboAtJudgement / 200f, 0.35f, 1.125f); + + explosion1.Scale = new Vector2(1, 0.9f); + explosion1.Position = new Vector2(explosionOffset, 0); + + explosion1.FadeOutFromOne(300); + explosion1.ScaleTo(new Vector2(16 * scale, 1.1f), 160, Easing.Out); + } + + explosion2.Scale = new Vector2(0.9f, 1); + explosion2.Position = new Vector2(explosionOffset, 0); + + explosion2.FadeOutFromOne(700); + explosion2.ScaleTo(new Vector2(0.9f, 1.3f), 500, Easing.Out); + + this.Delay(700).FadeOutFromOne(); + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 9fd4610e6e..5cd85aac56 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -23,6 +23,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { + [Cached] public class Catcher : SkinReloadableDrawable { /// @@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Width of the area that can be used to attempt catches during gameplay. /// - private readonly float catchWidth; + public readonly float CatchWidth; private readonly SkinnableCatcher body; @@ -133,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.UI if (difficulty != null) Scale = calculateScale(difficulty); - catchWidth = CalculateCatchWidth(Scale); + CatchWidth = CalculateCatchWidth(Scale); InternalChildren = new Drawable[] { @@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Catch.UI if (!(hitObject is PalpableCatchHitObject fruit)) return false; - float halfCatchWidth = catchWidth * 0.5f; + float halfCatchWidth = CatchWidth * 0.5f; return fruit.EffectiveX >= X - halfCatchWidth && fruit.EffectiveX <= X + halfCatchWidth; } @@ -216,7 +217,7 @@ namespace osu.Game.Rulesets.Catch.UI placeCaughtObject(palpableObject, positionInStack); if (hitLighting.Value) - addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value); + addLighting(result, drawableObject.AccentColour.Value, positionInStack.X); } // droplet doesn't affect the catcher state @@ -365,8 +366,8 @@ namespace osu.Game.Rulesets.Catch.UI return position; } - private void addLighting(CatchHitObject hitObject, float x, Color4 colour) => - hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed)); + private void addLighting(JudgementResult judgementResult, Color4 colour, float x) => + hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, judgementResult, colour, x)); private CaughtObject getCaughtObject(PalpableCatchHitObject source) { diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs index d9ab428231..955b1e6edb 100644 --- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs @@ -1,129 +1,56 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Rulesets.Objects.Pooling; -using osu.Game.Utils; -using osuTK; -using osuTK.Graphics; +using osu.Game.Skinning; + +#nullable enable namespace osu.Game.Rulesets.Catch.UI { public class HitExplosion : PoolableDrawableWithLifetime { - private readonly CircularContainer largeFaint; - private readonly CircularContainer smallFaint; - private readonly CircularContainer directionalGlow1; - private readonly CircularContainer directionalGlow2; + private readonly SkinnableDrawable skinnableExplosion; public HitExplosion() { - Size = new Vector2(20); - Anchor = Anchor.TopCentre; + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.BottomCentre; Origin = Anchor.BottomCentre; - // scale roughly in-line with visual appearance of notes - const float initial_height = 10; - - InternalChildren = new Drawable[] + InternalChild = skinnableExplosion = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.HitExplosion), _ => new DefaultHitExplosion()) { - largeFaint = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Blending = BlendingParameters.Additive, - }, - smallFaint = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Blending = BlendingParameters.Additive, - }, - directionalGlow1 = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Size = new Vector2(0.01f, initial_height), - Blending = BlendingParameters.Additive, - }, - directionalGlow2 = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Size = new Vector2(0.01f, initial_height), - Blending = BlendingParameters.Additive, - } + CentreComponent = false, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre }; } protected override void OnApply(HitExplosionEntry entry) { - X = entry.Position; - Scale = new Vector2(entry.Scale); - setColour(entry.ObjectColour); - - using (BeginAbsoluteSequence(entry.LifetimeStart)) - applyTransforms(entry.RNGSeed); + base.OnApply(entry); + if (IsLoaded) + apply(entry); } - private void applyTransforms(int randomSeed) + protected override void LoadComplete() { + base.LoadComplete(); + apply(Entry); + } + + private void apply(HitExplosionEntry? entry) + { + if (entry == null) + return; + + ApplyTransformsAt(double.MinValue, true); ClearTransforms(true); - const double duration = 400; - - // we want our size to be very small so the glow dominates it. - largeFaint.Size = new Vector2(0.8f); - largeFaint - .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) - .FadeOut(duration * 2); - - const float angle_variangle = 15; // should be less than 45 - directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4); - directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5); - - this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out).Expire(); - } - - private void setColour(Color4 objectColour) - { - const float roundness = 100; - - largeFaint.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), - Roundness = 160, - Radius = 200, - }; - - smallFaint.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), - Roundness = 20, - Radius = 50, - }; - - directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1), - Roundness = roundness, - Radius = 40, - }; + (skinnableExplosion.Drawable as IHitExplosion)?.Animate(entry); + LifetimeEnd = skinnableExplosion.Drawable.LatestTransformEndTime; } } } diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs index 094d88243a..6df13e52ef 100644 --- a/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects.Pooling; @@ -14,6 +15,8 @@ namespace osu.Game.Rulesets.Catch.UI public HitExplosionContainer() { + RelativeSizeAxes = Axes.Both; + AddInternal(pool = new DrawablePool(10)); } diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs index b142962a8a..88871c77f6 100644 --- a/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs +++ b/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs @@ -2,24 +2,42 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; using osuTK.Graphics; +#nullable enable + namespace osu.Game.Rulesets.Catch.UI { public class HitExplosionEntry : LifetimeEntry { - public readonly float Position; - public readonly float Scale; - public readonly Color4 ObjectColour; - public readonly int RNGSeed; + /// + /// The judgement result that triggered this explosion. + /// + public JudgementResult JudgementResult { get; } - public HitExplosionEntry(double startTime, float position, float scale, Color4 objectColour, int rngSeed) + /// + /// The hitobject which triggered this explosion. + /// + public CatchHitObject HitObject => (CatchHitObject)JudgementResult.HitObject; + + /// + /// The accent colour of the object caught. + /// + public Color4 ObjectColour { get; } + + /// + /// The position at which the object was caught. + /// + public float Position { get; } + + public HitExplosionEntry(double startTime, JudgementResult judgementResult, Color4 objectColour, float position) { LifetimeStart = startTime; Position = position; - Scale = scale; + JudgementResult = judgementResult; ObjectColour = objectColour; - RNGSeed = rngSeed; } } } diff --git a/osu.Game.Rulesets.Catch/UI/IHitExplosion.cs b/osu.Game.Rulesets.Catch/UI/IHitExplosion.cs new file mode 100644 index 0000000000..c744c00d9a --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/IHitExplosion.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +namespace osu.Game.Rulesets.Catch.UI +{ + /// + /// Common interface for all hit explosion skinnables. + /// + public interface IHitExplosion + { + /// + /// Begins animating this . + /// + void Animate(HitExplosionEntry entry); + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index 0e61c02e2d..d4f1602a46 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -41,6 +41,11 @@ namespace osu.Game.Rulesets.Osu.Edit protected override GameplayCursorContainer CreateCursor() => null; + public OsuEditorPlayfield() + { + HitPolicy = new AnyOrderHitPolicy(); + } + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index f6fd3e36ab..587ff4b573 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -9,6 +9,7 @@ using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { @@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private double lastTrailTime; private IBindable cursorSize; + private Vector2? currentPosition; + public LegacyCursorTrail(ISkin skin) { this.skin = skin; @@ -54,22 +57,34 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } protected override double FadeDuration => disjointTrail ? 150 : 500; + protected override float FadeExponent => 1; protected override bool InterpolateMovements => !disjointTrail; protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1); + protected override void Update() + { + base.Update(); + + if (!disjointTrail || !currentPosition.HasValue) + return; + + if (Time.Current - lastTrailTime >= disjoint_trail_time_separation) + { + lastTrailTime = Time.Current; + AddTrail(currentPosition.Value); + } + } + protected override bool OnMouseMove(MouseMoveEvent e) { if (!disjointTrail) return base.OnMouseMove(e); - if (Time.Current - lastTrailTime >= disjoint_trail_time_separation) - { - lastTrailTime = Time.Current; - return base.OnMouseMove(e); - } + currentPosition = e.ScreenSpaceMousePosition; + // Intentionally block the base call as we're adding the trails ourselves. return false; } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs index 744ded37c9..1c8dfeac52 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs @@ -3,9 +3,9 @@ using System; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Utils; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // Roughly matches osu!stable's slider border portions. => base.CalculatedBorderPortion * 0.77f; - public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); + public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, 0.7f); protected override Color4 ColourAt(float position) { @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Color4 outerColour = AccentColour.Darken(0.1f); Color4 innerColour = lighten(AccentColour, 0.5f); - return Interpolation.ValueAt(position / realGradientPortion, outerColour, innerColour, 0, 1); + return LegacyUtils.InterpolateNonLinear(position / realGradientPortion, outerColour, innerColour, 0, 1); } /// diff --git a/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs new file mode 100644 index 0000000000..b4de91562b --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// An which allows hitobjects to be hit in any order. + /// + public class AnyOrderHitPolicy : IHitPolicy + { + public IHitObjectContainer HitObjectContainer { get; set; } + + public bool IsHittable(DrawableHitObject hitObject, double time) => true; + + public void HandleHit(DrawableHitObject hitObject) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 7f86e9daf7..7a95111c91 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { private const int max_sprites = 2048; + /// + /// An exponentiating factor to ease the trail fade. + /// + protected virtual float FadeExponent => 1.7f; + private readonly TrailPart[] parts = new TrailPart[max_sprites]; private int currentIndex; private IShader shader; @@ -141,22 +146,25 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor protected override bool OnMouseMove(MouseMoveEvent e) { - Vector2 pos = e.ScreenSpaceMousePosition; + AddTrail(e.ScreenSpaceMousePosition); + return base.OnMouseMove(e); + } - if (lastPosition == null) + protected void AddTrail(Vector2 position) + { + if (InterpolateMovements) { - lastPosition = pos; - resampler.AddPosition(lastPosition.Value); - return base.OnMouseMove(e); - } - - foreach (Vector2 pos2 in resampler.AddPosition(pos)) - { - Trace.Assert(lastPosition.HasValue); - - if (InterpolateMovements) + if (!lastPosition.HasValue) { - // ReSharper disable once PossibleInvalidOperationException + lastPosition = position; + resampler.AddPosition(lastPosition.Value); + return; + } + + foreach (Vector2 pos2 in resampler.AddPosition(position)) + { + Trace.Assert(lastPosition.HasValue); + Vector2 pos1 = lastPosition.Value; Vector2 diff = pos2 - pos1; float distance = diff.Length; @@ -170,14 +178,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor addPart(lastPosition.Value); } } - else - { - lastPosition = pos2; - addPart(lastPosition.Value); - } } - - return base.OnMouseMove(e); + else + { + lastPosition = position; + addPart(lastPosition.Value); + } } private void addPart(Vector2 screenSpacePosition) @@ -206,10 +212,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private Texture texture; private float time; + private float fadeExponent; private readonly TrailPart[] parts = new TrailPart[max_sprites]; private Vector2 size; - private Vector2 originPosition; private readonly QuadBatch vertexBatch = new QuadBatch(max_sprites, 1); @@ -227,6 +233,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor texture = Source.texture; size = Source.partSize; time = Source.time; + fadeExponent = Source.FadeExponent; originPosition = Vector2.Zero; @@ -249,6 +256,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor shader.Bind(); shader.GetUniform("g_FadeClock").UpdateValue(ref time); + shader.GetUniform("g_FadeExponent").UpdateValue(ref fadeExponent); texture.TextureGL.Bind(); diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index 0ce71696bd..58f4c4c8db 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Visual; namespace osu.Game.Tests.Gameplay @@ -121,6 +123,18 @@ namespace osu.Game.Tests.Gameplay AddAssert("Drawable lifetime is restored", () => dho.LifetimeStart == 666 && dho.LifetimeEnd == 999); } + [Test] + public void TestStateChangeBeforeLoadComplete() + { + TestDrawableHitObject dho = null; + AddStep("Add DHO and apply result", () => + { + Child = dho = new TestDrawableHitObject(new HitObject { StartTime = Time.Current }); + dho.MissForcefully(); + }); + AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss); + } + private class TestDrawableHitObject : DrawableHitObject { public const double INITIAL_LIFETIME_OFFSET = 100; @@ -141,6 +155,19 @@ namespace osu.Game.Tests.Gameplay if (SetLifetimeStartOnApply) LifetimeStart = LIFETIME_ON_APPLY; } + + public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); + + protected override void UpdateHitStateTransforms(ArmedState state) + { + if (state != ArmedState.Miss) + { + base.UpdateHitStateTransforms(state); + return; + } + + this.FadeOut(1000); + } } private class TestLifetimeEntry : HitObjectLifetimeEntry diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 0983b806e2..07ec86b0e7 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -24,6 +24,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddRepeatStep("add some users", () => Client.AddUser(new User { Id = id++ }), 5); checkPlayingUserCount(0); + AddAssert("playlist item is available", () => Client.CurrentMatchPlayingItem.Value != null); + changeState(3, MultiplayerUserState.WaitingForLoad); checkPlayingUserCount(3); @@ -41,6 +43,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddStep("leave room", () => Client.LeaveRoom()); checkPlayingUserCount(0); + + AddAssert("playlist item is null", () => Client.CurrentMatchPlayingItem.Value == null); } [Test] diff --git a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs index 2236f85b92..cc8503589d 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; +using osu.Framework.Testing; using osu.Game.Graphics.Sprites; using osu.Game.Online; using osuTK; @@ -15,6 +16,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Components { + [HeadlessTest] public class TestScenePollingComponent : OsuTestScene { private Container pollBox; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs new file mode 100644 index 0000000000..299bbacf08 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -0,0 +1,168 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; +using osu.Game.Overlays; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneDrawableRoom : OsuTestScene + { + [Cached] + private readonly Bindable selectedRoom = new Bindable(); + + [Cached] + protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + [Test] + public void TestMultipleStatuses() + { + AddStep("create rooms", () => + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.9f), + Spacing = new Vector2(10), + Children = new Drawable[] + { + createDrawableRoom(new Room + { + Name = { Value = "Flyte's Trash Playlist" }, + Status = { Value = new RoomStatusOpen() }, + EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, + Playlist = + { + new PlaylistItem + { + Beatmap = + { + Value = new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = + { + StarDifficulty = 2.5 + } + }.BeatmapInfo, + } + } + } + }), + createDrawableRoom(new Room + { + Name = { Value = "Room 2" }, + Status = { Value = new RoomStatusPlaying() }, + EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, + Playlist = + { + new PlaylistItem + { + Beatmap = + { + Value = new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = + { + StarDifficulty = 2.5 + } + }.BeatmapInfo, + } + }, + new PlaylistItem + { + Beatmap = + { + Value = new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = + { + StarDifficulty = 4.5 + } + }.BeatmapInfo, + } + } + } + }), + createDrawableRoom(new Room + { + Name = { Value = "Room 3" }, + Status = { Value = new RoomStatusEnded() }, + EndDate = { Value = DateTimeOffset.Now }, + }), + createDrawableRoom(new Room + { + Name = { Value = "Room 4 (realtime)" }, + Status = { Value = new RoomStatusOpen() }, + Category = { Value = RoomCategory.Realtime }, + }), + createDrawableRoom(new Room + { + Name = { Value = "Room 4 (spotlight)" }, + Status = { Value = new RoomStatusOpen() }, + Category = { Value = RoomCategory.Spotlight }, + }), + } + }; + }); + } + + [Test] + public void TestEnableAndDisablePassword() + { + DrawableRoom drawableRoom = null; + Room room = null; + + AddStep("create room", () => Child = drawableRoom = createDrawableRoom(room = new Room + { + Name = { Value = "Room with password" }, + Status = { Value = new RoomStatusOpen() }, + Category = { Value = RoomCategory.Realtime }, + })); + + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); + + AddStep("set password", () => room.Password.Value = "password"); + AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType().Single().Alpha)); + + AddStep("unset password", () => room.Password.Value = string.Empty); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); + } + + private DrawableRoom createDrawableRoom(Room room) + { + room.Host.Value ??= new User { Username = "peppy", Id = 2 }; + + if (room.RecentParticipants.Count == 0) + { + room.RecentParticipants.AddRange(Enumerable.Range(0, 20).Select(i => new User + { + Id = i, + Username = $"User {i}" + })); + } + + var drawableRoom = new DrawableRoom(room) { MatchingFilter = true }; + drawableRoom.Action = () => drawableRoom.State = drawableRoom.State == SelectionState.Selected ? SelectionState.NotSelected : SelectionState.Selected; + + return drawableRoom; + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index dfb78a235b..93bdbb79f4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -14,7 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; using osu.Game.Online.Rooms; -using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Multiplayer assertDownloadButtonVisible(false); void assertDownloadButtonVisible(bool visible) => AddUntilStep($"download button {(visible ? "shown" : "hidden")}", - () => playlist.ChildrenOfType().Single().Alpha == (visible ? 1 : 0)); + () => playlist.ChildrenOfType().Single().Alpha == (visible ? 1 : 0)); } [Test] @@ -229,7 +229,7 @@ namespace osu.Game.Tests.Visual.Multiplayer createPlaylist(byOnlineId, byChecksum); - AddAssert("download buttons shown", () => playlist.ChildrenOfType().All(d => d.IsPresent)); + AddAssert("download buttons shown", () => playlist.ChildrenOfType().All(d => d.IsPresent)); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs deleted file mode 100644 index 471d0b6c98..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; -using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Tests.Visual.OnlinePlay; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestSceneLoungeRoomInfo : OnlinePlayTestScene - { - [SetUp] - public new void Setup() => Schedule(() => - { - SelectedRoom.Value = new Room(); - - Child = new RoomInfo - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 500 - }; - }); - - [Test] - public void TestNonSelectedRoom() - { - AddStep("set null room", () => SelectedRoom.Value.RoomID.Value = null); - } - - [Test] - public void TestOpenRoom() - { - AddStep("set open room", () => - { - SelectedRoom.Value.RoomID.Value = 0; - SelectedRoom.Value.Name.Value = "Room 0"; - SelectedRoom.Value.Host.Value = new User { Username = "peppy", Id = 2 }; - SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMonths(1); - SelectedRoom.Value.Status.Value = new RoomStatusOpen(); - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index e14df62af1..ade24b8740 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -6,9 +6,11 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Timing; +using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play.HUD; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { @@ -31,7 +33,10 @@ namespace osu.Game.Tests.Visual.Multiplayer }; foreach (var (userId, _) in clocks) + { SpectatorClient.StartPlay(userId, 0); + OnlinePlayDependencies.Client.AddUser(new User { Id = userId }); + } }); AddStep("create leaderboard", () => @@ -41,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var scoreProcessor = new OsuScoreProcessor(); scoreProcessor.ApplyBeatmap(playable); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add); + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) { Expanded = { Value = true } }, Add); }); AddUntilStep("wait for load", () => leaderboard.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 072e32370d..65b1d6d53a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -8,10 +8,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Rulesets.UI; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { @@ -25,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private MultiSpectatorScreen spectatorScreen; - private readonly List playingUserIds = new List(); + private readonly List playingUsers = new List(); private BeatmapSetInfo importedSet; private BeatmapInfo importedBeatmap; @@ -40,17 +43,18 @@ namespace osu.Game.Tests.Visual.Multiplayer } [SetUp] - public new void Setup() => Schedule(() => playingUserIds.Clear()); + public new void Setup() => Schedule(() => playingUsers.Clear()); [Test] public void TestDelayedStart() { AddStep("start players silently", () => { - Client.CurrentMatchPlayingUserIds.Add(PLAYER_1_ID); - Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID); - playingUserIds.Add(PLAYER_1_ID); - playingUserIds.Add(PLAYER_2_ID); + OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true); + OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true); + + playingUsers.Add(new MultiplayerRoomUser(PLAYER_1_ID)); + playingUsers.Add(new MultiplayerRoomUser(PLAYER_2_ID)); }); loadSpectateScreen(false); @@ -76,6 +80,38 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait a bit", 20); } + [Test] + public void TestTeamDisplay() + { + AddStep("start players", () => + { + var player1 = OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true); + player1.MatchState = new TeamVersusUserState + { + TeamID = 0, + }; + + var player2 = OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true); + player2.MatchState = new TeamVersusUserState + { + TeamID = 1, + }; + + SpectatorClient.StartPlay(player1.UserID, importedBeatmapId); + SpectatorClient.StartPlay(player2.UserID, importedBeatmapId); + + playingUsers.Add(player1); + playingUsers.Add(player2); + }); + + loadSpectateScreen(); + + sendFrames(PLAYER_1_ID, 1000); + sendFrames(PLAYER_2_ID, 1000); + + AddWaitStep("wait a bit", 20); + } + [Test] public void TestTimeDoesNotProgressWhileAllPlayersPaused() { @@ -252,7 +288,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap); Ruleset.Value = importedBeatmap.Ruleset; - LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUserIds.ToArray())); + LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUsers.ToArray())); }); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); @@ -264,9 +300,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { foreach (int id in userIds) { - Client.CurrentMatchPlayingUserIds.Add(id); + OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true); + SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); - playingUserIds.Add(id); + playingUsers.Add(new MultiplayerRoomUser(id)); } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index cd3c50cf14..08b3fb98a8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -25,7 +25,7 @@ using osu.Game.Screens; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Resources; @@ -87,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestEmpty() { // used to test the flow of multiplayer from visual tests. + AddStep("empty step", () => { }); } [Test] @@ -312,6 +313,8 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); + AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddStep("start match externally", () => client.StartMatch()); AddAssert("play not started", () => multiplayerScreen.IsCurrentScreen()); @@ -348,6 +351,8 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); + AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddStep("start match externally", () => client.StartMatch()); AddStep("restore beatmap", () => @@ -396,7 +401,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("open mod overlay", () => this.ChildrenOfType().ElementAt(2).TriggerClick()); + AddStep("open mod overlay", () => this.ChildrenOfType().Single().TriggerClick()); AddStep("invoke on back button", () => multiplayerScreen.OnBackButton()); @@ -404,8 +409,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); - testLeave("lounge tab item", () => this.ChildrenOfType.BreadcrumbTabItem>().First().TriggerClick()); - testLeave("back button", () => multiplayerScreen.OnBackButton()); // mimics home button and OS window close @@ -423,10 +426,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createRoom(Func room) { - AddStep("open room", () => - { - multiplayerScreen.OpenNewRoom(room()); - }); + AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + AddStep("open room", () => multiplayerScreen.ChildrenOfType().Single().Open(room())); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); AddWaitStep("wait for transition", 2); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 0e368b59dd..3317ddc767 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu.Scoring; @@ -20,6 +21,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { @@ -50,22 +52,23 @@ namespace osu.Game.Tests.Visual.Multiplayer OsuScoreProcessor scoreProcessor; Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + var multiplayerUsers = new List(); foreach (var user in users) + { SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); - - // Todo: This is REALLY bad. - Client.CurrentMatchPlayingUserIds.AddRange(users); + multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true)); + } Children = new Drawable[] { scoreProcessor = new OsuScoreProcessor(), }; - scoreProcessor.ApplyBeatmap(playable); + scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, users.ToArray()) + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs new file mode 100644 index 0000000000..dfaf2f1dc3 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -0,0 +1,121 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.OnlinePlay; +using osu.Game.Tests.Visual.Spectator; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerTestScene + { + private static IEnumerable users => Enumerable.Range(0, 16); + + public new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient SpectatorClient => + (TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient; + + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + + protected class TestDependencies : MultiplayerTestSceneDependencies + { + protected override TestSpectatorClient CreateSpectatorClient() => new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient(); + } + + private MultiplayerGameplayLeaderboard leaderboard; + private GameplayMatchScoreDisplay gameplayScoreDisplay; + + protected override Room CreateRoom() + { + var room = base.CreateRoom(); + room.Type.Value = MatchType.TeamVersus; + return room; + } + + [SetUpSteps] + public override void SetUpSteps() + { + AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result); + + AddStep("create leaderboard", () => + { + leaderboard?.Expire(); + + OsuScoreProcessor scoreProcessor; + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + + var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + var multiplayerUsers = new List(); + + foreach (var user in users) + { + SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + var roomUser = OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true); + + roomUser.MatchState = new TeamVersusUserState + { + TeamID = RNG.Next(0, 2) + }; + + multiplayerUsers.Add(roomUser); + } + + Children = new Drawable[] + { + scoreProcessor = new OsuScoreProcessor(), + }; + + scoreProcessor.ApplyBeatmap(playableBeatmap); + + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, gameplayLeaderboard => + { + LoadComponentAsync(new MatchScoreDisplay + { + Team1Score = { BindTarget = leaderboard.TeamScores[0] }, + Team2Score = { BindTarget = leaderboard.TeamScores[1] } + }, Add); + + LoadComponentAsync(gameplayScoreDisplay = new GameplayMatchScoreDisplay + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Team1Score = { BindTarget = leaderboard.TeamScores[0] }, + Team2Score = { BindTarget = leaderboard.TeamScores[1] } + }, Add); + + Add(gameplayLeaderboard); + }); + }); + + AddUntilStep("wait for load", () => leaderboard.IsLoaded); + AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0); + } + + [Test] + public void TestScoreUpdates() + { + AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100); + AddToggleStep("switch compact mode", expanded => + { + leaderboard.Expanded.Value = expanded; + gameplayScoreDisplay.Expanded.Value = expanded; + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 955be6ca21..ea10fc1b8b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -129,6 +129,8 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); + AddUntilStep("wait for spectating user state", () => Client.LocalUser?.State == MultiplayerUserState.Spectating); + AddUntilStep("wait for ready button to be enabled", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Enabled.Value); AddStep("click ready button", () => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 6526f7eea7..a3e6c8de3b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -155,6 +155,42 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); } + [Test] + public void TestKickButtonOnlyPresentWhenHost() + { + AddStep("add user", () => Client.AddUser(new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); + + AddStep("make second user host", () => Client.TransferHost(3)); + + AddUntilStep("kick buttons not visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 0); + + AddStep("make local user host again", () => Client.TransferHost(API.LocalUser.Value.Id)); + + AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); + } + + [Test] + public void TestKickButtonKicks() + { + AddStep("add user", () => Client.AddUser(new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddStep("kick second user", () => this.ChildrenOfType().Single(d => d.IsPresent).TriggerClick()); + + AddAssert("second user kicked", () => Client.Room?.Users.Single().UserID == API.LocalUser.Value.Id); + } + [Test] public void TestManyUsers() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs new file mode 100644 index 0000000000..9e03743e8d --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneRankRangePill : MultiplayerTestScene + { + [SetUp] + public new void Setup() => Schedule(() => + { + Child = new RankRangePill + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }; + }); + + [Test] + public void TestSingleUser() + { + AddStep("add user", () => + { + Client.AddUser(new User + { + Id = 2, + Statistics = { GlobalRank = 1234 } + }); + + // Remove the local user so only the one above is displayed. + Client.RemoveUser(API.LocalUser.Value); + }); + } + + [Test] + public void TestMultipleUsers() + { + AddStep("add users", () => + { + Client.AddUser(new User + { + Id = 2, + Statistics = { GlobalRank = 1234 } + }); + + Client.AddUser(new User + { + Id = 3, + Statistics = { GlobalRank = 3333 } + }); + + Client.AddUser(new User + { + Id = 4, + Statistics = { GlobalRank = 4321 } + }); + + // Remove the local user so only the ones above are displayed. + Client.RemoveUser(API.LocalUser.Value); + }); + } + + [TestCase(1, 10)] + [TestCase(10, 100)] + [TestCase(100, 1000)] + [TestCase(1000, 10000)] + [TestCase(10000, 100000)] + [TestCase(100000, 1000000)] + [TestCase(1000000, 10000000)] + public void TestRange(int min, int max) + { + AddStep("add users", () => + { + Client.AddUser(new User + { + Id = 2, + Statistics = { GlobalRank = min } + }); + + Client.AddUser(new User + { + Id = 3, + Statistics = { GlobalRank = max } + }); + + // Remove the local user so only the ones above are displayed. + Client.RemoveUser(API.LocalUser.Value); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs new file mode 100644 index 0000000000..50ec2bf3ac --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs @@ -0,0 +1,143 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Tests.Visual.OnlinePlay; +using osu.Game.Users; +using osu.Game.Users.Drawables; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneRecentParticipantsList : OnlinePlayTestScene + { + private RecentParticipantsList list; + + [SetUp] + public new void Setup() => Schedule(() => + { + SelectedRoom.Value = new Room { Name = { Value = "test room" } }; + + Child = list = new RecentParticipantsList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + NumberOfCircles = 4 + }; + }); + + [Test] + public void TestCircleCountNearLimit() + { + AddStep("add 8 users", () => + { + for (int i = 0; i < 8; i++) + addUser(i); + }); + + AddStep("set 8 circles", () => list.NumberOfCircles = 8); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + + AddStep("add one more user", () => addUser(9)); + AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); + + AddStep("remove first user", () => removeUserAt(0)); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + + AddStep("add one more user", () => addUser(9)); + AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); + + AddStep("remove last user", () => removeUserAt(8)); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + } + + [Test] + public void TestHiddenUsersBecomeDisplayed() + { + AddStep("add 8 users", () => + { + for (int i = 0; i < 8; i++) + addUser(i); + }); + + AddStep("set 3 circles", () => list.NumberOfCircles = 3); + + for (int i = 0; i < 8; i++) + { + AddStep("remove user", () => removeUserAt(0)); + int remainingUsers = 7 - i; + + int displayedUsers = remainingUsers > 3 ? 2 : remainingUsers; + AddAssert($"{displayedUsers} avatars displayed", () => list.ChildrenOfType().Count() == displayedUsers); + } + } + + [Test] + public void TestCircleCount() + { + AddStep("add 50 users", () => + { + for (int i = 0; i < 50; i++) + addUser(i); + }); + + AddStep("set 3 circles", () => list.NumberOfCircles = 3); + AddAssert("2 users displayed", () => list.ChildrenOfType().Count() == 2); + AddAssert("48 hidden users", () => list.ChildrenOfType().Single().Count == 48); + + AddStep("set 10 circles", () => list.NumberOfCircles = 10); + AddAssert("9 users displayed", () => list.ChildrenOfType().Count() == 9); + AddAssert("41 hidden users", () => list.ChildrenOfType().Single().Count == 41); + } + + [Test] + public void TestAddAndRemoveUsers() + { + AddStep("add 50 users", () => + { + for (int i = 0; i < 50; i++) + addUser(i); + }); + + AddStep("remove from start", () => removeUserAt(0)); + AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); + + AddStep("remove from end", () => removeUserAt(SelectedRoom.Value.RecentParticipants.Count - 1)); + AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); + + AddRepeatStep("remove 45 users", () => removeUserAt(0), 45); + AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("hidden users bubble hidden", () => list.ChildrenOfType().Single().Alpha < 0.5f); + + AddStep("remove another user", () => removeUserAt(0)); + AddAssert("2 circles displayed", () => list.ChildrenOfType().Count() == 2); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + + AddRepeatStep("remove the remaining two users", () => removeUserAt(0), 2); + AddAssert("0 circles displayed", () => !list.ChildrenOfType().Any()); + } + + private void addUser(int id) + { + SelectedRoom.Value.RecentParticipants.Add(new User + { + Id = id, + Username = $"User {id}" + }); + SelectedRoom.Value.ParticipantCount.Value++; + } + + private void removeUserAt(int index) + { + SelectedRoom.Value.RecentParticipants.RemoveAt(index); + SelectedRoom.Value.ParticipantCount.Value--; + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs deleted file mode 100644 index 8c4133418c..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; -using osu.Game.Screens.OnlinePlay.Lounge.Components; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestSceneRoomStatus : OsuTestScene - { - [Test] - public void TestMultipleStatuses() - { - AddStep("create rooms", () => - { - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Children = new Drawable[] - { - new DrawableRoom(new Room - { - Name = { Value = "Open - ending in 1 day" }, - Status = { Value = new RoomStatusOpen() }, - EndDate = { Value = DateTimeOffset.Now.AddDays(1) } - }) { MatchingFilter = true }, - new DrawableRoom(new Room - { - Name = { Value = "Playing - ending in 1 day" }, - Status = { Value = new RoomStatusPlaying() }, - EndDate = { Value = DateTimeOffset.Now.AddDays(1) } - }) { MatchingFilter = true }, - new DrawableRoom(new Room - { - Name = { Value = "Ended" }, - Status = { Value = new RoomStatusEnded() }, - EndDate = { Value = DateTimeOffset.Now } - }) { MatchingFilter = true }, - new DrawableRoom(new Room - { - Name = { Value = "Open" }, - Status = { Value = new RoomStatusOpen() }, - Category = { Value = RoomCategory.Realtime } - }) { MatchingFilter = true }, - } - }; - }); - } - - [Test] - public void TestEnableAndDisablePassword() - { - DrawableRoom drawableRoom = null; - Room room = null; - - AddStep("create room", () => Child = drawableRoom = new DrawableRoom(room = new Room - { - Name = { Value = "Room with password" }, - Status = { Value = new RoomStatusOpen() }, - Category = { Value = RoomCategory.Realtime }, - }) { MatchingFilter = true }); - - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); - - AddStep("set password", () => room.Password.Value = "password"); - AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType().Single().Alpha)); - - AddStep("unset password", () => room.Password.Value = string.Empty); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index e19665497d..a8fda19c60 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; @@ -150,10 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createRoom(Func room) { - AddStep("open room", () => - { - multiplayerScreen.OpenNewRoom(room()); - }); + AddStep("open room", () => multiplayerScreen.ChildrenOfType().Single().Open(room())); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); AddWaitStep("wait for transition", 2); diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs similarity index 98% rename from osu.Game.Tests/Visual/TestSceneOsuGame.cs rename to osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index c52d846a68..26641214b1 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.Platform; -using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -30,10 +29,9 @@ using osu.Game.Skinning; using osu.Game.Utils; using osuTK.Graphics; -namespace osu.Game.Tests.Visual +namespace osu.Game.Tests.Visual.Navigation { [TestFixture] - [HeadlessTest] public class TestSceneOsuGame : OsuTestScene { private IReadOnlyList requiredGameDependencies => new[] diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 7188a4e57f..3c65f46c79 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -16,6 +16,7 @@ using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; @@ -316,7 +317,8 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => multiplayer = new TestMultiplayer()); - AddStep("open room", () => multiplayer.OpenNewRoom()); + AddUntilStep("wait for lounge", () => multiplayer.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + AddStep("open room", () => multiplayer.ChildrenOfType().Single().Open()); AddStep("press back button", () => Game.ChildrenOfType().First().Action()); AddWaitStep("wait two frames", 2); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 8818ac75b1..8f000afb91 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Humanizer; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -95,9 +96,11 @@ namespace osu.Game.Tests.Visual.Online AddAssert(@"no stream selected", () => changelog.Header.Streams.Current.Value == null); } - [Test] - public void ShowWithBuild() + [TestCase(false)] + [TestCase(true)] + public void ShowWithBuild(bool isSupporter) { + AddStep(@"set supporter", () => dummyAPI.LocalUser.Value.IsSupporter = isSupporter); showBuild(() => new APIChangelogBuild { Version = "2018.712.0", @@ -155,6 +158,8 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep(@"wait for streams", () => changelog.Streams?.Count > 0); AddAssert(@"correct build displayed", () => changelog.Current.Value.Version == "2018.712.0"); AddAssert(@"correct stream selected", () => changelog.Header.Streams.Current.Value.Id == 5); + AddUntilStep(@"wait for content load", () => changelog.ChildrenOfType().Any()); + AddAssert(@"supporter promo showed", () => changelog.ChildrenOfType().First().Alpha == (isSupporter ? 0 : 1)); } [Test] diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs new file mode 100644 index 0000000000..22220a7d9c --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays; +using osu.Game.Overlays.Changelog; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneChangelogSupporterPromo : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + public TestSceneChangelogSupporterPromo() + { + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new ChangelogSupporterPromo(), + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsPage.cs b/osu.Game.Tests/Visual/Online/TestSceneOfflineCommentsContainer.cs similarity index 70% rename from osu.Game.Tests/Visual/Online/TestSceneCommentsPage.cs rename to osu.Game.Tests/Visual/Online/TestSceneOfflineCommentsContainer.cs index 7fdf0708e0..628ae0971b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsPage.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneOfflineCommentsContainer.cs @@ -3,84 +3,52 @@ using System; using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; using osu.Game.Overlays.Comments; using osu.Game.Overlays; using osu.Framework.Allocation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Users; -using osu.Game.Graphics.UserInterface; -using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics; -using osuTK; using JetBrains.Annotations; -using NUnit.Framework; +using osu.Framework.Testing; namespace osu.Game.Tests.Visual.Online { - public class TestSceneCommentsPage : OsuTestScene + public class TestSceneOfflineCommentsContainer : OsuTestScene { [Cached] - private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private readonly BindableBool showDeleted = new BindableBool(); - private readonly Container content; + private TestCommentsContainer comments; - private TestCommentsPage commentsPage; - - public TestSceneCommentsPage() + [SetUp] + public void SetUp() => Schedule(() => { - Add(new FillFlowContainer + Clear(); + Add(new BasicScrollContainer { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - new Container - { - AutoSizeAxes = Axes.Y, - Width = 200, - Child = new OsuCheckbox - { - Current = showDeleted, - LabelText = @"Show Deleted" - } - }, - content = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - } + RelativeSizeAxes = Axes.Both, + Child = comments = new TestCommentsContainer() }); - } + }); [Test] public void TestAppendDuplicatedComment() { - AddStep("Create page", () => createPage(getCommentBundle())); - AddAssert("Dictionary length is 10", () => commentsPage?.DictionaryLength == 10); - AddStep("Append existing comment", () => commentsPage?.AppendComments(getCommentSubBundle())); - AddAssert("Dictionary length is 10", () => commentsPage?.DictionaryLength == 10); + AddStep("Add comment bundle", () => comments.ShowComments(getCommentBundle())); + AddUntilStep("Dictionary length is 10", () => comments.DictionaryLength == 10); + AddStep("Append existing comment", () => comments.AppendComments(getCommentSubBundle())); + AddAssert("Dictionary length is 10", () => comments.DictionaryLength == 10); } [Test] - public void TestEmptyBundle() + public void TestLocalCommentBundle() { - AddStep("Create page", () => createPage(getEmptyCommentBundle())); - AddAssert("Dictionary length is 0", () => commentsPage?.DictionaryLength == 0); - } - - private void createPage(CommentBundle commentBundle) - { - commentsPage = null; - content.Clear(); - content.Add(commentsPage = new TestCommentsPage(commentBundle) - { - ShowDeleted = { BindTarget = showDeleted } - }); + AddStep("Add comment bundle", () => comments.ShowComments(getCommentBundle())); + AddStep("Add empty comment bundle", () => comments.ShowComments(getEmptyCommentBundle())); } private CommentBundle getEmptyCommentBundle() => new CommentBundle @@ -193,6 +161,7 @@ namespace osu.Game.Tests.Visual.Online Username = "Good_Admin" } }, + Total = 10 }; private CommentBundle getCommentSubBundle() => new CommentBundle @@ -211,16 +180,18 @@ namespace osu.Game.Tests.Visual.Online IncludedComments = new List(), }; - private class TestCommentsPage : CommentsPage + private class TestCommentsContainer : CommentsContainer { - public TestCommentsPage(CommentBundle commentBundle) - : base(commentBundle) - { - } - public new void AppendComments([NotNull] CommentBundle bundle) => base.AppendComments(bundle); public int DictionaryLength => CommentDictionary.Count; + + public void ShowComments(CommentBundle bundle) + { + this.ChildrenOfType().Single().Current.Value = 0; + ClearComments(); + OnSuccess(bundle); + } } } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs deleted file mode 100644 index 40e191dd7e..0000000000 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Game.Screens.OnlinePlay.Lounge.Components; - -namespace osu.Game.Tests.Visual.Playlists -{ - public class TestScenePlaylistsFilterControl : OsuTestScene - { - public TestScenePlaylistsFilterControl() - { - Child = new PlaylistsFilterControl - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Width = 0.7f, - Height = 80, - }; - } - } -} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs index 53693d1b70..3b43f8485a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -68,13 +70,40 @@ namespace osu.Game.Tests.Visual.UserInterface ); } - private class MyContextMenuContainer : Container, IHasContextMenu + private static MenuItem[] makeMenu() { - public MenuItem[] ContextMenuItems => new MenuItem[] + return new MenuItem[] { new OsuMenuItem(@"Some option"), new OsuMenuItem(@"Highlighted option", MenuItemType.Highlighted), new OsuMenuItem(@"Another option"), + new OsuMenuItem(@"Nested option >") + { + Items = new MenuItem[] + { + new OsuMenuItem(@"Sub-One"), + new OsuMenuItem(@"Sub-Two"), + new OsuMenuItem(@"Sub-Three"), + new OsuMenuItem(@"Sub-Nested option >") + { + Items = new MenuItem[] + { + new OsuMenuItem(@"Double Sub-One"), + new OsuMenuItem(@"Double Sub-Two"), + new OsuMenuItem(@"Double Sub-Three"), + new OsuMenuItem(@"Sub-Sub-Nested option >") + { + Items = new MenuItem[] + { + new OsuMenuItem(@"Too Deep One"), + new OsuMenuItem(@"Too Deep Two"), + new OsuMenuItem(@"Too Deep Three"), + } + } + } + } + } + }, new OsuMenuItem(@"Choose me please"), new OsuMenuItem(@"And me too"), new OsuMenuItem(@"Trying to fill"), @@ -82,17 +111,29 @@ namespace osu.Game.Tests.Visual.UserInterface }; } + private class MyContextMenuContainer : Container, IHasContextMenu + { + public MenuItem[] ContextMenuItems => makeMenu(); + } + private class AnotherContextMenuContainer : Container, IHasContextMenu { - public MenuItem[] ContextMenuItems => new MenuItem[] + public MenuItem[] ContextMenuItems { - new OsuMenuItem(@"Simple option"), - new OsuMenuItem(@"Simple very very long option"), - new OsuMenuItem(@"Change width", MenuItemType.Highlighted, () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint)), - new OsuMenuItem(@"Change height", MenuItemType.Highlighted, () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint)), - new OsuMenuItem(@"Change width back", MenuItemType.Destructive, () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint)), - new OsuMenuItem(@"Change height back", MenuItemType.Destructive, () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint)), - }; + get + { + List items = makeMenu().ToList(); + items.AddRange(new MenuItem[] + { + new OsuMenuItem(@"Change width", MenuItemType.Highlighted, () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint)), + new OsuMenuItem(@"Change height", MenuItemType.Highlighted, () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint)), + new OsuMenuItem(@"Change width back", MenuItemType.Destructive, () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint)), + new OsuMenuItem(@"Change height back", MenuItemType.Destructive, () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint)), + }); + + return items.ToArray(); + } + } } } } diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs index acd5d53310..11b5cc7556 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tournament.Tests.Components public TestSceneMatchScoreDisplay() { - Add(new MatchScoreDisplay + Add(new TournamentMatchScoreDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs similarity index 97% rename from osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs rename to osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs index 695c6d6f3e..994dee4da0 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs @@ -16,7 +16,8 @@ using osuTK; namespace osu.Game.Tournament.Screens.Gameplay.Components { - public class MatchScoreDisplay : CompositeDrawable + // TODO: Update to derive from osu-side class? + public class TournamentMatchScoreDisplay : CompositeDrawable { private const float bar_height = 18; @@ -29,7 +30,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private readonly Drawable score1Bar; private readonly Drawable score2Bar; - public MatchScoreDisplay() + public TournamentMatchScoreDisplay() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index f61506d7f2..540b45eb56 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tournament.Screens.Gameplay }, } }, - scoreDisplay = new MatchScoreDisplay + scoreDisplay = new TournamentMatchScoreDisplay { Y = -147, Anchor = Anchor.BottomCentre, @@ -148,7 +148,7 @@ namespace osu.Game.Tournament.Screens.Gameplay } private ScheduledDelegate scheduledOperation; - private MatchScoreDisplay scoreDisplay; + private TournamentMatchScoreDisplay scoreDisplay; private TourneyState lastState; private MatchHeader header; diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index cd0e601a2f..7a43fee013 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -26,8 +26,8 @@ namespace osu.Game.Tournament { public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE; - public static readonly Color4 COLOUR_RED = Color4Extensions.FromHex("#AA1414"); - public static readonly Color4 COLOUR_BLUE = Color4Extensions.FromHex("#1462AA"); + public static readonly Color4 COLOUR_RED = new OsuColour().TeamColourRed; + public static readonly Color4 COLOUR_BLUE = new OsuColour().TeamColourBlue; public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = Color4Extensions.FromHex("#fff"); public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = Color4Extensions.FromHex("#000"); diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 7f7e5565f1..8c314f1617 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -13,8 +13,16 @@ namespace osu.Game.Database public interface IModelManager where TModel : class { + /// + /// A bindable which contains a weak reference to the last item that was updated. + /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. + /// IBindable> ItemUpdated { get; } + /// + /// A bindable which contains a weak reference to the last item that was removed. + /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. + /// IBindable> ItemRemoved { get; } } } diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index fbb3fa0e6c..171ad4ee65 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; @@ -9,6 +10,14 @@ namespace osu.Game.Graphics.Cursor { public class OsuContextMenuContainer : ContextMenuContainer { - protected override Menu CreateMenu() => new OsuContextMenu(); + [Cached] + private OsuContextMenuSamples samples = new OsuContextMenuSamples(); + + public OsuContextMenuContainer() + { + AddInternal(samples); + } + + protected override Menu CreateMenu() => new OsuContextMenu(true); } } diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 1f87c06dd2..d7cfc4094c 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -130,6 +130,9 @@ namespace osu.Game.Graphics return Gray(brightness > 0.5f ? 0.2f : 0.9f); } + public readonly Color4 TeamColourRed = Color4Extensions.FromHex("#AA1414"); + public readonly Color4 TeamColourBlue = Color4Extensions.FromHex("#1462AA"); + // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff"); public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff"); diff --git a/osu.Game/Graphics/UserInterface/GrayButton.cs b/osu.Game/Graphics/UserInterface/GrayButton.cs index 88c46f29e0..0a2c83d5a8 100644 --- a/osu.Game/Graphics/UserInterface/GrayButton.cs +++ b/osu.Game/Graphics/UserInterface/GrayButton.cs @@ -27,7 +27,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load() { - Children = new Drawable[] + AddRange(new Drawable[] { Background = new Box { @@ -42,7 +42,7 @@ namespace osu.Game.Graphics.UserInterface Size = new Vector2(13), Icon = icon, }, - }; + }); } } } diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs index 18d8b880ea..8d686e8c2f 100644 --- a/osu.Game/Graphics/UserInterface/Nub.cs +++ b/osu.Game/Graphics/UserInterface/Nub.cs @@ -21,6 +21,9 @@ namespace osu.Game.Graphics.UserInterface private const float border_width = 3; + private const double animate_in_duration = 150; + private const double animate_out_duration = 500; + public Nub() { Box fill; @@ -77,20 +80,26 @@ namespace osu.Game.Graphics.UserInterface if (value) { - this.FadeColour(GlowingAccentColour, 500, Easing.OutQuint); - FadeEdgeEffectTo(1, 500, Easing.OutQuint); + this.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint); + FadeEdgeEffectTo(1, animate_in_duration, Easing.OutQuint); } else { - FadeEdgeEffectTo(0, 500); - this.FadeColour(AccentColour, 500); + FadeEdgeEffectTo(0, animate_out_duration); + this.FadeColour(AccentColour, animate_out_duration); } } } public bool Expanded { - set => this.ResizeTo(new Vector2(value ? EXPANDED_SIZE : COLLAPSED_SIZE, 12), 500, Easing.OutQuint); + set + { + if (value) + this.ResizeTo(new Vector2(EXPANDED_SIZE, 12), animate_in_duration, Easing.OutQuint); + else + this.ResizeTo(new Vector2(COLLAPSED_SIZE, 12), animate_out_duration, Easing.OutQuint); + } } private readonly Bindable current = new Bindable(); diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index 8c7b44f952..cf201b18b4 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -3,6 +3,7 @@ using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; @@ -14,7 +15,14 @@ namespace osu.Game.Graphics.UserInterface { private const int fade_duration = 250; - public OsuContextMenu() + [Resolved] + private OsuContextMenuSamples samples { get; set; } + + // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. + private bool wasOpened; + private readonly bool playClickSample; + + public OsuContextMenu(bool playClickSample = false) : base(Direction.Vertical) { MaskingContainer.CornerRadius = 5; @@ -28,16 +36,38 @@ namespace osu.Game.Graphics.UserInterface ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL }; MaxHeight = 250; + + this.playClickSample = playClickSample; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { BackgroundColour = colours.ContextMenuGray; } - protected override void AnimateOpen() => this.FadeIn(fade_duration, Easing.OutQuint); - protected override void AnimateClose() => this.FadeOut(fade_duration, Easing.OutQuint); + protected override void AnimateOpen() + { + this.FadeIn(fade_duration, Easing.OutQuint); + + if (playClickSample) + samples.PlayClickSample(); + + if (!wasOpened) + samples.PlayOpenSample(); + + wasOpened = true; + } + + protected override void AnimateClose() + { + this.FadeOut(fade_duration, Easing.OutQuint); + + if (wasOpened) + samples.PlayCloseSample(); + + wasOpened = false; + } protected override Menu CreateSubMenu() => new OsuContextMenu(); } diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs b/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs new file mode 100644 index 0000000000..d67ea499e5 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions; +using osu.Framework.Graphics; + +namespace osu.Game.Graphics.UserInterface +{ + public class OsuContextMenuSamples : Component + { + private Sample sampleClick; + private Sample sampleOpen; + private Sample sampleClose; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, AudioManager audio) + { + sampleClick = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); + sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); + sampleClose = audio.Samples.Get(@"UI/dropdown-close"); + } + + public void PlayClickSample() => Scheduler.AddOnce(playClickSample); + private void playClickSample() => sampleClick.Play(); + + public void PlayOpenSample() => Scheduler.AddOnce(playOpenSample); + private void playOpenSample() => sampleOpen.Play(); + + public void PlayCloseSample() => Scheduler.AddOnce(playCloseSample); + private void playCloseSample() => sampleClose.Play(); + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index b97f12df02..61dd5fb2d9 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -288,7 +288,7 @@ namespace osu.Game.Graphics.UserInterface }, }; - AddInternal(new HoverSounds()); + AddInternal(new HoverClickSounds()); } [BackgroundDependencyLoader] diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index e7bf4f66ee..a16adcbd57 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -13,6 +16,12 @@ namespace osu.Game.Graphics.UserInterface { public class OsuMenu : Menu { + private Sample sampleOpen; + private Sample sampleClose; + + // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. + private bool wasOpened; + public OsuMenu(Direction direction, bool topLevelMenu = false) : base(direction, topLevelMenu) { @@ -22,8 +31,30 @@ namespace osu.Game.Graphics.UserInterface ItemsContainer.Padding = new MarginPadding(5); } - protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint); - protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint); + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); + sampleClose = audio.Samples.Get(@"UI/dropdown-close"); + } + + protected override void AnimateOpen() + { + if (!TopLevelMenu && !wasOpened) + sampleOpen?.Play(); + + this.FadeIn(300, Easing.OutQuint); + wasOpened = true; + } + + protected override void AnimateClose() + { + if (!TopLevelMenu && wasOpened) + sampleClose?.Play(); + + this.FadeOut(300, Easing.OutQuint); + wasOpened = false; + } protected override void UpdateSize(Vector2 newSize) { diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index b26c4d8201..da637c229f 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -27,6 +27,14 @@ namespace osu.Game.Online.Multiplayer /// If the user is not in a room. Task TransferHost(int userId); + /// + /// As the host, kick another user from the room. + /// + /// The user to kick.. + /// A user other than the current host is attempting to kick a user. + /// If the user is not in a room. + Task KickUser(int userId); + /// /// As the host, update the settings of the currently joined room. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index bffb2d341a..4607211cdf 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -62,7 +62,9 @@ namespace osu.Game.Online.Multiplayer /// /// The users in the joined which are participating in the current gameplay loop. /// - public readonly BindableList CurrentMatchPlayingUserIds = new BindableList(); + public IBindableList CurrentMatchPlayingUserIds => PlayingUserIds; + + protected readonly BindableList PlayingUserIds = new BindableList(); public readonly Bindable CurrentMatchPlayingItem = new Bindable(); @@ -179,7 +181,8 @@ namespace osu.Game.Online.Multiplayer { APIRoom = null; Room = null; - CurrentMatchPlayingUserIds.Clear(); + CurrentMatchPlayingItem.Value = null; + PlayingUserIds.Clear(); RoomUpdated?.Invoke(); }); @@ -290,6 +293,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task TransferHost(int userId); + public abstract Task KickUser(int userId); + public abstract Task ChangeSettings(MultiplayerRoomSettings settings); public abstract Task ChangeState(MultiplayerUserState newState); @@ -376,7 +381,7 @@ namespace osu.Game.Online.Multiplayer return; Room.Users.Remove(user); - CurrentMatchPlayingUserIds.Remove(user.UserID); + PlayingUserIds.Remove(user.UserID); RoomUpdated?.Invoke(); }, false); @@ -659,16 +664,16 @@ namespace osu.Game.Online.Multiplayer /// The new state of the user. private void updateUserPlayingState(int userId, MultiplayerUserState state) { - bool wasPlaying = CurrentMatchPlayingUserIds.Contains(userId); + bool wasPlaying = PlayingUserIds.Contains(userId); bool isPlaying = state >= MultiplayerUserState.WaitingForLoad && state <= MultiplayerUserState.FinishedPlay; if (isPlaying == wasPlaying) return; if (isPlaying) - CurrentMatchPlayingUserIds.Add(userId); + PlayingUserIds.Add(userId); else - CurrentMatchPlayingUserIds.Remove(userId); + PlayingUserIds.Remove(userId); } private Task scheduleAsync(Action action, CancellationToken cancellationToken = default) diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 8b8d10ce4f..55477a9fc7 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -91,6 +91,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId); } + public override Task KickUser(int userId) + { + if (!IsConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.KickUser), userId); + } + public override Task ChangeSettings(MultiplayerRoomSettings settings) { if (!IsConnected.Value) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 72ea84d4a8..86879ba245 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -59,8 +59,8 @@ namespace osu.Game.Online.Rooms protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet) { - int? beatmapId = SelectedItem.Value.Beatmap.Value.OnlineBeatmapID; - string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; + int? beatmapId = SelectedItem.Value?.Beatmap.Value.OnlineBeatmapID; + string checksum = SelectedItem.Value?.Beatmap.Value.MD5Hash; var matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs index c852f86f6b..01f3ae368b 100644 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs @@ -8,7 +8,7 @@ namespace osu.Game.Online.Rooms.RoomStatuses { public class RoomStatusEnded : RoomStatus { - public override string Message => @"Ended"; + public override string Message => "Ended"; public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDarker; } } diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs index 4f7f0d6f5d..686d4f4033 100644 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs @@ -8,7 +8,7 @@ namespace osu.Game.Online.Rooms.RoomStatuses { public class RoomStatusOpen : RoomStatus { - public override string Message => @"Welcoming Players"; + public override string Message => "Open"; public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight; } } diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs index f04f1b23af..83f1acf52a 100644 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs @@ -8,7 +8,7 @@ namespace osu.Game.Online.Rooms.RoomStatuses { public class RoomStatusPlaying : RoomStatus { - public override string Message => @"Now Playing"; + public override string Message => "Playing"; public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple; } } diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs index cec1a5ac12..47b477ef9a 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs @@ -37,6 +37,13 @@ namespace osu.Game.Overlays.BeatmapListing.Panels RelativeSizeAxes = Axes.Both, }, }; + + button.Add(new DownloadProgressBar(beatmapSet) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Depth = -1, + }); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs index 8b89d63aab..93486274fc 100644 --- a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs @@ -71,6 +71,17 @@ namespace osu.Game.Overlays.Changelog Colour = colourProvider.Background6, Margin = new MarginPadding { Top = 30 }, }, + new ChangelogSupporterPromo + { + Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1, + }, + new Box + { + RelativeSizeAxes = Axes.X, + Height = 2, + Colour = colourProvider.Background6, + Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1, + }, comments = new CommentsContainer() }; diff --git a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs new file mode 100644 index 0000000000..f617b4fc82 --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; +using osu.Game.Resources.Localisation.Web; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogSupporterPromo : CompositeDrawable + { + private const float image_container_width = 164; + + private readonly FillFlowContainer textContainer; + private readonly Container imageContainer; + + public ChangelogSupporterPromo() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding + { + Vertical = 20, + Horizontal = 50, + }; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + CornerRadius = 6, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Offset = new Vector2(0, 1), + Radius = 3, + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.3f), + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 200, + Padding = new MarginPadding { Horizontal = 75 }, + Children = new Drawable[] + { + textContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Right = 50 + image_container_width }, + }, + imageContainer = new Container + { + RelativeSizeAxes = Axes.Y, + Width = image_container_width, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + } + }, + } + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour, TextureStore textures) + { + SupporterPromoLinkFlowContainer supportLinkText; + textContainer.Children = new Drawable[] + { + new OsuSpriteText + { + Text = ChangelogStrings.SupportHeading, + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light), + Margin = new MarginPadding { Bottom = 20 }, + }, + supportLinkText = new SupporterPromoLinkFlowContainer(t => + { + t.Font = t.Font.With(size: 14); + t.Colour = colour.PinkLighter; + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + new OsuTextFlowContainer(t => + { + t.Font = t.Font.With(size: 12); + t.Colour = colour.PinkLighter; + }) + { + Text = ChangelogStrings.SupportText2.ToString(), + Margin = new MarginPadding { Top = 10 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + }; + + supportLinkText.AddText("Support further development of osu! and "); + supportLinkText.AddLink("become and osu!supporter", "https://osu.ppy.sh/home/support", t => t.Font = t.Font.With(weight: FontWeight.Bold)); + supportLinkText.AddText(" today!"); + + imageContainer.Children = new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + Texture = textures.Get(@"Online/supporter-pippi"), + }, + new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 75, + Height = 75, + Margin = new MarginPadding { Top = 70 }, + Texture = textures.Get(@"Online/supporter-heart"), + }, + }; + } + + private class SupporterPromoLinkFlowContainer : LinkFlowContainer + { + public SupporterPromoLinkFlowContainer(Action defaultCreationParameters) + : base(defaultCreationParameters) + { + } + + public new void AddLink(string text, string url, Action creationParameters) => + AddInternal(new SupporterPromoLinkCompiler(AddText(text, creationParameters)) { Url = url }); + + private class SupporterPromoLinkCompiler : DrawableLinkCompiler + { + [Resolved(CanBeNull = true)] + private OsuGame game { get; set; } + + public string Url; + + public SupporterPromoLinkCompiler(IEnumerable parts) + : base(parts) + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + TooltipText = Url; + Action = () => game?.HandleLink(Url); + IdleColour = colour.PinkDark; + HoverColour = Color4.White; + } + } + } + } +} diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 513fabf52a..fe8d6f0178 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -14,6 +14,9 @@ using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Threading; using osu.Game.Users; +using System.Collections.Generic; +using JetBrains.Annotations; +using osu.Game.Graphics.Sprites; namespace osu.Game.Overlays.Comments { @@ -147,7 +150,7 @@ namespace osu.Game.Overlays.Comments private void refetchComments() { - clearComments(); + ClearComments(); getComments(); } @@ -160,50 +163,125 @@ namespace osu.Game.Overlays.Comments loadCancellation?.Cancel(); scheduledCommentsLoad?.Cancel(); request = new GetCommentsRequest(id.Value, type.Value, Sort.Value, currentPage++, 0); - request.Success += res => scheduledCommentsLoad = Schedule(() => onSuccess(res)); + request.Success += res => scheduledCommentsLoad = Schedule(() => OnSuccess(res)); api.PerformAsync(request); } - private void clearComments() + protected void ClearComments() { currentPage = 1; deletedCommentsCounter.Count.Value = 0; moreButton.Show(); moreButton.IsLoading = true; content.Clear(); + CommentDictionary.Clear(); } - private void onSuccess(CommentBundle response) + protected readonly Dictionary CommentDictionary = new Dictionary(); + + protected void OnSuccess(CommentBundle response) { - loadCancellation = new CancellationTokenSource(); + commentCounter.Current.Value = response.Total; - LoadComponentAsync(new CommentsPage(response) + if (!response.Comments.Any()) { - ShowDeleted = { BindTarget = ShowDeleted }, - Sort = { BindTarget = Sort }, - Type = { BindTarget = type }, - CommentableId = { BindTarget = id } - }, loaded => + content.Add(new NoCommentsPlaceholder()); + moreButton.Hide(); + return; + } + + AppendComments(response); + } + + /// + /// Appends retrieved comments to the subtree rooted of comments in this page. + /// + /// The bundle of comments to add. + protected void AppendComments([NotNull] CommentBundle bundle) + { + var topLevelComments = new List(); + var orphaned = new List(); + + foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments)) { - content.Add(loaded); + // Exclude possible duplicated comments. + if (CommentDictionary.ContainsKey(comment.Id)) + continue; - deletedCommentsCounter.Count.Value += response.Comments.Count(c => c.IsDeleted && c.IsTopLevel); + addNewComment(comment); + } - if (response.HasMore) + // Comments whose parents were seen later than themselves can now be added. + foreach (var o in orphaned) + addNewComment(o); + + if (topLevelComments.Any()) + { + LoadComponentsAsync(topLevelComments, loaded => { - int loadedTopLevelComments = 0; - content.Children.OfType().ForEach(p => loadedTopLevelComments += p.Children.OfType().Count()); + content.AddRange(loaded); - moreButton.Current.Value = response.TopLevelCount - loadedTopLevelComments; - moreButton.IsLoading = false; + deletedCommentsCounter.Count.Value += topLevelComments.Select(d => d.Comment).Count(c => c.IsDeleted && c.IsTopLevel); + + if (bundle.HasMore) + { + int loadedTopLevelComments = 0; + content.Children.OfType().ForEach(p => loadedTopLevelComments++); + + moreButton.Current.Value = bundle.TopLevelCount - loadedTopLevelComments; + moreButton.IsLoading = false; + } + else + { + moreButton.Hide(); + } + }, (loadCancellation = new CancellationTokenSource()).Token); + } + + void addNewComment(Comment comment) + { + var drawableComment = getDrawableComment(comment); + + if (comment.ParentId == null) + { + // Comments that have no parent are added as top-level comments to the flow. + topLevelComments.Add(drawableComment); + } + else if (CommentDictionary.TryGetValue(comment.ParentId.Value, out var parentDrawable)) + { + // The comment's parent has already been seen, so the parent<-> child links can be added. + comment.ParentComment = parentDrawable.Comment; + parentDrawable.Replies.Add(drawableComment); } else { - moreButton.Hide(); + // The comment's parent has not been seen yet, so keep it orphaned for the time being. This can occur if the comments arrive out of order. + // Since this comment has now been seen, any further children can be added to it without being orphaned themselves. + orphaned.Add(comment); } + } + } - commentCounter.Current.Value = response.Total; - }, loadCancellation.Token); + private DrawableComment getDrawableComment(Comment comment) + { + if (CommentDictionary.TryGetValue(comment.Id, out var existing)) + return existing; + + return CommentDictionary[comment.Id] = new DrawableComment(comment) + { + ShowDeleted = { BindTarget = ShowDeleted }, + Sort = { BindTarget = Sort }, + RepliesRequested = onCommentRepliesRequested + }; + } + + private void onCommentRepliesRequested(DrawableComment drawableComment, int page) + { + var req = new GetCommentsRequest(id.Value, type.Value, Sort.Value, page, drawableComment.Comment.Id); + + req.Success += response => Schedule(() => AppendComments(response)); + + api.PerformAsync(req); } protected override void Dispose(bool isDisposing) @@ -212,5 +290,30 @@ namespace osu.Game.Overlays.Comments loadCancellation?.Cancel(); base.Dispose(isDisposing); } + + private class NoCommentsPlaceholder : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Height = 80; + RelativeSizeAxes = Axes.X; + AddRangeInternal(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4 + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 50 }, + Text = @"No comments yet." + } + }); + } + } } } diff --git a/osu.Game/Overlays/Comments/CommentsPage.cs b/osu.Game/Overlays/Comments/CommentsPage.cs deleted file mode 100644 index 9b146b0a7d..0000000000 --- a/osu.Game/Overlays/Comments/CommentsPage.cs +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics; -using osu.Framework.Bindables; -using osu.Game.Online.API.Requests.Responses; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.Sprites; -using System.Linq; -using osu.Game.Online.API.Requests; -using osu.Game.Online.API; -using System.Collections.Generic; -using JetBrains.Annotations; - -namespace osu.Game.Overlays.Comments -{ - public class CommentsPage : CompositeDrawable - { - public readonly BindableBool ShowDeleted = new BindableBool(); - public readonly Bindable Sort = new Bindable(); - public readonly Bindable Type = new Bindable(); - public readonly BindableLong CommentableId = new BindableLong(); - - [Resolved] - private IAPIProvider api { get; set; } - - private readonly CommentBundle commentBundle; - private FillFlowContainer flow; - - public CommentsPage(CommentBundle commentBundle) - { - this.commentBundle = commentBundle; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - AddRangeInternal(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5 - }, - flow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - } - }); - - if (!commentBundle.Comments.Any()) - { - flow.Add(new NoCommentsPlaceholder()); - return; - } - - AppendComments(commentBundle); - } - - private DrawableComment getDrawableComment(Comment comment) - { - if (CommentDictionary.TryGetValue(comment.Id, out var existing)) - return existing; - - return CommentDictionary[comment.Id] = new DrawableComment(comment) - { - ShowDeleted = { BindTarget = ShowDeleted }, - Sort = { BindTarget = Sort }, - RepliesRequested = onCommentRepliesRequested - }; - } - - private void onCommentRepliesRequested(DrawableComment drawableComment, int page) - { - var request = new GetCommentsRequest(CommentableId.Value, Type.Value, Sort.Value, page, drawableComment.Comment.Id); - - request.Success += response => Schedule(() => AppendComments(response)); - - api.PerformAsync(request); - } - - protected readonly Dictionary CommentDictionary = new Dictionary(); - - /// - /// Appends retrieved comments to the subtree rooted of comments in this page. - /// - /// The bundle of comments to add. - protected void AppendComments([NotNull] CommentBundle bundle) - { - var orphaned = new List(); - - foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments)) - { - // Exclude possible duplicated comments. - if (CommentDictionary.ContainsKey(comment.Id)) - continue; - - addNewComment(comment); - } - - // Comments whose parents were seen later than themselves can now be added. - foreach (var o in orphaned) - addNewComment(o); - - void addNewComment(Comment comment) - { - var drawableComment = getDrawableComment(comment); - - if (comment.ParentId == null) - { - // Comments that have no parent are added as top-level comments to the flow. - flow.Add(drawableComment); - } - else if (CommentDictionary.TryGetValue(comment.ParentId.Value, out var parentDrawable)) - { - // The comment's parent has already been seen, so the parent<-> child links can be added. - comment.ParentComment = parentDrawable.Comment; - parentDrawable.Replies.Add(drawableComment); - } - else - { - // The comment's parent has not been seen yet, so keep it orphaned for the time being. This can occur if the comments arrive out of order. - // Since this comment has now been seen, any further children can be added to it without being orphaned themselves. - orphaned.Add(comment); - } - } - } - - private class NoCommentsPlaceholder : CompositeDrawable - { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Height = 80; - RelativeSizeAxes = Axes.X; - AddRangeInternal(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4 - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = 50 }, - Text = @"No comments yet." - } - }); - } - } - } -} diff --git a/osu.Game/Overlays/News/Sidebar/MonthSection.cs b/osu.Game/Overlays/News/Sidebar/MonthSection.cs index cd6ab224a9..948f312f15 100644 --- a/osu.Game/Overlays/News/Sidebar/MonthSection.cs +++ b/osu.Game/Overlays/News/Sidebar/MonthSection.cs @@ -79,8 +79,6 @@ namespace osu.Game.Overlays.News.Sidebar private readonly SpriteIcon icon; - protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); - public DropdownHeader(int month, int year) { var date = new DateTime(year, month, 1); diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index 008e7696e1..e7b3e6d873 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.cs @@ -18,6 +18,7 @@ namespace osu.Game.Overlays public static OverlayColourProvider Green { get; } = new OverlayColourProvider(OverlayColourScheme.Green); public static OverlayColourProvider Purple { get; } = new OverlayColourProvider(OverlayColourScheme.Purple); public static OverlayColourProvider Blue { get; } = new OverlayColourProvider(OverlayColourScheme.Blue); + public static OverlayColourProvider Plum { get; } = new OverlayColourProvider(OverlayColourScheme.Plum); public OverlayColourProvider(OverlayColourScheme colourScheme) { @@ -80,6 +81,9 @@ namespace osu.Game.Overlays case OverlayColourScheme.Blue: return 200 / 360f; + + case OverlayColourScheme.Plum: + return 320 / 360f; } } } @@ -92,6 +96,7 @@ namespace osu.Game.Overlays Lime, Green, Purple, - Blue + Blue, + Plum, } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 25f3b8931a..29d8a475ef 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -190,7 +190,8 @@ namespace osu.Game.Rulesets.Objects.Drawables comboIndexBindable.BindValueChanged(_ => UpdateComboColour()); comboIndexWithOffsetsBindable.BindValueChanged(_ => UpdateComboColour(), true); - updateState(ArmedState.Idle, true); + // Apply transforms + updateState(State.Value, true); } /// diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs b/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs deleted file mode 100644 index bcc256bcff..0000000000 --- a/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; - -namespace osu.Game.Screens.OnlinePlay.Components -{ - public class RoomStatusInfo : OnlinePlayComposite - { - public RoomStatusInfo() - { - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load() - { - StatusPart statusPart; - EndDatePart endDatePart; - - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - statusPart = new StatusPart - { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14) - }, - endDatePart = new EndDatePart { Font = OsuFont.GetFont(size: 14) } - } - }; - - statusPart.EndDate.BindTo(EndDate); - statusPart.Status.BindTo(Status); - statusPart.Availability.BindTo(Availability); - endDatePart.EndDate.BindTo(EndDate); - } - - private class EndDatePart : DrawableDate - { - public readonly IBindable EndDate = new Bindable(); - - public EndDatePart() - : base(DateTimeOffset.UtcNow) - { - EndDate.BindValueChanged(date => - { - // If null, set a very large future date to prevent unnecessary schedules. - Date = date.NewValue ?? DateTimeOffset.Now.AddYears(1); - }, true); - } - - protected override string Format() - { - if (EndDate.Value == null) - return string.Empty; - - var diffToNow = Date.Subtract(DateTimeOffset.Now); - - if (diffToNow.TotalSeconds < -5) - return $"Closed {base.Format()}"; - - if (diffToNow.TotalSeconds < 0) - return "Closed"; - - if (diffToNow.TotalSeconds < 5) - return "Closing soon"; - - return $"Closing {base.Format()}"; - } - } - - private class StatusPart : EndDatePart - { - public readonly IBindable Status = new Bindable(); - public readonly IBindable Availability = new Bindable(); - - [Resolved] - private OsuColour colours { get; set; } - - public StatusPart() - { - EndDate.BindValueChanged(_ => Format()); - Status.BindValueChanged(_ => Format()); - Availability.BindValueChanged(_ => Format()); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Text = Format(); - } - - protected override string Format() - { - if (!IsLoaded) - return string.Empty; - - RoomStatus status = Date < DateTimeOffset.Now ? new RoomStatusEnded() : Status.Value ?? new RoomStatusOpen(); - - this.FadeColour(status.GetAppropriateColour(colours), 100); - return $"{Availability.Value.GetDescription()}, {status.Message}"; - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index 8c1b10e3bd..a27b27b8ad 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Screens.Ranking.Expanded; @@ -85,6 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Components minDisplay.Current.Value = minDifficulty; maxDisplay.Current.Value = maxDifficulty; + maxDisplay.Alpha = Precision.AlmostEquals(Math.Round(minDifficulty.Stars, 2), Math.Round(maxDifficulty.Stars, 2)) ? 0 : 1; minBackground.Colour = colours.ForStarDifficulty(minDifficulty.Stars); maxBackground.Colour = colours.ForStarDifficulty(maxDifficulty.Stars); diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs index bf0a53cbb6..b0db9256f5 100644 --- a/osu.Game/Screens/OnlinePlay/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -2,19 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using Humanizer; +using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay { @@ -22,52 +18,30 @@ namespace osu.Game.Screens.OnlinePlay { public const float HEIGHT = 80; + private readonly ScreenStack stack; + private readonly MultiHeaderTitle title; + public Header(string mainTitle, ScreenStack stack) { + this.stack = stack; + RelativeSizeAxes = Axes.X; Height = HEIGHT; + Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING }; - HeaderBreadcrumbControl breadcrumbs; - MultiHeaderTitle title; - - Children = new Drawable[] + Child = title = new MultiHeaderTitle(mainTitle) { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"#1f1921"), - }, - new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, - Children = new Drawable[] - { - title = new MultiHeaderTitle(mainTitle) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomLeft, - }, - breadcrumbs = new HeaderBreadcrumbControl(stack) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft - } - }, - }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }; - breadcrumbs.Current.ValueChanged += screen => - { - if (screen.NewValue is IOnlinePlaySubScreen onlineSubScreen) - title.Screen = onlineSubScreen; - }; - - breadcrumbs.Current.TriggerChange(); + // unnecessary to unbind these as this header has the same lifetime as the screen stack we are attaching to. + stack.ScreenPushed += (_, __) => updateSubScreenTitle(); + stack.ScreenExited += (_, __) => updateSubScreenTitle(); } + private void updateSubScreenTitle() => title.Screen = stack.CurrentScreen as IOnlinePlaySubScreen; + private class MultiHeaderTitle : CompositeDrawable { private const float spacing = 6; @@ -75,9 +49,10 @@ namespace osu.Game.Screens.OnlinePlay private readonly OsuSpriteText dot; private readonly OsuSpriteText pageTitle; + [CanBeNull] public IOnlinePlaySubScreen Screen { - set => pageTitle.Text = value.ShortTitle.Titleize(); + set => pageTitle.Text = value?.ShortTitle.Titleize() ?? string.Empty; } public MultiHeaderTitle(string mainTitle) @@ -125,35 +100,5 @@ namespace osu.Game.Screens.OnlinePlay pageTitle.Colour = dot.Colour = colours.Yellow; } } - - private class HeaderBreadcrumbControl : ScreenBreadcrumbControl - { - public HeaderBreadcrumbControl(ScreenStack stack) - : base(stack) - { - RelativeSizeAxes = Axes.X; - StripColour = Color4.Transparent; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - AccentColour = Color4Extensions.FromHex("#e35c99"); - } - - protected override TabItem CreateTabItem(IScreen value) => new HeaderBreadcrumbTabItem(value) - { - AccentColour = AccentColour - }; - - private class HeaderBreadcrumbTabItem : BreadcrumbTabItem - { - public HeaderBreadcrumbTabItem(IScreen value) - : base(value) - { - Bar.Colour = Color4.Transparent; - } - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index c455cb0c50..193fb0cf57 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -5,10 +5,13 @@ using System; using System.Collections.Generic; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; @@ -18,7 +21,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -26,6 +28,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; using osuTK; using osuTK.Graphics; @@ -35,19 +38,18 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public class DrawableRoom : OsuClickableContainer, IStateful, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler { public const float SELECTION_BORDER_WIDTH = 4; - private const float corner_radius = 5; + private const float corner_radius = 10; private const float transition_duration = 60; - private const float content_padding = 10; - private const float height = 110; - private const float side_strip_width = 5; - private const float cover_width = 145; + private const float height = 100; public event Action StateChanged; - private readonly Box selectionBox; + protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); + + private Drawable selectionBox; [Resolved(canBeNull: true)] - private OnlinePlayScreen parentScreen { get; set; } + private LoungeSubScreen loungeScreen { get; set; } [Resolved] private BeatmapManager beatmaps { get; set; } @@ -62,19 +64,26 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private SelectionState state; + private Sample sampleSelect; + private Sample sampleJoin; + public SelectionState State { get => state; set { - if (value == state) return; + if (value == state) + return; state = value; - if (state == SelectionState.Selected) - selectionBox.FadeIn(transition_duration); - else - selectionBox.FadeOut(transition_duration); + if (selectionBox != null) + { + if (state == SelectionState.Selected) + selectionBox.FadeIn(transition_duration); + else + selectionBox.FadeOut(transition_duration); + } StateChanged?.Invoke(State); } @@ -101,6 +110,25 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + private int numberOfAvatars = 7; + + public int NumberOfAvatars + { + get => numberOfAvatars; + set + { + numberOfAvatars = value; + + if (recentParticipantsList != null) + recentParticipantsList.NumberOfCircles = value; + } + } + + private readonly Bindable roomCategory = new Bindable(); + + private RecentParticipantsList recentParticipantsList; + private RoomSpecialCategoryPill specialCategoryPill; + public bool FilteringActive { get; set; } private PasswordProtectedIcon passwordIcon; @@ -112,115 +140,212 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Room = room; RelativeSizeAxes = Axes.X; - Height = height + SELECTION_BORDER_WIDTH * 2; - CornerRadius = corner_radius + SELECTION_BORDER_WIDTH / 2; - Masking = true; + Height = height; - // create selectionBox here so State can be set before being loaded - selectionBox = new Box + Masking = true; + CornerRadius = corner_radius + SELECTION_BORDER_WIDTH / 2; + EdgeEffect = new EdgeEffectParameters { - RelativeSizeAxes = Axes.Both, - Alpha = 0f, + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(40), + Radius = 5, }; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colours, AudioManager audio) { - float stripWidth = side_strip_width * (Room.Category.Value == RoomCategory.Spotlight ? 2 : 1); - Children = new Drawable[] { - new StatusColouredContainer(transition_duration) + // This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites. + new BufferedContainer { RelativeSizeAxes = Axes.Both, - Child = selectionBox + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Background5, + }, + new OnlinePlayBackgroundSprite + { + RelativeSizeAxes = Axes.Both + }, + } }, new Container { + Name = @"Room content", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(SELECTION_BORDER_WIDTH), + // This negative padding resolves 1px gaps between this background and the background above. + Padding = new MarginPadding { Left = 20, Vertical = -0.5f }, Child = new Container { RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = corner_radius, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(40), - Radius = 5, - }, Children = new Drawable[] { - new Box + // This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites. + new BufferedContainer { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"212121"), - }, - new StatusColouredContainer(transition_duration) - { - RelativeSizeAxes = Axes.Y, - Width = stripWidth, - Child = new Box { RelativeSizeAxes = Axes.Both } - }, - new Container - { - RelativeSizeAxes = Axes.Y, - Width = cover_width, - Masking = true, - Margin = new MarginPadding { Left = stripWidth }, - Child = new OnlinePlayBackgroundSprite(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both } + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.2f) + }, + Content = new[] + { + new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Background5, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)) + }, + } + } + }, + }, }, new Container { + Name = @"Left details", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Vertical = content_padding, - Left = stripWidth + cover_width + content_padding, - Right = content_padding, + Left = 20, + Vertical = 5 }, Children = new Drawable[] { new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(5f), Children = new Drawable[] { - new RoomName { Font = OsuFont.GetFont(size: 18) }, - new ParticipantInfo(), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new RoomStatusPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + specialCategoryPill = new RoomSpecialCategoryPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new EndDateInfo + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 3 }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new RoomNameText(), + new RoomHostText(), + } + } }, }, new FillFlowContainer { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), Children = new Drawable[] { - new RoomStatusInfo(), - new BeatmapTitle { TextSize = 14 }, - }, - }, - new ModeTypeInfo - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - }, + new PlaylistCountPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new StarRatingRangeDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.8f) + } + } + } + } + }, + new FillFlowContainer + { + Name = "Right content", + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Right = 10, + Vertical = 5 }, + Children = new Drawable[] + { + recentParticipantsList = new RecentParticipantsList + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + NumberOfCircles = NumberOfAvatars + } + } }, passwordIcon = new PasswordProtectedIcon { Alpha = 0 } }, }, }, + new StatusColouredContainer(transition_duration) + { + RelativeSizeAxes = Axes.Both, + Child = selectionBox = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = state == SelectionState.Selected ? 1 : 0, + Masking = true, + CornerRadius = corner_radius, + BorderThickness = SELECTION_BORDER_WIDTH, + BorderColour = Color4.White, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + }, }; + + sampleSelect = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); + sampleJoin = audio.Samples.Get($@"UI/{HoverSampleSet.Submit.GetDescription()}-select"); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -240,6 +365,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components else Alpha = 0; + roomCategory.BindTo(Room.Category); + roomCategory.BindValueChanged(c => + { + if (c.NewValue == RoomCategory.Spotlight) + specialCategoryPill.Show(); + else + specialCategoryPill.Hide(); + }, true); + hasPassword.BindTo(Room.HasPassword); hasPassword.BindValueChanged(v => passwordIcon.Alpha = v.NewValue ? 1 : 0, true); } @@ -250,7 +384,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - parentScreen?.OpenNewRoom(Room.DeepClone()); + lounge?.Open(Room.DeepClone()); }) }; @@ -273,32 +407,40 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { } - protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected; + protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected || child is HoverSounds; protected override bool OnClick(ClickEvent e) { if (Room != selectedRoom.Value) { + sampleSelect?.Play(); selectedRoom.Value = Room; return true; } if (Room.HasPassword.Value) { + sampleJoin?.Play(); this.ShowPopover(); return true; } + sampleJoin?.Play(); lounge?.Join(Room, null); return base.OnClick(e); } - private class RoomName : OsuSpriteText + private class RoomNameText : OsuSpriteText { [Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))] private Bindable name { get; set; } + public RoomNameText() + { + Font = OsuFont.GetFont(size: 28); + } + [BackgroundDependencyLoader] private void load() { @@ -306,6 +448,41 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + private class RoomHostText : OnlinePlayComposite + { + private LinkFlowContainer hostText; + + public RoomHostText() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 16)) + { + AutoSizeAxes = Axes.Both + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Host.BindValueChanged(host => + { + hostText.Clear(); + + if (host.NewValue != null) + { + hostText.AddText("hosted by "); + hostText.AddUserLink(host.NewValue); + } + }, true); + } + } + public class PasswordProtectedIcon : CompositeDrawable { [BackgroundDependencyLoader] @@ -353,7 +530,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private OsuPasswordTextBox passwordTextbox; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Child = new FillFlowContainer { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs new file mode 100644 index 0000000000..3207d373db --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + public class EndDateInfo : OnlinePlayComposite + { + public EndDateInfo() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new EndDatePart + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12), + EndDate = { BindTarget = EndDate } + }; + } + + private class EndDatePart : DrawableDate + { + public readonly IBindable EndDate = new Bindable(); + + public EndDatePart() + : base(DateTimeOffset.UtcNow) + { + EndDate.BindValueChanged(date => + { + // If null, set a very large future date to prevent unnecessary schedules. + Date = date.NewValue ?? DateTimeOffset.Now.AddYears(1); + }, true); + } + + protected override string Format() + { + if (EndDate.Value == null) + return string.Empty; + + var diffToNow = Date.Subtract(DateTimeOffset.Now); + + if (diffToNow.TotalSeconds < -5) + return $"Closed {base.Format()}"; + + if (diffToNow.TotalSeconds < 0) + return "Closed"; + + if (diffToNow.TotalSeconds < 5) + return "Closing soon"; + + return $"Closing {base.Format()}"; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs deleted file mode 100644 index 7fc1c670ca..0000000000 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Threading; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; -using osuTK.Graphics; - -namespace osu.Game.Screens.OnlinePlay.Lounge.Components -{ - public abstract class FilterControl : CompositeDrawable - { - protected const float VERTICAL_PADDING = 10; - protected const float HORIZONTAL_PADDING = 80; - - [Resolved(CanBeNull = true)] - private Bindable filter { get; set; } - - [Resolved] - private IBindable ruleset { get; set; } - - private readonly Box tabStrip; - private readonly SearchTextBox search; - private readonly PageTabControl tabs; - - protected FilterControl() - { - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.25f, - }, - tabStrip = new Box - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = 1, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Top = VERTICAL_PADDING, - Horizontal = HORIZONTAL_PADDING - }, - Children = new Drawable[] - { - search = new FilterSearchTextBox - { - RelativeSizeAxes = Axes.X, - }, - tabs = new PageTabControl - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - }, - } - } - }; - - tabs.Current.Value = RoomStatusFilter.Open; - tabs.Current.TriggerChange(); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - filter ??= new Bindable(); - tabStrip.Colour = colours.Yellow; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - search.Current.BindValueChanged(_ => updateFilterDebounced()); - ruleset.BindValueChanged(_ => UpdateFilter()); - tabs.Current.BindValueChanged(_ => UpdateFilter(), true); - } - - private ScheduledDelegate scheduledFilterUpdate; - - private void updateFilterDebounced() - { - scheduledFilterUpdate?.Cancel(); - scheduledFilterUpdate = Scheduler.AddDelayed(UpdateFilter, 200); - } - - protected void UpdateFilter() => Scheduler.AddOnce(updateFilter); - - private void updateFilter() - { - scheduledFilterUpdate?.Cancel(); - - var criteria = CreateCriteria(); - criteria.SearchString = search.Current.Value; - criteria.Status = tabs.Current.Value; - criteria.Ruleset = ruleset.Value; - - filter.Value = criteria; - } - - protected virtual FilterCriteria CreateCriteria() => new FilterCriteria(); - - public bool HoldFocus - { - get => search.HoldFocus; - set => search.HoldFocus = value; - } - - public void TakeFocus() => search.TakeFocus(); - - private class FilterSearchTextBox : SearchTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = OsuColour.Gray(0.06f); - BackgroundFocused = OsuColour.Gray(0.12f); - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs deleted file mode 100644 index bc4506b78e..0000000000 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Humanizer; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Users.Drawables; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Lounge.Components -{ - public class ParticipantInfo : OnlinePlayComposite - { - public ParticipantInfo() - { - RelativeSizeAxes = Axes.X; - Height = 15f; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - OsuSpriteText summary; - Container flagContainer; - LinkFlowContainer hostText; - - InternalChildren = 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 = 22f, - RelativeSizeAxes = Axes.Y, - }, - hostText = new LinkFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both - } - }, - }, - new FillFlowContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Colour = colours.Gray9, - Children = new[] - { - summary = new OsuSpriteText - { - Text = "0 participants", - } - }, - }, - }; - - Host.BindValueChanged(host => - { - hostText.Clear(); - flagContainer.Clear(); - - if (host.NewValue != null) - { - hostText.AddText("hosted by "); - hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold, italics: true)); - - flagContainer.Child = new UpdateableFlag(host.NewValue.Country) { RelativeSizeAxes = Axes.Both }; - } - }, true); - - ParticipantCount.BindValueChanged(count => summary.Text = "participant".ToQuantity(count.NewValue), true); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs new file mode 100644 index 0000000000..109851a16b --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs @@ -0,0 +1,81 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + /// + /// Displays contents in a "pill". + /// + public class PillContainer : Container + { + private const float padding = 8; + + public readonly Drawable Background; + + protected override Container Content => content; + private readonly Container content; + + public PillContainer() + { + AutoSizeAxes = Axes.X; + Height = 16; + + InternalChild = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Masking = true, + Children = new[] + { + Background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = padding }, + Child = new GridContainer + { + AutoSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize, minSize: 80 - 2 * padding) + }, + Content = new[] + { + new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Bottom = 2 }, + Child = content = new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + } + } + } + } + } + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs new file mode 100644 index 0000000000..2fe3c7b668 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Specialized; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + /// + /// A pill that displays the playlist item count. + /// + public class PlaylistCountPill : OnlinePlayComposite + { + private OsuTextFlowContainer count; + + public PlaylistCountPill() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new PillContainer + { + Child = count = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playlist.BindCollectionChanged(updateCount, true); + } + + private void updateCount(object sender, NotifyCollectionChangedEventArgs e) + { + count.Clear(); + count.AddText(Playlist.Count.ToString(), s => s.Font = s.Font.With(weight: FontWeight.Bold)); + count.AddText(" "); + count.AddText("Beatmap".ToQuantity(Playlist.Count, ShowQuantityAs.None)); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs deleted file mode 100644 index a463742097..0000000000 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Screens.OnlinePlay.Lounge.Components -{ - public class PlaylistsFilterControl : FilterControl - { - private readonly Dropdown dropdown; - - public PlaylistsFilterControl() - { - AddInternal(dropdown = new SlimEnumDropdown - { - Anchor = Anchor.BottomRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.None, - Width = 160, - X = -HORIZONTAL_PADDING, - Y = -30 - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - dropdown.Current.BindValueChanged(_ => UpdateFilter()); - } - - protected override FilterCriteria CreateCriteria() - { - var criteria = base.CreateCriteria(); - - switch (dropdown.Current.Value) - { - case PlaylistsCategory.Normal: - criteria.Category = "normal"; - break; - - case PlaylistsCategory.Spotlight: - criteria.Category = "spotlight"; - break; - } - - return criteria; - } - - private enum PlaylistsCategory - { - Any, - Normal, - Spotlight - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs new file mode 100644 index 0000000000..42fe0bfecd --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + public class RankRangePill : MultiplayerRoomComposite + { + private OsuTextFlowContainer rankFlow; + + public RankRangePill() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new PillContainer + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(4), + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(8), + Icon = FontAwesome.Solid.User + }, + rankFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + } + } + } + }; + } + + protected override void OnRoomUpdated() + { + base.OnRoomUpdated(); + + rankFlow.Clear(); + + if (Room == null || Room.Users.All(u => u.User == null)) + { + rankFlow.AddText("-"); + return; + } + + int minRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min(); + int maxRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max(); + + rankFlow.AddText("#"); + rankFlow.AddText(minRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold)); + + rankFlow.AddText(" - "); + + rankFlow.AddText("#"); + rankFlow.AddText(maxRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold)); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs new file mode 100644 index 0000000000..bc658f45e4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs @@ -0,0 +1,278 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Specialized; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Users; +using osu.Game.Users.Drawables; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + public class RecentParticipantsList : OnlinePlayComposite + { + private const float avatar_size = 36; + + private FillFlowContainer avatarFlow; + + private HiddenUserCount hiddenUsers; + private OsuSpriteText totalCount; + + public RecentParticipantsList() + { + AutoSizeAxes = Axes.X; + Height = 60; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) + { + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Shear = new Vector2(0.2f, 0), + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Background4, + } + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Padding = new MarginPadding { Right = 16 }, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(16), + Margin = new MarginPadding { Left = 8 }, + Icon = FontAwesome.Solid.User, + }, + totalCount = new OsuSpriteText + { + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + avatarFlow = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Margin = new MarginPadding { Left = 4 }, + }, + hiddenUsers = new HiddenUserCount + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + RecentParticipants.BindCollectionChanged(onParticipantsChanged, true); + ParticipantCount.BindValueChanged(_ => + { + updateHiddenUsers(); + totalCount.Text = ParticipantCount.Value.ToString(); + }, true); + } + + private int numberOfCircles = 4; + + /// + /// The maximum number of circles visible (including the "hidden count" circle in the overflow case). + /// + public int NumberOfCircles + { + get => numberOfCircles; + set + { + numberOfCircles = value; + + if (LoadState < LoadState.Loaded) + return; + + // Reinitialising the list looks janky, but this is unlikely to be used in a setting where it's visible. + clearUsers(); + foreach (var u in RecentParticipants) + addUser(u); + + updateHiddenUsers(); + } + } + + private void onParticipantsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (var added in e.NewItems.OfType()) + addUser(added); + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var removed in e.OldItems.OfType()) + removeUser(removed); + break; + + case NotifyCollectionChangedAction.Reset: + clearUsers(); + break; + + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Move: + // Easiest is to just reinitialise the whole list. These are unlikely to ever be use cases. + clearUsers(); + foreach (var u in RecentParticipants) + addUser(u); + break; + } + + updateHiddenUsers(); + } + + private int displayedCircles => avatarFlow.Count + (hiddenUsers.Count > 0 ? 1 : 0); + + private void addUser(User user) + { + if (displayedCircles < NumberOfCircles) + avatarFlow.Add(new CircularAvatar { User = user }); + } + + private void removeUser(User user) + { + avatarFlow.RemoveAll(a => a.User == user); + } + + private void clearUsers() + { + avatarFlow.Clear(); + updateHiddenUsers(); + } + + private void updateHiddenUsers() + { + int hiddenCount = 0; + if (RecentParticipants.Count > NumberOfCircles) + hiddenCount = ParticipantCount.Value - NumberOfCircles + 1; + + hiddenUsers.Count = hiddenCount; + + if (displayedCircles > NumberOfCircles) + avatarFlow.Remove(avatarFlow.Last()); + else if (displayedCircles < NumberOfCircles) + { + var nextUser = RecentParticipants.FirstOrDefault(u => avatarFlow.All(a => a.User != u)); + if (nextUser != null) addUser(nextUser); + } + } + + private class CircularAvatar : CompositeDrawable + { + public User User + { + get => avatar.User; + set => avatar.User = value; + } + + private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both }; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) + { + Size = new Vector2(avatar_size); + + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + Colour = colours.Background5, + RelativeSizeAxes = Axes.Both, + }, + avatar + } + }; + } + } + + public class HiddenUserCount : CompositeDrawable + { + public int Count + { + get => count; + set + { + count = value; + countText.Text = $"+{count}"; + + if (count > 0) + Show(); + else + Hide(); + } + } + + private int count; + + private readonly SpriteText countText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Default.With(weight: FontWeight.Bold), + }; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) + { + Size = new Vector2(avatar_size); + Alpha = 0; + + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Background5, + }, + countText + } + }; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs deleted file mode 100644 index a0a7f2dc28..0000000000 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Screens.OnlinePlay.Components; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Lounge.Components -{ - public class RoomInfo : OnlinePlayComposite - { - private readonly List statusElements = new List(); - private readonly OsuTextFlowContainer roomName; - - public RoomInfo() - { - AutoSizeAxes = Axes.Y; - - RoomLocalUserInfo localUserInfo; - RoomStatusInfo statusInfo; - ModeTypeInfo typeInfo; - ParticipantInfo participantInfo; - - InternalChild = new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Spacing = new Vector2(0, 10), - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - roomName = new OsuTextFlowContainer(t => t.Font = OsuFont.GetFont(size: 30)) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - participantInfo = new ParticipantInfo(), - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - statusInfo = new RoomStatusInfo(), - typeInfo = new ModeTypeInfo - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight - } - } - }, - localUserInfo = new RoomLocalUserInfo(), - } - }; - - statusElements.AddRange(new Drawable[] - { - statusInfo, typeInfo, participantInfo, localUserInfo - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - if (RoomID.Value == null) - statusElements.ForEach(e => e.FadeOut()); - RoomID.BindValueChanged(id => - { - if (id.NewValue == null) - statusElements.ForEach(e => e.FadeOut(100)); - else - statusElements.ForEach(e => e.FadeIn(100)); - }, true); - RoomName.BindValueChanged(name => - { - roomName.Text = name.NewValue ?? "No room selected"; - }, true); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs deleted file mode 100644 index c28354c753..0000000000 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Screens.OnlinePlay.Components; -using osuTK.Graphics; - -namespace osu.Game.Screens.OnlinePlay.Lounge.Components -{ - public class RoomInspector : OnlinePlayComposite - { - private const float transition_duration = 100; - - private readonly MarginPadding contentPadding = new MarginPadding { Horizontal = 20, Vertical = 10 }; - - [Resolved] - private BeatmapManager beatmaps { get; set; } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - OverlinedHeader participantsHeader; - - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.25f - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 30 }, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new RoomInfo - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Vertical = 60 }, - }, - participantsHeader = new OverlinedHeader("Recent Participants"), - new ParticipantsDisplay(Direction.Vertical) - { - RelativeSizeAxes = Axes.X, - Height = ParticipantsList.TILE_SIZE * 3, - Details = { BindTarget = participantsHeader.Details } - } - } - } - }, - new Drawable[] { new OverlinedPlaylistHeader(), }, - new Drawable[] - { - new DrawableRoomPlaylist(false, false) - { - RelativeSizeAxes = Axes.Both, - Items = { BindTarget = Playlist } - }, - }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - } - } - }; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs new file mode 100644 index 0000000000..6cdbeb2af4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + public class RoomSpecialCategoryPill : OnlinePlayComposite + { + private SpriteText text; + + public RoomSpecialCategoryPill() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChild = new PillContainer + { + Background = + { + Colour = colours.Pink, + Alpha = 1 + }, + Child = text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12), + Colour = Color4.Black + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Category.BindValueChanged(c => text.Text = c.NewValue.ToString(), true); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs new file mode 100644 index 0000000000..1d43f2dc65 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + /// + /// A pill that displays the room's current status. + /// + public class RoomStatusPill : OnlinePlayComposite + { + [Resolved] + private OsuColour colours { get; set; } + + private PillContainer pill; + private SpriteText statusText; + + public RoomStatusPill() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = pill = new PillContainer + { + Child = statusText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12), + Colour = Color4.Black + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + EndDate.BindValueChanged(_ => updateDisplay()); + Status.BindValueChanged(_ => updateDisplay(), true); + + FinishTransforms(true); + } + + private void updateDisplay() + { + RoomStatus status = getDisplayStatus(); + + pill.Background.Alpha = 1; + pill.Background.FadeColour(status.GetAppropriateColour(colours), 100); + statusText.Text = status.Message; + } + + private RoomStatus getDisplayStatus() + { + if (EndDate.Value < DateTimeOffset.Now) + return new RoomStatusEnded(); + + return Status.Value; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index d2253b2d2c..5e5863c7c4 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -50,6 +50,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + // account for the fact we are in a scroll container and want a bit of spacing from the scroll bar. + Padding = new MarginPadding { Right = 5 }; + InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, @@ -59,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(2), + Spacing = new Vector2(10), } }; } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 68bd3cd613..7249ccdd93 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -9,15 +10,20 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Screens; +using osu.Framework.Threading; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Overlays; +using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge { @@ -28,11 +34,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); + protected Container Buttons { get; } = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both + }; + private readonly IBindable initialRoomsReceived = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); - private FilterControl filter; - private Container content; private LoadingLayer loadingLayer; [Resolved] @@ -44,53 +55,109 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved(CanBeNull = true)] private OngoingOperationTracker ongoingOperationTracker { get; set; } + [Resolved(CanBeNull = true)] + private Bindable filter { get; set; } + + [Resolved] + private IBindable ruleset { get; set; } + [CanBeNull] private IDisposable joiningRoomOperation { get; set; } private RoomsContainer roomsContainer; + private SearchTextBox searchTextBox; + private Dropdown statusDropdown; [BackgroundDependencyLoader] private void load() { + filter ??= new Bindable(new FilterCriteria()); + OsuScrollContainer scrollContainer; InternalChildren = new Drawable[] { - content = new Container + new Container { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Padding = new MarginPadding { - new Container + Left = WaveOverlayContainer.WIDTH_PADDING, + Right = WaveOverlayContainer.WIDTH_PADDING, + }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - RelativeSizeAxes = Axes.Both, - Width = 0.55f, - Children = new Drawable[] + new Dimension(GridSizeMode.Absolute, Header.HEIGHT), + new Dimension(GridSizeMode.Absolute, 25), + new Dimension(GridSizeMode.Absolute, 20) + }, + Content = new[] + { + new Drawable[] { - scrollContainer = new OsuScrollContainer + searchTextBox = new LoungeSearchTextBox + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + Width = 0.6f, + }, + }, + new Drawable[] + { + new Container { RelativeSizeAxes = Axes.Both, - ScrollbarOverlapsContent = false, - Padding = new MarginPadding(10), - Child = roomsContainer = new RoomsContainer() + Depth = float.MinValue, // Contained filters should appear over the top of rooms. + Children = new Drawable[] + { + Buttons.WithChild(CreateNewRoomButton().With(d => + { + d.Anchor = Anchor.BottomLeft; + d.Origin = Anchor.BottomLeft; + d.Size = new Vector2(150, 37.5f); + d.Action = () => Open(); + })), + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d => + { + d.Anchor = Anchor.TopRight; + d.Origin = Anchor.TopRight; + })) + } + } + } + }, + null, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + scrollContainer = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarOverlapsContent = false, + Child = roomsContainer = new RoomsContainer() + }, + loadingLayer = new LoadingLayer(true), + } }, - loadingLayer = new LoadingLayer(true), } - }, - new RoomInspector - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Both, - Width = 0.45f, - }, + } }, - }, - filter = CreateFilterControl().With(d => - { - d.RelativeSizeAxes = Axes.X; - d.Height = 80; - }) + } }; // scroll selected room into view on selection. @@ -106,6 +173,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { base.LoadComplete(); + searchTextBox.Current.BindValueChanged(_ => updateFilterDebounced()); + ruleset.BindValueChanged(_ => UpdateFilter()); + initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived); initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer()); @@ -114,24 +184,49 @@ namespace osu.Game.Screens.OnlinePlay.Lounge operationInProgress.BindTo(ongoingOperationTracker.InProgress); operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true); } + + updateFilter(); } - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); + #region Filtering - content.Padding = new MarginPadding + protected void UpdateFilter() => Scheduler.AddOnce(updateFilter); + + private ScheduledDelegate scheduledFilterUpdate; + + private void updateFilterDebounced() + { + scheduledFilterUpdate?.Cancel(); + scheduledFilterUpdate = Scheduler.AddDelayed(UpdateFilter, 200); + } + + private void updateFilter() + { + scheduledFilterUpdate?.Cancel(); + filter.Value = CreateFilterCriteria(); + } + + protected virtual FilterCriteria CreateFilterCriteria() => new FilterCriteria + { + SearchString = searchTextBox.Current.Value, + Ruleset = ruleset.Value, + Status = statusDropdown.Current.Value + }; + + protected virtual IEnumerable CreateFilterControls() + { + statusDropdown = new SlimEnumDropdown { - Top = filter.DrawHeight, - Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING, - Right = WaveOverlayContainer.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING, + RelativeSizeAxes = Axes.None, + Width = 160, }; + + statusDropdown.Current.BindValueChanged(_ => UpdateFilter()); + + yield return statusDropdown; } - protected override void OnFocus(FocusEvent e) - { - filter.TakeFocus(); - } + #endregion public override void OnEntering(IScreen last) { @@ -164,14 +259,19 @@ namespace osu.Game.Screens.OnlinePlay.Lounge base.OnSuspending(next); } + protected override void OnFocus(FocusEvent e) + { + searchTextBox.TakeFocus(); + } + private void onReturning() { - filter.HoldFocus = true; + searchTextBox.HoldFocus = true; } private void onLeaving() { - filter.HoldFocus = false; + searchTextBox.HoldFocus = false; // ensure any password prompt is dismissed. this.HidePopover(); @@ -199,13 +299,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge /// /// Push a room as a new subscreen. /// - public void Open(Room room) => Schedule(() => + /// An optional template to use when creating the room. + public void Open(Room room = null) => Schedule(() => { // Handles the case where a room is clicked 3 times in quick succession if (!this.IsCurrentScreen()) return; - OpenNewRoom(room); + OpenNewRoom(room ?? CreateNewRoom()); }); protected virtual void OpenNewRoom(Room room) @@ -215,7 +316,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge this.Push(CreateRoomSubScreen(room)); } - protected abstract FilterControl CreateFilterControl(); + protected abstract OsuButton CreateNewRoomButton(); + + /// + /// Creates a new room. + /// + /// The created . + protected abstract Room CreateNewRoom(); protected abstract RoomSubScreen CreateRoomSubScreen(Room room); @@ -226,5 +333,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge else loadingLayer.Hide(); } + + private class LoungeSearchTextBox : SearchTextBox + { + [BackgroundDependencyLoader] + private void load() + { + BackgroundUnfocused = OsuColour.Gray(0.06f); + BackgroundFocused = OsuColour.Gray(0.12f); + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs b/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs index cd4dee5e3a..3801463095 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs @@ -12,6 +12,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components [BackgroundDependencyLoader] private void load() { + SpriteText.Font = SpriteText.Font.With(size: 14); Triangles.TriangleScale = 1.5f; } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index a53e253581..243d2abf74 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -8,8 +8,10 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -17,6 +19,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.OnlinePlay.Match.Components; namespace osu.Game.Screens.OnlinePlay.Match { @@ -61,8 +64,15 @@ namespace osu.Game.Screens.OnlinePlay.Match protected RoomSubScreen() { + Padding = new MarginPadding { Top = Header.HEIGHT }; + AddRangeInternal(new Drawable[] { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // This is super temporary. + }, BeatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker { SelectedItem = { BindTarget = SelectedItem } @@ -250,5 +260,9 @@ namespace osu.Game.Screens.OnlinePlay.Match private class UserModSelectOverlay : LocalPlayerModSelectOverlay { } + + public class UserModSelectButton : PurpleTriangleButton + { + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs new file mode 100644 index 0000000000..20a88545c5 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class GameplayMatchScoreDisplay : MatchScoreDisplay + { + public Bindable Expanded = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Scale = new Vector2(0.5f); + + Expanded.BindValueChanged(expandedChanged, true); + } + + private void expandedChanged(ValueChangedEvent expanded) + { + if (expanded.NewValue) + { + Score1Text.FadeIn(500, Easing.OutQuint); + Score2Text.FadeIn(500, Easing.OutQuint); + this.ResizeWidthTo(2, 500, Easing.OutQuint); + } + else + { + Score1Text.FadeOut(500, Easing.OutQuint); + Score2Text.FadeOut(500, Easing.OutQuint); + this.ResizeWidthTo(1, 500, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index d906cc8110..45928505bb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -4,9 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; @@ -54,20 +52,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})"); } - protected override Room CreateNewRoom() => - new Room - { - Name = { Value = $"{API.LocalUser}'s awesome room" }, - Category = { Value = RoomCategory.Realtime }, - Type = { Value = MatchType.HeadToHead }, - }; - protected override string ScreenTitle => "Multiplayer"; protected override RoomManager CreateRoomManager() => new MultiplayerRoomManager(); protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); - - protected override OsuButton CreateNewMultiplayerGameButton() => new CreateMultiplayerMatchButton(); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs deleted file mode 100644 index 37e0fd109a..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Screens.OnlinePlay.Lounge.Components; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer -{ - public class MultiplayerFilterControl : FilterControl - { - protected override FilterCriteria CreateCriteria() - { - var criteria = base.CreateCriteria(); - criteria.Category = "realtime"; - return criteria; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 7062994479..ad7882abc2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -3,6 +3,8 @@ using osu.Framework.Allocation; using osu.Framework.Logging; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; @@ -13,13 +15,30 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerLoungeSubScreen : LoungeSubScreen { - protected override FilterControl CreateFilterControl() => new MultiplayerFilterControl(); - - protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); + [Resolved] + private IAPIProvider api { get; set; } [Resolved] private MultiplayerClient client { get; set; } + protected override FilterCriteria CreateFilterCriteria() + { + var criteria = base.CreateFilterCriteria(); + criteria.Category = @"realtime"; + return criteria; + } + + protected override OsuButton CreateNewRoomButton() => new CreateMultiplayerMatchButton(); + + protected override Room CreateNewRoom() => new Room + { + Name = { Value = $"{api.LocalUser}'s awesome room" }, + Category = { Value = RoomCategory.Realtime }, + Type = { Value = MatchType.HeadToHead }, + }; + + protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); + protected override void OpenNewRoom(Room room) { if (client?.IsConnected.Value != true) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 561fa220c8..a8e44dd56c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -176,7 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Spacing = new Vector2(10, 0), Children = new Drawable[] { - new PurpleTriangleButton + new UserModSelectButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -475,16 +475,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override Screen CreateGameplayScreen() { Debug.Assert(client.LocalUser != null); + Debug.Assert(client.Room != null); int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); + MultiplayerRoomUser[] users = userIds.Select(id => client.Room.Users.First(u => u.UserID == id)).ToArray(); switch (client.LocalUser.State) { case MultiplayerUserState.Spectating: - return new MultiSpectatorScreen(userIds); + return new MultiSpectatorScreen(users.Take(PlayerGrid.MAX_PLAYERS).ToArray()); default: - return new PlayerLoader(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); + return new PlayerLoader(() => new MultiplayerPlayer(SelectedItem.Value, users)); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index b54a4a7726..3ba7b8b982 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -3,9 +3,12 @@ using System; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; @@ -34,16 +37,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private MultiplayerGameplayLeaderboard leaderboard; - private readonly int[] userIds; + private readonly MultiplayerRoomUser[] users; private LoadingLayer loadingDisplay; + private FillFlowContainer leaderboardFlow; /// /// Construct a multiplayer player. /// /// The playlist item to be played. - /// The users which are participating in this game. - public MultiplayerPlayer(PlaylistItem playlistItem, int[] userIds) + /// The users which are participating in this game. + public MultiplayerPlayer(PlaylistItem playlistItem, MultiplayerRoomUser[] users) : base(playlistItem, new PlayerConfiguration { AllowPause = false, @@ -51,14 +55,41 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer AllowSkipping = false, }) { - this.userIds = userIds; + this.users = users; } [BackgroundDependencyLoader] private void load() { + if (!LoadedBeatmapSuccessfully) + return; + + HUDOverlay.Add(leaderboardFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + }); + // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, users), l => + { + if (!LoadedBeatmapSuccessfully) + return; + + ((IBindable)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud); + + leaderboardFlow.Add(l); + + if (leaderboard.TeamScores.Count >= 2) + { + LoadComponentAsync(new GameplayMatchScoreDisplay + { + Team1Score = { BindTarget = leaderboard.TeamScores.First().Value }, + Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value }, + Expanded = { BindTarget = HUDOverlay.ShowHud }, + }, leaderboardFlow.Add); + } + }); HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); } @@ -67,6 +98,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadAsyncComplete(); + if (!LoadedBeatmapSuccessfully) + return; + if (!ValidForResume) return; // token retrieval may have failed. @@ -92,13 +126,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(client.Room != null); } - protected override void LoadComplete() - { - base.LoadComplete(); - - ((IBindable)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud); - } - protected override void StartGameplay() { // block base call, but let the server know we are ready to start. @@ -118,6 +145,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void Update() { base.Update(); + + if (!LoadedBeatmapSuccessfully) + return; + adjustLeaderboardPosition(); } @@ -125,7 +156,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { const float padding = 44; // enough margin to avoid the hit error display. - leaderboard.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight); + leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight); } private void onMatchStarted() => Scheduler.Add(() => diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 89431445d3..1787480e1f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -42,6 +43,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private ModDisplay userModsDisplay; private StateDisplay userStateDisplay; + private IconButton kickButton; + public ParticipantPanel(MultiplayerRoomUser user) { User = user; @@ -64,7 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { new Dimension(GridSizeMode.Absolute, 18), new Dimension(GridSizeMode.AutoSize), - new Dimension() + new Dimension(), + new Dimension(GridSizeMode.AutoSize), }, Content = new[] { @@ -157,7 +161,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Margin = new MarginPadding { Right = 10 }, } } - } + }, + kickButton = new KickButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + Margin = new MarginPadding(4), + Action = () => + { + Debug.Assert(user != null); + + Client.KickUser(user.Id); + } + }, }, } }; @@ -167,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { base.OnRoomUpdated(); - if (Room == null) + if (Room == null || Client.LocalUser == null) return; const double fade_time = 50; @@ -179,6 +196,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); + if (Client.IsHost && !User.Equals(Client.LocalUser)) + kickButton.FadeIn(fade_time); + else + kickButton.FadeOut(fade_time); + if (Room.Host?.Equals(User) == true) crown.FadeIn(fade_time); else @@ -211,13 +233,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants new OsuMenuItem("Give host", MenuItemType.Standard, () => { // Ensure the local user is still host. - if (Room.Host?.UserID != api.LocalUser.Value.Id) + if (!Client.IsHost) return; Client.TransferHost(targetUser); + }), + new OsuMenuItem("Kick", MenuItemType.Destructive, () => + { + // Ensure the local user is still host. + if (!Client.IsHost) + return; + + Client.KickUser(targetUser); }) }; } } + + public class KickButton : IconButton + { + public KickButton() + { + Icon = FontAwesome.Solid.UserTimes; + TooltipText = "Kick"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + IconHoverColour = colours.Red; + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs index 55c4270c70..1614828a78 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs @@ -4,6 +4,7 @@ using System; using JetBrains.Annotations; using osu.Framework.Timing; +using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; @@ -11,8 +12,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard { - public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, int[] userIds) - : base(scoreProcessor, userIds) + public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) + : base(scoreProcessor, users) { } @@ -32,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate ((SpectatingTrackedUserData)data).Clock = null; } - protected override TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(userId, scoreProcessor); + protected override TrackedUserData CreateUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(user, scoreProcessor); protected override void Update() { @@ -47,8 +48,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [CanBeNull] public IClock Clock; - public SpectatingTrackedUserData(int userId, ScoreProcessor scoreProcessor) - : base(userId, scoreProcessor) + public SpectatingTrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) + : base(user, scoreProcessor) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 56ed7a9564..d10917259d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Spectate; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate @@ -45,20 +46,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private PlayerArea currentAudioSource; private bool canStartMasterClock; + private readonly MultiplayerRoomUser[] users; + /// /// Creates a new . /// - /// The players to spectate. - public MultiSpectatorScreen(int[] userIds) - : base(userIds.Take(PlayerGrid.MAX_PLAYERS).ToArray()) + /// The players to spectate. + public MultiSpectatorScreen(MultiplayerRoomUser[] users) + : base(users.Select(u => u.UserID).ToArray()) { - instances = new PlayerArea[UserIds.Count]; + this.users = users; + + instances = new PlayerArea[Users.Count]; } [BackgroundDependencyLoader] private void load() { Container leaderboardContainer; + Container scoreDisplayContainer; + masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0); InternalChildren = new[] @@ -67,28 +74,44 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate masterClockContainer.WithChild(new GridContainer { RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, Content = new[] { new Drawable[] { - leaderboardContainer = new Container + scoreDisplayContainer = new Container { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y }, - grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + Content = new[] + { + new Drawable[] + { + leaderboardContainer = new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + }, + grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } + } + } + } } } }) }; - for (int i = 0; i < UserIds.Count; i++) + for (int i = 0; i < Users.Count; i++) { - grid.Add(instances[i] = new PlayerArea(UserIds[i], masterClockContainer.GameplayClock)); + grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer.GameplayClock)); syncManager.AddPlayerClock(instances[i].GameplayClock); } @@ -97,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds.ToArray()) + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, users) { Expanded = { Value = true }, Anchor = Anchor.CentreLeft, @@ -108,6 +131,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate leaderboard.AddClock(instance.UserId, instance.GameplayClock); leaderboardContainer.Add(leaderboard); + + if (leaderboard.TeamScores.Count == 2) + { + LoadComponentAsync(new MatchScoreDisplay + { + Team1Score = { BindTarget = leaderboard.TeamScores.First().Value }, + Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value }, + }, scoreDisplayContainer.Add); + } }); } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 49524660db..24b3b4ec94 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -35,6 +35,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected BindableList Playlist { get; private set; } + [Resolved(typeof(Room))] + protected Bindable Category { get; private set; } + [Resolved(typeof(Room))] protected BindableList RecentParticipants { get; private set; } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 117ceab6f2..c2ad0285b1 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -13,7 +13,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -22,15 +21,18 @@ using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay { [Cached] public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack { + [Cached] + protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + public override bool CursorVisible => (screenStack?.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; // this is required due to PlayerLoader eventually being pushed to the main stack @@ -38,12 +40,8 @@ namespace osu.Game.Screens.OnlinePlay public override bool DisallowExternalBeatmapRulesetChanges => true; private MultiplayerWaveContainer waves; - - private OsuButton createButton; - - private ScreenStack screenStack; - private LoungeSubScreen loungeSubScreen; + private ScreenStack screenStack; private readonly IBindable isIdle = new BindableBool(); @@ -74,9 +72,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] private OsuLogo logo { get; set; } - private Drawable header; - private Drawable headerBackground; - protected OnlinePlayScreen() { Anchor = Anchor.Centre; @@ -107,59 +102,31 @@ namespace osu.Game.Screens.OnlinePlay new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = Header.HEIGHT }, - Children = new[] + Children = new Drawable[] { - header = new Container + new BufferedContainer { - RelativeSizeAxes = Axes.X, - Height = 400, - Children = new[] + RelativeSizeAxes = Axes.Both, + BlurSigma = new Vector2(10), + Child = new BeatmapBackgroundSprite { - headerBackground = new Container - { - RelativeSizeAxes = Axes.Both, - Width = 1.25f, - Masking = true, - Children = new Drawable[] - { - new HeaderBackgroundSprite - { - RelativeSizeAxes = Axes.X, - Height = 400 // Keep a static height so the header doesn't change as it's resized between subscreens - }, - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = -1 }, // 1px padding to avoid a 1px gap due to masking - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(backgroundColour.Opacity(0.5f), backgroundColour) - }, - } + RelativeSizeAxes = Axes.Both } }, - screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both } + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.9f), Color4.Black.Opacity(0.6f)) + }, + screenStack = new OnlinePlaySubScreenStack + { + RelativeSizeAxes = Axes.Both + } } }, new Header(ScreenTitle, screenStack), - createButton = CreateNewMultiplayerGameButton().With(button => - { - button.Anchor = Anchor.TopRight; - button.Origin = Anchor.TopRight; - button.Size = new Vector2(150, Header.HEIGHT - 20); - button.Margin = new MarginPadding - { - Top = 10, - Right = 10 + HORIZONTAL_OVERFLOW_PADDING, - }; - button.Action = () => OpenNewRoom(); - }), RoomManager, - ongoingOperationTracker, + ongoingOperationTracker } }; } @@ -292,18 +259,6 @@ namespace osu.Game.Screens.OnlinePlay logo.Delay(WaveContainer.DISAPPEAR_DURATION / 2).FadeOut(); } - /// - /// Creates and opens the newly-created room. - /// - /// An optional template to use when creating the room. - public void OpenNewRoom(Room room = null) => loungeSubScreen.Open(room ?? CreateNewRoom()); - - /// - /// Creates a new room. - /// - /// The created . - protected abstract Room CreateNewRoom(); - private void screenPushed(IScreen lastScreen, IScreen newScreen) { subScreenChanged(lastScreen, newScreen); @@ -319,19 +274,6 @@ namespace osu.Game.Screens.OnlinePlay private void subScreenChanged(IScreen lastScreen, IScreen newScreen) { - switch (newScreen) - { - case LoungeSubScreen _: - header.Delay(OnlinePlaySubScreen.RESUME_TRANSITION_DELAY).ResizeHeightTo(400, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint); - headerBackground.MoveToX(0, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint); - break; - - case RoomSubScreen _: - header.ResizeHeightTo(135, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint); - headerBackground.MoveToX(-OnlinePlaySubScreen.X_SHIFT, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint); - break; - } - if (lastScreen is IOsuScreen lastOsuScreen) Activity.UnbindFrom(lastOsuScreen.Activity); @@ -339,7 +281,6 @@ namespace osu.Game.Screens.OnlinePlay ((IBindable)Activity).BindTo(newOsuScreen.Activity); UpdatePollingRate(isIdle.Value); - createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200); } protected IScreen CurrentSubScreen => screenStack.CurrentScreen; @@ -350,8 +291,6 @@ namespace osu.Game.Screens.OnlinePlay protected abstract LoungeSubScreen CreateLounge(); - protected abstract OsuButton CreateNewMultiplayerGameButton(); - private class MultiplayerWaveContainer : WaveContainer { protected override bool StartHidden => true; @@ -365,13 +304,13 @@ namespace osu.Game.Screens.OnlinePlay } } - private class HeaderBackgroundSprite : OnlinePlayBackgroundSprite + private class BeatmapBackgroundSprite : OnlinePlayBackgroundSprite { protected override UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new BackgroundSprite { RelativeSizeAxes = Axes.Both }; private class BackgroundSprite : UpdateableBeatmapBackgroundSprite { - protected override double TransformDuration => 200; + protected override double LoadDelay => 200; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index be28de5c43..1502463022 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -59,6 +59,8 @@ namespace osu.Game.Screens.OnlinePlay [BackgroundDependencyLoader] private void load() { + LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; + initialBeatmap = Beatmap.Value; initialRuleset = Ruleset.Value; initialMods = Mods.Value.ToList(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs index 4f02651f02..6a78e24ba1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs @@ -3,8 +3,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Match; @@ -46,21 +44,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Logger.Log($"Polling adjusted (listing: {playlistsManager.TimeBetweenListingPolls.Value}, selection: {playlistsManager.TimeBetweenSelectionPolls.Value})"); } - protected override Room CreateNewRoom() - { - return new Room - { - Name = { Value = $"{API.LocalUser}'s awesome playlist" }, - Type = { Value = MatchType.Playlists } - }; - } - protected override string ScreenTitle => "Playlists"; protected override RoomManager CreateRoomManager() => new PlaylistsRoomManager(); protected override LoungeSubScreen CreateLounge() => new PlaylistsLoungeSubScreen(); - - protected override OsuButton CreateNewMultiplayerGameButton() => new CreatePlaylistsRoomButton(); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index bfbff4240c..eee4d4f407 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -1,6 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -10,8 +17,60 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsLoungeSubScreen : LoungeSubScreen { - protected override FilterControl CreateFilterControl() => new PlaylistsFilterControl(); + [Resolved] + private IAPIProvider api { get; set; } + + private Dropdown categoryDropdown; + + protected override IEnumerable CreateFilterControls() + { + categoryDropdown = new SlimEnumDropdown + { + RelativeSizeAxes = Axes.None, + Width = 160, + }; + + categoryDropdown.Current.BindValueChanged(_ => UpdateFilter()); + + return base.CreateFilterControls().Append(categoryDropdown); + } + + protected override FilterCriteria CreateFilterCriteria() + { + var criteria = base.CreateFilterCriteria(); + + switch (categoryDropdown.Current.Value) + { + case PlaylistsCategory.Normal: + criteria.Category = @"normal"; + break; + + case PlaylistsCategory.Spotlight: + criteria.Category = @"spotlight"; + break; + } + + return criteria; + } + + protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton(); + + protected override Room CreateNewRoom() + { + return new Room + { + Name = { Value = $"{api.LocalUser}'s awesome playlist" }, + Type = { Value = MatchType.Playlists } + }; + } protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); + + private enum PlaylistsCategory + { + Any, + Normal, + Spotlight + } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 45aca24ab2..953c687087 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Spacing = new Vector2(10, 0), Children = new Drawable[] { - new PurpleTriangleButton + new UserModSelectButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 34efeab54c..63cb4f89f5 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -48,10 +48,9 @@ namespace osu.Game.Screens.Play.HUD /// public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked) { - var drawable = new GameplayLeaderboardScore(user, isTracked) - { - Expanded = { BindTarget = Expanded }, - }; + var drawable = CreateLeaderboardScoreDrawable(user, isTracked); + + drawable.Expanded.BindTo(Expanded); base.Add(drawable); drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true); @@ -61,6 +60,9 @@ namespace osu.Game.Screens.Play.HUD return drawable; } + protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked) => + new GameplayLeaderboardScore(user, isTracked); + public sealed override void Add(GameplayLeaderboardScore drawable) { throw new NotSupportedException($"Use {nameof(AddPlayer)} instead."); diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 10476e5565..433bf78e9b 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -54,6 +54,10 @@ namespace osu.Game.Screens.Play.HUD public BindableInt Combo { get; } = new BindableInt(); public BindableBool HasQuit { get; } = new BindableBool(); + public Color4? BackgroundColour { get; set; } + + public Color4? TextColour { get; set; } + private int? scorePosition; public int? ScorePosition @@ -331,19 +335,19 @@ namespace osu.Game.Screens.Play.HUD if (scorePosition == 1) { widthExtension = true; - panelColour = Color4Extensions.FromHex("7fcc33"); - textColour = Color4.White; + panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33"); + textColour = TextColour ?? Color4.White; } else if (trackedPlayer) { widthExtension = true; - panelColour = Color4Extensions.FromHex("ffd966"); - textColour = Color4Extensions.FromHex("2e576b"); + panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966"); + textColour = TextColour ?? Color4Extensions.FromHex("2e576b"); } else { - panelColour = Color4Extensions.FromHex("3399cc"); - textColour = Color4.White; + panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc"); + textColour = TextColour ?? Color4.White; } this.TransformTo(nameof(SizeContainerLeftPadding), widthExtension ? -top_player_left_width_extension : 0, panel_transition_duration, Easing.OutElastic); diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs new file mode 100644 index 0000000000..c77b872786 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs @@ -0,0 +1,176 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public class MatchScoreDisplay : CompositeDrawable + { + private const float bar_height = 18; + private const float font_size = 50; + + public BindableInt Team1Score = new BindableInt(); + public BindableInt Team2Score = new BindableInt(); + + protected MatchScoreCounter Score1Text; + protected MatchScoreCounter Score2Text; + + private Drawable score1Bar; + private Drawable score2Bar; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new Box + { + Name = "top bar red (static)", + RelativeSizeAxes = Axes.X, + Height = bar_height / 4, + Width = 0.5f, + Colour = colours.TeamColourRed, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight + }, + new Box + { + Name = "top bar blue (static)", + RelativeSizeAxes = Axes.X, + Height = bar_height / 4, + Width = 0.5f, + Colour = colours.TeamColourBlue, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft + }, + score1Bar = new Box + { + Name = "top bar red", + RelativeSizeAxes = Axes.X, + Height = bar_height, + Width = 0, + Colour = colours.TeamColourRed, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight + }, + score2Bar = new Box + { + Name = "top bar blue", + RelativeSizeAxes = Axes.X, + Height = bar_height, + Width = 0, + Colour = colours.TeamColourBlue, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = font_size + bar_height, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + Score1Text = new MatchScoreCounter + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + Score2Text = new MatchScoreCounter + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Team1Score.BindValueChanged(_ => updateScores()); + Team2Score.BindValueChanged(_ => updateScores()); + } + + private void updateScores() + { + Score1Text.Current.Value = Team1Score.Value; + Score2Text.Current.Value = Team2Score.Value; + + int comparison = Team1Score.Value.CompareTo(Team2Score.Value); + + if (comparison > 0) + { + Score1Text.Winning = true; + Score2Text.Winning = false; + } + else if (comparison < 0) + { + Score1Text.Winning = false; + Score2Text.Winning = true; + } + else + { + Score1Text.Winning = false; + Score2Text.Winning = false; + } + + var winningBar = Team1Score.Value > Team2Score.Value ? score1Bar : score2Bar; + var losingBar = Team1Score.Value <= Team2Score.Value ? score1Bar : score2Bar; + + var diff = Math.Max(Team1Score.Value, Team2Score.Value) - Math.Min(Team1Score.Value, Team2Score.Value); + + losingBar.ResizeWidthTo(0, 400, Easing.OutQuint); + winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + Score1Text.X = -Math.Max(5 + Score1Text.DrawWidth / 2, score1Bar.DrawWidth); + Score2Text.X = Math.Max(5 + Score2Text.DrawWidth / 2, score2Bar.DrawWidth); + } + + protected class MatchScoreCounter : ScoreCounter + { + private OsuSpriteText displayedSpriteText; + + public MatchScoreCounter() + { + Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; + } + + public bool Winning + { + set => updateFont(value); + } + + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => + { + displayedSpriteText = s; + displayedSpriteText.Spacing = new Vector2(-6); + updateFont(false); + }); + + private void updateFont(bool winning) + => displayedSpriteText.Font = winning + ? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true) + : OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index a10c16fcd5..3f9258930e 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -7,12 +7,17 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; +using osu.Game.Users; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -21,6 +26,11 @@ namespace osu.Game.Screens.Play.HUD { protected readonly Dictionary UserScores = new Dictionary(); + public readonly SortedDictionary TeamScores = new SortedDictionary(); + + [Resolved] + private OsuColour colours { get; set; } + [Resolved] private SpectatorClient spectatorClient { get; set; } @@ -31,21 +41,24 @@ namespace osu.Game.Screens.Play.HUD private UserLookupCache userLookupCache { get; set; } private readonly ScoreProcessor scoreProcessor; - private readonly BindableList playingUsers; + private readonly MultiplayerRoomUser[] playingUsers; private Bindable scoringMode; + private readonly IBindableList playingUserIds = new BindableList(); + + private bool hasTeams => TeamScores.Count > 0; + /// /// Construct a new leaderboard. /// /// A score processor instance to handle score calculation for scores of users in the match. - /// IDs of all users in this match. - public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) + /// IDs of all users in this match. + public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) { // todo: this will eventually need to be created per user to support different mod combinations. this.scoreProcessor = scoreProcessor; - // todo: this will likely be passed in as User instances. - playingUsers = new BindableList(userIds); + playingUsers = users; } [BackgroundDependencyLoader] @@ -53,14 +66,17 @@ namespace osu.Game.Screens.Play.HUD { scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); - foreach (var userId in playingUsers) + foreach (var user in playingUsers) { - var trackedUser = CreateUserData(userId, scoreProcessor); + var trackedUser = CreateUserData(user, scoreProcessor); trackedUser.ScoringMode.BindTo(scoringMode); - UserScores[userId] = trackedUser; + UserScores[user.UserID] = trackedUser; + + if (trackedUser.Team is int team && !TeamScores.ContainsKey(team)) + TeamScores.Add(team, new BindableInt()); } - userLookupCache.GetUsersAsync(playingUsers.ToArray()).ContinueWith(users => Schedule(() => + userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray()).ContinueWith(users => Schedule(() => { foreach (var user in users.Result) { @@ -83,23 +99,50 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. - foreach (int userId in playingUsers) + foreach (var user in playingUsers) { - spectatorClient.WatchUser(userId); + spectatorClient.WatchUser(user.UserID); - if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(userId)) - usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId })); + if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(user.UserID)) + usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID })); } // bind here is to support players leaving the match. // new players are not supported. - playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); - playingUsers.BindCollectionChanged(usersChanged); + playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); + playingUserIds.BindCollectionChanged(usersChanged); // this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer). spectatorClient.OnNewFrames += handleIncomingFrames; } + protected virtual TrackedUserData CreateUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) => new TrackedUserData(user, scoreProcessor); + + protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked) + { + var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked); + + if (UserScores[user.Id].Team is int team) + { + leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f); + leaderboardScore.TextColour = Color4.White; + } + + return leaderboardScore; + } + + private Color4 getTeamColour(int team) + { + switch (team) + { + case 0: + return colours.TeamColourRed; + + default: + return colours.TeamColourBlue; + } + } + private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -124,9 +167,26 @@ namespace osu.Game.Screens.Play.HUD trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); trackedData.UpdateScore(); + + updateTotals(); }); - protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor); + private void updateTotals() + { + if (!hasTeams) + return; + + foreach (var scores in TeamScores.Values) scores.Value = 0; + + foreach (var u in UserScores.Values) + { + if (u.Team == null) + continue; + + if (TeamScores.TryGetValue(u.Team.Value, out var team)) + team.Value += (int)u.Score.Value; + } + } protected override void Dispose(bool isDisposing) { @@ -136,7 +196,7 @@ namespace osu.Game.Screens.Play.HUD { foreach (var user in playingUsers) { - spectatorClient.StopWatchingUser(user); + spectatorClient.StopWatchingUser(user.UserID); } spectatorClient.OnNewFrames -= handleIncomingFrames; @@ -145,7 +205,7 @@ namespace osu.Game.Screens.Play.HUD protected class TrackedUserData { - public readonly int UserId; + public readonly MultiplayerRoomUser User; public readonly ScoreProcessor ScoreProcessor; public readonly BindableDouble Score = new BindableDouble(); @@ -157,9 +217,11 @@ namespace osu.Game.Screens.Play.HUD public readonly List Frames = new List(); - public TrackedUserData(int userId, ScoreProcessor scoreProcessor) + public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID; + + public TrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) { - UserId = userId; + User = user; ScoreProcessor = scoreProcessor; ScoringMode.BindValueChanged(_ => UpdateScore()); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 270addc8e6..0e113a71bc 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -79,6 +79,8 @@ namespace osu.Game.Screens.Select protected BeatmapCarousel Carousel { get; private set; } + protected Container LeftArea { get; private set; } + private BeatmapInfoWedge beatmapInfoWedge; private DialogOverlay dialogOverlay; @@ -186,12 +188,12 @@ namespace osu.Game.Screens.Select { new Drawable[] { - new Container + LeftArea = new Container { Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - + Padding = new MarginPadding { Top = left_area_padding }, Children = new Drawable[] { beatmapInfoWedge = new BeatmapInfoWedge @@ -200,7 +202,6 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.X, Margin = new MarginPadding { - Top = left_area_padding, Right = left_area_padding, }, }, @@ -210,7 +211,7 @@ namespace osu.Game.Screens.Select Padding = new MarginPadding { Bottom = Footer.HEIGHT, - Top = WEDGE_HEIGHT + left_area_padding, + Top = WEDGE_HEIGHT, Left = left_area_padding, Right = left_area_padding * 2, }, diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index b6eafe496f..f0a68ea078 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -24,9 +24,9 @@ namespace osu.Game.Screens.Spectate /// public abstract class SpectatorScreen : OsuScreen { - protected IReadOnlyList UserIds => userIds; + protected IReadOnlyList Users => users; - private readonly List userIds = new List(); + private readonly List users = new List(); [Resolved] private BeatmapManager beatmaps { get; set; } @@ -50,17 +50,17 @@ namespace osu.Game.Screens.Spectate /// /// Creates a new . /// - /// The users to spectate. - protected SpectatorScreen(params int[] userIds) + /// The users to spectate. + protected SpectatorScreen(params int[] users) { - this.userIds.AddRange(userIds); + this.users.AddRange(users); } protected override void LoadComplete() { base.LoadComplete(); - userLookupCache.GetUsersAsync(userIds.ToArray()).ContinueWith(users => Schedule(() => + userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(users => Schedule(() => { foreach (var u in users.Result) { @@ -207,7 +207,7 @@ namespace osu.Game.Screens.Spectate { onUserStateRemoved(userId); - userIds.Remove(userId); + users.Remove(userId); userMap.Remove(userId); spectatorClient.StopWatchingUser(userId); diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 67280e4acd..b1cd1f86c0 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Screens.Play.HUD; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -83,10 +84,10 @@ namespace osu.Game.Skinning private static Color4 getFillColour(double hp) { if (hp < 0.2) - return Interpolation.ValueAt(0.2 - hp, Color4.Black, Color4.Red, 0, 0.2); + return LegacyUtils.InterpolateNonLinear(0.2 - hp, Color4.Black, Color4.Red, 0, 0.2); if (hp < epic_cutoff) - return Interpolation.ValueAt(0.5 - hp, Color4.White, Color4.Black, 0, 0.5); + return LegacyUtils.InterpolateNonLinear(0.5 - hp, Color4.White, Color4.Black, 0, 0.5); return Color4.White; } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 42345b7266..f259784170 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -36,24 +36,29 @@ namespace osu.Game.Tests.Visual.Multiplayer { if (joinRoom) { - var room = new Room - { - Name = { Value = "test name" }, - Playlist = - { - new PlaylistItem - { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } - } - } - }; + var room = CreateRoom(); RoomManager.CreateRoom(room); SelectedRoom.Value = room; } }); + protected virtual Room CreateRoom() + { + return new Room + { + Name = { Value = "test name" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + } + } + }; + } + public override void SetUpSteps() { base.SetUpSteps(); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 43aadf5acb..a28b4140ca 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -50,7 +50,16 @@ namespace osu.Game.Tests.Visual.Multiplayer public void Disconnect() => isConnected.Value = false; - public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user }); + public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false) + { + var roomUser = new MultiplayerRoomUser(user.Id) { User = user }; + ((IMultiplayerClient)this).UserJoined(roomUser); + + if (markAsPlaying) + PlayingUserIds.Add(user.Id); + + return roomUser; + } public void AddNullUser(int userId) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(userId)); @@ -165,6 +174,13 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); + public override Task KickUser(int userId) + { + Debug.Assert(Room != null); + + return ((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.UserID == userId)); + } + public override async Task ChangeSettings(MultiplayerRoomSettings settings) { Debug.Assert(Room != null); diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index ddbbfe501b..05ba509a73 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -46,6 +47,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay CacheAs(Filter); CacheAs(OngoingOperationTracker); CacheAs(AvailabilityTracker); + CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); } public object Get(Type type) diff --git a/osu.Game/Utils/LegacyUtils.cs b/osu.Game/Utils/LegacyUtils.cs new file mode 100644 index 0000000000..64306adf50 --- /dev/null +++ b/osu.Game/Utils/LegacyUtils.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; +using osuTK.Graphics; + +namespace osu.Game.Utils +{ + public static class LegacyUtils + { + public static Color4 InterpolateNonLinear(double time, Color4 startColour, Color4 endColour, double startTime, double endTime, Easing easing = Easing.None) + => InterpolateNonLinear(time, startColour, endColour, startTime, endTime, new DefaultEasingFunction(easing)); + + public static Colour4 InterpolateNonLinear(double time, Colour4 startColour, Colour4 endColour, double startTime, double endTime, Easing easing = Easing.None) + => InterpolateNonLinear(time, startColour, endColour, startTime, endTime, new DefaultEasingFunction(easing)); + + /// + /// Interpolates between two sRGB s directly in sRGB space. + /// + public static Color4 InterpolateNonLinear(double time, Color4 startColour, Color4 endColour, double startTime, double endTime, TEasing easing) where TEasing : IEasingFunction + { + if (startColour == endColour) + return startColour; + + double current = time - startTime; + double duration = endTime - startTime; + + if (duration == 0 || current == 0) + return startColour; + + float t = Math.Max(0, Math.Min(1, (float)easing.ApplyEasing(current / duration))); + + return new Color4( + startColour.R + t * (endColour.R - startColour.R), + startColour.G + t * (endColour.G - startColour.G), + startColour.B + t * (endColour.B - startColour.B), + startColour.A + t * (endColour.A - startColour.A)); + } + + /// + /// Interpolates between two sRGB s directly in sRGB space. + /// + public static Colour4 InterpolateNonLinear(double time, Colour4 startColour, Colour4 endColour, double startTime, double endTime, TEasing easing) where TEasing : IEasingFunction + { + if (startColour == endColour) + return startColour; + + double current = time - startTime; + double duration = endTime - startTime; + + if (duration == 0 || current == 0) + return startColour; + + float t = Math.Max(0, Math.Min(1, (float)easing.ApplyEasing(current / duration))); + + return new Colour4( + startColour.R + t * (endColour.R - startColour.R), + startColour.G + t * (endColour.G - startColour.G), + startColour.B + t * (endColour.B - startColour.B), + startColour.A + t * (endColour.A - startColour.A)); + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ec59b28ceb..d4dba9330f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,8 +36,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/osu.iOS.props b/osu.iOS.props index bcc4b5f254..7e514afe74 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,8 +70,8 @@ - - + + @@ -93,7 +93,7 @@ - +