diff --git a/osu.Desktop.VisualTests/Tests/TestCasePauseOverlay.cs b/osu.Desktop.VisualTests/Tests/TestCasePauseOverlay.cs new file mode 100644 index 0000000000..dbcd1b93af --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCasePauseOverlay.cs @@ -0,0 +1,44 @@ +using System; +using OpenTK.Graphics; +using osu.Framework.Logging; +using osu.Framework.Graphics; +using osu.Game.Overlays.Pause; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Colour; +using osu.Framework.GameModes.Testing; +using osu.Framework.Graphics.UserInterface; + +namespace osu.Desktop.VisualTests.Tests +{ + class TestCasePauseOverlay : TestCase + { + public override string Name => @"PauseOverlay"; + + public override string Description => @"Tests the pause overlay"; + + private PauseOverlay pauseOverlay; + private int retryCount; + + public override void Reset() + { + base.Reset(); + + Add(pauseOverlay = new PauseOverlay + { + Depth = -1, + OnResume = () => Logger.Log(@"Resume"), + OnRetry = () => Logger.Log(@"Retry"), + OnQuit = () => Logger.Log(@"Quit") + }); + AddButton("Pause", pauseOverlay.Show); + AddButton("Add Retry", delegate + { + retryCount++; + pauseOverlay.Retries = retryCount; + }); + + retryCount = 0; + } + } +} diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index 70dd0fee46..cd24771967 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -186,6 +186,7 @@ + diff --git a/osu.Game/Overlays/Pause/PauseButton.cs b/osu.Game/Overlays/Pause/PauseButton.cs new file mode 100644 index 0000000000..ed56cc7b85 --- /dev/null +++ b/osu.Game/Overlays/Pause/PauseButton.cs @@ -0,0 +1,233 @@ +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transformations; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Containers; +using osu.Framework.Audio.Sample; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; + +namespace osu.Game.Overlays.Pause +{ + public class PauseButton : ClickableContainer + { + private const float hoverWidth = 0.9f; + private const float hoverDuration = 500; + private const float glowFadeDuration = 250; + private const float clickDuration = 200; + + private Color4 backgroundColour = OsuColour.Gray(34); + + private Color4 buttonColour; + public Color4 ButtonColour + { + get + { + return buttonColour; + } + set + { + buttonColour = value; + updateGlow(); + if (colourContainer == null) return; + colourContainer.Colour = ButtonColour; + } + } + + private string text; + public string Text + { + get + { + return text; + } + set + { + text = value; + if (spriteText == null) return; + spriteText.Text = Text; + } + } + + public AudioSample SampleClick, SampleHover; + + private Container backgroundContainer, colourContainer, glowContainer; + private Box leftGlow, centerGlow, rightGlow; + private SpriteText spriteText; + + private bool didClick; // Used for making sure that the OnMouseDown animation can call instead of OnHoverLost's when clicking + + public override bool Contains(Vector2 screenSpacePos) => backgroundContainer.Contains(screenSpacePos); + + protected override bool OnMouseDown(Framework.Input.InputState state, MouseDownEventArgs args) + { + didClick = true; + colourContainer.ResizeTo(new Vector2(1.5f, 1f), clickDuration, EasingTypes.In); + flash(); + SampleClick?.Play(); + Action?.Invoke(); + + Delay(clickDuration); + Schedule(delegate { + colourContainer.ResizeTo(new Vector2(0.8f, 1f), 0, EasingTypes.None); + spriteText.Spacing = Vector2.Zero; + glowContainer.FadeOut(); + }); + + return true; + } + + protected override bool OnClick(Framework.Input.InputState state) => false; + + protected override bool OnHover(Framework.Input.InputState state) + { + colourContainer.ResizeTo(new Vector2(hoverWidth, 1f), hoverDuration, EasingTypes.OutElastic); + spriteText.TransformSpacingTo(new Vector2(3f, 0f), hoverDuration, EasingTypes.OutElastic); + glowContainer.FadeIn(glowFadeDuration, EasingTypes.Out); + SampleHover?.Play(); + return true; + } + + protected override void OnHoverLost(Framework.Input.InputState state) + { + if (!didClick) + { + colourContainer.ResizeTo(new Vector2(0.8f, 1f), hoverDuration, EasingTypes.OutElastic); + spriteText.TransformSpacingTo(Vector2.Zero, hoverDuration, EasingTypes.OutElastic); + glowContainer.FadeOut(glowFadeDuration, EasingTypes.Out); + } + + didClick = false; + } + + private void flash() + { + var flash = new Box + { + RelativeSizeAxes = Axes.Both + }; + + colourContainer.Add(flash); + + flash.Colour = ButtonColour; + flash.BlendingMode = BlendingMode.Additive; + flash.Alpha = 0.3f; + flash.FadeOutFromOne(clickDuration); + flash.Expire(); + } + + private void updateGlow() + { + leftGlow.ColourInfo = ColourInfo.GradientHorizontal(new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f), ButtonColour); + centerGlow.Colour = ButtonColour; + rightGlow.ColourInfo = ColourInfo.GradientHorizontal(ButtonColour, new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f)); + } + + public PauseButton() + { + Children = new Drawable[] + { + backgroundContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Width = 1f, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + } + } + }, + glowContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Width = 1f, + Alpha = 0f, + Children = new Drawable[] + { + leftGlow = new Box + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Width = 0.125f + }, + centerGlow = new Box + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Width = 0.75f + }, + rightGlow = new Box + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + Width = 0.125f + } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Masking = true, + Children = new Drawable[] + { + colourContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Width = 0.8f, + Masking = true, + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.2f), + Radius = 5 + }, + Colour = ButtonColour, + Shear = new Vector2(0.2f, 0), + Children = new Drawable[] + { + new Box + { + EdgeSmoothness = new Vector2(2, 0), + RelativeSizeAxes = Axes.Both + }, + new Triangles + { + BlendingMode = BlendingMode.Additive, + RelativeSizeAxes = Axes.Both, + TriangleScale = 4, + Alpha = 0.05f, + Shear = new Vector2(-0.2f, 0) + } + } + } + } + }, + spriteText = new SpriteText + { + Text = Text, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + TextSize = 28, + Font = "Exo2.0-Bold", + Shadow = true, + ShadowColour = new Color4(0, 0, 0, 0.1f), + Colour = Color4.White + } + }; + + updateGlow(); + } + } +} diff --git a/osu.Game/Overlays/Pause/PauseOverlay.cs b/osu.Game/Overlays/Pause/PauseOverlay.cs new file mode 100644 index 0000000000..733133f005 --- /dev/null +++ b/osu.Game/Overlays/Pause/PauseOverlay.cs @@ -0,0 +1,212 @@ +using System; +using OpenTK; +using OpenTK.Input; +using OpenTK.Graphics; +using osu.Game.Graphics; +using osu.Framework.Input; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transformations; +using System.Threading.Tasks; + +namespace osu.Game.Overlays.Pause +{ + public class PauseOverlay : OverlayContainer + { + private const int transition_duration = 200; + private const int button_height = 70; + private const float background_alpha = 0.75f; + + public Action OnResume; + public Action OnRetry; + public Action OnQuit; + + public int Retries + { + set + { + if (retryCounterContainer != null) + { + // "You've retried 1,065 times in this session" + // "You've retried 1 time in this session" + + retryCounterContainer.Children = new Drawable[] + { + new SpriteText + { + Text = "You've retried ", + Shadow = true, + ShadowColour = new Color4(0, 0, 0, 0.25f), + TextSize = 18 + }, + new SpriteText + { + Text = String.Format("{0:n0}", value), + Font = @"Exo2.0-Bold", + Shadow = true, + ShadowColour = new Color4(0, 0, 0, 0.25f), + TextSize = 18 + }, + new SpriteText + { + Text = $" time{((value == 1) ? "" : "s")} in this session", + Shadow = true, + ShadowColour = new Color4(0, 0, 0, 0.25f), + TextSize = 18 + } + }; + } + } + } + + private FlowContainer retryCounterContainer; + + public override bool Contains(Vector2 screenSpacePos) => true; + public override bool HandleInput => State == Visibility.Visible; + + protected override void PopIn() => FadeIn(transition_duration, EasingTypes.In); + protected override void PopOut() => FadeOut(transition_duration, EasingTypes.In); + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Key == Key.Escape) + { + if (State == Visibility.Hidden) return false; + resume(); + return true; + } + return base.OnKeyDown(state, args); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = background_alpha, + }, + new FlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FlowDirection.VerticalOnly, + Spacing = new Vector2(0f, 50f), + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Children = new Drawable[] + { + new FlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FlowDirection.VerticalOnly, + Spacing = new Vector2(0f, 20f), + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Children = new Drawable[] + { + new SpriteText + { + Text = @"paused", + Font = @"Exo2.0-Medium", + Spacing = new Vector2(5, 0), + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + TextSize = 30, + Colour = colours.Yellow, + Shadow = true, + ShadowColour = new Color4(0, 0, 0, 0.25f) + }, + new SpriteText + { + Text = @"you're not going to do what i think you're going to do, are ya?", + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Shadow = true, + ShadowColour = new Color4(0, 0, 0, 0.25f) + } + } + }, + new FlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.6f), + Radius = 50 + }, + Children = new Drawable[] + { + new ResumeButton + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Height = button_height, + Action = resume + }, + new RetryButton + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Height = button_height, + Action = delegate + { + Hide(); + OnRetry?.Invoke(); + } + }, + new QuitButton + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Height = button_height, + Action = delegate + { + Hide(); + OnQuit?.Invoke(); + } + } + } + }, + retryCounterContainer = new FlowContainer + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre + } + } + }, + new PauseProgressBar + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Width = 1f + } + }; + + Retries = 0; + } + + private void resume() + { + Hide(); + OnResume?.Invoke(); + } + + public PauseOverlay() + { + RelativeSizeAxes = Axes.Both; + } + } +} diff --git a/osu.Game/Overlays/Pause/PauseProgressBar.cs b/osu.Game/Overlays/Pause/PauseProgressBar.cs new file mode 100644 index 0000000000..28824cb7ea --- /dev/null +++ b/osu.Game/Overlays/Pause/PauseProgressBar.cs @@ -0,0 +1,147 @@ +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Framework.Graphics.Primitives; + +namespace osu.Game.Overlays.Pause +{ + public class PauseProgressBar : Container + { + private Color4 fillColour = new Color4(221, 255, 255, 255); + private Color4 glowColour = new Color4(221, 255, 255, 150); + + private Container fill; + private WorkingBeatmap current; + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame) + { + current = osuGame.Beatmap.Value; + } + + protected override void Update() + { + base.Update(); + + if (current?.TrackLoaded ?? false) + { + fill.Width = (float)(current.Track.CurrentTime / current.Track.Length); + } + } + + public PauseProgressBar() + { + RelativeSizeAxes = Axes.X; + Height = 60; + + Children = new Drawable[] + { + new PauseProgressGraph + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Height = 35, + Margin = new MarginPadding + { + Bottom = 5 + } + }, + new Container + { + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + RelativeSizeAxes = Axes.X, + Height = 5, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f + } + } + }, + fill = new Container + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Width = 0, + Height = 60, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Masking = true, + Children = new Drawable[] + { + new Container + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = 5, + Masking = true, + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Glow, + Colour = glowColour, + Radius = 5 + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = fillColour + } + } + } + } + }, + new Container + { + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + Width = 2, + Height = 35, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White + }, + new Container + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.TopCentre, + Width = 14, + Height = 25, + CornerRadius = 5, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White + } + } + } + } + } + } + } + }; + } + } +} diff --git a/osu.Game/Overlays/Pause/PauseProgressGraph.cs b/osu.Game/Overlays/Pause/PauseProgressGraph.cs new file mode 100644 index 0000000000..fab9f37923 --- /dev/null +++ b/osu.Game/Overlays/Pause/PauseProgressGraph.cs @@ -0,0 +1,9 @@ +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Overlays.Pause +{ + public class PauseProgressGraph : Container + { + // TODO: Implement the pause progress graph + } +} diff --git a/osu.Game/Overlays/Pause/QuitButton.cs b/osu.Game/Overlays/Pause/QuitButton.cs new file mode 100644 index 0000000000..05d6171d2e --- /dev/null +++ b/osu.Game/Overlays/Pause/QuitButton.cs @@ -0,0 +1,23 @@ +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.Pause +{ + public class QuitButton : PauseButton + { + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + ButtonColour = new Color4(170, 27, 39, 255); // The red from the design isn't in the palette so it's used directly + SampleHover = audio.Sample.Get(@"Menu/menuclick"); + SampleClick = audio.Sample.Get(@"Menu/menuback"); + } + + public QuitButton() + { + Text = @"Quit to Main Menu"; + } + } +} diff --git a/osu.Game/Overlays/Pause/ResumeButton.cs b/osu.Game/Overlays/Pause/ResumeButton.cs new file mode 100644 index 0000000000..216a852993 --- /dev/null +++ b/osu.Game/Overlays/Pause/ResumeButton.cs @@ -0,0 +1,22 @@ +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.Pause +{ + public class ResumeButton : PauseButton + { + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + ButtonColour = colours.Green; + SampleHover = audio.Sample.Get(@"Menu/menuclick"); + SampleClick = audio.Sample.Get(@"Menu/menuback"); + } + + public ResumeButton() + { + Text = @"Continue"; + } + } +} diff --git a/osu.Game/Overlays/Pause/RetryButton.cs b/osu.Game/Overlays/Pause/RetryButton.cs new file mode 100644 index 0000000000..12378ca43b --- /dev/null +++ b/osu.Game/Overlays/Pause/RetryButton.cs @@ -0,0 +1,22 @@ +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.Pause +{ + public class RetryButton : PauseButton + { + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + ButtonColour = colours.YellowDark; + SampleHover = audio.Sample.Get(@"Menu/menuclick"); + SampleClick = audio.Sample.Get(@"Menu/menu-play-click"); + } + + public RetryButton() + { + Text = @"Retry"; + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index d6594dd2be..4fb0d4dfab 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -22,6 +22,7 @@ using osu.Framework.GameModes; using osu.Game.Modes.UI; using osu.Game.Screens.Ranking; using osu.Game.Configuration; +using osu.Game.Overlays.Pause; using osu.Framework.Configuration; using System; using OpenTK.Graphics; @@ -39,7 +40,23 @@ namespace osu.Game.Screens.Play public BeatmapInfo BeatmapInfo; public PlayMode PreferredPlayMode; - + + private bool isPaused; + public bool IsPaused + { + get + { + return isPaused; + } + } + + public int RestartCount; + + private double pauseCooldown = 1000; + private double lastPauseActionTime = 0; + + private bool canPause => Time.Current >= (lastPauseActionTime + pauseCooldown); + private IAdjustableClock sourceClock; private Ruleset ruleset; @@ -48,6 +65,10 @@ namespace osu.Game.Screens.Play private HitRenderer hitRenderer; private Bindable dimLevel; + private ScoreOverlay scoreOverlay; + private PauseOverlay pauseOverlay; + private PlayerInputManager playerInputManager; + [BackgroundDependencyLoader] private void load(AudioManager audio, BeatmapDatabase beatmaps, OsuGameBase game, OsuConfigManager config) { @@ -92,9 +113,20 @@ namespace osu.Game.Screens.Play ruleset = Ruleset.GetRuleset(usablePlayMode); - var scoreOverlay = ruleset.CreateScoreOverlay(); + scoreOverlay = ruleset.CreateScoreOverlay(); scoreOverlay.BindProcessor(scoreProcessor = ruleset.CreateScoreProcessor(beatmap.HitObjects.Count)); + pauseOverlay = new PauseOverlay + { + Depth = -1, + OnResume = delegate { + Delay(400); + Schedule(Resume); + }, + OnRetry = Restart, + OnQuit = Exit + }; + hitRenderer = ruleset.CreateHitRendererWith(beatmap.HitObjects); //bind HitRenderer to ScoreProcessor and ourselves (for a pass situation) @@ -109,7 +141,7 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { - new PlayerInputManager(game.Host) + playerInputManager = new PlayerInputManager(game.Host) { Clock = new InterpolatingFramedClock(sourceClock), PassThrough = false, @@ -119,9 +151,62 @@ namespace osu.Game.Screens.Play } }, scoreOverlay, + pauseOverlay }; } + public void Pause(bool force = false) + { + if (canPause || force) + { + lastPauseActionTime = Time.Current; + playerInputManager.PassThrough = true; + scoreOverlay.KeyCounter.IsCounting = false; + pauseOverlay.Retries = RestartCount; + pauseOverlay.Show(); + sourceClock.Stop(); + isPaused = true; + } + else + { + isPaused = false; + } + } + + public void Resume() + { + lastPauseActionTime = Time.Current; + playerInputManager.PassThrough = false; + scoreOverlay.KeyCounter.IsCounting = true; + pauseOverlay.Hide(); + sourceClock.Start(); + isPaused = false; + } + + public void TogglePaused() + { + isPaused = !IsPaused; + if (IsPaused) Pause(); else Resume(); + } + + public void Restart() + { + sourceClock.Stop(); // If the clock is running and Restart is called the game will lag until relaunch + + var newPlayer = new Player(); + + newPlayer.Preload(Game, delegate + { + newPlayer.RestartCount = RestartCount + 1; + ValidForResume = false; + + if (!Push(newPlayer)) + { + // Error(?) + } + }); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -176,9 +261,19 @@ namespace osu.Game.Screens.Play protected override bool OnExiting(GameMode next) { - dimLevel.ValueChanged -= dimChanged; - Background?.FadeTo(1f, 200); - return base.OnExiting(next); + if (!canPause) return true; + + if (!IsPaused && sourceClock.IsRunning) // For if the user presses escape quickly when entering the map + { + Pause(); + return true; + } + else + { + dimLevel.ValueChanged -= dimChanged; + Background?.FadeTo(1f, 200); + return base.OnExiting(next); + } } private void dimChanged(object sender, EventArgs e) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6e00f895a9..7356889b18 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -237,6 +237,13 @@ + + + + + + +