diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs index 181bc7341c20..49ee83dc177f 100644 --- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs +++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs @@ -44,7 +44,7 @@ protected override void LoadComplete() protected override void Update() { - TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value; + TimeRange.Value = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value; base.Update(); } } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index d9a03d1c3099..c73e37e39c63 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -9,9 +9,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Input; -using osu.Framework.Platform; using osu.Framework.Threading; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Input.Handlers; @@ -63,16 +61,11 @@ public partial class DrawableManiaRuleset : DrawableScrollingRuleset? mods = null) : base(ruleset, beatmap, mods) { @@ -119,7 +112,7 @@ private void load(ISkinSource source) TargetTimeRange = ComputeScrollTime(speed.NewValue); }); - TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); + TimeRange.Value = TargetTimeRange = ComputeScrollTime(configScrollSpeed.Value); Config.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout); mobileLayout.BindValueChanged(_ => updateMobileLayout(), true); @@ -179,9 +172,7 @@ private void updateTimeRange() // This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position. float scale = lengthToHitPosition / length_to_default_hit_position; - // we're intentionally using the game host's update clock here to decouple the time range tween from the gameplay clock (which can be arbitrarily paused, or even rewinding) - currentTimeRange = Interpolation.DampContinuously(currentTimeRange, TargetTimeRange, 50, gameHost.UpdateThread.Clock.ElapsedFrameTime); - TimeRange.Value = currentTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale; + TimeRange.Value = TargetTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale; } /// diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 68aaba6c68f0..f24767bd5422 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -472,7 +472,7 @@ public void TestScrollSpeedAdjustDuringGameplay() void checkScrollSpeed(double configValue, double gameplayValue) { AddUntilStep($"config value is {configValue}", () => getConfigManager().Get(ManiaRulesetSetting.ScrollSpeed), () => Is.EqualTo(configValue)); - AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType().Single().TargetTimeRange, + AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType().Single().ScrollingInfo.TimeRange.Value, () => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue))); } diff --git a/osu.Game.Tests/Visual/RankedPlay/TestSceneBubbleChatHistory.cs b/osu.Game.Tests/Visual/RankedPlay/TestSceneBubbleChatHistory.cs new file mode 100644 index 000000000000..8d06e9d75feb --- /dev/null +++ b/osu.Game.Tests/Visual/RankedPlay/TestSceneBubbleChatHistory.cs @@ -0,0 +1,52 @@ +// 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.Online.API.Requests.Responses; +using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components; + +namespace osu.Game.Tests.Visual.RankedPlay +{ + public partial class TestSceneBubbleChatHistory : OsuTestScene + { + private RankedPlayChatDisplay.BubbleChatHistory history = null!; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = history = new RankedPlayChatDisplay.BubbleChatHistory + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + Width = 300 + }; + }); + + [Test] + public void TestPostMessages() + { + int messageId = 1; + AddRepeatStep("post message", () => history.PostMessage(new APIUser { Id = 2 }, $"message {messageId++}"), 20); + } + + [Test] + public void TestCollapse() + { + AddStep("set expanded", () => history.Expand()); + + AddStep("post some messages", () => + { + for (int i = 0; i < 10; i++) + history.PostMessage(new APIUser { Id = 2 }, $"message {i}"); + }); + + AddWaitStep("wait a bit", 10); + AddStep("set collapsed", () => history.Collapse()); + AddWaitStep("wait a bit", 10); + AddStep("set expanded", () => history.Expand()); + AddWaitStep("wait a bit", 10); + AddStep("set collapsed", () => history.Collapse()); + } + } +} diff --git a/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayChat.cs b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayChat.cs new file mode 100644 index 000000000000..839d51249997 --- /dev/null +++ b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayChat.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 System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay; +using osu.Game.Tests.Visual.Multiplayer; + +namespace osu.Game.Tests.Visual.RankedPlay +{ + public partial class TestSceneRankedPlayChat : MultiplayerTestScene + { + private ChannelManager channelManager = null!; + private Channel testChannel = null!; + private int messageIdSequence; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var api = parent.Get(); + Add(channelManager = new ChannelManager(api)); + + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.Cache(channelManager); + + return dependencies; + } + + [SetUp] + public void SetUp() => Schedule(() => + { + messageIdSequence = 0; + testChannel = channelManager.JoinChannel(new Channel { Id = 1, Type = ChannelType.Multiplayer }); + }); + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => + { + var room = CreateDefaultRoom(MatchType.RankedPlay); + room.ChannelId = 1; + JoinRoom(room); + }); + + WaitForJoined(); + + AddStep("join other user", () => MultiplayerClient.AddUser(new APIUser { Id = 2 })); + + AddStep("load screen", () => LoadScreen(new RankedPlayScreen(MultiplayerClient.ClientRoom!))); + } + + [Test] + public void TestDiscardCardStage() + { + AddStep("set discard phase", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardDiscard).WaitSafely()); + + postLocalUserMessage("this is a message from the local user"); + postOpponentMessage("this is a message from the opponent"); + } + + [Test] + public void TestResultsStage() + { + AddStep("set results state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.Results, state => + { + int losingPlayer = state.Users.Keys.First(); + + foreach (var (id, userInfo) in state.Users) + { + if (id == losingPlayer) + { + userInfo.DamageInfo = new RankedPlayDamageInfo + { + RawDamage = 123_456, + Damage = 123_456, + OldLife = 500_000, + NewLife = 500_000 - 123_456, + }; + + userInfo.Life = 500_000 - 123_456; + } + else + { + userInfo.DamageInfo = new RankedPlayDamageInfo + { + RawDamage = 0, + Damage = 0, + OldLife = 1_000_000, + NewLife = 1_000_000, + }; + } + } + }).WaitSafely()); + } + + private void postLocalUserMessage(string content) + { + AddStep("add local user message", () => testChannel.AddNewMessages(new Message(messageIdSequence++) + { + Timestamp = DateTimeOffset.Now, + Sender = API.LocalUser.Value, + Content = content + })); + } + + private void postOpponentMessage(string content) + { + AddStep("add opponent message", () => testChannel.AddNewMessages(new Message(messageIdSequence++) + { + Timestamp = DateTimeOffset.Now, + Sender = new APIUser + { + Id = 2, + Username = "peppy" + }, + Content = content + })); + } + } +} diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 92b5b956b4fb..cfc034087a2b 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -29,6 +29,8 @@ public partial class BeatmapBackgroundWithStoryboard : BeatmapBackground public Action? StoryboardLoaded { get; set; } + public readonly BindableBool ShowStoryboard = new BindableBool(true); + [Resolved(CanBeNull = true)] private MusicController? musicController { get; set; } @@ -75,13 +77,11 @@ public void LoadStoryboard(bool async = true) void finishLoad(DrawableStoryboard s) { - if (Beatmap.Storyboard.ReplacesBackground) - Sprite.FadeOut(BackgroundScreen.TRANSITION_LENGTH, Easing.InQuint); - Storyboard.FadeInFromZero(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint); Storyboard.Add(s); StoryboardLoaded?.Invoke(); + updateStoryboardVisibility(); } } @@ -97,8 +97,7 @@ public void UnloadStoryboard() Storyboard.Clear(); drawableStoryboard = null; - - Sprite.Alpha = 1f; + updateStoryboardVisibility(); } protected override void LoadComplete() @@ -108,6 +107,17 @@ protected override void LoadComplete() musicController.TrackChanged += onTrackChanged; updateStoryboardClockSource(Beatmap); + + ShowStoryboard.BindValueChanged(_ => updateStoryboardVisibility(), true); + } + + private void updateStoryboardVisibility() + { + bool showStoryboard = drawableStoryboard != null && ShowStoryboard.Value; + bool showBackground = !showStoryboard || !Beatmap.Storyboard.ReplacesBackground; + + Storyboard.FadeTo(showStoryboard ? 1 : 0, BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint); + Sprite.FadeTo(showBackground ? 1 : 0, BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint); } private void onTrackChanged(WorkingBeatmap newBeatmap, TrackChangeDirection _) => updateStoryboardClockSource(newBeatmap); diff --git a/osu.Game/Overlays/AfToggleSection.cs b/osu.Game/Overlays/AfToggleSection.cs new file mode 100644 index 000000000000..b9aaa0f64e97 --- /dev/null +++ b/osu.Game/Overlays/AfToggleSection.cs @@ -0,0 +1,473 @@ +// 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 System.Linq; +using ManagedBass.Fx; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Framework.Threading; +using osu.Framework.Utils; +using osu.Game.Audio.Effects; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; +using osu.Game.Overlays.Settings; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays +{ + public partial class AfToggleSection : SettingsSection + { + public override LocalisableString Header => "Toggles"; + + public override Drawable CreateIcon() => new SpriteIcon + { + Icon = FontAwesome.Regular.Dizzy, + }; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new ToggleSettings(), + }; + } + + public partial class ToggleSettings : SettingsSubsection + { + [Resolved] + private OsuGameBase game { get; set; } = null!; + + [Resolved] + private GameHost host { get; set; } = null!; + + [Resolved] + private MusicController musicController { get; set; } = null!; + + protected override LocalisableString Header => "You wanted toggles, we give you toggles"; + + private List words = new List(); + + private List>> heroActions = new List>>(); + + private readonly List tasks = new List(); + + private GlobalActionContainer target = null!; + + private AutoWahParameters autoWahParameters = null!; + private ChorusParameters chorusParameters = null!; + private PhaserParameters phaserParameters = null!; + private Bindable balanceAdjustment = null!; + + private Sprite? dvdLogo; + private Vector2 dvdLogoVelocity = new Vector2(0.005f); + + private ScheduledDelegate? dvdLogoMovement; + + private AudioManager audio = null!; + + private double lastUpdate; + + [Resolved] + private TextureStore textures { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + autoWahParameters = new AutoWahParameters { fDryMix = 0.5f, fWetMix = 0.5f }; + chorusParameters = new ChorusParameters { fDryMix = 0.5f, fWetMix = 0.5f }; + phaserParameters = new PhaserParameters { fDryMix = 0.5f, fWetMix = 0.5f }; + balanceAdjustment = new Bindable(1); + + audio = game.Audio; + + target = game.ChildrenOfType().First(); + target.Anchor = Anchor.Centre; + target.Origin = Anchor.Centre; + + reset(); + } + + protected override void Update() + { + base.Update(); + + lastUpdate = Clock.CurrentTime; + + tasks.RemoveAll(t => t.Cancelled || t.Completed); + + if (tasks.Count > 40) + explode(); + } + + private void explode() + { + foreach (var t in tasks) + t.Cancel(); + + target.FadeColour(Color4.Black, 2000); + target.ScaleTo(1) + .ScaleTo(10, 4000); + + audio.Samples.Get("Gameplay/Argon/failsound-alt")?.Play(); + musicController.DuckMomentarily(2000, new DuckParameters + { + DuckDuration = 0, + DuckVolumeTo = 0, + DuckCutoffTo = AudioFilter.MAX_LOWPASS_CUTOFF, + RestoreDuration = 3000, + RestoreEasing = Easing.InQuint, + }); + + host.UpdateThread.Scheduler.AddDelayed(() => + { + foreach (var c in getCheckboxes()) + { + c.Current.Disabled = false; + c.Current.Value = false; + } + + target.FinishTransforms(); + target.ScaleTo(1) + .FadeColour(Color4.White, 2000, Easing.OutQuint); + + Schedule(() => + { + var settingsOverlay = game.ChildrenOfType().First(); + + settingsOverlay.SectionsContainer.ScrollTo(settingsOverlay.ChildrenOfType().Single()); + }); + + Clear(); + + AddRange(new Drawable[] + { + new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.TopCentre, + Text = "..on second thought, no more toggles for you." + }, + new SettingsButtonV2 + { + Text = "But please peppy", + Action = () => + { + if (Children.Count < 5 || RNG.NextSingle() > 0.08f) + { + audio.Samples.Get("Gameplay/sectionfail")?.Play(); + Add(new OsuTextFlowContainer(p => p.Font = OsuFont.Default.With(size: RNG.Next(10, 30))) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.TopCentre, + Text = "no" + }); + } + else + { + audio.Samples.Get("Gameplay/sectionpass")?.Play(); + reset(); + } + } + } + }); + }, 3000); + } + + private void reset() + { + Clear(); + + words = new List( + [ + "toggle", "toggleability", "inversion", "momentum", "uncertainty", "viscosity", "gravity", "shyness", "peer pressure", "amnesia", "rebellion", "telepathy", "commitment issues", + "democracy", "entropy", "recursion", "quantum superposition", "paperclips", "stubbornness", "existentialism", "nostalgia", "anxiety", "procrastination", "synesthesia", + "narcissism", + "chaos", + "determinism", "nihilism", "optimism", "pessimism", "apathy", "enthusiasm", "seven twenty seven", "lethargy", "hyperactivity", "confusion", "clarity", "ambiguity", "precision", + "vagueness", + "specificity", "generality", "locality", "annihilation", "nonlocality", "causality", "acausality", "reversibility", "irreversibility", "coherence", "decoherence", "entanglement", + "disentanglement", "superposition" + ]); + + heroActions = new List>> + { + val => target.FadeColour(OsuColour.Gray(val.NewValue ? 0.5f : 1), 2500, Easing.OutPow10), + val => + { + if (val.NewValue) + { + target.ScaleTo(new Vector2(RNG.NextBool() ? -1 : 1, RNG.NextBool() ? -1 : 1)) + .Delay(1500) + .ScaleTo(Vector2.One); + } + }, + val => + { + if (val.NewValue) + { + target.FadeColour(new Color4( + (byte)RNG.Next(200, 255), + (byte)RNG.Next(200, 255), + (byte)RNG.Next(200, 255), + 255), 1500, Easing.OutQuint); + } + else + target.FadeColour(Color4.White, 2000); + }, + val => + { + if (val.NewValue) + target.RotateTo(RNG.NextSingle(-2f, 2f), 4000, Easing.InOutSine); + else + target.RotateTo(0, 4000, Easing.InOutSine); + }, + val => target.ScaleTo(val.NewValue ? 1.05f : 1f, 3000, Easing.OutQuint), + val => + { + if (val.NewValue) + target.ScaleTo(1.02f, 2000, Easing.InOutSine).Then().ScaleTo(0.98f, 2000, Easing.InOutSine).Loop(); + else + target.ScaleTo(1f, 1000); + }, + val => + { + if (val.NewValue) + { + var colours = new[] { new Color4(255, 230, 230, 255), new Color4(230, 255, 230, 255), new Color4(230, 230, 255, 255) }; + target.FadeColour(colours[RNG.Next(0, colours.Length)], 300).Loop(); + } + else + target.FadeColour(Color4.White, 3000); + }, + val => + { + if (val.NewValue) + target.MoveToX(10, 500, Easing.OutQuad).Then().MoveToX(0, 500, Easing.InQuad); + }, + val => + { + if (val.NewValue) + target.MoveToY(-10, 500, Easing.OutQuad).Then().MoveToY(0, 500, Easing.InQuad); + }, + _ => game.ChildrenOfType().FirstOrDefault()?.ToggleVisibility(), + val => + { + if (val.NewValue) + audio.TrackMixer.AddEffect(chorusParameters); + else + audio.TrackMixer.RemoveEffect(chorusParameters); + }, + val => + { + if (val.NewValue) + audio.TrackMixer.AddEffect(autoWahParameters); + else + audio.TrackMixer.RemoveEffect(autoWahParameters); + }, + val => + { + if (val.NewValue) + audio.TrackMixer.AddEffect(phaserParameters); + else + audio.TrackMixer.RemoveEffect(phaserParameters); + }, + + // game blinks at you + val => + { + if (val.NewValue) + { + target.ScaleTo(new Vector2(1, 0), 50, Easing.OutQuint) + .Then().ScaleTo(Vector2.One, 50, Easing.OutQuint); + } + }, + + // messing with balance + val => + { + if (val.NewValue) + { + audio.AddAdjustment(AdjustableProperty.Balance, balanceAdjustment); + this.TransformBindableTo(balanceAdjustment, 0.3f, 5_000) + .Then().TransformBindableTo(balanceAdjustment, 0.7f, 10_000) + .Then().TransformBindableTo(balanceAdjustment, 0.5f, 5_000) + .Loop(); + } + else + { + audio.RemoveAdjustment(AdjustableProperty.Balance, balanceAdjustment); + } + }, + + // do a barrel roll! (press Z or R twice) + val => + { + if (val.NewValue) + { + target.RotateTo(360, 1000) + .Then().RotateTo(0); + } + }, + val => + { + if (val.NewValue) + { + if (dvdLogo == null) + { + target.Add(dvdLogo = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Depth = float.MinValue, + Size = new Vector2(30), + Texture = textures.Get(@"Menu/logo"), + RelativePositionAxes = Axes.Both, + Position = new Vector2(RNG.NextSingle(), RNG.NextSingle()), + Alpha = 0, + }); + } + + dvdLogo.Alpha = 1; + dvdLogoMovement = host.UpdateThread.Scheduler.AddDelayed(() => + { + var logoPosition = dvdLogo.ScreenSpaceDrawQuad; + var bounds = target.ScreenSpaceDrawQuad; + + if (logoPosition.TopLeft.X < bounds.TopLeft.X) + dvdLogoVelocity = new Vector2(0.005f, dvdLogoVelocity.Y); + else if (logoPosition.BottomRight.X > bounds.BottomRight.X) + dvdLogoVelocity = new Vector2(-0.005f, dvdLogoVelocity.Y); + + if (logoPosition.TopLeft.Y < bounds.TopLeft.Y) + dvdLogoVelocity = new Vector2(dvdLogoVelocity.X, 0.005f); + else if (logoPosition.BottomRight.Y > bounds.BottomRight.Y) + dvdLogoVelocity = new Vector2(dvdLogoVelocity.X, -0.005f); + + dvdLogo.Position += dvdLogoVelocity; + }, 15, true); + } + else + { + dvdLogo?.Hide(); + dvdLogoMovement?.Cancel(); + } + }, + val => + { + if (val.NewValue) + { + string[] samples = + [ + "Keyboard/key-caps.mp3", + "Keyboard/key-confirm.mp3", + "Keyboard/key-delete.mp3", + "Keyboard/key-movement.mp3", + "Keyboard/key-press-1.mp3", + "Keyboard/key-press-2.mp3", + "Keyboard/key-press-3.mp3", + "Keyboard/key-press-4.mp3", + "Keyboard/deselect.wav", + "Keyboard/key-invalid.wav", + "Keyboard/select-all.wav", + "Keyboard/select-char.wav", + "Keyboard/select-word.wav" + ]; + + var repeat = Scheduler.AddDelayed(() => + { + audio.Samples.Get(samples[RNG.Next(0, samples.Length)])?.Play(); + }, 100, true); + + Scheduler.AddDelayed(repeat.Cancel, 1000); + } + } + }; + + addNewToggle(); + } + + private void doRandomThing() + { + // avoid runaway if settings panel closes + if (Math.Abs(Clock.CurrentTime - lastUpdate) > 100) + return; + + tasks.Add(host.UpdateThread.Scheduler.AddDelayed(() => + { + var formCheckBoxes = getCheckboxes(); + + if (heroActions.Count > 0 && RNG.NextSingle() > 0.5f) + { + addNewToggle(); + return; + } + + foreach (var c in formCheckBoxes.Take(Math.Max(1, formCheckBoxes.Length / 8))) + { + if (RNG.NextSingle() > 0.5f && formCheckBoxes.Count(b => !b.Current.Disabled) > 5) + c.Current.Disabled = !c.Current.Disabled; + + if (c.Current.Disabled && RNG.NextSingle() > 0.4f) + c.Current.Disabled = false; + + if (RNG.NextSingle() > 0.3f) + c.TriggerClick(); + } + }, RNG.Next(400, 2000))); + } + + private FormCheckBox[] getCheckboxes() => + Children.OfType().Select(s => s.Control) + .OfType() + .OrderBy(_ => RNG.Next(-1, 1)) + .ToArray(); + + private void addNewToggle() + { + if (words.Count == 0) + return; + + FormCheckBox checkbox; + + string word = words[RNG.Next(0, words.Count)]; + words.Remove(word); + + Add(new SettingsItemV2(checkbox = new FormCheckBox + { + Caption = $"Toggle {word}" + }) + { + Depth = RNG.NextSingle(), + }); + + checkbox.Current.BindValueChanged(val => + { + if (val.NewValue) + doRandomThing(); + }); + + if (RNG.NextSingle() > 0.6f) + { + int i = RNG.Next(0, heroActions.Count); + checkbox.Current.BindValueChanged(heroActions[i]); + heroActions.RemoveAt(i); + } + } + } + } +} diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 3065a4d1bddf..e625afe94122 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -30,7 +31,7 @@ public partial class SettingsOverlay : SettingsPanel, INamedOverlayComponent protected override IEnumerable CreateSections() { - return new List + var sections = new List { // This list should be kept in sync with ScreenBehaviour. new GeneralSection(), @@ -45,6 +46,12 @@ protected override IEnumerable CreateSections() new MaintenanceSection(), new DebugSection() }; + + var today = DateTimeOffset.Now; + if (today.Month == 4 && today.Day == 1) + sections.Insert(9, new AfToggleSection()); + + return sections; } private readonly List subPanels = new List(); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c138808890ef..84b786c45a8d 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Testing; @@ -106,7 +107,7 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); [BackgroundDependencyLoader(true)] - private void load(OsuConfigManager config, [CanBeNull] Editor editor) + private void load(OsuConfigManager config, [CanBeNull] Editor editor, ReadableKeyCombinationProvider keyCombinationProvider) { autoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement); @@ -135,6 +136,9 @@ private void load(OsuConfigManager config, [CanBeNull] Editor editor) dependencies.CacheAs(Playfield); + string shiftDisplay = keyCombinationProvider.GetReadableString(new KeyCombination(InputKey.Shift)); + string altDisplay = keyCombinationProvider.GetReadableString(new KeyCombination(InputKey.Alt)); + InternalChildren = new[] { PlayfieldContentContainer = new Container @@ -180,7 +184,7 @@ private void load(OsuConfigManager config, [CanBeNull] Editor editor) Spacing = new Vector2(0, 5), }, }, - new EditorToolboxGroup("bank (Shift/Alt-Q~R)") + new EditorToolboxGroup($"bank ({shiftDisplay}/{altDisplay}-Q~R)") { Child = new FillFlowContainer { diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index 5479d512cb18..01269fdcbad6 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -43,7 +43,7 @@ public class ModAccuracyChallenge : ModFailCondition, IApplicableToScoreProcesso get { if (!MinimumAccuracy.IsDefault) - yield return ("Minimum accuracy", $"{MinimumAccuracy.Value:##%}"); + yield return ("Minimum accuracy", MinimumAccuracy.Value.ToLocalisableString(@"P1")); if (!AccuracyJudgeMode.IsDefault) yield return ("Accuracy mode", AccuracyJudgeMode.Value.ToLocalisableString()); diff --git a/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs b/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs index f44cbbea819d..e0b71151c60c 100644 --- a/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs +++ b/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs @@ -25,7 +25,7 @@ public partial class EditorBackgroundScreen : BackgroundScreen private Bindable dimLevel = null!; private Bindable showStoryboard = null!; - private BeatmapBackgroundWithStoryboard? background; + private BeatmapBackgroundWithStoryboard background = null!; private readonly Container content; private readonly Box blackBox; @@ -65,7 +65,6 @@ private void load(OsuConfigManager config) showStoryboard = config.GetBindable(OsuSetting.EditorShowStoryboard); content.Child = createContent(); - updateState(withAnimation: false); } protected override void LoadComplete() @@ -73,9 +72,6 @@ protected override void LoadComplete() base.LoadComplete(); dimLevel.BindValueChanged(_ => dimContainer.FadeColour(OsuColour.Gray(1 - dimLevel.Value), 500, Easing.OutQuint), true); - showStoryboard.BindValueChanged(_ => updateState()); - - updateState(withAnimation: false); } public override void OnEntering(ScreenTransitionEvent e) @@ -87,7 +83,7 @@ public override void OnEntering(ScreenTransitionEvent e) public override bool OnExiting(ScreenExitEvent e) { // The storyboard will do weird things with clock time changing on exit, so let's just hide it instead. - background?.UnloadStoryboard(); + background.UnloadStoryboard(); return base.OnExiting(e); } @@ -95,27 +91,15 @@ public override bool OnExiting(ScreenExitEvent e) public void RefreshBackgroundAsync() { cancellationTokenSource?.Cancel(); - LoadComponentAsync(createContent(), loaded => - { - content.Child = loaded; - updateState(withAnimation: false); - }, (cancellationTokenSource = new CancellationTokenSource()).Token); + LoadComponentAsync(createContent(), d => content.Child = d, (cancellationTokenSource = new CancellationTokenSource()).Token); } private Drawable createContent() => background = new BeatmapBackgroundWithStoryboard(beatmap.Value) { RelativeSizeAxes = Axes.Both, - StoryboardLoaded = () => updateState(withAnimation: false) + ShowStoryboard = { BindTarget = showStoryboard }, }; - private void updateState(bool withAnimation = true) - { - background?.Storyboard.FadeTo(showStoryboard.Value ? 1 : 0, withAnimation ? 500 : 0, Easing.OutQuint); - // if the storyboard is disabled, in some cases (e.g. involving `StoryboardReplacesBackground`) - // we still need to show the background sprite, because if we don't, then there will be no background shown at all - background?.Sprite.FadeTo(showStoryboard.Value ? 0 : 1, withAnimation ? 500 : 0, Easing.OutQuint); - } - public override bool Equals(BackgroundScreen? other) { if (other is not EditorBackgroundScreen otherBeatmapBackground) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayChatDisplay.cs new file mode 100644 index 000000000000..94cef92fc896 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayChatDisplay.cs @@ -0,0 +1,401 @@ +// 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 System.Linq; +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.Transforms; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osu.Game.Input; +using osu.Game.Input.Bindings; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Online.Multiplayer; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Users.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components +{ + public partial class RankedPlayChatDisplay : CompositeDrawable, IKeyBindingHandler + { + [Resolved] + private ChannelManager? channelManager { get; set; } + + [Resolved] + private RealmKeyBindingStore keyBindingStore { get; set; } = null!; + + private readonly MultiplayerRoom room; + + private ChatTextBox textbox = null!; + private BubbleChatHistory chatHistory = null!; + + private Channel? channel; + + private const float width = 320; + + public RankedPlayChatDisplay(MultiplayerRoom room) + { + Size = new Vector2(width, 160); + this.room = room; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + textbox = new ChatTextBox + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + RelativeSizeAxes = Axes.X, + Height = 30, + CornerRadius = 10, + ReleaseFocusOnCommit = true, + HoldFocus = false, + Focus = onFocusGained, + FocusLost = onFocusLost + }, + new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Bottom = 35 }, + Child = chatHistory = new BubbleChatHistory + { + RelativeSizeAxes = Axes.X + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + resetPlaceholderText(); + textbox.OnCommit += onCommit; + + channel = channelManager?.JoinChannel(new Channel { Id = room.ChannelID, Type = ChannelType.Multiplayer, Name = $"#lazermp_{room.RoomID}" }); + if (channel != null) + channel.NewMessagesArrived += onNewMessagesArrived; + } + + private void onCommit(TextBox sender, bool newText) + { + string text = textbox.Text.Trim(); + + if (string.IsNullOrWhiteSpace(text)) + return; + + if (text[0] == '/') + channelManager?.PostCommand(text[1..], channel); + else + channelManager?.PostMessage(text, target: channel); + + textbox.Text = string.Empty; + } + + private void onNewMessagesArrived(IEnumerable bundle) + { + foreach (var message in bundle) + chatHistory.PostMessage(message.Sender, message.Content); + } + + private void onFocusGained() + { + textbox.PlaceholderText = ChatStrings.InputPlaceholder; + chatHistory.Expand(); + } + + private void onFocusLost() + { + resetPlaceholderText(); + chatHistory.Collapse(); + } + + private void resetPlaceholderText() + { + textbox.PlaceholderText = Localisation.ChatStrings.InGameInputPlaceholder(keyBindingStore.GetBindingsStringFor(GlobalAction.ToggleChatFocus)); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.Back: + if (textbox.HasFocus) + { + Schedule(() => textbox.KillFocus()); + return true; + } + + break; + + case GlobalAction.ToggleChatFocus: + if (!textbox.HasFocus) + { + Schedule(() => textbox.TakeFocus()); + return true; + } + + break; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + public void Appear() + { + FinishTransforms(); + + this.MoveToY(150f) + .FadeOut() + .MoveToY(0f, 240, Easing.OutCubic) + .FadeIn(240, Easing.OutCubic); + } + + public TransformSequence Disappear() + { + FinishTransforms(); + + return this.FadeOut(240, Easing.InOutCubic) + .MoveToY(150f, 240, Easing.InOutCubic); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (channel != null) + channel.NewMessagesArrived -= onNewMessagesArrived; + } + + private partial class ChatTextBox : StandAloneChatDisplay.ChatTextBox + { + protected override void LoadComplete() + { + base.LoadComplete(); + + BackgroundFocused = Colour4.FromHex("222228"); + BackgroundUnfocused = BackgroundFocused.Opacity(0.7f); + Placeholder.Colour = Color4.White; + } + } + + public partial class BubbleChatHistory : CompositeDrawable + { + /// + /// Maximum number of recent messages to keep. + /// + private const int max_length = 10; + + /// + /// The vertical spacing between messages. + /// + private const float message_spacing = 2; + + /// + /// When in a collapsed state, the time before a newly-posted message disappears from view. + /// + private const float time_before_disappear = 5000; + + private readonly Container messageContainer; + + private bool expanded; + + public BubbleChatHistory() + { + AutoSizeAxes = Axes.Y; + + InternalChild = messageContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + } + + /// + /// Collapses the display such that only new messages are temporarily shown. + /// + public void Collapse() + { + expanded = false; + + foreach (var child in messageContainer.Reverse().Take(max_length).Reverse()) + { + // Normally we wait an amount of time to preview messages before they disappear. + // When quickly toggling expanded and collapsed states, we want to still consider this preview window. + double previewTimeRemaining = Math.Max(0, time_before_disappear - (Time.Current - child.PostTime)); + + using (BeginDelayedSequence(previewTimeRemaining)) + child.Hide(); + } + } + + /// + /// Expands the display such that all historical messages are shown. + /// + public void Expand() + { + expanded = true; + + foreach (var child in messageContainer.Reverse().Take(max_length)) + child.Show(); + } + + /// + /// Posts a message. + /// + /// The user that posted the message. + /// The message content. + public void PostMessage(APIUser user, string content) + { + var newMessage = new MessageBubble(user, content) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + PostTime = Time.Current + }; + + messageContainer.Add(newMessage); + + float offset = 0; + + ScheduleAfterChildren(() => + { + // Layout bubbles, pushing all others upwards to make room for the new one. + foreach (var child in messageContainer.Reverse()) + { + child.MoveToY(-offset, 400, Easing.OutPow10); + offset += child.DrawHeight + message_spacing; + } + }); + + // Hide any overflowing message. + // Only need to handle the most-recently-overflowing one, because others would be handled in prior calls to this method. + if (messageContainer.Count > max_length) + { + var lastBubble = messageContainer[messageContainer.Count - max_length - 1]; + + lastBubble.Hide(); + lastBubble.Expire(); + } + + newMessage.Show(); + + // If not in the expanded state, hide the new message after a short while. + if (!expanded) + { + using (BeginDelayedSequence(time_before_disappear)) + newMessage.Hide(); + } + } + + private partial class MessageBubble : CompositeDrawable + { + private readonly APIUser user; + private readonly string message; + + /// + /// The time at which this message was posted. + /// + public required double PostTime { get; init; } + + public MessageBubble(APIUser user, string message) + { + this.user = user; + this.message = message; + AutoSizeAxes = Axes.Both; + + Scale = Vector2.Zero; + Alpha = 0; + } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + CornerRadius = 8, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = api.LocalUser.Value.Id == user.Id + ? RankedPlayColourScheme.Blue.PrimaryDarkest + : RankedPlayColourScheme.Red.PrimaryDarkest, + }, + new Container + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(8), + Children = new Drawable[] + { + new CircularContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(16), + Masking = true, + Child = new UpdateableAvatar(user) + { + DelayedLoad = false, + RelativeSizeAxes = Axes.Both + } + }, + new OsuTextFlowContainer + { + X = 20, + MaximumSize = new Vector2(width * 1.5f, 0), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Text = message, + } + } + } + } + } + }; + } + + public override void Show() + { + this.ScaleTo(1, 400, Easing.OutElasticQuarter) + .FadeIn(200, Easing.OutQuint); + } + + public override void Hide() + { + this.FadeOut(200, Easing.OutQuint); + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs index ee4556e1b824..d8571b9db5b2 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs @@ -24,7 +24,6 @@ using osu.Game.Overlays.Volume; using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay; using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card; @@ -71,7 +70,7 @@ public partial class RankedPlayScreen : OsuScreen, IPreviewTrackOwner, IHandlePr private readonly MultiplayerRoom room; private readonly Container screenContainer; - private readonly MatchmakingChatDisplay chat; + private readonly RankedPlayChatDisplay chat; private IBindable stage = null!; @@ -110,11 +109,10 @@ public RankedPlayScreen(MultiplayerRoom room) { RelativeSizeAxes = Axes.Both, }, - chat = new MatchmakingChatDisplay(new Room(room)) + chat = new RankedPlayChatDisplay(room) { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Size = new Vector2(320, 160), Margin = new MarginPadding { Bottom = 10, @@ -173,14 +171,6 @@ protected override void LoadComplete() }, ]); - cornerPieceVisibility.BindValueChanged(e => - { - if (e.NewValue == Visibility.Visible) - chat.Appear(); - else - chat.Disappear(); - }); - stage.BindValueChanged(e => onStageChanged(e.NewValue)); } @@ -232,9 +222,12 @@ private void onLoadRequested() => Scheduler.Add(() => private void onStageChanged(RankedPlayStage stage) { + chat.Appear(); + switch (stage) { case RankedPlayStage.RoundWarmup when matchInfo.CurrentRound == 1: + chat.Disappear(); ShowScreen(new IntroScreen()); break; @@ -284,6 +277,7 @@ private void onStageChanged(RankedPlayStage stage) public override void OnSuspending(ScreenTransitionEvent e) { + chat.Disappear(); previewTrackManager.StopAnyPlaying(this); base.OnSuspending(e); @@ -331,6 +325,7 @@ public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); + chat.Appear(); if (e.Last is not MultiplayerPlayerLoader playerLoader) return; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 64ebaab783a2..eed9d41f772f 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -260,7 +260,8 @@ protected override async Task JoinRoomInternal(long roomId, str }, Playlist = ServerAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)).ToList(), Users = { localUser }, - Host = localUser + Host = localUser, + ChannelID = ServerAPIRoom.ChannelId }; await changeMatchType(ServerRoom.Settings.MatchType).ConfigureAwait(false); diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 21153ecfc339..5210e8235aca 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -43,7 +43,9 @@ public APIUser? User set => base.EdgeEffect = value; } - protected override double LoadDelay => 200; + public bool DelayedLoad = true; + + protected override double LoadDelay => DelayedLoad ? 200 : 0; private readonly bool isInteractive; private readonly bool showGuestOnNull; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4ca602fcf9fb..8850de3a7cf4 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -40,7 +40,7 @@ - +