1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 22:07:25 +08:00

Merge branch 'master' into update-framework

This commit is contained in:
Dean Herbert 2024-03-06 12:36:05 +08:00
commit 06c7483347
No known key found for this signature in database
16 changed files with 621 additions and 301 deletions

View File

@ -1,26 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using Moq;
using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Users; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
[TestFixture] [TestFixture]
public partial class TestSceneMedalOverlay : OsuTestScene public partial class TestSceneMedalOverlay : OsuManualInputManagerTestScene
{ {
public TestSceneMedalOverlay() private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private MedalOverlay overlay = null!;
[SetUpSteps]
public void SetUpSteps()
{ {
AddStep(@"display", () => var overlayManagerMock = new Mock<IOverlayManager>();
overlayManagerMock.Setup(mock => mock.OverlayActivationMode).Returns(overlayActivationMode);
AddStep("create overlay", () => Child = new DependencyProvidingContainer
{ {
LoadComponentAsync(new MedalOverlay(new Medal Child = overlay = new MedalOverlay(),
{ RelativeSizeAxes = Axes.Both,
Name = @"Animations", CachedDependencies =
InternalName = @"all-intro-doubletime", [
Description = @"More complex than you think.", (typeof(IOverlayManager), overlayManagerMock.Object)
}), Add); ]
}); });
} }
[Test]
public void TestBasicAward()
{
awardMedal(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
});
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddUntilStep("wait for load", () => this.ChildrenOfType<MedalAnimation>().Any());
AddRepeatStep("dismiss", () => InputManager.Key(Key.Escape), 2);
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
}
[Test]
public void TestMultipleMedalsInQuickSuccession()
{
awardMedal(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
});
awardMedal(new UserAchievementUnlock
{
Title = "S-Ranker",
Description = "Accuracy is really underrated.",
Slug = @"all-secret-rank-s"
});
awardMedal(new UserAchievementUnlock
{
Title = "500 Combo",
Description = "500 big ones! You're moving up in the world!",
Slug = @"osu-combo-500"
});
}
[Test]
public void TestDelayMedalDisplayUntilActivationModeAllowsIt()
{
AddStep("disable overlay activation", () => overlayActivationMode.Value = OverlayActivation.Disabled);
awardMedal(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
});
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("re-enable overlay activation", () => overlayActivationMode.Value = OverlayActivation.All);
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
}
private void awardMedal(UserAchievementUnlock unlock) => AddStep("award medal", () => dummyAPI.NotificationsClient.Receive(new SocketMessage
{
Event = @"new",
Data = JObject.FromObject(new NewPrivateNotificationEvent
{
Name = @"user_achievement_unlock",
Details = JObject.FromObject(unlock)
})
}));
} }
} }

View File

@ -6,6 +6,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Configuration;
@ -24,6 +25,8 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
@ -340,6 +343,28 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen); AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen);
} }
[Test]
public void TestShowMedalAtResults()
{
playToResults();
AddStep("award medal", () => ((DummyAPIAccess)API).NotificationsClient.Receive(new SocketMessage
{
Event = @"new",
Data = JObject.FromObject(new NewPrivateNotificationEvent
{
Name = @"user_achievement_unlock",
Details = JObject.FromObject(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
})
})
}));
AddUntilStep("medal overlay shown", () => Game.ChildrenOfType<MedalOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test] [Test]
public void TestRetryFromResults() public void TestRetryFromResults()
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -20,10 +18,10 @@ namespace osu.Game.Graphics.Containers
[Cached(typeof(IPreviewTrackOwner))] [Cached(typeof(IPreviewTrackOwner))]
public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction> public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction>
{ {
private Sample samplePopIn; protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
private Sample samplePopOut;
protected virtual string PopInSampleName => "UI/overlay-pop-in"; protected virtual string? PopInSampleName => @"UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out"; protected virtual string? PopOutSampleName => @"UI/overlay-pop-out";
protected virtual double PopInOutSampleBalance => 0; protected virtual double PopInOutSampleBalance => 0;
protected override bool BlockNonPositionalInput => true; protected override bool BlockNonPositionalInput => true;
@ -34,19 +32,23 @@ namespace osu.Game.Graphics.Containers
/// </summary> /// </summary>
protected virtual bool DimMainContent => true; protected virtual bool DimMainContent => true;
[Resolved(CanBeNull = true)] [Resolved]
private IOverlayManager overlayManager { get; set; } private IOverlayManager? overlayManager { get; set; }
[Resolved] [Resolved]
private PreviewTrackManager previewTrackManager { get; set; } private PreviewTrackManager previewTrackManager { get; set; } = null!;
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All); private Sample? samplePopIn;
private Sample? samplePopOut;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager? audio)
{ {
samplePopIn = audio.Samples.Get(PopInSampleName); if (!string.IsNullOrEmpty(PopInSampleName))
samplePopOut = audio.Samples.Get(PopOutSampleName); samplePopIn = audio?.Samples.Get(PopInSampleName);
if (!string.IsNullOrEmpty(PopOutSampleName))
samplePopOut = audio?.Samples.Get(PopOutSampleName);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -13,6 +13,8 @@ using osu.Framework.Logging;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.Notifications.WebSocket; using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Online.Notifications.WebSocket.Requests;
namespace osu.Game.Online.Chat namespace osu.Game.Online.Chat
{ {

View File

@ -8,7 +8,7 @@ using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
namespace osu.Game.Online.Notifications.WebSocket namespace osu.Game.Online.Notifications.WebSocket.Events
{ {
/// <summary> /// <summary>
/// A websocket message sent from the server when new messages arrive. /// A websocket message sent from the server when new messages arrive.

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace osu.Game.Online.Notifications.WebSocket.Events
{
/// <summary>
/// Reference: https://github.com/ppy/osu-web/blob/master/app/Events/NewPrivateNotificationEvent.php
/// </summary>
public class NewPrivateNotificationEvent
{
[JsonProperty("id")]
public ulong ID { get; set; }
[JsonProperty("name")]
public string Name { get; set; } = string.Empty;
[JsonProperty("created_at")]
public DateTimeOffset CreatedAt { get; set; }
[JsonProperty("object_type")]
public string ObjectType { get; set; } = string.Empty;
[JsonProperty("object_id")]
public ulong ObjectId { get; set; }
[JsonProperty("source_user_id")]
public uint SourceUserID { get; set; }
[JsonProperty("is_read")]
public bool IsRead { get; set; }
[JsonProperty("details")]
public JObject? Details { get; set; }
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
namespace osu.Game.Online.Notifications.WebSocket.Events
{
/// <summary>
/// Reference: https://github.com/ppy/osu-web/blob/master/app/Jobs/Notifications/UserAchievementUnlock.php
/// </summary>
public class UserAchievementUnlock
{
[JsonProperty("achievement_id")]
public uint AchievementId { get; set; }
[JsonProperty("achievement_mode")]
public ushort? AchievementMode { get; set; }
[JsonProperty("cover_url")]
public string CoverUrl { get; set; } = string.Empty;
[JsonProperty("slug")]
public string Slug { get; set; } = string.Empty;
[JsonProperty("title")]
public string Title { get; set; } = string.Empty;
[JsonProperty("description")]
public string Description { get; set; } = string.Empty;
[JsonProperty("user_id")]
public uint UserId { get; set; }
}
}

View File

@ -3,7 +3,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace osu.Game.Online.Notifications.WebSocket namespace osu.Game.Online.Notifications.WebSocket.Requests
{ {
/// <summary> /// <summary>
/// A websocket message notifying the server that the client no longer wants to receive chat messages. /// A websocket message notifying the server that the client no longer wants to receive chat messages.

View File

@ -3,7 +3,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace osu.Game.Online.Notifications.WebSocket namespace osu.Game.Online.Notifications.WebSocket.Requests
{ {
/// <summary> /// <summary>
/// A websocket message notifying the server that the client wants to receive chat messages. /// A websocket message notifying the server that the client wants to receive chat messages.

View File

@ -1083,6 +1083,7 @@ namespace osu.Game
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile<IDialogOverlay>(new DialogOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile<IDialogOverlay>(new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add);
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);

View File

@ -0,0 +1,312 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osuTK;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Game.Users;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Overlays.MedalSplash;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Shapes;
using System;
using System.Diagnostics;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
namespace osu.Game.Overlays
{
public partial class MedalAnimation : VisibilityContainer
{
public const float DISC_SIZE = 400;
private const float border_width = 5;
private readonly Medal medal;
private readonly Box background;
private readonly Container backgroundStrip, particleContainer;
private readonly BackgroundStrip leftStrip, rightStrip;
private readonly CircularContainer disc;
private readonly Sprite innerSpin, outerSpin;
private DrawableMedal? drawableMedal;
private Sample? getSample;
private readonly Container content;
public MedalAnimation(Medal medal)
{
this.medal = medal;
RelativeSizeAxes = Axes.Both;
Child = content = new Container
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(60),
},
outerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(DISC_SIZE + 500),
Alpha = 0f,
},
backgroundStrip = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = border_width,
Alpha = 0f,
Children = new[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Width = 0.5f,
Padding = new MarginPadding { Right = DISC_SIZE / 2 },
Children = new[]
{
leftStrip = new BackgroundStrip(0f, 1f)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
Width = 0.5f,
Padding = new MarginPadding { Left = DISC_SIZE / 2 },
Children = new[]
{
rightStrip = new BackgroundStrip(1f, 0f),
},
},
},
},
particleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
},
disc = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0f,
Masking = true,
AlwaysPresent = true,
BorderColour = Color4.White,
BorderThickness = border_width,
Size = new Vector2(DISC_SIZE),
Scale = new Vector2(0.8f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"05262f"),
},
new Triangles
{
RelativeSizeAxes = Axes.Both,
TriangleScale = 2,
ColourDark = Color4Extensions.FromHex(@"04222b"),
ColourLight = Color4Extensions.FromHex(@"052933"),
},
innerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1.05f),
Alpha = 0.25f,
},
},
},
}
};
Show();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, AudioManager audio)
{
getSample = audio.Samples.Get(@"MedalSplash/medal-get");
innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin");
disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 50,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(drawableMedal = new DrawableMedal(medal)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
}, loaded =>
{
disc.Add(loaded);
startAnimation();
});
}
protected override void Update()
{
base.Update();
particleContainer.Add(new MedalParticle(RNG.Next(0, 359)));
}
private const double initial_duration = 400;
private const double step_duration = 900;
private void startAnimation()
{
content.Show();
background.FlashColour(Color4.White.Opacity(0.25f), 400);
getSample?.Play();
innerSpin.Spin(20000, RotationDirection.Clockwise);
outerSpin.Spin(40000, RotationDirection.Clockwise);
using (BeginDelayedSequence(200))
{
disc.FadeIn(initial_duration)
.ScaleTo(1f, initial_duration * 2, Easing.OutElastic);
particleContainer.FadeIn(initial_duration);
outerSpin.FadeTo(0.1f, initial_duration * 2);
using (BeginDelayedSequence(initial_duration + 200))
{
backgroundStrip.FadeIn(step_duration);
leftStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
rightStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
Debug.Assert(drawableMedal != null);
this.Animate().Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Icon;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.MedalUnlocked;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Full;
});
}
}
}
protected override void PopIn()
{
this.FadeIn(200);
}
protected override void PopOut()
{
this.FadeOut(200);
}
public void Dismiss()
{
if (drawableMedal != null && drawableMedal.State != DisplayState.Full)
{
// if we haven't yet, play out the animation fully
drawableMedal.State = DisplayState.Full;
FinishTransforms(true);
return;
}
Hide();
Expire();
}
private partial class BackgroundStrip : Container
{
public BackgroundStrip(float start, float end)
{
RelativeSizeAxes = Axes.Both;
Width = 0f;
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(start), Color4.White.Opacity(end));
Masking = true;
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
}
};
}
}
private partial class MedalParticle : CircularContainer
{
private readonly float direction;
private Vector2 positionForOffset(float offset) => new Vector2((float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction)));
public MedalParticle(float direction)
{
this.direction = direction;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Position = positionForOffset(DISC_SIZE / 2);
Masking = true;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 5,
};
this.MoveTo(positionForOffset(DISC_SIZE / 2 + 200), 500);
this.FadeOut(500);
Expire();
}
}
}
}

View File

@ -1,324 +1,130 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable using System.Collections.Generic;
using System.Linq;
using osuTK; using osu.Framework.Allocation;
using osuTK.Graphics; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Game.Users;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Overlays.MedalSplash;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osuTK.Input;
using osu.Framework.Graphics.Shapes;
using System;
using osu.Framework.Graphics.Effects;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Users;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public partial class MedalOverlay : FocusedOverlayContainer public partial class MedalOverlay : OsuFocusedOverlayContainer
{ {
public const float DISC_SIZE = 400; protected override string? PopInSampleName => null;
protected override string? PopOutSampleName => null;
private const float border_width = 5; public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
private readonly Medal medal; protected override void PopIn() => this.FadeIn();
private readonly Box background;
private readonly Container backgroundStrip, particleContainer;
private readonly BackgroundStrip leftStrip, rightStrip;
private readonly CircularContainer disc;
private readonly Sprite innerSpin, outerSpin;
private DrawableMedal drawableMedal;
private Sample getSample; protected override void PopOut() => this.FadeOut();
private readonly Container content; private readonly Queue<MedalAnimation> queuedMedals = new Queue<MedalAnimation>();
public MedalOverlay(Medal medal) [Resolved]
{ private IAPIProvider api { get; set; } = null!;
this.medal = medal;
RelativeSizeAxes = Axes.Both;
Child = content = new Container private Container<Drawable> medalContainer = null!;
{ private MedalAnimation? lastAnimation;
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(60),
},
outerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(DISC_SIZE + 500),
Alpha = 0f,
},
backgroundStrip = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = border_width,
Alpha = 0f,
Children = new[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Width = 0.5f,
Padding = new MarginPadding { Right = DISC_SIZE / 2 },
Children = new[]
{
leftStrip = new BackgroundStrip(0f, 1f)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
Width = 0.5f,
Padding = new MarginPadding { Left = DISC_SIZE / 2 },
Children = new[]
{
rightStrip = new BackgroundStrip(1f, 0f),
},
},
},
},
particleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
},
disc = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0f,
Masking = true,
AlwaysPresent = true,
BorderColour = Color4.White,
BorderThickness = border_width,
Size = new Vector2(DISC_SIZE),
Scale = new Vector2(0.8f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"05262f"),
},
new Triangles
{
RelativeSizeAxes = Axes.Both,
TriangleScale = 2,
ColourDark = Color4Extensions.FromHex(@"04222b"),
ColourLight = Color4Extensions.FromHex(@"052933"),
},
innerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1.05f),
Alpha = 0.25f,
},
},
},
}
};
Show();
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, AudioManager audio) private void load()
{ {
getSample = audio.Samples.Get(@"MedalSplash/medal-get"); RelativeSizeAxes = Axes.Both;
innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin");
disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters api.NotificationsClient.MessageReceived += handleMedalMessages;
Add(medalContainer = new Container
{ {
Type = EdgeEffectType.Glow, RelativeSizeAxes = Axes.Both
Colour = colours.Blue.Opacity(0.5f), });
Radius = 50,
};
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
LoadComponentAsync(drawableMedal = new DrawableMedal(medal) OverlayActivationMode.BindValueChanged(val =>
{ {
Anchor = Anchor.TopCentre, if (val.NewValue == OverlayActivation.All && (queuedMedals.Any() || medalContainer.Any() || lastAnimation?.IsLoaded == false))
Origin = Anchor.TopCentre, Show();
RelativeSizeAxes = Axes.Both, }, true);
}, loaded => }
private void handleMedalMessages(SocketMessage obj)
{
if (obj.Event != @"new")
return;
var data = obj.Data?.ToObject<NewPrivateNotificationEvent>();
if (data == null || data.Name != @"user_achievement_unlock")
return;
var details = data.Details?.ToObject<UserAchievementUnlock>();
if (details == null)
return;
var medal = new Medal
{ {
disc.Add(loaded); Name = details.Title,
startAnimation(); InternalName = details.Slug,
}); Description = details.Description,
};
var medalAnimation = new MedalAnimation(medal);
queuedMedals.Enqueue(medalAnimation);
if (OverlayActivationMode.Value == OverlayActivation.All)
Scheduler.AddOnce(Show);
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
particleContainer.Add(new MedalParticle(RNG.Next(0, 359))); if (medalContainer.Any() || lastAnimation?.IsLoaded == false)
return;
if (!queuedMedals.TryDequeue(out lastAnimation))
{
Hide();
return;
}
LoadComponentAsync(lastAnimation, medalContainer.Add);
} }
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
dismiss(); lastAnimation?.Dismiss();
return true; return true;
} }
protected override void OnFocusLost(FocusLostEvent e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (e.CurrentState.Keyboard.Keys.IsPressed(Key.Escape)) dismiss(); if (e.Action == GlobalAction.Back)
}
private const double initial_duration = 400;
private const double step_duration = 900;
private void startAnimation()
{
content.Show();
background.FlashColour(Color4.White.Opacity(0.25f), 400);
getSample.Play();
innerSpin.Spin(20000, RotationDirection.Clockwise);
outerSpin.Spin(40000, RotationDirection.Clockwise);
using (BeginDelayedSequence(200))
{ {
disc.FadeIn(initial_duration) lastAnimation?.Dismiss();
.ScaleTo(1f, initial_duration * 2, Easing.OutElastic); return true;
particleContainer.FadeIn(initial_duration);
outerSpin.FadeTo(0.1f, initial_duration * 2);
using (BeginDelayedSequence(initial_duration + 200))
{
backgroundStrip.FadeIn(step_duration);
leftStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
rightStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
this.Animate().Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Icon;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.MedalUnlocked;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Full;
});
}
}
}
protected override void PopIn()
{
this.FadeIn(200);
}
protected override void PopOut()
{
this.FadeOut(200);
}
private void dismiss()
{
if (drawableMedal.State != DisplayState.Full)
{
// if we haven't yet, play out the animation fully
drawableMedal.State = DisplayState.Full;
FinishTransforms(true);
return;
} }
Hide(); return base.OnPressed(e);
Expire();
} }
private partial class BackgroundStrip : Container protected override void Dispose(bool isDisposing)
{ {
public BackgroundStrip(float start, float end) base.Dispose(isDisposing);
{
RelativeSizeAxes = Axes.Both;
Width = 0f;
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(start), Color4.White.Opacity(end));
Masking = true;
Children = new[] if (api.IsNotNull())
{ api.NotificationsClient.MessageReceived -= handleMedalMessages;
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
}
};
}
}
private partial class MedalParticle : CircularContainer
{
private readonly float direction;
private Vector2 positionForOffset(float offset) => new Vector2((float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction)));
public MedalParticle(float direction)
{
this.direction = direction;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Position = positionForOffset(DISC_SIZE / 2);
Masking = true;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 5,
};
this.MoveTo(positionForOffset(DISC_SIZE / 2 + 200), 500);
this.FadeOut(500);
Expire();
}
} }
} }
} }

View File

@ -38,7 +38,7 @@ namespace osu.Game.Overlays.MedalSplash
public DrawableMedal(Medal medal) public DrawableMedal(Medal medal)
{ {
this.medal = medal; this.medal = medal;
Position = new Vector2(0f, MedalOverlay.DISC_SIZE / 2); Position = new Vector2(0f, MedalAnimation.DISC_SIZE / 2);
FillFlowContainer infoFlow; FillFlowContainer infoFlow;
Children = new Drawable[] Children = new Drawable[]
@ -174,7 +174,7 @@ namespace osu.Game.Overlays.MedalSplash
.ScaleTo(1); .ScaleTo(1);
this.ScaleTo(scale_when_unlocked, duration, Easing.OutExpo); this.ScaleTo(scale_when_unlocked, duration, Easing.OutExpo);
this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 30, duration, Easing.OutExpo); this.MoveToY(MedalAnimation.DISC_SIZE / 2 - 30, duration, Easing.OutExpo);
unlocked.FadeInFromZero(duration); unlocked.FadeInFromZero(duration);
break; break;
@ -184,7 +184,7 @@ namespace osu.Game.Overlays.MedalSplash
.ScaleTo(1); .ScaleTo(1);
this.ScaleTo(scale_when_full, duration, Easing.OutExpo); this.ScaleTo(scale_when_full, duration, Easing.OutExpo);
this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 60, duration, Easing.OutExpo); this.MoveToY(MedalAnimation.DISC_SIZE / 2 - 60, duration, Easing.OutExpo);
unlocked.Show(); unlocked.Show();
name.FadeInFromZero(duration + 100); name.FadeInFromZero(duration + 100);
description.FadeInFromZero(duration * 2); description.FadeInFromZero(duration * 2);

View File

@ -11,3 +11,6 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("osu.Game.Tests.Dynamic")] [assembly: InternalsVisibleTo("osu.Game.Tests.Dynamic")]
[assembly: InternalsVisibleTo("osu.Game.Tests.iOS")] [assembly: InternalsVisibleTo("osu.Game.Tests.iOS")]
[assembly: InternalsVisibleTo("osu.Game.Tests.Android")] [assembly: InternalsVisibleTo("osu.Game.Tests.Android")]
// intended for Moq usage
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

View File

@ -31,6 +31,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// </summary> /// </summary>
public partial class AccuracyCircle : CompositeDrawable public partial class AccuracyCircle : CompositeDrawable
{ {
/// <summary>
/// The total duration of the animation.
/// </summary>
public const double TOTAL_DURATION = APPEAR_DURATION + ACCURACY_TRANSFORM_DELAY + ACCURACY_TRANSFORM_DURATION;
/// <summary> /// <summary>
/// Duration for the transforms causing this component to appear. /// Duration for the transforms causing this component to appear.
/// </summary> /// </summary>

View File

@ -25,8 +25,10 @@ using osu.Game.Input.Bindings;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Placeholders; using osu.Game.Online.Placeholders;
using osu.Game.Overlays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking.Expanded.Accuracy;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osuTK; using osuTK;
@ -41,6 +43,8 @@ namespace osu.Game.Screens.Ranking
public override bool? AllowGlobalTrackControl => true; public override bool? AllowGlobalTrackControl => true;
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>(); public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
[CanBeNull] [CanBeNull]
@ -172,6 +176,10 @@ namespace osu.Game.Screens.Ranking
bool shouldFlair = player != null && !Score.User.IsBot; bool shouldFlair = player != null && !Score.User.IsBot;
ScorePanelList.AddScore(Score, shouldFlair); ScorePanelList.AddScore(Score, shouldFlair);
// this is mostly for medal display.
// we don't want the medal animation to trample on the results screen animation, so we (ab)use `OverlayActivationMode`
// to give the results screen enough time to play the animation out before the medals can be shown.
Scheduler.AddDelayed(() => OverlayActivationMode.Value = OverlayActivation.All, shouldFlair ? AccuracyCircle.TOTAL_DURATION + 1000 : 0);
} }
if (AllowWatchingReplay) if (AllowWatchingReplay)