1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-10 01:33:39 +08:00
Files
osu-lazer/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs
T
2026-01-05 15:22:06 +09:00

641 lines
25 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.Globalization;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Online.Matchmaking.Events;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results;
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
/// <summary>
/// A panel used throughout matchmaking to represent a user, including local information like their
/// rank and high level statistics in the matchmaking system.
/// </summary>
public partial class PlayerPanel : OsuClickableContainer, IHasContextMenu
{
private static readonly Vector2 size_horizontal = new Vector2(300, 100);
private static readonly Vector2 size_vertical = new Vector2(150, 200);
private static readonly Vector2 avatar_size = new Vector2(80);
public readonly MultiplayerRoomUser RoomUser;
/// <summary>
/// Perform an action in addition to showing the user's profile.
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX).
/// </summary>
public new Action? Action;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private UserProfileOverlay? profileOverlay { get; set; }
[Resolved]
private ChannelManager? channelManager { get; set; }
[Resolved]
private ChatOverlay? chatOverlay { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
private OverlayColourProvider? colourProvider { get; set; }
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private MultiplayerClient? multiplayerClient { get; set; }
[Resolved]
private MetadataClient? metadataClient { get; set; }
public readonly APIUser User;
private readonly Action viewProfile;
private OsuSpriteText rankText = null!;
private OsuSpriteText scoreText = null!;
private Drawable avatarPositionTarget = null!;
private Drawable avatarJumpTarget = null!;
private Drawable avatar = null!;
private OsuSpriteText username = null!;
private Container mainContent = null!;
private Box solidBackgroundLayer = null!;
private Drawable background = null!;
private OsuSpriteText quitText = null!;
private BufferedContainer backgroundQuitTarget = null!;
private BufferedContainer avatarQuitTarget = null!;
private Box downloadProgressBar = null!;
private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal;
private bool hasQuit;
private enum InteractionSampleType
{
PlayerJump,
PlayerReJump,
OtherPlayerJump,
}
private Dictionary<InteractionSampleType, Sample?> interactionSamples = new Dictionary<InteractionSampleType, Sample?>();
private readonly Dictionary<InteractionSampleType, SampleChannel?> interactionSampleChannels = new Dictionary<InteractionSampleType, SampleChannel?>();
private double samplePitch;
private double? lastSamplePlayback;
public PlayerPanel(MultiplayerRoomUser user)
: base(HoverSampleSet.Button)
{
ArgumentNullException.ThrowIfNull(user.User);
User = user.User;
RoomUser = user;
base.Action = viewProfile = () =>
{
Action?.Invoke();
profileOverlay?.ShowUser(User);
};
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
Content.Masking = true;
Content.CornerRadius = 10;
Content.CornerExponent = 10;
Content.Anchor = Anchor.Centre;
Content.Origin = Anchor.Centre;
Child = backgroundQuitTarget = new BufferedContainer
{
FrameBufferScale = new Vector2(1.5f),
RelativeSizeAxes = Axes.Both,
Children = new[]
{
solidBackgroundLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider?.Background5 ?? colours.Gray1
},
background = new UserCoverBackground
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colours.Gray7,
User = User
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
mainContent = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
quitText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "QUIT",
Font = OsuFont.Default.With(weight: "Bold", size: 70),
Rotation = -22.5f,
Colour = OsuColour.Gray(0.3f),
Blending = BlendingParameters.Additive
},
avatarPositionTarget = new Container
{
Origin = Anchor.Centre,
Size = avatar_size,
Child = avatarJumpTarget = new Container
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Child = avatar = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
// Needs to be re-buffered as the avatar is proxied outside of the parent buffered container.
Child = avatarQuitTarget = new BufferedContainer
{
FrameBufferScale = new Vector2(1.5f),
RelativeSizeAxes = Axes.Both,
Child = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = Vector2.One
}
}
},
}
},
rankText = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomCentre,
Blending = BlendingParameters.Additive,
Margin = new MarginPadding(4),
Text = "-",
Font = OsuFont.Style.Title.With(size: 55),
},
username = new TruncatingSpriteText
{
Alpha = 0,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Text = User.Username,
Font = OsuFont.Style.Heading1,
MaxWidth = 120
},
scoreText = new OsuSpriteText
{
Alpha = 0,
Margin = new MarginPadding(10),
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Style.Heading2,
Text = "0 pts"
}
}
},
downloadProgressBar = new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Size = new Vector2(0, 4),
Colour = colourProvider?.Content2 ?? colours.Gray3
}
}
}
}
};
// Allow avatar to exist outside of masking for when it jumps around and stuff.
AddInternal(avatar.CreateProxy());
interactionSamples = new Dictionary<InteractionSampleType, Sample?>
{
{ InteractionSampleType.PlayerJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-jump") },
{ InteractionSampleType.PlayerReJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-rejump") },
{ InteractionSampleType.OtherPlayerJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-jump-other") }
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateLayout(true);
client.MatchRoomStateChanged += onRoomStateChanged;
client.MatchEvent += onMatchEvent;
client.BeatmapAvailabilityChanged += onBeatmapAvailabilityChanged;
onRoomStateChanged(client.Room!.MatchState);
avatar.ScaleTo(0)
.ScaleTo(1, 500, Easing.OutElasticHalf)
.FadeIn(200);
// pick a random pitch to be used by the player for duration of this session
samplePitch = 0.75f + RNG.NextDouble(0f, 0.75f);
}
public PlayerPanelDisplayMode DisplayMode
{
get => displayMode;
set
{
displayMode = value;
if (IsLoaded)
updateLayout(false);
}
}
public bool HasQuit
{
get => hasQuit;
set
{
hasQuit = value;
if (IsLoaded)
updateLayout(false);
}
}
private bool horizontal => displayMode == PlayerPanelDisplayMode.Horizontal;
private Vector2 avatarPosition
{
get
{
switch (displayMode)
{
case PlayerPanelDisplayMode.AvatarOnly:
return avatar_size / 2;
case PlayerPanelDisplayMode.Horizontal:
return new Vector2(50);
case PlayerPanelDisplayMode.Vertical:
return new Vector2(75, 50);
default:
throw new ArgumentOutOfRangeException();
}
}
}
private void updateLayout(bool instant)
{
double duration = instant ? 0 : 1000;
avatarPositionTarget.MoveTo(avatarPosition, duration, Easing.OutPow10);
switch (displayMode)
{
case PlayerPanelDisplayMode.AvatarOnly:
rankText.Hide();
scoreText.Hide();
username.Hide();
background.FadeOut(200, Easing.OutQuint);
solidBackgroundLayer.FadeOut(200, Easing.OutQuint);
this.ResizeTo(avatar_size, duration, Easing.OutPow10);
break;
case PlayerPanelDisplayMode.Horizontal:
case PlayerPanelDisplayMode.Vertical:
background.FadeIn(200);
solidBackgroundLayer.FadeIn(200);
using (BeginDelayedSequence(100))
{
username.FadeIn(600);
using (BeginDelayedSequence(100))
{
scoreText.FadeIn(600);
using (BeginDelayedSequence(100))
{
rankText.FadeTo(1, 600);
}
}
}
this.ResizeTo(horizontal ? size_horizontal : size_vertical, duration, Easing.OutPow10);
rankText.MoveTo(horizontal ? new Vector2(-40, -20) : new Vector2(-70, 0), duration, Easing.OutPow10);
username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10);
scoreText.MoveTo(horizontal ? new Vector2(0, -16) : new Vector2(0, -56), duration, Easing.OutPow10);
quitText.MoveTo(horizontal ? new Vector2(40, 0) : new Vector2(0, 40), duration, Easing.OutPow10);
break;
default:
throw new ArgumentOutOfRangeException();
}
// quit text doesn't fit on avataronly mode.
if (HasQuit && displayMode != PlayerPanelDisplayMode.AvatarOnly)
quitText.FadeIn(duration, Easing.OutPow10);
else
quitText.FadeOut(duration, Easing.OutPow10);
if (HasQuit)
{
backgroundQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10);
avatarQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10);
}
else
{
backgroundQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10);
avatarQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10);
}
}
protected override void Update()
{
base.Update();
// Not sure why this is required but it is.
avatarQuitTarget.Alpha = Alpha;
}
protected override bool OnHover(HoverEvent e)
{
Content.ScaleTo(1.03f, 2000, Easing.OutPow10);
mainContent.ScaleTo(1.03f, 2000, Easing.OutPow10);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
Content.ScaleTo(1f, 750, Easing.OutPow10);
mainContent.ScaleTo(1, 750, Easing.OutPow10);
mainContent.MoveTo(Vector2.Zero, 1250, Easing.OutPow10);
avatarPositionTarget.MoveTo(avatarPosition, 1250, Easing.OutPow10);
base.OnHoverLost(e);
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
var offset = (avatarPositionTarget.ToLocalSpace(e.ScreenSpaceMousePosition) - avatarPositionTarget.DrawSize / 2) * 0.02f;
mainContent.MoveTo(offset * 0.5f, 2000, Easing.OutPow10);
avatarPositionTarget.MoveTo(avatarPosition + offset, 2000, Easing.OutPow10);
return base.OnMouseMove(e);
}
private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() =>
{
if (state is not MatchmakingRoomState matchmakingState)
return;
if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore))
return;
if (userScore.Placement == null)
return;
rankText.Text = userScore.Placement.Value.Ordinalize(CultureInfo.CurrentCulture);
rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement.Value));
scoreText.Text = $"{userScore.Points} pts";
});
private int consecutiveJumps;
private void onMatchEvent(MatchServerEvent e)
{
switch (e)
{
case MatchmakingAvatarActionEvent action:
if (action.UserId != RoomUser.UserID)
break;
switch (action.Action)
{
case MatchmakingAvatarAction.Jump:
var movement = avatarJumpTarget.Delay(0);
var scale = avatarJumpTarget.Delay(0);
// only increase height if the user jumps again while in a "jumped" state.
// this avoids building up large jumps from very quick spam, and adds a timing game.
bool isConsecutive = avatarJumpTarget.Y < 0;
if (isConsecutive)
{
consecutiveJumps++;
if (avatarJumpTarget.Y > 0)
movement = movement.MoveToY(0);
movement = movement.MoveToY(5, 100, Easing.Out);
scale = scale.ScaleTo(new Vector2(1, 0.95f), 100, Easing.Out);
}
else
{
consecutiveJumps = 0;
}
float multiplier = 1 + 0.3f * Math.Min(10, consecutiveJumps);
movement.Then().MoveToY(-10 * multiplier, 200, Easing.Out)
.Then().MoveToY(0, 200, Easing.In);
scale.Then().ScaleTo(new Vector2(1, 1.05f), 200, Easing.Out)
.Then().ScaleTo(new Vector2(1, 0.95f), 200, Easing.In)
.Then().ScaleTo(Vector2.One, 800, Easing.OutElastic);
// only play jump sample if panel is visible
if (Alpha > 0)
playJumpSample(isConsecutive);
break;
}
break;
}
}
private void onBeatmapAvailabilityChanged(MultiplayerRoomUser user, BeatmapAvailability availability) => Scheduler.Add(() =>
{
if (!user.Equals(RoomUser))
return;
if (availability.State == DownloadState.Downloading)
downloadProgressBar.FadeIn(200, Easing.OutPow10);
else
downloadProgressBar.FadeOut(200, Easing.OutPow10);
downloadProgressBar.ResizeWidthTo(availability.DownloadProgress ?? 0, 200, Easing.OutPow10);
});
private void playJumpSample(bool rejumping)
{
bool isLocalUser = User.OnlineID == client.LocalUser?.UserID;
if (isLocalUser)
playInteractionSample(rejumping ? InteractionSampleType.PlayerReJump : InteractionSampleType.PlayerJump);
else
playInteractionSample(InteractionSampleType.OtherPlayerJump);
}
private void playInteractionSample(InteractionSampleType sampleType)
{
bool enoughTimePassedSinceLastPlayback = lastSamplePlayback == null || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME;
if (!enoughTimePassedSinceLastPlayback)
return;
Sample? targetSample = interactionSamples[sampleType];
SampleChannel? targetChannel = interactionSampleChannels.GetValueOrDefault(sampleType);
targetChannel?.Stop();
targetChannel = targetSample?.GetChannel();
if (targetChannel == null)
return;
float horizontalPos = BoundingBox.Centre.X / Parent!.ToLocalSpace(Parent!.ScreenSpaceDrawQuad).Width;
// rescale balance from 0..1 to -1..1
float balance = -1f + horizontalPos * 2f;
targetChannel.Frequency.Value = samplePitch;
targetChannel.Balance.Value = balance * OsuGameBase.SFX_STEREO_STRENGTH;
targetChannel.Play();
interactionSampleChannels[sampleType] = targetChannel;
lastSamplePlayback = Time.Current;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
{
client.MatchRoomStateChanged -= onRoomStateChanged;
client.MatchEvent -= onMatchEvent;
client.BeatmapAvailabilityChanged -= onBeatmapAvailabilityChanged;
}
}
public MenuItem[] ContextMenuItems
{
get
{
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, viewProfile)
};
if (User.Equals(api.LocalUser.Value))
return items.ToArray();
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, () =>
{
channelManager?.OpenPrivateChannel(User);
chatOverlay?.Show();
}));
items.Add(!isUserBlocked()
? new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Block(User)))
: new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Unblock(User))));
if (isUserOnline())
{
items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () =>
{
if (isUserOnline())
performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User)));
}));
if (canInviteUser())
{
items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () =>
{
if (canInviteUser())
multiplayerClient!.InvitePlayer(User.Id);
}));
}
}
return items.ToArray();
bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null;
bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true;
bool isUserBlocked() => api.LocalUserState.Blocks.Any(b => b.TargetID == User.OnlineID);
}
}
}
public enum PlayerPanelDisplayMode
{
AvatarOnly,
Horizontal,
Vertical
}
}