From e0a46fefaf0f234370132d055e2edd7be069b242 Mon Sep 17 00:00:00 2001 From: Marvin Date: Fri, 27 Mar 2026 19:22:58 +0100 Subject: [PATCH] Refactor card hand layout to be stateless --- .../Online/RankedPlay/RankedPlayCardState.cs | 18 ++ .../RankedPlay/Hand/HandOfCards.HandCard.cs | 10 +- .../RankedPlay/Hand/HandOfCards.cs | 184 +++++++++++------- .../RankedPlay/Hand/OpponentHandOfCards.cs | 12 ++ .../Hand/PlayerHandOfCards.PlayerHandCard.cs | 16 +- .../RankedPlay/Hand/PlayerHandOfCards.cs | 9 - 6 files changed, 150 insertions(+), 99 deletions(-) diff --git a/osu.Game/Online/RankedPlay/RankedPlayCardState.cs b/osu.Game/Online/RankedPlay/RankedPlayCardState.cs index cf3bac35e6..97c9d3947d 100644 --- a/osu.Game/Online/RankedPlay/RankedPlayCardState.cs +++ b/osu.Game/Online/RankedPlay/RankedPlayCardState.cs @@ -3,6 +3,7 @@ using System; using MessagePack; +using osuTK; namespace osu.Game.Online.RankedPlay { @@ -21,5 +22,22 @@ namespace osu.Game.Online.RankedPlay [Key(3)] public required bool Dragged { get; init; } + + [Key(4)] + public float DragX { get; init; } + + [Key(5)] + public float DragY { get; init; } + + [IgnoreMember] + public Vector2 DragPosition + { + get => new Vector2(DragX, DragY); + init + { + DragX = value.X; + DragY = value.Y; + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/HandOfCards.HandCard.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/HandOfCards.HandCard.cs index f9a895de93..8d4688add8 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/HandOfCards.HandCard.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/HandOfCards.HandCard.cs @@ -16,10 +16,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand { public partial class HandCard : CompositeDrawable { - public float LayoutScale => CardHoveredOrDragged ? HOVER_SCALE : 1; - - public float LayoutWidth => RankedPlayCard.SIZE.X * LayoutScale; - private readonly Bindable state = new Bindable(); public RankedPlayCardState State @@ -54,6 +50,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand public bool CardHoveredOrDragged => CardHovered || CardDragged; + public Vector2 DragPosition + { + get => State.DragPosition; + set => State = State with { DragPosition = value }; + } + [Resolved] private HandOfCards handOfCards { get; set; } = null!; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/HandOfCards.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/HandOfCards.cs index 846cadbbd9..d64b40727c 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/HandOfCards.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/HandOfCards.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Graphics; @@ -25,6 +24,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand { protected const float HOVER_SCALE = 1.2f; + private const float card_spacing = -20; + public IEnumerable Cards => cardContainer.Children; /// @@ -176,91 +177,132 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand if (Contracted) return; - const float spacing = -20; - - float totalWidth = cardContainer.Sum(it => it.LayoutWidth + spacing) - spacing; - - float x = -totalWidth / 2; - - const int no_card_hovered = -1; - int hoverIndex = no_card_hovered; - - for (int i = 0; i < cardContainer.Count; i++) - { - // the mouse can temporarily leave the currently dragged card and hover a different card. - // in that case the hovered card should take precedence here - if (cardContainer[i].CardDragged) - { - hoverIndex = i; - break; - } - - if (cardContainer[i].CardHovered) - { - hoverIndex = i; - } - } - double delay = 0; + int activeCardIndex = getActiveCardIndex(); + for (int i = 0; i < cardContainer.Count; i++) { var child = cardContainer[i]; - x += child.LayoutWidth / 2; + Vector2 position; + float rotation; + float scale; - float yOffset = 0; + if (child.CardDragged) + CalculateDraggedCardLayout(child.DragPosition, out position, out rotation, out scale); + else + CalculateCardLayout(i, activeCardIndex, out position, out rotation, out scale); - var position = new Vector2(x, MathF.Pow(MathF.Abs(x / 250), 2) * 20 - 10); + if (Flipped) + position *= -1; - if (hoverIndex != no_card_hovered && cardContainer.Children.Count > 1) - { - int distance = Math.Abs(i - hoverIndex); - int direction = Math.Sign(i - hoverIndex); - - position.X += direction switch - { - 0 => 0, - - // special case for the left card when there's only 2 cards - // too much offset looks kinda odd here so it's reduced - < 0 when cardContainer.Count == 2 => -3, - - < 0 => -10 / MathF.Pow(distance, 3), - - // cards right to the hovered card have a higher offset because they are partially - // covering the cards to their left - > 0 => 20 / MathF.Pow(distance, 2), - }; - } - - if (child.CardHovered) - yOffset = -HoverYOffset; - - float rotation = x * 0.03f; - - float angle = MathHelper.DegreesToRadians(rotation + 90); - - position += new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * yOffset; - - position *= Flipped ? -1 : 1; - - float scale = child.LayoutScale; - - ApplyLayoutToCard(child, position, rotation, scale, delay); - - x += child.LayoutWidth / 2 + spacing; + child.Delay(delay) + .MoveTo(position, 300, Easing.OutExpo) + .RotateTo(rotation, 300, Easing.OutExpo) + .ScaleTo(scale, 400, Easing.OutElasticQuarter); delay += stagger; } } - protected virtual void ApplyLayoutToCard(HandCard card, Vector2 position, float rotation, float scale, double delay) + private int getActiveCardIndex() { - card.Delay(delay) - .MoveTo(position, 300, Easing.OutExpo) - .RotateTo(rotation, 300, Easing.OutExpo) - .ScaleTo(scale, 400, Easing.OutElasticQuarter); + // the mouse can temporarily leave the dragged card, so dragged card should take precedence + for (int i = 0; i < cardContainer.Count; i++) + { + if (cardContainer[i].CardDragged) + return i; + } + + for (int i = 0; i < cardContainer.Count; i++) + { + if (cardContainer[i].CardHovered) + return i; + } + + return -1; + } + + protected void CalculateCardLayout( + int index, + int activeIndex, + out Vector2 position, + out float rotation, + out float scale) + { + float x = GetCardX(index, activeIndex); + + position = GetArcPosition(x); + rotation = GetArcRotation(x); + scale = index == activeIndex ? HOVER_SCALE : 1; + + if (index == activeIndex) + position += GetCardUpwardsDirection(rotation) * HoverYOffset; + } + + protected virtual void CalculateDraggedCardLayout(Vector2 dragPosition, out Vector2 position, out float rotation, out float scale) + { + position = dragPosition; + rotation = 0; + scale = HOVER_SCALE; + } + + /// + /// Represents the total width of the layout for all cards in the hand. + /// + /// + /// Does not account for extra space needed for spreading the cards adjacent to the active card apart. + /// + protected float TotalLayoutWidth => cardContainer.Count * (RankedPlayCard.SIZE.X + card_spacing) - card_spacing; + + protected float GetCardX(int index, int activeIndex) + { + float x = -TotalLayoutWidth / 2 + + index * (RankedPlayCard.SIZE.X + card_spacing) + + RankedPlayCard.SIZE.X / 2; + + if (activeIndex < 0 || cardContainer.Count <= 1) + return x; + + // if a card is hovered or dragged, the adjacent cards should get spread apart + int distance = Math.Abs(index - activeIndex); + int direction = Math.Sign(index - activeIndex); + + float baseOffset = RankedPlayCard.SIZE.X * 0.1f; + + switch (direction) + { + // special case for the left card when there's only 2 cards + // too much offset looks kinda odd here so it's reduced + case -1 when cardContainer.Count == 2: + x -= baseOffset + 3; + break; + + case -1: + x -= baseOffset + 10 / MathF.Pow(distance, 2); + break; + + case 1: + // cards right to the active card have a higher offset because they are partially + // covering the cards to their left + x += baseOffset + 20 / MathF.Pow(distance, 2); + break; + } + + return x; + } + + protected static Vector2 GetArcPosition(float x) => + new Vector2(x, MathF.Pow(MathF.Abs(x / 250), 2) * 20 - 10); + + protected static float GetArcRotation(float x) => x * 0.03f; + + protected static Vector2 GetCardUpwardsDirection(float rotation) + { + float angle = MathHelper.DegreesToRadians(rotation - 90); + + return new Vector2(MathF.Cos(angle), MathF.Sin(angle)); } #endregion diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/OpponentHandOfCards.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/OpponentHandOfCards.cs index e2e27d820d..a0aa0d9659 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/OpponentHandOfCards.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/OpponentHandOfCards.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Game.Online.RankedPlay; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand { @@ -24,5 +25,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand card.State = cardState; } } + + protected override void CalculateDraggedCardLayout(Vector2 dragPosition, out Vector2 position, out float rotation, out float scale) + { + float maxExtent = TotalLayoutWidth / 2; + + float x = float.Clamp(dragPosition.X, -maxExtent, maxExtent); + + scale = HOVER_SCALE; + rotation = GetArcRotation(x); + position = GetArcPosition(x) + GetCardUpwardsDirection(rotation) * 60; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/PlayerHandOfCards.PlayerHandCard.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/PlayerHandOfCards.PlayerHandCard.cs index f46f6cc388..4eb1d647b0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/PlayerHandOfCards.PlayerHandCard.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/PlayerHandOfCards.PlayerHandCard.cs @@ -88,14 +88,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand AddInternal(new HoverSounds()); } - protected override void Update() - { - base.Update(); - - if (IsDragged) - updateDragMovement(); - } - protected override void OnStateChanged(ValueChangedEvent state) { base.OnStateChanged(state); @@ -177,7 +169,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand #region Drag/Drop private Vector2 dragOffset; - private Vector2 dragPosition; protected override bool OnDragStart(DragStartEvent e) { @@ -194,7 +185,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand protected override void OnDrag(DragEvent e) { - dragPosition = e.MousePosition - AnchorPosition + dragOffset; + DragPosition = e.MousePosition - AnchorPosition + dragOffset; } protected override void OnDragEnd(DragEndEvent e) @@ -204,11 +195,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand CardDragged = false; } - private void updateDragMovement() - { - Position = Vector2.Lerp(dragPosition, Position, MathF.Exp(-0.03f * (float)Time.Elapsed)); - } - #endregion } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/PlayerHandOfCards.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/PlayerHandOfCards.cs index d890148ea6..7ec73b7662 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/PlayerHandOfCards.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/PlayerHandOfCards.cs @@ -12,7 +12,6 @@ using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Online.RankedPlay; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card; -using osuTK; using osuTK.Input; namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand @@ -216,13 +215,5 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand if (SelectionMode == HandSelectionMode.Single && !card.Selected) card.TriggerClick(); } - - protected override void ApplyLayoutToCard(HandCard card, Vector2 position, float rotation, float scale, double delay) - { - if (card.IsDragged) - return; - - base.ApplyLayoutToCard(card, position, rotation, scale, delay); - } } }