using Godot; using RobotAndDonkey.Game.Cards; using RobotAndDonkey.Game.Modifiers; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using RobotAndDonkey.Game; public partial class CardRow : Control { public override void _Ready() { CacheBackgroundPanels(); UpdateBackgroundVisibility(); } public override void _Process(double delta) { if (s_CardPressActive && !Input.IsMouseButtonPressed(MouseButton.Left)) s_CardPressActive = false; if (OrderedCards.Count == 0) { m_LayoutDirty = false; return; } var needsLayout = m_LayoutDirty || GlobalPosition != m_LastPosition || Size != m_LastSize; if (!needsLayout) return; m_LayoutDirty = false; LayoutCards(); if (GetParent() is not Container) SetAnchorsPreset(LayoutPreset.Center); } public override void _GuiInput(InputEvent @event) { if (@event is InputEventMouseButton mouseButton && mouseButton.ButtonIndex == MouseButton.Left && mouseButton.Pressed) ClearSelection(); } public void Configure(IReadOnlyList cards, Tween tween, bool celebrateModifiers, bool drag) { Debug.WriteLine($"Configure {cards.Count} cards, celebrate={celebrateModifiers}, drag={drag}"); if (CardScene == null) { GD.PushWarning($"{nameof(CardRow)} has no CardScene assigned. No cards will be shown."); return; } if (cards.SequenceEqual(OrderedCards)) return; var newSet = new HashSet(cards.Select(c => c.CardId)); var toRemove = OrderedCards.Where(c => !newSet.Contains(c.CardId)).ToList(); foreach (var card in toRemove) { if (!CardToControl.Remove(card.CardId, out var control)) continue; control.QueueFree(); } OrderedCards.Clear(); OrderedCards.AddRange(cards); var newControls = new List(); for (var index = 0; index < cards.Count; index++) { var card = cards[index]; if (!CardToControl.TryGetValue(card.CardId, out var control)) { Debug.WriteLine($"Add new card {card.Id}"); control = CreateCardControl(card); CardToControl.Add(card.CardId, control); AddChild(control); newControls.Add(control); } else { control.Configure(card, this); } } LayoutCards(); foreach (var control in newControls) control.SnapToDesired(); if (celebrateModifiers) { foreach (var card in cards) { foreach (var modifier in card.Modifiers) { ModifyCard(card, modifier.Id, tween); } } } } private void GetLayoutMetrics(Card card, out float visualWidth, out float leftOffset, out float topOffset, out float bottomOffset) { var size = CardSize; var desiredScale = Vector2.One; var desiredPivot = size / 2f; if (CardToControl.TryGetValue(card.CardId, out var control)) { size = control.Size; desiredScale = control.DesiredScale; desiredPivot = control.DesiredPivotOffset; } visualWidth = size.X * desiredScale.X; leftOffset = (1f - desiredScale.X) * desiredPivot.X; topOffset = (1f - desiredScale.Y) * desiredPivot.Y; bottomOffset = topOffset + size.Y * desiredScale.Y; } private void CalculateCardPositions(IReadOnlyList cards) { m_CardPositions.Clear(); var count = cards.Count; if (count == 0) return; var cardHeight = CardSize.Y; var cardIds = new Guid[count]; var visualWidths = new float[count]; var leftOffsets = new float[count]; var topOffsets = new float[count]; var bottomOffsets = new float[count]; var totalVisualWidth = 0f; for (var index = 0; index < count; index++) { var card = cards[index]; cardIds[index] = card.CardId; GetLayoutMetrics(card, out var visualWidth, out var leftOffset, out var topOffset, out var bottomOffset); visualWidths[index] = visualWidth; leftOffsets[index] = leftOffset; topOffsets[index] = topOffset; bottomOffsets[index] = bottomOffset; totalVisualWidth += visualWidth; } var availableWidth = Size.X > 0 ? Size.X : CardSize.X; var availableHeight = Size.Y > 0 ? Size.Y : CardSize.Y; if (availableWidth <= 0) availableWidth = totalVisualWidth + (count - 1) * CardSpacing; if (availableHeight <= 0) availableHeight = cardHeight; if (WrapCards) { var rowSpacing = CardSpacing; var rowTopOffsets = new List(); var rowBottomOffsets = new List(); var rowIndices = new int[count]; var rowWidth = 0f; var rowIndex = 0; var rowHasCards = false; var rowTopOffset = 0f; var rowBottomOffset = 0f; for (var index = 0; index < count; index++) { var cardWidthForLayout = visualWidths[index]; var spacing = rowWidth > 0f ? CardSpacing : 0f; if (rowWidth > 0f && rowWidth + spacing + cardWidthForLayout > availableWidth) { rowTopOffsets.Add(rowTopOffset); rowBottomOffsets.Add(rowBottomOffset); rowIndex += 1; rowWidth = 0f; rowHasCards = false; spacing = 0f; } if (!rowHasCards) { rowTopOffset = topOffsets[index]; rowBottomOffset = bottomOffsets[index]; rowHasCards = true; } else { rowTopOffset = Mathf.Min(rowTopOffset, topOffsets[index]); rowBottomOffset = Mathf.Max(rowBottomOffset, bottomOffsets[index]); } rowIndices[index] = rowIndex; rowWidth += spacing + cardWidthForLayout; } if (rowHasCards) { rowTopOffsets.Add(rowTopOffset); rowBottomOffsets.Add(rowBottomOffset); } var rowTops = new float[rowTopOffsets.Count]; if (rowTops.Length > 0) { rowTops[0] = 0f; for (var index = 1; index < rowTopOffsets.Count; index++) { var previousBottom = rowTops[index - 1] + rowBottomOffsets[index - 1]; rowTops[index] = previousBottom + rowSpacing - rowTopOffsets[index]; } } var currentRow = 0; var currentVisualLeft = 0f; for (var index = 0; index < count; index++) { var cardRowIndex = rowIndices[index]; if (cardRowIndex != currentRow) { currentRow = cardRowIndex; currentVisualLeft = 0f; } if (currentVisualLeft > 0f) currentVisualLeft += CardSpacing; var positionX = currentVisualLeft - leftOffsets[index]; var positionY = rowTops[cardRowIndex]; m_CardPositions[cardIds[index]] = new Vector2(positionX, positionY); currentVisualLeft += visualWidths[index]; } if (rowTopOffsets.Count > 0) { var totalHeight = rowTops[^1] + rowBottomOffsets[^1]; CustomMinimumSize = new(0, totalHeight); } } else { var spacing = CardSpacing; var widthWithBaseSpacing = totalVisualWidth + (count - 1) * CardSpacing; if (count > 1) { if (widthWithBaseSpacing > availableWidth) { spacing = (availableWidth - totalVisualWidth) / (count - 1); } } var startVisualLeft = (availableWidth - widthWithBaseSpacing) * 0.5f; var y = (availableHeight - cardHeight) * 0.5f; var currentVisualLeft = startVisualLeft; for (var index = 0; index < count; index++) { var positionX = currentVisualLeft - leftOffsets[index]; m_CardPositions[cardIds[index]] = new Vector2(positionX, y); currentVisualLeft += visualWidths[index] + spacing; } } } public void RunCard(Card card, Tween tween) { if (!CardToControl.Remove(card.CardId, out var control)) return; Main.Instance.Music.Play(MusicManager.ESound.Card); control.QueueFree(); OrderedCards.Remove(card); LayoutCards(); } public void ModifyCard(Card card, EModifierId modifier, Tween tween) { if (!CardToControl.TryGetValue(card.CardId, out var control)) { Debug.WriteLine($"Failed to modify {card.Id}"); return; } var hasModifier = card.Modifiers.Any(m => m.Id == modifier && m.DebuffSources.Count == 0); Debug.WriteLine($"Modify {card.Id}"); control.Configure(card, this); if (hasModifier) Main.Instance.Music.Play(modifier); } private CardControl CreateCardControl(Card card) { var node = CardScene.Instantiate(); node.Name = $"Card_{card.Id}"; node.SetAnchorsPreset(LayoutPreset.TopLeft); node.Size = CardSize; node.PivotOffset = CardSize / 2f; node.CanDrag = CardsCanDrag; node.CanSelect = CardsCanSelect; node.Configure(card, this); node.Connect(CardControl.SignalName.CardDroppedOn, new(this, nameof(OnCardDroppedOn))); node.Connect(CardControl.SignalName.CardSelectionChanged, new(this, nameof(OnCardSelectionChanged))); node.Connect(CardControl.SignalName.CardStateChanged, new(this, nameof(OnCardStateChanged))); return node; } private void OnCardStateChanged(CardControl card) { m_LayoutDirty = true; } private void OnCardSelectionChanged(CardControl card, bool selected) { if (!m_SuppressSelectionValidation && CanChangeSelection != null && !CanChangeSelection(card, selected)) { m_SuppressSelectionValidation = true; card.Selected = !selected; m_SuppressSelectionValidation = false; return; } if (m_SuppressSelectionSignals) return; if (selected) SelectionContext = this; if (selected && !SupportsMultiSelect) { m_SuppressSelectionValidation = true; foreach (var other in CardToControl.Values) { if (other != card && other.Selected) other.Selected = false; } m_SuppressSelectionValidation = false; } EmitSignal(SignalName.SelectionChanged, CardToControl.Values.Where(c => c.Selected).ToArray()); } private void OnCardDroppedOn(CardControl source, CardControl target, bool before) { if (source == target) return; var sourceRow = source.CardRow; var targetRow = target.CardRow; var fromCardId = sourceRow.CardToControl.FirstOrDefault(kv => kv.Value == source).Key; var toCardId = targetRow.CardToControl.FirstOrDefault(kv => kv.Value == target).Key; if (fromCardId == Guid.Empty || toCardId == Guid.Empty) return; var fromCard = sourceRow.OrderedCards.Single(c => c.CardId == fromCardId); var toCard = OrderedCards.Single(c => c.CardId == toCardId); var draggedCards = GetDraggedCards(sourceRow, fromCard); if (draggedCards.Contains(toCard)) { if (SelectedCards.Contains(toCard)) { draggedCards = [fromCard]; } else { return; } } var targetCards = targetRow.OrderedCards; var toIndex = targetCards.IndexOf(toCard); if (toIndex == -1) return; var insertIndex = before ? toIndex : toIndex + 1; MoveCards(sourceRow, targetRow, draggedCards, insertIndex); } public override bool _CanDropData(Vector2 atPosition, Variant data) { if (Disabled || !CardsCanDrag) return false; if (data.Obj is not CardControl sourceControl) return false; var sourceRow = sourceControl.CardRow; var fromCardId = sourceRow.CardToControl.FirstOrDefault(kv => kv.Value == sourceControl).Key; if (fromCardId == Guid.Empty) return false; var fromCard = sourceRow.OrderedCards.Single(c => c.CardId == fromCardId); var draggedCards = GetDraggedCards(sourceRow, fromCard); if (draggedCards.Count == 0) return false; if (sourceRow != this && OccupiedSpace + draggedCards.Select(c => c.OccupiedSpace).Sum() > MaxCards) return false; sourceControl.UpdateDragPreviewForDropTarget(null); return true; } public override void _DropData(Vector2 atPosition, Variant data) { if (Disabled || !CardsCanDrag) return; if (data.Obj is not CardControl sourceControl) return; var sourceRow = sourceControl.CardRow; var fromCardId = sourceRow?.CardToControl.FirstOrDefault(kv => kv.Value == sourceControl).Key; if (fromCardId == Guid.Empty) return; var fromCard = sourceRow.OrderedCards.Single(c => c.CardId == fromCardId); var draggedCards = GetDraggedCards(sourceRow, fromCard); if (draggedCards.Count == 0) return; if (OccupiedSpace + draggedCards.Select(c => c.OccupiedSpace).Sum() > MaxCards) return; var insertIndex = GetDropIndex(atPosition); MoveCards(sourceRow, this, draggedCards, insertIndex); } private List GetDraggedCards(CardRow sourceRow, Card fromCard) { if (fromCard == null) return []; if (!sourceRow.SupportsMultiDrag) return [fromCard]; var selected = sourceRow.SelectedCardIndices.Select(i => sourceRow.OrderedCards[i]).ToList(); if (selected.Count > 1 && selected.Contains(fromCard)) { var originalOrder = sourceRow.OrderedCards; return selected.OrderBy(c => originalOrder.IndexOf(c)).ToList(); } return [fromCard]; } private void MoveCards(CardRow sourceRow, CardRow targetRow, List draggedCards, int rawInsertIndex) { if (draggedCards == null || draggedCards.Count == 0) return; var draggedSet = new HashSet(draggedCards); if (sourceRow == targetRow) { var original = sourceRow.OrderedCards.ToList(); if (original.Count == 0) return; var insertIndex = Mathf.Clamp(rawInsertIndex, 0, original.Count); var removedBefore = 0; foreach (var card in draggedCards) { var idx = original.IndexOf(card); if (idx >= 0 && idx < insertIndex) removedBefore++; } var adjustedIndex = insertIndex - removedBefore; var newOrder = original.Where(c => !draggedSet.Contains(c)).ToList(); adjustedIndex = Mathf.Clamp(adjustedIndex, 0, newOrder.Count); newOrder.InsertRange(adjustedIndex, draggedCards); sourceRow.Configure(newOrder, null, false, true); sourceRow.EmitSignal(SignalName.OrderChanged); } else { var sourceList = sourceRow.OrderedCards.ToList(); var targetList = targetRow.OrderedCards.ToList(); foreach (var card in draggedCards) { sourceList.Remove(card); targetList.Remove(card); } var insertIndex = Mathf.Clamp(rawInsertIndex, 0, targetList.Count); targetList.InsertRange(insertIndex, draggedCards); sourceRow.Configure(sourceList, null, false, true); targetRow.Configure(targetList, null, false, true); targetRow.EmitSignal(SignalName.OrderChanged); } } private int GetDropIndex(Vector2 localPosition) { var count = OrderedCards.Count; if (count == 0) return 0; var orderedControls = OrderedCards.Select(card => CardToControl.GetValueOrDefault(card.CardId)).Where(control => control != null).ToList(); if (orderedControls.Count == 0) return 0; var mouseX = localPosition.X; if (mouseX <= orderedControls[0].Position.X) return 0; var last = orderedControls[^1]; if (mouseX >= last.Position.X + CardSize.X) return orderedControls.Count; for (var i = 0; i < orderedControls.Count; i++) { var control = orderedControls[i]; if (mouseX < control.Position.X) return i; } return orderedControls.Count; } private void LayoutCards() { Debug.WriteLine("Layout"); var count = OrderedCards.Count; if (count == 0) return; m_LastPosition = GlobalPosition; m_LastSize = Size; CalculateCardPositions(OrderedCards); foreach (var card in OrderedCards) { if (!CardToControl.TryGetValue(card.CardId, out var control)) continue; if (m_CardPositions.TryGetValue(card.CardId, out var targetPos)) control.SetDesiredPosition(targetPos); } } private void CacheBackgroundPanels() { m_Backgrounds.Clear(); foreach (var child in GetChildren()) { if (child is Control control && control.Name.ToString().StartsWith("Background")) { control.ZIndex = 0; control.MouseFilter = MouseFilterEnum.Ignore; m_Backgrounds.Add(control); } } } private void UpdateBackgroundVisibility() { if (m_Backgrounds.Count == 0) return; var clamped = Mathf.Clamp(BackgroundVariant, 0, m_Backgrounds.Count - 1); for (var i = 0; i < m_Backgrounds.Count; i++) m_Backgrounds[i].Visible = i == clamped; } public void ApplySelection(IReadOnlyCollection selectedCardIds, bool suppressSignals) { m_SuppressSelectionValidation = true; m_SuppressSelectionSignals = suppressSignals; foreach (var kvp in CardToControl) { kvp.Value.Selected = selectedCardIds.Contains(kvp.Key); } m_SuppressSelectionSignals = false; m_SuppressSelectionValidation = false; } public void ClearSelection() { ApplySelection([], true); EmitSignal(SignalName.SelectionChanged, []); } public static List GetSortedCards(IEnumerable cards) { return cards.Order(Comparer.Create((a, b) => { var cmp = a.Rarity.CompareTo(b.Rarity); if (cmp == 0) cmp = a.Id.CompareTo(b.Id); if (cmp == 0) cmp = a.Modifiers.Count.CompareTo(b.Modifiers.Count); if (cmp == 0) cmp = a.CardId.CompareTo(b.CardId); return cmp; })).ToList(); } [Export] public PackedScene CardScene { get; set; } [Export] public Vector2 CardSize { get; set; } = new(96, 96); [Export] public float CardSpacing { get; set; } = 8f; [Export] public float AnimationDuration { get; set; } = 0.15f; [Export] public bool CardsCanDrag { get; set; } = true; [Export] public bool CardsCanSelect { get; set; } = false; [Export] public bool SupportsMultiSelect { get; set; } = false; [Export] public bool SupportsMultiDrag { get; set; } = false; [Export] public int BackgroundVariant { get => m_BackgroundVariant; set { if (m_BackgroundVariant == value) return; m_BackgroundVariant = value; UpdateBackgroundVisibility(); } } public Func CanChangeSelection { get; set; } public static bool IsCardPressActive => s_CardPressActive; public static void NotifyCardPress(bool pressed) { s_CardPressActive = pressed; } public static CardRow SelectionContext { get => s_SelectionContext; set { if (s_SelectionContext == value) return; if (s_SelectionContext != null) { foreach (var card in s_SelectionContext.CardToControl) card.Value.Selected = false; } s_SelectionContext = value; } } [Export] public bool Disabled { get => m_Disabled; set { if (m_Disabled == value) return; m_Disabled = value; foreach (var card in CardToControl) card.Value.Disabled = m_Disabled; } } [Export] public int MaxCards { get; set; } = int.MaxValue; [Export] public bool WrapCards { get; set; } = false; public int OccupiedSpace => OrderedCards.Select(c => c.OccupiedSpace).Sum(); public IEnumerable SelectedCardIndices => OrderedCards.Select((c, i) => (Card: CardToControl[c.CardId], Index: i)).Where(pair => pair.Card.Selected).Select(pair => pair.Index); public IReadOnlyList SelectedCardIds => OrderedCards.Where(card => CardToControl.TryGetValue(card.CardId, out var control) && control.Selected) .Select(card => card.CardId) .ToList(); public IReadOnlyList SelectedCards => OrderedCards.Where(card => CardToControl.TryGetValue(card.CardId, out var control) && control.Selected) .ToList(); public List OrderedCards { get; } = []; public Dictionary CardToControl => m_CardToControl; [Signal] public delegate void OrderChangedEventHandler(); [Signal] public delegate void SelectionChangedEventHandler(CardControl[] selection); private static CardRow s_SelectionContext; private static bool s_CardPressActive; private readonly List m_Backgrounds = []; private readonly Dictionary m_CardToControl = new(); private readonly Dictionary m_CardPositions = []; private int m_BackgroundVariant; private bool m_Disabled; private bool m_SuppressSelectionValidation; private bool m_SuppressSelectionSignals; private bool m_LayoutDirty; private Vector2 m_LastPosition; private Vector2 m_LastSize; }