mirror of
https://github.com/ppy/osu.git
synced 2026-05-19 14:40:24 +08:00
0988552567
I don't really have much to say here. Instead, I'll give a brief history rundown that lists many pages of documentation you can read, if interested. - Started off as BTMC + Happy24 (Vivi)'s ["The Vision"](https://docs.google.com/document/d/1p1IpPmd2RICp8G4OqkCSs7u8Ug8FbFv8qqP0mfSrHf0/edit?tab=t.0#heading=h.fol093d9f9xi) - Initial [designs](https://www.figma.com/design/f5qqC57t9jFlgpzhRqUNVX/The-Vision?node-id=0-1&p=f) were led by Vivi. - Designs [morphed](https://www.figma.com/design/vtFmLrXKvWNyYiRjTezFTM/Untitled--Copy-?node-id=0-1&p=f) during development into what's currently present, led by @minetoblend. - There is some more ongoing work creating a [game design document](https://docs.google.com/document/d/1iffJFCsIBfYF0D4ogItSBEj6YBmbp-rdCpItAeaJiTA/edit?tab=t.0). **tl;dr:** Create something with the mechanics of a trading card game within osu!. The name of this is "ranked play". --- To be frank, a lot of stuff is missing here. Some of it I don't want to mention, because the point of this exercise is to get the system into the hands of players, gather feedback especially around mechanics, and discuss any further direction with the team. I am expecting a blanket approval on all of the new code, with particular attention to changes in existing components that I'll point out in a self review. There is also some [ongoing work](https://github.com/smoogipoo/osu/pulls) that may arrive in this branch prior to being merged. --------- Co-authored-by: maarvin <minetoblend@gmail.com> Co-authored-by: Marvin <m.schuerz@hautzy.com> Co-authored-by: Jamie Taylor <me@nekodex.net> Co-authored-by: ArijanJ <arijanj@proton.me> Co-authored-by: Dean Herbert <pe@ppy.sh> Co-authored-by: Tim Oliver <git@tim.dev> Co-authored-by: Joseph Madamba <madamba.joehu@outlook.com> Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com> Co-authored-by: nil <25884226+voidstar0@users.noreply.github.com> Co-authored-by: Ботников Максим <mr.botnikoff@ya.ru> Co-authored-by: Denis Titovets <den232titovets@yandex.ru> Co-authored-by: Michael Middlezong <119022671+mmiddlezong@users.noreply.github.com> Co-authored-by: SupDos <6813986+SupDos@users.noreply.github.com> Co-authored-by: failaip12 <86018517+failaip12@users.noreply.github.com>
541 lines
19 KiB
C#
541 lines
19 KiB
C#
// 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 System.Collections.Generic;
|
|
using System.Linq;
|
|
using osu.Framework;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Audio;
|
|
using osu.Framework.Audio.Sample;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
using osu.Framework.Extensions.LocalisationExtensions;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Sprites;
|
|
using osu.Framework.Input.Bindings;
|
|
using osu.Framework.Input.Events;
|
|
using osu.Framework.Logging;
|
|
using osu.Framework.Platform;
|
|
using osu.Framework.Threading;
|
|
using osu.Game.Graphics;
|
|
using osu.Game.Graphics.Containers;
|
|
using osu.Game.Input;
|
|
using osu.Game.Input.Bindings;
|
|
using osu.Game.Localisation;
|
|
using osu.Game.Online.API;
|
|
using osu.Game.Online.Rooms;
|
|
using osu.Game.Overlays;
|
|
using osuTK;
|
|
using osuTK.Graphics;
|
|
using osuTK.Input;
|
|
|
|
namespace osu.Game.Screens.Menu
|
|
{
|
|
public partial class ButtonSystem : Container, IStateful<ButtonSystemState>, IKeyBindingHandler<GlobalAction>
|
|
{
|
|
public const float BUTTON_WIDTH = 140f;
|
|
public const float WEDGE_WIDTH = 20;
|
|
|
|
public event Action<ButtonSystemState>? StateChanged;
|
|
|
|
public Action? OnEditBeatmap;
|
|
public Action? OnEditSkin;
|
|
public Action<UIEvent>? OnExit;
|
|
public Action? OnBeatmapListing;
|
|
public Action? OnSolo;
|
|
public Action? OnSettings;
|
|
public Action? OnMultiplayer;
|
|
public Action? OnQuickPlay;
|
|
public Action? OnRankedPlay;
|
|
public Action? OnPlaylists;
|
|
public Action<Room>? OnDailyChallenge;
|
|
|
|
private readonly IBindable<bool> isIdle = new BindableBool();
|
|
|
|
private OsuLogo? logo;
|
|
|
|
/// <summary>
|
|
/// Assign the <see cref="OsuLogo"/> that this ButtonSystem should manage the position of.
|
|
/// </summary>
|
|
/// <param name="logo">The instance of the logo to be assigned. If null, we are suspending from the screen that uses this ButtonSystem.</param>
|
|
public void SetOsuLogo(OsuLogo? logo)
|
|
{
|
|
this.logo = logo;
|
|
|
|
if (this.logo != null)
|
|
{
|
|
this.logo.Action = onOsuLogo;
|
|
|
|
// osuLogo.SizeForFlow relies on loading to be complete.
|
|
buttonArea.Flow.Position = new Vector2(WEDGE_WIDTH * 2 - (BUTTON_WIDTH + this.logo.SizeForFlow / 4), 0);
|
|
|
|
updateLogoState();
|
|
}
|
|
else
|
|
{
|
|
// We should stop tracking as the facade is now out of scope.
|
|
logoTracking?.Dispose();
|
|
logoTracking = null;
|
|
}
|
|
}
|
|
|
|
private readonly ButtonArea buttonArea;
|
|
|
|
private readonly MainMenuButton backButton;
|
|
|
|
private readonly List<MainMenuButton> buttonsTopLevel = new List<MainMenuButton>();
|
|
private readonly List<MainMenuButton> buttonsPlay = new List<MainMenuButton>();
|
|
private readonly List<MainMenuButton> buttonsMulti = new List<MainMenuButton>();
|
|
private readonly List<MainMenuButton> buttonsEdit = new List<MainMenuButton>();
|
|
|
|
private Sample? sampleBackToLogo;
|
|
private Sample? sampleLogoSwoosh;
|
|
|
|
private readonly LogoTrackingContainer logoTrackingContainer;
|
|
|
|
public bool ReturnToTopOnIdle { get; set; } = true;
|
|
|
|
public ButtonSystem()
|
|
{
|
|
RelativeSizeAxes = Axes.Both;
|
|
|
|
Child = logoTrackingContainer = new LogoTrackingContainer
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Child = buttonArea = new ButtonArea()
|
|
};
|
|
|
|
buttonArea.AddRange(new Drawable[]
|
|
{
|
|
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), (_, _) => OnSettings?.Invoke(), Key.O, Key.S)
|
|
{
|
|
Padding = new MarginPadding { Right = WEDGE_WIDTH },
|
|
},
|
|
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), (_, _) =>
|
|
{
|
|
switch (State)
|
|
{
|
|
case ButtonSystemState.Multi:
|
|
State = ButtonSystemState.Play;
|
|
break;
|
|
|
|
default:
|
|
State = ButtonSystemState.TopLevel;
|
|
break;
|
|
}
|
|
})
|
|
{
|
|
Padding = new MarginPadding { Right = WEDGE_WIDTH },
|
|
VisibleStateMin = ButtonSystemState.Play,
|
|
VisibleStateMax = ButtonSystemState.Edit,
|
|
},
|
|
logoTrackingContainer.LogoFacade.With(d => d.Scale = new Vector2(0.74f))
|
|
});
|
|
|
|
buttonArea.Flow.CentreTarget = logoTrackingContainer.LogoFacade;
|
|
}
|
|
|
|
[Resolved]
|
|
private IAPIProvider api { get; set; } = null!;
|
|
|
|
[Resolved]
|
|
private OsuGame? game { get; set; }
|
|
|
|
[Resolved]
|
|
private LoginOverlay? loginOverlay { get; set; }
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host)
|
|
{
|
|
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => OnSolo?.Invoke(), Key.P)
|
|
{
|
|
Padding = new MarginPadding { Left = WEDGE_WIDTH },
|
|
});
|
|
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), (_, _) => State = ButtonSystemState.Multi, Key.M));
|
|
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L));
|
|
buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D));
|
|
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
|
|
|
|
buttonsMulti.Add(new MainMenuButton(ButtonSystemStrings.Lounge, @"button-default-select", FontAwesome.Solid.Couch, new Color4(94, 63, 186, 255), onMultiplayer, Key.L, Key.M)
|
|
{
|
|
Padding = new MarginPadding { Left = WEDGE_WIDTH }
|
|
});
|
|
#if DEBUG
|
|
buttonsMulti.Add(new MainMenuButton(ButtonSystemStrings.RankedPlay, @"button-daily-select", FontAwesome.Solid.Crown, new Color4(94, 63, 186, 255), onRankedPlay, Key.R));
|
|
#else
|
|
buttonsMulti.Add(new MainMenuButton(ButtonSystemStrings.QuickPlay, @"button-daily-select", FontAwesome.Solid.Bolt, new Color4(94, 63, 186, 255), onQuickPlay, Key.Q));
|
|
#endif
|
|
buttonsMulti.ForEach(b => b.VisibleState = ButtonSystemState.Multi);
|
|
|
|
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B,
|
|
Key.E)
|
|
{
|
|
Padding = new MarginPadding { Left = WEDGE_WIDTH },
|
|
});
|
|
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), (_, _) => OnEditSkin?.Invoke(), Key.S));
|
|
buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit);
|
|
|
|
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), (_, _) => State = ButtonSystemState.Play, Key.P, Key.M,
|
|
Key.L)
|
|
{
|
|
Padding = new MarginPadding { Left = WEDGE_WIDTH },
|
|
});
|
|
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), (_, _) => State = ButtonSystemState.Edit, Key.E));
|
|
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), (_, _) => OnBeatmapListing?.Invoke(), Key.B,
|
|
Key.D));
|
|
|
|
if (host.CanExit)
|
|
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), (_, e) => OnExit?.Invoke(e), Key.Q));
|
|
|
|
buttonArea.AddRange(buttonsMulti);
|
|
buttonArea.AddRange(buttonsPlay);
|
|
buttonArea.AddRange(buttonsEdit);
|
|
buttonArea.AddRange(buttonsTopLevel);
|
|
|
|
buttonArea.ForEach(b =>
|
|
{
|
|
if (b is MainMenuButton)
|
|
{
|
|
b.Origin = Anchor.CentreLeft;
|
|
b.Anchor = Anchor.CentreLeft;
|
|
}
|
|
});
|
|
|
|
isIdle.ValueChanged += idle => updateIdleState(idle.NewValue);
|
|
|
|
if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle);
|
|
|
|
sampleBackToLogo = audio.Samples.Get(@"Menu/back-to-logo");
|
|
sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh");
|
|
}
|
|
|
|
private void onMultiplayer(MainMenuButton mainMenuButton, UIEvent uiEvent)
|
|
{
|
|
if (api.State.Value != APIState.Online)
|
|
{
|
|
loginOverlay?.Show();
|
|
return;
|
|
}
|
|
|
|
OnMultiplayer?.Invoke();
|
|
}
|
|
|
|
private void onQuickPlay(MainMenuButton mainMenuButton, UIEvent uiEvent)
|
|
{
|
|
if (api.State.Value != APIState.Online)
|
|
{
|
|
loginOverlay?.Show();
|
|
return;
|
|
}
|
|
|
|
OnQuickPlay?.Invoke();
|
|
}
|
|
|
|
private void onRankedPlay(MainMenuButton mainMenuButton, UIEvent uiEvent)
|
|
{
|
|
if (api.State.Value != APIState.Online)
|
|
{
|
|
loginOverlay?.Show();
|
|
return;
|
|
}
|
|
|
|
OnRankedPlay?.Invoke();
|
|
}
|
|
|
|
private void onPlaylists(MainMenuButton mainMenuButton, UIEvent uiEvent)
|
|
{
|
|
if (api.State.Value != APIState.Online)
|
|
{
|
|
loginOverlay?.Show();
|
|
return;
|
|
}
|
|
|
|
OnPlaylists?.Invoke();
|
|
}
|
|
|
|
private void onDailyChallenge(MainMenuButton button, UIEvent uiEvent)
|
|
{
|
|
if (api.State.Value != APIState.Online)
|
|
{
|
|
loginOverlay?.Show();
|
|
return;
|
|
}
|
|
|
|
var dailyChallengeButton = (DailyChallengeButton)button;
|
|
|
|
if (dailyChallengeButton.Room != null)
|
|
OnDailyChallenge?.Invoke(dailyChallengeButton.Room);
|
|
}
|
|
|
|
private void updateIdleState(bool isIdle)
|
|
{
|
|
if (!ReturnToTopOnIdle)
|
|
return;
|
|
|
|
if (isIdle && State != ButtonSystemState.Exit && State != ButtonSystemState.EnteringMode)
|
|
State = ButtonSystemState.Initial;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Triggers the <see cref="logo"/> if the current <see cref="State"/> is <see cref="ButtonSystemState.Initial"/>.
|
|
/// </summary>
|
|
/// <returns><c>true</c> if the <see cref="logo"/> was triggered, <c>false</c> otherwise.</returns>
|
|
private bool triggerInitialOsuLogo()
|
|
{
|
|
if (State == ButtonSystemState.Initial)
|
|
{
|
|
StopSamplePlayback();
|
|
logo?.TriggerClick();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected override bool OnKeyDown(KeyDownEvent e)
|
|
{
|
|
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
|
|
return false;
|
|
|
|
if (e.Key >= Key.F1 && e.Key <= Key.F35)
|
|
return false;
|
|
|
|
if (e.Key >= Key.Mute && e.Key <= Key.TrackNext)
|
|
return false;
|
|
|
|
switch (e.Key)
|
|
{
|
|
case Key.Escape:
|
|
return false;
|
|
}
|
|
|
|
if (triggerInitialOsuLogo())
|
|
return true;
|
|
|
|
return base.OnKeyDown(e);
|
|
}
|
|
|
|
protected override bool OnJoystickPress(JoystickPressEvent e)
|
|
{
|
|
if (triggerInitialOsuLogo())
|
|
return true;
|
|
|
|
return base.OnJoystickPress(e);
|
|
}
|
|
|
|
protected override bool OnMidiDown(MidiDownEvent e)
|
|
{
|
|
if (triggerInitialOsuLogo())
|
|
return true;
|
|
|
|
return base.OnMidiDown(e);
|
|
}
|
|
|
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
|
{
|
|
if (e.Repeat)
|
|
return false;
|
|
|
|
switch (e.Action)
|
|
{
|
|
case GlobalAction.Back:
|
|
return goBack();
|
|
|
|
case GlobalAction.Select:
|
|
logo?.TriggerClick();
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
|
{
|
|
}
|
|
|
|
private bool goBack()
|
|
{
|
|
switch (State)
|
|
{
|
|
case ButtonSystemState.TopLevel:
|
|
State = ButtonSystemState.Initial;
|
|
|
|
// Samples are explicitly played here in response to user interaction and not when transitioning due to idle.
|
|
StopSamplePlayback();
|
|
sampleBackToLogo?.Play();
|
|
|
|
return true;
|
|
|
|
case ButtonSystemState.Edit:
|
|
case ButtonSystemState.Play:
|
|
case ButtonSystemState.Multi:
|
|
StopSamplePlayback();
|
|
backButton.TriggerClick();
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void StopSamplePlayback()
|
|
{
|
|
buttonsPlay.ForEach(button => button.StopSamplePlayback());
|
|
buttonsMulti.ForEach(button => button.StopSamplePlayback());
|
|
buttonsTopLevel.ForEach(button => button.StopSamplePlayback());
|
|
logo?.StopSamplePlayback();
|
|
}
|
|
|
|
private bool onOsuLogo()
|
|
{
|
|
switch (state)
|
|
{
|
|
default:
|
|
return false;
|
|
|
|
case ButtonSystemState.Initial:
|
|
State = ButtonSystemState.TopLevel;
|
|
return true;
|
|
|
|
case ButtonSystemState.TopLevel:
|
|
buttonsTopLevel.First().TriggerClick();
|
|
return false;
|
|
|
|
case ButtonSystemState.Play:
|
|
buttonsPlay.First().TriggerClick();
|
|
return false;
|
|
|
|
case ButtonSystemState.Multi:
|
|
buttonsMulti.First().TriggerClick();
|
|
return false;
|
|
|
|
case ButtonSystemState.Edit:
|
|
buttonsEdit.First().TriggerClick();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private ButtonSystemState state = ButtonSystemState.Initial;
|
|
|
|
public override bool HandleNonPositionalInput => state != ButtonSystemState.Exit;
|
|
public override bool HandlePositionalInput => state != ButtonSystemState.Exit;
|
|
|
|
public ButtonSystemState State
|
|
{
|
|
get => state;
|
|
|
|
set
|
|
{
|
|
if (state == value) return;
|
|
|
|
ButtonSystemState lastState = state;
|
|
state = value;
|
|
|
|
updateLogoState(lastState);
|
|
|
|
Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}");
|
|
|
|
buttonArea.FinishTransforms(true);
|
|
|
|
using (buttonArea.BeginDelayedSequence(lastState == ButtonSystemState.Initial ? 150 : 0))
|
|
{
|
|
buttonArea.ButtonSystemState = state;
|
|
|
|
foreach (var b in buttonArea.OfType<MainMenuButton>())
|
|
b.ButtonSystemState = state;
|
|
}
|
|
|
|
StateChanged?.Invoke(State);
|
|
}
|
|
}
|
|
|
|
private ScheduledDelegate? logoDelayedAction;
|
|
private IDisposable? logoTracking;
|
|
|
|
private void updateLogoState(ButtonSystemState lastState = ButtonSystemState.Initial)
|
|
{
|
|
if (logo == null) return;
|
|
|
|
switch (state)
|
|
{
|
|
case ButtonSystemState.Exit:
|
|
case ButtonSystemState.Initial:
|
|
logoDelayedAction?.Cancel();
|
|
logoDelayedAction = Scheduler.AddDelayed(() =>
|
|
{
|
|
logoTracking?.Dispose();
|
|
logoTracking = null;
|
|
|
|
game?.Toolbar.Hide();
|
|
|
|
logo?.ClearTransforms(targetMember: nameof(Position));
|
|
logo?.MoveTo(new Vector2(0.5f), 800, Easing.OutExpo);
|
|
logo?.ScaleTo(1, 800, Easing.OutExpo);
|
|
}, buttonArea.Alpha * 150);
|
|
|
|
if (lastState == ButtonSystemState.TopLevel)
|
|
sampleLogoSwoosh?.Play();
|
|
break;
|
|
|
|
case ButtonSystemState.TopLevel:
|
|
case ButtonSystemState.Play:
|
|
switch (lastState)
|
|
{
|
|
case ButtonSystemState.TopLevel: // coming from toplevel to play
|
|
break;
|
|
|
|
case ButtonSystemState.Initial:
|
|
logo.ClearTransforms(targetMember: nameof(Position));
|
|
|
|
bool impact = logo.Scale.X > 0.6f;
|
|
|
|
logo.ScaleTo(0.5f, 200, Easing.In);
|
|
|
|
logoTracking?.Dispose();
|
|
logoTracking = logoTrackingContainer.StartTracking(logo, 200, Easing.In);
|
|
|
|
logoDelayedAction?.Cancel();
|
|
logoDelayedAction = Scheduler.AddDelayed(() =>
|
|
{
|
|
if (impact)
|
|
logo?.Impact();
|
|
|
|
game?.Toolbar.Show();
|
|
}, 200);
|
|
break;
|
|
|
|
default:
|
|
logo.ClearTransforms(targetMember: nameof(Position));
|
|
|
|
logoTracking?.Dispose();
|
|
logoTracking = logoTrackingContainer.StartTracking(logo, 0, Easing.In);
|
|
|
|
logo.ScaleTo(0.5f, 200, Easing.OutQuint);
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
case ButtonSystemState.EnteringMode:
|
|
logoTracking?.Dispose();
|
|
logoTracking = logoTrackingContainer.StartTracking(logo, lastState == ButtonSystemState.Initial ? MainMenu.FADE_OUT_DURATION : 0, Easing.InSine);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum ButtonSystemState
|
|
{
|
|
Exit,
|
|
Initial,
|
|
TopLevel,
|
|
Play,
|
|
Multi,
|
|
Edit,
|
|
EnteringMode,
|
|
}
|
|
}
|