Code up the card part

This commit is contained in:
Michal Pikulski
2025-11-06 11:11:15 +01:00
parent b6d8586eab
commit 95daea8d34
24 changed files with 3179 additions and 4 deletions

View File

@@ -153,6 +153,10 @@ namespace UI.CardSystem
{
if (boosterOpeningPage != null && UIPageController.Instance != null)
{
// Pass current booster count to the opening page
int boosterCount = CardSystemManager.Instance?.GetBoosterPackCount() ?? 0;
boosterOpeningPage.SetAvailableBoosterCount(boosterCount);
UIPageController.Instance.PushPage(boosterOpeningPage);
}
}

View File

@@ -1,9 +1,12 @@
using System.Collections;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
using Data.CardSystem;
using Pixelplacement;
using UI.Core;
using UI.CardSystem.DragDrop;
using UI.DragAndDrop.Core;
using UnityEngine;
using UnityEngine.UI;
@@ -11,7 +14,7 @@ namespace UI.CardSystem
{
/// <summary>
/// UI page for opening booster packs and displaying the cards received.
/// Automatically triggers the opening when the page is shown.
/// Manages the entire booster opening flow with drag-and-drop interaction.
/// </summary>
public class BoosterOpeningPage : UIPage
{
@@ -19,13 +22,26 @@ namespace UI.CardSystem
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private Button closeButton;
[Header("Booster Management")]
[SerializeField] private GameObject[] boosterPackInstances; // Booster prefabs/instances
[SerializeField] private SlotContainer bottomRightSlots; // Holds waiting boosters
[SerializeField] private DraggableSlot centerOpeningSlot; // Where booster goes to open
[Header("Card Display")]
[SerializeField] private Transform cardDisplayContainer;
[SerializeField] private CardDisplay cardDisplayPrefab;
[SerializeField] private GameObject flippableCardPrefab; // Placeholder for card backs
[SerializeField] private float cardSpacing = 150f;
[Header("Settings")]
[SerializeField] private float cardRevealDelay = 0.5f;
[SerializeField] private float cardSpacing = 50f;
[SerializeField] private float boosterDisappearDuration = 0.5f;
private int _availableBoosterCount;
private BoosterPackDraggable _currentBoosterInCenter;
private List<GameObject> _currentRevealedCards = new List<GameObject>();
private CardData[] _currentCardData;
private int _revealedCardCount;
private bool _isProcessingOpening;
private void Awake()
{
@@ -51,6 +67,15 @@ namespace UI.CardSystem
{
closeButton.onClick.RemoveListener(OnCloseButtonClicked);
}
// Unsubscribe from slot events
if (centerOpeningSlot != null)
{
centerOpeningSlot.OnOccupied -= OnBoosterPlacedInCenter;
}
// Unsubscribe from booster events
UnsubscribeFromAllBoosters();
}
private void OnCloseButtonClicked()
@@ -61,6 +86,371 @@ namespace UI.CardSystem
}
}
/// <summary>
/// Set the number of available booster packs before showing the page
/// </summary>
public void SetAvailableBoosterCount(int count)
{
_availableBoosterCount = count;
}
public override void TransitionIn()
{
base.TransitionIn();
InitializeBoosterDisplay();
}
public override void TransitionOut()
{
CleanupPage();
base.TransitionOut();
}
/// <summary>
/// Initialize the booster pack display based on available count
/// </summary>
private void InitializeBoosterDisplay()
{
if (boosterPackInstances == null || boosterPackInstances.Length == 0)
{
Debug.LogWarning("BoosterOpeningPage: No booster pack instances assigned!");
return;
}
// Calculate how many boosters to show (capped by array size)
int visibleCount = Mathf.Min(_availableBoosterCount, boosterPackInstances.Length);
// Show/hide boosters and assign to slots
for (int i = 0; i < boosterPackInstances.Length; i++)
{
if (boosterPackInstances[i] == null) continue;
bool shouldShow = i < visibleCount;
boosterPackInstances[i].SetActive(shouldShow);
if (shouldShow)
{
// Get the booster draggable component
BoosterPackDraggable booster = boosterPackInstances[i].GetComponent<BoosterPackDraggable>();
if (booster != null)
{
// Reset state
booster.ResetTapCount();
booster.SetTapToOpenEnabled(false); // Disable tap-to-open until in center
// Subscribe to events
booster.OnReadyToOpen += OnBoosterReadyToOpen;
// Assign to bottom-right slot if slots available
if (bottomRightSlots != null && i < bottomRightSlots.SlotCount)
{
DraggableSlot slot = bottomRightSlots.GetSlotAtIndex(i);
if (slot != null)
{
booster.AssignToSlot(slot, false);
}
}
}
}
}
// Subscribe to center slot events
if (centerOpeningSlot != null)
{
centerOpeningSlot.OnOccupied += OnBoosterPlacedInCenter;
}
}
/// <summary>
/// Handle when a booster is placed in the center opening slot
/// </summary>
private void OnBoosterPlacedInCenter(DraggableObject draggable)
{
BoosterPackDraggable booster = draggable as BoosterPackDraggable;
if (booster == null) return;
_currentBoosterInCenter = booster;
// Lock the slot so it can't be dragged out
centerOpeningSlot.SetLocked(true);
// Enable tap-to-open and reset tap count
booster.ResetTapCount();
booster.SetTapToOpenEnabled(true);
}
/// <summary>
/// Handle tap-to-place: When player taps a booster in bottom slots, move it to center
/// </summary>
public void OnBoosterTappedInBottomSlot(BoosterPackDraggable booster)
{
if (_currentBoosterInCenter != null || centerOpeningSlot == null)
return; // Center slot already occupied
// Move booster to center slot
booster.AssignToSlot(centerOpeningSlot, true);
}
/// <summary>
/// Handle when booster is ready to open (after max taps)
/// </summary>
private void OnBoosterReadyToOpen(BoosterPackDraggable booster)
{
if (_isProcessingOpening) return;
StartCoroutine(ProcessBoosterOpening(booster));
}
/// <summary>
/// Process the booster opening sequence
/// </summary>
private IEnumerator ProcessBoosterOpening(BoosterPackDraggable booster)
{
_isProcessingOpening = true;
// Call CardSystemManager to open the pack
if (CardSystemManager.Instance != null)
{
List<CardData> revealedCardsList = CardSystemManager.Instance.OpenBoosterPack();
_currentCardData = revealedCardsList.ToArray();
// Animate booster disappearing
yield return StartCoroutine(AnimateBoosterDisappear(booster));
// Show card backs
SpawnCardBacks(_currentCardData.Length);
// Wait for player to reveal all cards
yield return StartCoroutine(WaitForCardReveals());
// Check if more boosters available
_availableBoosterCount--;
if (_availableBoosterCount > 0)
{
// Show next booster
yield return StartCoroutine(ShowNextBooster());
}
else
{
// No more boosters, auto-close page
yield return new WaitForSeconds(1f);
if (UIPageController.Instance != null)
{
UIPageController.Instance.PopPage();
}
}
}
_isProcessingOpening = false;
}
/// <summary>
/// Animate the booster pack disappearing
/// </summary>
private IEnumerator AnimateBoosterDisappear(BoosterPackDraggable booster)
{
if (booster == null) yield break;
// Scale down and fade out
Transform boosterTransform = booster.transform;
Tween.LocalScale(boosterTransform, Vector3.zero, boosterDisappearDuration, 0f, Tween.EaseInBack);
// Also fade the visual if it has a CanvasGroup
CanvasGroup boosterCg = booster.GetComponentInChildren<CanvasGroup>();
if (boosterCg != null)
{
Tween.Value(1f, 0f, (val) => boosterCg.alpha = val, boosterDisappearDuration, 0f);
}
yield return new WaitForSeconds(boosterDisappearDuration);
// Destroy the booster
Destroy(booster.gameObject);
_currentBoosterInCenter = null;
// Unlock center slot
centerOpeningSlot.SetLocked(false);
}
/// <summary>
/// Spawn card back placeholders for revealing
/// </summary>
private void SpawnCardBacks(int count)
{
if (flippableCardPrefab == null || cardDisplayContainer == null)
{
Debug.LogWarning("BoosterOpeningPage: Missing card prefab or container!");
return;
}
_currentRevealedCards.Clear();
_revealedCardCount = 0;
// Calculate positions
float totalWidth = (count - 1) * cardSpacing;
float startX = -totalWidth / 2f;
for (int i = 0; i < count; i++)
{
GameObject cardObj = Instantiate(flippableCardPrefab, cardDisplayContainer);
RectTransform cardRect = cardObj.GetComponent<RectTransform>();
if (cardRect != null)
{
cardRect.anchoredPosition = new Vector2(startX + (i * cardSpacing), 0);
}
// Add button to handle reveal on click
Button cardButton = cardObj.GetComponent<Button>();
if (cardButton == null)
{
cardButton = cardObj.AddComponent<Button>();
}
int cardIndex = i; // Capture for closure
cardButton.onClick.AddListener(() => OnCardClicked(cardIndex, cardObj));
_currentRevealedCards.Add(cardObj);
// Animate cards flying in
cardRect.localScale = Vector3.zero;
Tween.LocalScale(cardRect, Vector3.one, 0.3f, i * 0.1f, Tween.EaseOutBack);
}
}
/// <summary>
/// Handle card click to reveal
/// </summary>
private void OnCardClicked(int cardIndex, GameObject cardObj)
{
if (cardIndex >= _currentCardData.Length) return;
// Flip/reveal animation (placeholder - just show card data for now)
CardDisplay cardDisplay = cardObj.GetComponent<CardDisplay>();
if (cardDisplay != null)
{
cardDisplay.SetupCard(_currentCardData[cardIndex]);
}
// Disable button so it can't be clicked again
Button cardButton = cardObj.GetComponent<Button>();
if (cardButton != null)
{
cardButton.interactable = false;
}
// Scale punch animation
Tween.LocalScale(cardObj.transform, Vector3.one * 1.2f, 0.15f, 0f, Tween.EaseOutBack,
completeCallback: () => {
Tween.LocalScale(cardObj.transform, Vector3.one, 0.15f, 0f, Tween.EaseInBack);
});
_revealedCardCount++;
}
/// <summary>
/// Wait until all cards are revealed
/// </summary>
private IEnumerator WaitForCardReveals()
{
while (_revealedCardCount < _currentCardData.Length)
{
yield return null;
}
// All cards revealed, wait a moment
yield return new WaitForSeconds(1f);
// Clear cards
foreach (GameObject card in _currentRevealedCards)
{
if (card != null)
{
// Animate out
Tween.LocalScale(card.transform, Vector3.zero, 0.3f, 0f, Tween.EaseInBack,
completeCallback: () => Destroy(card));
}
}
_currentRevealedCards.Clear();
yield return new WaitForSeconds(0.5f);
}
/// <summary>
/// Show the next booster pack
/// </summary>
private IEnumerator ShowNextBooster()
{
// Find the next inactive booster and activate it
for (int i = 0; i < boosterPackInstances.Length; i++)
{
if (boosterPackInstances[i] != null && !boosterPackInstances[i].activeSelf)
{
boosterPackInstances[i].SetActive(true);
BoosterPackDraggable booster = boosterPackInstances[i].GetComponent<BoosterPackDraggable>();
if (booster != null)
{
booster.ResetTapCount();
booster.SetTapToOpenEnabled(false);
booster.OnReadyToOpen += OnBoosterReadyToOpen;
// Assign to first available slot
DraggableSlot slot = bottomRightSlots?.GetAvailableSlots().FirstOrDefault();
if (slot != null)
{
booster.AssignToSlot(slot, true);
}
}
break;
}
}
yield return null;
}
/// <summary>
/// Clean up the page when hidden
/// </summary>
private void CleanupPage()
{
UnsubscribeFromAllBoosters();
// Clear any remaining cards
foreach (GameObject card in _currentRevealedCards)
{
if (card != null)
Destroy(card);
}
_currentRevealedCards.Clear();
_currentBoosterInCenter = null;
_isProcessingOpening = false;
}
/// <summary>
/// Unsubscribe from all booster events
/// </summary>
private void UnsubscribeFromAllBoosters()
{
if (boosterPackInstances == null) return;
foreach (GameObject boosterObj in boosterPackInstances)
{
if (boosterObj == null) continue;
BoosterPackDraggable booster = boosterObj.GetComponent<BoosterPackDraggable>();
if (booster != null)
{
booster.OnReadyToOpen -= OnBoosterReadyToOpen;
}
}
}
protected override void DoTransitionIn(System.Action onComplete)
{
// Simple fade in animation

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 053a2ff2538541699b134b07a07edecb
timeCreated: 1762420654

View File

@@ -0,0 +1,120 @@
using UI.DragAndDrop.Core;
using UnityEngine;
namespace UI.CardSystem.DragDrop
{
/// <summary>
/// Booster pack specific implementation of DraggableObject.
/// Manages booster pack behavior and opening logic.
/// </summary>
public class BoosterPackDraggable : DraggableObject
{
[Header("Booster Pack Settings")]
[SerializeField] private bool canOpenOnDrop = true;
[SerializeField] private bool canOpenOnDoubleClick = true;
[Header("Tap to Open")]
[SerializeField] private bool canTapToOpen = true;
[SerializeField] private int maxTapsToOpen = 3;
// Events
public event System.Action<BoosterPackDraggable> OnBoosterOpened;
public event System.Action<BoosterPackDraggable, int, int> OnTapped; // (booster, currentTap, maxTaps)
public event System.Action<BoosterPackDraggable> OnReadyToOpen; // Final tap reached
private bool _isOpening;
private float _lastClickTime;
private int _currentTapCount;
public bool IsOpening => _isOpening;
public int CurrentTapCount => _currentTapCount;
protected override void OnPointerUpHook(bool longPress)
{
base.OnPointerUpHook(longPress);
// Handle tap-to-open logic (only when in slot and not dragged)
if (canTapToOpen && !_wasDragged && !longPress && CurrentSlot != null)
{
_currentTapCount++;
OnTapped?.Invoke(this, _currentTapCount, maxTapsToOpen);
if (_currentTapCount >= maxTapsToOpen)
{
OnReadyToOpen?.Invoke(this);
}
return; // Don't process double-click if tap-to-open is active
}
// Check for double click
if (canOpenOnDoubleClick && !longPress && !_wasDragged)
{
float timeSinceLastClick = Time.time - _lastClickTime;
if (timeSinceLastClick < 0.3f) // Double click threshold
{
TriggerOpen();
}
_lastClickTime = Time.time;
}
}
protected override void OnDragEndedHook()
{
base.OnDragEndedHook();
// Optionally trigger open when dropped in specific zones
if (canOpenOnDrop)
{
// Could check if dropped in an "opening zone"
// For now, just a placeholder
}
}
/// <summary>
/// Trigger the booster pack opening animation and logic
/// </summary>
public void TriggerOpen()
{
if (_isOpening)
return;
_isOpening = true;
OnBoosterOpened?.Invoke(this);
// The actual opening logic (calling CardSystemManager) should be handled
// by the UI page or controller that manages this booster pack
// Visual feedback would be handled by the BoosterPackVisual
}
/// <summary>
/// Reset the opening state
/// </summary>
public void ResetOpeningState()
{
_isOpening = false;
}
/// <summary>
/// Reset tap count (useful when starting a new opening sequence)
/// </summary>
public void ResetTapCount()
{
_currentTapCount = 0;
}
/// <summary>
/// Enable or disable tap-to-open functionality at runtime
/// </summary>
public void SetTapToOpenEnabled(bool enabled)
{
canTapToOpen = enabled;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f95c1542aaa549d1867b43f6dc21e90f
timeCreated: 1762420681

View File

@@ -0,0 +1,189 @@
using Pixelplacement;
using UI.DragAndDrop.Core;
using UnityEngine;
using UnityEngine.UI;
namespace UI.CardSystem.DragDrop
{
/// <summary>
/// Visual representation for BoosterPackDraggable.
/// Displays the booster pack sprite and handles opening animations.
/// </summary>
public class BoosterPackVisual : DraggableVisual
{
[Header("Booster Pack Visual")]
[SerializeField] private Image packImage;
[SerializeField] private Sprite packSprite;
[SerializeField] private ParticleSystem glowEffect;
[SerializeField] private Transform glowTransform;
[Header("Opening Animation")]
[SerializeField] private float openingScalePunch = 0.5f;
[SerializeField] private float openingRotationPunch = 360f;
[SerializeField] private float openingDuration = 0.5f;
private BoosterPackDraggable _boosterDraggable;
public override void Initialize(DraggableObject parent)
{
base.Initialize(parent);
_boosterDraggable = parent as BoosterPackDraggable;
// Get pack image if not assigned
if (packImage == null)
{
packImage = GetComponentInChildren<Image>();
}
// Set initial sprite
if (packImage != null && packSprite != null)
{
packImage.sprite = packSprite;
}
// Subscribe to booster events
if (_boosterDraggable != null)
{
_boosterDraggable.OnBoosterOpened += HandleBoosterOpened;
_boosterDraggable.OnTapped += HandleTapped;
}
// Start glow effect if available
if (glowEffect != null && !glowEffect.isPlaying)
{
glowEffect.Play();
}
}
protected override void UpdateVisualContent()
{
// Update glow rotation for visual interest
if (glowTransform != null)
{
glowTransform.Rotate(Vector3.forward * 30f * Time.deltaTime);
}
}
private void HandleBoosterOpened(BoosterPackDraggable booster)
{
PlayOpeningAnimation();
}
private void HandleTapped(BoosterPackDraggable booster, int currentTap, int maxTaps)
{
PlayShakeAnimation(currentTap, maxTaps);
}
/// <summary>
/// Play progressive shake animation based on tap intensity
/// </summary>
public void PlayShakeAnimation(int intensity, int maxIntensity)
{
float normalizedIntensity = (float)intensity / maxIntensity;
float shakeAmount = Mathf.Lerp(5f, 30f, normalizedIntensity);
float shakeDuration = 0.15f;
// Shake rotation
Vector3 shakeRotation = new Vector3(
Random.Range(-shakeAmount, shakeAmount),
Random.Range(-shakeAmount, shakeAmount),
Random.Range(-shakeAmount, shakeAmount)
);
Tween.Rotation(transform, transform.eulerAngles + shakeRotation,
shakeDuration, 0f, Tween.EaseOutBack,
completeCallback: () => {
Tween.Rotation(transform, Vector3.zero,
shakeDuration, 0f, Tween.EaseInBack);
});
// Scale punch (gets bigger with each tap)
float punchScale = 1f + (normalizedIntensity * 0.2f);
Tween.LocalScale(transform, Vector3.one * punchScale,
shakeDuration / 2f, 0f, Tween.EaseOutBack,
completeCallback: () => {
Tween.LocalScale(transform, Vector3.one,
shakeDuration / 2f, 0f, Tween.EaseInBack);
});
// Extra glow burst on final tap
if (intensity == maxIntensity && glowEffect != null)
{
var emission = glowEffect.emission;
emission.rateOverTimeMultiplier = 50f;
}
}
/// <summary>
/// Play the booster pack opening animation
/// </summary>
public void PlayOpeningAnimation()
{
// Scale punch
Vector3 targetScale = transform.localScale * (1f + openingScalePunch);
Tween.LocalScale(transform, targetScale, openingDuration / 2f, 0f, Tween.EaseOutBack,
completeCallback: () => {
Tween.LocalScale(transform, Vector3.one, openingDuration / 2f, 0f, Tween.EaseInBack);
});
// Rotation
Tween.Rotation(transform, transform.eulerAngles + Vector3.forward * openingRotationPunch,
openingDuration, 0f, Tween.EaseOutBack);
// Glow burst
if (glowEffect != null)
{
glowEffect.Stop();
glowEffect.Play();
}
}
/// <summary>
/// Set the booster pack sprite
/// </summary>
public void SetPackSprite(Sprite sprite)
{
packSprite = sprite;
if (packImage != null)
{
packImage.sprite = packSprite;
}
}
protected override void OnPointerEnterVisual()
{
base.OnPointerEnterVisual();
// Extra glow when hovering
if (glowEffect != null)
{
var emission = glowEffect.emission;
emission.rateOverTimeMultiplier = 20f;
}
}
protected override void OnPointerExitVisual()
{
base.OnPointerExitVisual();
// Restore normal glow
if (glowEffect != null)
{
var emission = glowEffect.emission;
emission.rateOverTimeMultiplier = 10f;
}
}
protected override void OnDestroy()
{
base.OnDestroy();
if (_boosterDraggable != null)
{
_boosterDraggable.OnBoosterOpened -= HandleBoosterOpened;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a7d9474ece3b4d2ebad19ae178b22f4d
timeCreated: 1762420699

View File

@@ -0,0 +1,62 @@
using AppleHills.Data.CardSystem;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace UI.CardSystem.DragDrop
{
/// <summary>
/// Card-specific implementation of DraggableObject.
/// Manages card data and card-specific drag behavior.
/// </summary>
public class CardDraggable : DraggableObject
{
[Header("Card Data")]
[SerializeField] private CardData cardData;
// Events
public event System.Action<CardDraggable, CardData> OnCardDataChanged;
public CardData CardData => cardData;
/// <summary>
/// Set the card data for this draggable card
/// </summary>
public void SetCardData(CardData data)
{
cardData = data;
OnCardDataChanged?.Invoke(this, cardData);
// Update visual if it exists
if (_visualInstance != null && _visualInstance is CardDraggableVisual cardVisual)
{
cardVisual.RefreshCardDisplay();
}
}
protected override void OnDragStartedHook()
{
base.OnDragStartedHook();
// Card-specific drag started behavior
}
protected override void OnDragEndedHook()
{
base.OnDragEndedHook();
// Card-specific drag ended behavior
}
protected override void OnSelectionChangedHook(bool selected)
{
base.OnSelectionChangedHook(selected);
// Card-specific selection behavior
}
protected override void OnSlotChangedHook(DraggableSlot previousSlot, DraggableSlot newSlot)
{
base.OnSlotChangedHook(previousSlot, newSlot);
// Card-specific slot changed behavior
// Could trigger events for card collection reordering, etc.
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5a2741bb7299441b9f9bd44d746ebb4b
timeCreated: 1762420654

View File

@@ -0,0 +1,121 @@
using AppleHills.Data.CardSystem;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace UI.CardSystem.DragDrop
{
/// <summary>
/// Visual representation for CardDraggable.
/// Uses the existing CardDisplay component to render the card.
/// </summary>
public class CardDraggableVisual : DraggableVisual
{
[Header("Card Visual Components")]
[SerializeField] private CardDisplay cardDisplay;
[SerializeField] private Transform shadowTransform;
[SerializeField] private float shadowOffset = 20f;
private Vector3 _shadowInitialPosition;
private CardDraggable _cardDraggable;
public CardDisplay CardDisplay => cardDisplay;
public override void Initialize(DraggableObject parent)
{
base.Initialize(parent);
_cardDraggable = parent as CardDraggable;
// Get CardDisplay component if not assigned
if (cardDisplay == null)
{
cardDisplay = GetComponentInChildren<CardDisplay>();
}
// Initialize shadow
if (shadowTransform != null)
{
_shadowInitialPosition = shadowTransform.localPosition;
}
// Subscribe to card data changes
if (_cardDraggable != null)
{
_cardDraggable.OnCardDataChanged += HandleCardDataChanged;
// Initial card setup
if (_cardDraggable.CardData != null && cardDisplay != null)
{
cardDisplay.SetupCard(_cardDraggable.CardData);
}
}
}
protected override void UpdateVisualContent()
{
// CardDisplay handles its own rendering, no need to update every frame
// This is called every frame but we only update when card data changes
}
/// <summary>
/// Refresh the card display with current data
/// </summary>
public void RefreshCardDisplay()
{
if (cardDisplay != null && _cardDraggable != null && _cardDraggable.CardData != null)
{
cardDisplay.SetupCard(_cardDraggable.CardData);
}
}
private void HandleCardDataChanged(CardDraggable draggable, CardData data)
{
RefreshCardDisplay();
}
protected override void OnPointerDownVisual()
{
base.OnPointerDownVisual();
// Move shadow down when pressed
if (shadowTransform != null)
{
shadowTransform.localPosition = _shadowInitialPosition + (-Vector3.up * shadowOffset);
}
}
protected override void OnPointerUpVisual(bool longPress)
{
base.OnPointerUpVisual(longPress);
// Restore shadow position
if (shadowTransform != null)
{
shadowTransform.localPosition = _shadowInitialPosition;
}
}
protected override void OnDragStartedVisual()
{
base.OnDragStartedVisual();
// Card-specific visual effects when dragging starts
}
protected override void OnDragEndedVisual()
{
base.OnDragEndedVisual();
// Card-specific visual effects when dragging ends
}
protected override void OnDestroy()
{
base.OnDestroy();
if (_cardDraggable != null)
{
_cardDraggable.OnCardDataChanged -= HandleCardDataChanged;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2a4c3884410d44f98182cd8119a972a4
timeCreated: 1762420668

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9818aa1de299458b8b1fc95cdabc3f7f
timeCreated: 1762420597

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: de2fa1660c564a13ab22715e94b45e4c
timeCreated: 1762420597

View File

@@ -0,0 +1,476 @@
using System;
using System.Collections;
using Pixelplacement;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UI.DragAndDrop.Core
{
/// <summary>
/// Abstract base class for draggable UI objects.
/// Handles drag logic, slot snapping, and events.
/// Spawns and manages a separate DraggableVisual for rendering.
/// Touch-compatible via Unity's pointer event system.
/// </summary>
[RequireComponent(typeof(Image))]
public abstract class DraggableObject : MonoBehaviour,
IBeginDragHandler, IDragHandler, IEndDragHandler,
IPointerEnterHandler, IPointerExitHandler,
IPointerUpHandler, IPointerDownHandler
{
[Header("Draggable Settings")]
[SerializeField] protected float moveSpeed = 50f;
[SerializeField] protected bool smoothMovement = true;
[SerializeField] protected float snapDuration = 0.3f;
[Header("Visual")]
[SerializeField] protected GameObject visualPrefab;
[SerializeField] protected bool instantiateVisual = true;
[SerializeField] protected Transform visualParent;
[Header("Selection")]
[SerializeField] protected bool isSelectable = true;
[SerializeField] protected float selectionOffset = 50f;
// State
protected bool _isDragging;
protected bool _isHovering;
protected bool _isSelected;
protected bool _wasDragged;
// References
protected Canvas _canvas;
protected Image _imageComponent;
protected GraphicRaycaster _raycaster;
protected DraggableSlot _currentSlot;
protected DraggableVisual _visualInstance;
// Drag tracking
protected Vector3 _dragOffset;
protected Vector3 _lastPointerPosition;
protected float _pointerDownTime;
protected float _pointerUpTime;
// Events
public event Action<DraggableObject> OnDragStarted;
public event Action<DraggableObject> OnDragEnded;
public event Action<DraggableObject> OnPointerEntered;
public event Action<DraggableObject> OnPointerExited;
public event Action<DraggableObject> OnPointerDowned;
public event Action<DraggableObject, bool> OnPointerUpped; // bool = long press
public event Action<DraggableObject, bool> OnSelected; // bool = selected state
public event Action<DraggableObject, DraggableSlot> OnSlotChanged;
// Properties
public bool IsDragging => _isDragging;
public bool IsHovering => _isHovering;
public bool IsSelected => _isSelected;
public bool WasDragged => _wasDragged;
public DraggableSlot CurrentSlot => _currentSlot;
public DraggableVisual Visual => _visualInstance;
public Vector3 WorldPosition => transform.position;
public RectTransform RectTransform => transform as RectTransform;
protected virtual void Start()
{
Initialize();
}
protected virtual void Initialize()
{
_canvas = GetComponentInParent<Canvas>();
_imageComponent = GetComponent<Image>();
_raycaster = _canvas?.GetComponent<GraphicRaycaster>();
if (instantiateVisual && visualPrefab != null)
{
SpawnVisual();
}
// If we're already in a slot, register with it
DraggableSlot parentSlot = GetComponentInParent<DraggableSlot>();
if (parentSlot != null)
{
AssignToSlot(parentSlot, false);
}
}
protected virtual void SpawnVisual()
{
Transform parent = visualParent != null ? visualParent : _canvas.transform;
GameObject visualObj = Instantiate(visualPrefab, parent);
_visualInstance = visualObj.GetComponent<DraggableVisual>();
if (_visualInstance != null)
{
_visualInstance.Initialize(this);
}
}
protected virtual void Update()
{
if (_isDragging && smoothMovement)
{
SmoothMoveTowardPointer();
}
ClampToScreen();
}
protected virtual void SmoothMoveTowardPointer()
{
Vector3 targetPosition = _lastPointerPosition - _dragOffset;
Vector3 direction = (targetPosition - transform.position).normalized;
float distance = Vector3.Distance(transform.position, targetPosition);
float speed = Mathf.Min(moveSpeed, distance / Time.deltaTime);
transform.Translate(direction * speed * Time.deltaTime, Space.World);
}
protected virtual void ClampToScreen()
{
if (Camera.main == null || RectTransform == null)
return;
Vector3[] corners = new Vector3[4];
RectTransform.GetWorldCorners(corners);
// Simple clamping - can be improved
Vector3 clampedPosition = transform.position;
Vector2 screenBounds = Camera.main.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height, 0));
clampedPosition.x = Mathf.Clamp(clampedPosition.x, -screenBounds.x, screenBounds.x);
clampedPosition.y = Mathf.Clamp(clampedPosition.y, -screenBounds.y, screenBounds.y);
transform.position = clampedPosition;
}
#region Unity Pointer Event Handlers
public virtual void OnBeginDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
_isDragging = true;
_wasDragged = true;
// Calculate offset
Vector3 worldPointer = GetWorldPosition(eventData);
_dragOffset = worldPointer - transform.position;
_lastPointerPosition = worldPointer;
// Disable raycasting to allow detecting slots underneath
if (_raycaster != null)
_raycaster.enabled = false;
if (_imageComponent != null)
_imageComponent.raycastTarget = false;
// Notify current slot we're leaving
if (_currentSlot != null)
{
_currentSlot.Vacate();
}
OnDragStarted?.Invoke(this);
OnDragStartedHook();
}
public virtual void OnDrag(PointerEventData eventData)
{
if (!_isDragging)
return;
_lastPointerPosition = GetWorldPosition(eventData);
if (!smoothMovement)
{
transform.position = _lastPointerPosition - _dragOffset;
}
}
public virtual void OnEndDrag(PointerEventData eventData)
{
if (!_isDragging)
return;
_isDragging = false;
// Re-enable raycasting
if (_raycaster != null)
_raycaster.enabled = true;
if (_imageComponent != null)
_imageComponent.raycastTarget = true;
// Find closest slot and snap
FindAndSnapToSlot();
OnDragEnded?.Invoke(this);
OnDragEndedHook();
// Reset wasDragged after a frame
StartCoroutine(ResetWasDraggedFlag());
}
public virtual void OnPointerEnter(PointerEventData eventData)
{
_isHovering = true;
OnPointerEntered?.Invoke(this);
OnPointerEnterHook();
}
public virtual void OnPointerExit(PointerEventData eventData)
{
_isHovering = false;
OnPointerExited?.Invoke(this);
OnPointerExitHook();
}
public virtual void OnPointerDown(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
_pointerDownTime = Time.time;
OnPointerDowned?.Invoke(this);
OnPointerDownHook();
}
public virtual void OnPointerUp(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
_pointerUpTime = Time.time;
bool isLongPress = (_pointerUpTime - _pointerDownTime) > 0.2f;
OnPointerUpped?.Invoke(this, isLongPress);
OnPointerUpHook(isLongPress);
// Handle selection (only if not long press and not dragged)
if (!isLongPress && !_wasDragged && isSelectable)
{
ToggleSelection();
}
}
#endregion
#region Slot Management
protected virtual void FindAndSnapToSlot()
{
SlotContainer[] containers = FindObjectsOfType<SlotContainer>();
DraggableSlot closestSlot = null;
float closestDistance = float.MaxValue;
foreach (var container in containers)
{
DraggableSlot slot = container.FindClosestSlot(transform.position, this);
if (slot != null)
{
float distance = Vector3.Distance(transform.position, slot.WorldPosition);
if (distance < closestDistance)
{
closestDistance = distance;
closestSlot = slot;
}
}
}
if (closestSlot != null)
{
// Check if slot is occupied
if (closestSlot.IsOccupied && closestSlot.Occupant != this)
{
// Swap with occupant
SwapWithSlot(closestSlot);
}
else
{
// Move to empty slot
AssignToSlot(closestSlot, true);
}
}
else if (_currentSlot != null)
{
// Return to current slot if no valid slot found
SnapToCurrentSlot();
}
}
protected virtual void SwapWithSlot(DraggableSlot targetSlot)
{
DraggableSlot mySlot = _currentSlot;
DraggableObject otherObject = targetSlot.Occupant;
if (otherObject != null)
{
// Both objects swap slots
targetSlot.Vacate();
if (mySlot != null)
mySlot.Vacate();
AssignToSlot(targetSlot, true);
if (mySlot != null)
otherObject.AssignToSlot(mySlot, true);
}
}
public virtual void AssignToSlot(DraggableSlot slot, bool animate)
{
if (slot == null)
return;
DraggableSlot previousSlot = _currentSlot;
_currentSlot = slot;
if (slot.Occupy(this))
{
if (animate)
{
SnapToSlot(slot);
}
else
{
transform.SetParent(slot.transform);
transform.localPosition = _isSelected ? new Vector3(0, selectionOffset, 0) : Vector3.zero;
}
OnSlotChanged?.Invoke(this, slot);
OnSlotChangedHook(previousSlot, slot);
}
}
protected virtual void SnapToSlot(DraggableSlot slot)
{
transform.SetParent(slot.transform);
Vector3 targetLocalPos = _isSelected ? new Vector3(0, selectionOffset, 0) : Vector3.zero;
if (RectTransform != null)
{
Tween.LocalPosition(RectTransform, targetLocalPos, snapDuration, 0f, Tween.EaseOutBack);
}
else
{
transform.localPosition = targetLocalPos;
}
}
protected virtual void SnapToCurrentSlot()
{
if (_currentSlot != null)
{
SnapToSlot(_currentSlot);
}
}
#endregion
#region Selection
public virtual void ToggleSelection()
{
SetSelected(!_isSelected);
}
public virtual void SetSelected(bool selected)
{
if (!isSelectable)
return;
_isSelected = selected;
// Update position based on selection
Vector3 targetLocalPos = _isSelected ? new Vector3(0, selectionOffset, 0) : Vector3.zero;
if (RectTransform != null && _currentSlot != null)
{
Tween.LocalPosition(RectTransform, targetLocalPos, 0.15f, 0f, Tween.EaseOutBack);
}
OnSelected?.Invoke(this, _isSelected);
OnSelectionChangedHook(_isSelected);
}
public virtual void Deselect()
{
SetSelected(false);
}
#endregion
#region Helper Methods
protected Vector3 GetWorldPosition(PointerEventData eventData)
{
if (Camera.main == null)
return Vector3.zero;
// For screen space overlay canvas
if (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
return eventData.position;
}
// For world space or camera space
return Camera.main.ScreenToWorldPoint(new Vector3(eventData.position.x, eventData.position.y, _canvas.planeDistance));
}
protected IEnumerator ResetWasDraggedFlag()
{
yield return new WaitForEndOfFrame();
_wasDragged = false;
}
#endregion
#region Abstract/Virtual Hooks for Subclasses
protected virtual void OnDragStartedHook() { }
protected virtual void OnDragEndedHook() { }
protected virtual void OnPointerEnterHook() { }
protected virtual void OnPointerExitHook() { }
protected virtual void OnPointerDownHook() { }
protected virtual void OnPointerUpHook(bool longPress) { }
protected virtual void OnSelectionChangedHook(bool selected) { }
protected virtual void OnSlotChangedHook(DraggableSlot previousSlot, DraggableSlot newSlot) { }
#endregion
protected virtual void OnDestroy()
{
if (_visualInstance != null)
{
Destroy(_visualInstance.gameObject);
}
}
public int GetSiblingCount()
{
return _currentSlot != null && _currentSlot.transform.parent != null
? _currentSlot.transform.parent.childCount - 1
: 0;
}
public int GetSlotIndex()
{
return _currentSlot != null ? _currentSlot.SlotIndex : 0;
}
public float GetNormalizedSlotPosition()
{
if (_currentSlot == null || _currentSlot.transform.parent == null)
return 0f;
int siblingCount = _currentSlot.transform.parent.childCount - 1;
if (siblingCount <= 0)
return 0f;
return (float)_currentSlot.SlotIndex / siblingCount;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 062198d83a0940538c140da0999f4de9
timeCreated: 1762420597

View File

@@ -0,0 +1,139 @@
using System;
using Pixelplacement;
using UnityEngine;
namespace UI.DragAndDrop.Core
{
/// <summary>
/// Represents a position where draggable objects can snap to.
/// Can be occupied by one DraggableObject at a time.
/// </summary>
public class DraggableSlot : MonoBehaviour
{
[Header("Slot Settings")]
[SerializeField] private int slotIndex;
[SerializeField] private bool isLocked;
[Header("Type Filtering")]
[SerializeField] private bool filterByType;
[SerializeField] private string[] allowedTypeNames;
[Header("Scale Control")]
[SerializeField] private bool applyScaleToOccupant = false;
[SerializeField] private Vector3 occupantScale = Vector3.one;
[SerializeField] private float scaleTransitionDuration = 0.3f;
// Current occupant
private DraggableObject _occupant;
// Events
public event Action<DraggableObject> OnOccupied;
public event Action<DraggableObject> OnVacated;
public int SlotIndex => slotIndex;
public bool IsOccupied => _occupant != null;
public bool IsLocked => isLocked;
public DraggableObject Occupant => _occupant;
public Vector3 WorldPosition => transform.position;
public RectTransform RectTransform => transform as RectTransform;
/// <summary>
/// Attempt to occupy this slot with a draggable object
/// </summary>
public bool Occupy(DraggableObject draggable)
{
if (isLocked)
return false;
if (!CanAccept(draggable))
return false;
if (_occupant != null && _occupant != draggable)
return false;
_occupant = draggable;
draggable.transform.SetParent(transform);
// Apply scale if configured
if (applyScaleToOccupant)
{
Tween.LocalScale(draggable.transform, occupantScale, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
OnOccupied?.Invoke(draggable);
return true;
}
/// <summary>
/// Vacate this slot, removing the current occupant
/// </summary>
public void Vacate()
{
if (_occupant != null)
{
DraggableObject previousOccupant = _occupant;
_occupant = null;
OnVacated?.Invoke(previousOccupant);
}
}
/// <summary>
/// Check if this slot can accept a specific draggable type
/// </summary>
public bool CanAccept(DraggableObject draggable)
{
if (!filterByType || allowedTypeNames == null || allowedTypeNames.Length == 0)
return true;
string draggableTypeName = draggable.GetType().Name;
foreach (string allowedType in allowedTypeNames)
{
if (draggableTypeName == allowedType)
return true;
}
return false;
}
/// <summary>
/// Swap occupants with another slot
/// </summary>
public void SwapWith(DraggableSlot otherSlot)
{
if (otherSlot == null || otherSlot == this)
return;
DraggableObject thisOccupant = _occupant;
DraggableObject otherOccupant = otherSlot._occupant;
// Vacate both slots
Vacate();
otherSlot.Vacate();
// Occupy with swapped objects
if (otherOccupant != null)
Occupy(otherOccupant);
if (thisOccupant != null)
otherSlot.Occupy(thisOccupant);
}
/// <summary>
/// Lock/unlock this slot
/// </summary>
public void SetLocked(bool locked)
{
isLocked = locked;
}
/// <summary>
/// Set the slot index
/// </summary>
public void SetSlotIndex(int index)
{
slotIndex = index;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ee43b700f9dd44dba39deb8c5bcd688c
timeCreated: 1762420738

View File

@@ -0,0 +1,367 @@
using Pixelplacement;
using UnityEngine;
namespace UI.DragAndDrop.Core
{
/// <summary>
/// Abstract base class for visual representation of draggable objects.
/// Follows the parent DraggableObject with lerping and visual effects.
/// Inspired by Balatro's CardVisual system.
/// </summary>
public abstract class DraggableVisual : MonoBehaviour
{
[Header("References")]
[SerializeField] protected Canvas canvas;
[SerializeField] protected CanvasGroup canvasGroup;
[SerializeField] protected Transform tiltParent;
[SerializeField] protected Transform shakeParent;
[Header("Follow Parameters")]
[SerializeField] protected float followSpeed = 30f;
[SerializeField] protected bool useFollowDelay = true;
[Header("Rotation/Tilt Parameters")]
[SerializeField] protected float rotationAmount = 20f;
[SerializeField] protected float rotationSpeed = 20f;
[SerializeField] protected float autoTiltAmount = 30f;
[SerializeField] protected float manualTiltAmount = 20f;
[SerializeField] protected float tiltSpeed = 20f;
[Header("Scale Parameters")]
[SerializeField] protected bool useScaleAnimations = true;
[SerializeField] protected float scaleOnHover = 1.15f;
[SerializeField] protected float scaleOnDrag = 1.25f;
[SerializeField] protected float scaleTransitionDuration = 0.15f;
[Header("Idle Animation")]
[SerializeField] protected bool useIdleAnimation = true;
[SerializeField] protected float idleAnimationSpeed = 1f;
// State
protected DraggableObject _parentDraggable;
protected bool _isInitialized;
protected Vector3 _movementDelta;
protected Vector3 _rotationDelta;
protected int _savedSlotIndex;
protected Vector3 _lastPosition;
// Properties
public DraggableObject ParentDraggable => _parentDraggable;
public bool IsInitialized => _isInitialized;
/// <summary>
/// Initialize the visual with its parent draggable object
/// </summary>
public virtual void Initialize(DraggableObject parent)
{
_parentDraggable = parent;
// Get or add required components
if (canvas == null)
canvas = GetComponent<Canvas>();
if (canvas == null)
canvas = gameObject.AddComponent<Canvas>();
if (canvasGroup == null)
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
// Subscribe to parent events
SubscribeToParentEvents();
// Initial position
transform.position = parent.transform.position;
_lastPosition = transform.position;
_isInitialized = true;
OnInitialized();
}
protected virtual void SubscribeToParentEvents()
{
if (_parentDraggable == null)
return;
_parentDraggable.OnDragStarted += HandleDragStarted;
_parentDraggable.OnDragEnded += HandleDragEnded;
_parentDraggable.OnPointerEntered += HandlePointerEnter;
_parentDraggable.OnPointerExited += HandlePointerExit;
_parentDraggable.OnPointerDowned += HandlePointerDown;
_parentDraggable.OnPointerUpped += HandlePointerUp;
_parentDraggable.OnSelected += HandleSelection;
}
protected virtual void UnsubscribeFromParentEvents()
{
if (_parentDraggable == null)
return;
_parentDraggable.OnDragStarted -= HandleDragStarted;
_parentDraggable.OnDragEnded -= HandleDragEnded;
_parentDraggable.OnPointerEntered -= HandlePointerEnter;
_parentDraggable.OnPointerExited -= HandlePointerExit;
_parentDraggable.OnPointerDowned -= HandlePointerDown;
_parentDraggable.OnPointerUpped -= HandlePointerUp;
_parentDraggable.OnSelected -= HandleSelection;
}
protected virtual void Update()
{
if (!_isInitialized || _parentDraggable == null)
return;
UpdateFollowPosition();
UpdateRotation();
UpdateTilt();
UpdateVisualContent();
}
#region Position & Movement
protected virtual void UpdateFollowPosition()
{
if (_parentDraggable == null)
return;
Vector3 targetPosition = GetTargetPosition();
if (useFollowDelay)
{
transform.position = Vector3.Lerp(transform.position, targetPosition, followSpeed * Time.deltaTime);
}
else
{
transform.position = targetPosition;
}
// Calculate movement delta for tilt
Vector3 movement = transform.position - _lastPosition;
_movementDelta = Vector3.Lerp(_movementDelta, movement, 25f * Time.deltaTime);
_lastPosition = transform.position;
}
protected virtual Vector3 GetTargetPosition()
{
return _parentDraggable.transform.position;
}
#endregion
#region Rotation & Tilt
protected virtual void UpdateRotation()
{
if (_parentDraggable == null)
return;
// Rotation based on movement direction (like Balatro)
Vector3 movementRotation = _parentDraggable.IsDragging
? _movementDelta * rotationAmount
: (_lastPosition - _parentDraggable.transform.position) * rotationAmount;
_rotationDelta = Vector3.Lerp(_rotationDelta, movementRotation, rotationSpeed * Time.deltaTime);
float clampedZ = Mathf.Clamp(_rotationDelta.x, -60f, 60f);
transform.eulerAngles = new Vector3(transform.eulerAngles.x, transform.eulerAngles.y, clampedZ);
}
protected virtual void UpdateTilt()
{
if (tiltParent == null)
return;
// Save slot index when not dragging for idle animation
_savedSlotIndex = _parentDraggable.IsDragging
? _savedSlotIndex
: _parentDraggable.GetSlotIndex();
// Idle animation (sine/cosine wobble)
float idleMultiplier = _parentDraggable.IsHovering ? 0.2f : 1f;
float time = Time.time * idleAnimationSpeed + _savedSlotIndex;
float sineWobble = Mathf.Sin(time) * idleMultiplier;
float cosineWobble = Mathf.Cos(time) * idleMultiplier;
// Manual tilt based on pointer position (when hovering)
float manualTiltX = 0f;
float manualTiltY = 0f;
if (_parentDraggable.IsHovering && Camera.main != null)
{
Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(UnityEngine.Input.mousePosition);
Vector3 offset = transform.position - mouseWorldPos;
manualTiltX = (offset.y * -1f) * manualTiltAmount;
manualTiltY = offset.x * manualTiltAmount;
}
// Combine auto and manual tilt
float targetTiltX = manualTiltX + (useIdleAnimation ? sineWobble * autoTiltAmount : 0f);
float targetTiltY = manualTiltY + (useIdleAnimation ? cosineWobble * autoTiltAmount : 0f);
float targetTiltZ = _parentDraggable.IsDragging ? tiltParent.eulerAngles.z : 0f;
// Lerp to target tilt
float lerpX = Mathf.LerpAngle(tiltParent.eulerAngles.x, targetTiltX, tiltSpeed * Time.deltaTime);
float lerpY = Mathf.LerpAngle(tiltParent.eulerAngles.y, targetTiltY, tiltSpeed * Time.deltaTime);
float lerpZ = Mathf.LerpAngle(tiltParent.eulerAngles.z, targetTiltZ, (tiltSpeed / 2f) * Time.deltaTime);
tiltParent.eulerAngles = new Vector3(lerpX, lerpY, lerpZ);
}
#endregion
#region Event Handlers
protected virtual void HandleDragStarted(DraggableObject draggable)
{
if (useScaleAnimations)
{
Tween.LocalScale(transform, Vector3.one * scaleOnDrag, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
if (canvas != null)
{
canvas.overrideSorting = true;
}
OnDragStartedVisual();
}
protected virtual void HandleDragEnded(DraggableObject draggable)
{
if (canvas != null)
{
canvas.overrideSorting = false;
}
Tween.LocalScale(transform, Vector3.one, scaleTransitionDuration, 0f, Tween.EaseOutBack);
OnDragEndedVisual();
}
protected virtual void HandlePointerEnter(DraggableObject draggable)
{
if (useScaleAnimations)
{
Tween.LocalScale(transform, Vector3.one * scaleOnHover, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
// Punch rotation effect
if (shakeParent != null)
{
Tween.Rotation(shakeParent, shakeParent.eulerAngles + Vector3.forward * 5f, 0.15f, 0f, Tween.EaseOutBack);
}
OnPointerEnterVisual();
}
protected virtual void HandlePointerExit(DraggableObject draggable)
{
if (!draggable.WasDragged && useScaleAnimations)
{
Tween.LocalScale(transform, Vector3.one, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
OnPointerExitVisual();
}
protected virtual void HandlePointerDown(DraggableObject draggable)
{
if (useScaleAnimations)
{
Tween.LocalScale(transform, Vector3.one * scaleOnDrag, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
OnPointerDownVisual();
}
protected virtual void HandlePointerUp(DraggableObject draggable, bool longPress)
{
float targetScale = longPress ? scaleOnHover : scaleOnDrag;
if (useScaleAnimations)
{
Tween.LocalScale(transform, Vector3.one * targetScale, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
OnPointerUpVisual(longPress);
}
protected virtual void HandleSelection(DraggableObject draggable, bool selected)
{
// Punch effect on selection
if (shakeParent != null && selected)
{
Vector3 punchAmount = shakeParent.up * 20f;
Tween.Position(shakeParent, shakeParent.position + punchAmount, 0.15f, 0f, Tween.EaseOutBack,
completeCallback: () => Tween.Position(shakeParent, shakeParent.position - punchAmount, 0.15f, 0f, Tween.EaseInBack));
}
if (useScaleAnimations)
{
float targetScale = selected ? scaleOnDrag : scaleOnHover;
Tween.LocalScale(transform, Vector3.one * targetScale, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
OnSelectionVisual(selected);
}
#endregion
#region Abstract/Virtual Hooks for Subclasses
/// <summary>
/// Called after initialization is complete
/// </summary>
protected virtual void OnInitialized() { }
/// <summary>
/// Update the actual visual content (sprites, UI elements, etc.)
/// Called every frame
/// </summary>
protected abstract void UpdateVisualContent();
/// <summary>
/// Visual-specific behavior when drag starts
/// </summary>
protected virtual void OnDragStartedVisual() { }
/// <summary>
/// Visual-specific behavior when drag ends
/// </summary>
protected virtual void OnDragEndedVisual() { }
/// <summary>
/// Visual-specific behavior when pointer enters
/// </summary>
protected virtual void OnPointerEnterVisual() { }
/// <summary>
/// Visual-specific behavior when pointer exits
/// </summary>
protected virtual void OnPointerExitVisual() { }
/// <summary>
/// Visual-specific behavior when pointer down
/// </summary>
protected virtual void OnPointerDownVisual() { }
/// <summary>
/// Visual-specific behavior when pointer up
/// </summary>
protected virtual void OnPointerUpVisual(bool longPress) { }
/// <summary>
/// Visual-specific behavior when selection changes
/// </summary>
protected virtual void OnSelectionVisual(bool selected) { }
#endregion
protected virtual void OnDestroy()
{
UnsubscribeFromParentEvents();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 203c649ed73845c6999682bcf8383ee8
timeCreated: 1762420644

View File

@@ -0,0 +1,258 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UI.DragAndDrop.Core
{
/// <summary>
/// Manages a collection of DraggableSlots.
/// Handles layout, slot finding, and reordering.
/// </summary>
public class SlotContainer : MonoBehaviour
{
[Header("Container Settings")]
[SerializeField] private LayoutType layoutType = LayoutType.Horizontal;
[SerializeField] private float spacing = 100f;
[SerializeField] private bool centerSlots = true;
[SerializeField] private bool autoRegisterChildren = true;
[Header("Curve Layout (Horizontal Only)")]
[SerializeField] private bool useCurveLayout;
[SerializeField] private AnimationCurve positionCurve = AnimationCurve.Linear(0, 0, 1, 0);
[SerializeField] private float curveHeight = 50f;
private List<DraggableSlot> _slots = new List<DraggableSlot>();
public enum LayoutType
{
Horizontal,
Vertical,
Grid,
Custom
}
// Events
public event Action<DraggableSlot> OnSlotAdded;
public event Action<DraggableSlot> OnSlotRemoved;
public event Action OnLayoutChanged;
public List<DraggableSlot> Slots => _slots;
public int SlotCount => _slots.Count;
private void Awake()
{
if (autoRegisterChildren)
{
RegisterChildSlots();
}
}
/// <summary>
/// Automatically register all child DraggableSlot components
/// </summary>
private void RegisterChildSlots()
{
DraggableSlot[] childSlots = GetComponentsInChildren<DraggableSlot>();
foreach (var slot in childSlots)
{
RegisterSlot(slot);
}
}
/// <summary>
/// Register a slot with this container
/// </summary>
public void RegisterSlot(DraggableSlot slot)
{
if (slot == null || _slots.Contains(slot))
return;
_slots.Add(slot);
slot.SetSlotIndex(_slots.Count - 1);
OnSlotAdded?.Invoke(slot);
UpdateLayout();
}
/// <summary>
/// Unregister a slot from this container
/// </summary>
public void UnregisterSlot(DraggableSlot slot)
{
if (slot == null || !_slots.Contains(slot))
return;
_slots.Remove(slot);
OnSlotRemoved?.Invoke(slot);
// Re-index remaining slots
for (int i = 0; i < _slots.Count; i++)
{
_slots[i].SetSlotIndex(i);
}
UpdateLayout();
}
/// <summary>
/// Find the closest slot to a world position
/// </summary>
public DraggableSlot FindClosestSlot(Vector3 worldPosition, DraggableObject draggable = null)
{
if (_slots.Count == 0)
return null;
DraggableSlot closest = null;
float closestDistance = float.MaxValue;
foreach (var slot in _slots)
{
// Skip locked slots or slots that can't accept this type
if (slot.IsLocked)
continue;
if (draggable != null && !slot.CanAccept(draggable))
continue;
float distance = Vector3.Distance(worldPosition, slot.WorldPosition);
if (distance < closestDistance)
{
closestDistance = distance;
closest = slot;
}
}
return closest;
}
/// <summary>
/// Get all available (empty) slots
/// </summary>
public List<DraggableSlot> GetAvailableSlots()
{
return _slots.Where(s => !s.IsOccupied && !s.IsLocked).ToList();
}
/// <summary>
/// Get all occupied slots
/// </summary>
public List<DraggableSlot> GetOccupiedSlots()
{
return _slots.Where(s => s.IsOccupied).ToList();
}
/// <summary>
/// Get slot at specific index
/// </summary>
public DraggableSlot GetSlotAtIndex(int index)
{
if (index < 0 || index >= _slots.Count)
return null;
return _slots[index];
}
/// <summary>
/// Update the layout of all slots based on layout type
/// </summary>
public void UpdateLayout()
{
if (layoutType == LayoutType.Custom)
{
OnLayoutChanged?.Invoke();
return;
}
int count = _slots.Count;
if (count == 0)
return;
switch (layoutType)
{
case LayoutType.Horizontal:
LayoutHorizontal();
break;
case LayoutType.Vertical:
LayoutVertical();
break;
case LayoutType.Grid:
LayoutGrid();
break;
}
OnLayoutChanged?.Invoke();
}
private void LayoutHorizontal()
{
float totalWidth = (_slots.Count - 1) * spacing;
float startX = centerSlots ? -totalWidth / 2f : 0f;
for (int i = 0; i < _slots.Count; i++)
{
if (_slots[i].RectTransform != null)
{
float xPos = startX + (i * spacing);
float yPos = 0;
// Apply curve if enabled
if (useCurveLayout && _slots.Count > 1)
{
float normalizedPos = i / (float)(_slots.Count - 1);
yPos = positionCurve.Evaluate(normalizedPos) * curveHeight;
}
_slots[i].RectTransform.anchoredPosition = new Vector2(xPos, yPos);
}
}
}
private void LayoutVertical()
{
float totalHeight = (_slots.Count - 1) * spacing;
float startY = centerSlots ? totalHeight / 2f : 0f;
for (int i = 0; i < _slots.Count; i++)
{
if (_slots[i].RectTransform != null)
{
float yPos = startY - (i * spacing);
_slots[i].RectTransform.anchoredPosition = new Vector2(0, yPos);
}
}
}
private void LayoutGrid()
{
// Simple grid layout - can be expanded
int columns = Mathf.CeilToInt(Mathf.Sqrt(_slots.Count));
for (int i = 0; i < _slots.Count; i++)
{
if (_slots[i].RectTransform != null)
{
int row = i / columns;
int col = i % columns;
float xPos = col * spacing;
float yPos = -row * spacing;
_slots[i].RectTransform.anchoredPosition = new Vector2(xPos, yPos);
}
}
}
/// <summary>
/// Clear all slots
/// </summary>
public void ClearAllSlots()
{
foreach (var slot in _slots)
{
slot.Vacate();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f1347da8005d4687a85dbc4db9a1bbbb
timeCreated: 1762420766

View File

@@ -0,0 +1,365 @@
# Booster Opening System - Complete Setup Guide
## 📋 Overview
This guide walks you through setting up the complete booster opening flow, from the Album page to opening packs and revealing cards.
---
## 🎯 What Was Implemented
### **Core Components Enhanced:**
1. **DraggableSlot** - Added scale control for occupants
2. **BoosterPackDraggable** - Added tap-to-shake functionality
3. **BoosterPackVisual** - Added progressive shake animations
4. **BoosterOpeningPage** - Complete flow implementation
5. **AlbumViewPage** - Updated to pass booster count
### **Flow Summary:**
1. Album page shows booster buttons → Click → Opens BoosterOpeningPage
2. Opening page shows draggable boosters in bottom-right
3. Player taps or drags booster to center slot
4. Player taps booster X times (configurable, default 3)
5. Booster disappears, 3 card backs appear
6. Player clicks cards to reveal them one-by-one
7. After all revealed → Show next booster OR close page
---
## 🏗️ Scene Setup Instructions
### **Step 1: Create the BoosterOpeningPage GameObject**
1. **In your scene hierarchy:**
- Create an empty GameObject named `BoosterOpeningPage`
- Add component: `BoosterOpeningPage` script
- Add component: `Canvas Group` (for fade transitions)
- Ensure it's a child of your Canvas
2. **Create UI Structure:**
```
BoosterOpeningPage (BoosterOpeningPage component)
├── Background (Image - optional dark overlay)
├── CloseButton (Button)
├── BottomRightContainer (SlotContainer)
│ ├── Slot_0 (DraggableSlot)
│ ├── Slot_1 (DraggableSlot)
│ └── Slot_2 (DraggableSlot)
├── CenterSlot (DraggableSlot)
├── CardDisplayContainer (Empty RectTransform)
└── BoosterInstances (Empty container)
├── BoosterPack_0 (BoosterPackDraggable prefab)
├── BoosterPack_1 (BoosterPackDraggable prefab)
└── BoosterPack_2 (BoosterPackDraggable prefab)
```
---
## 📐 Detailed Component Setup
### **A. Bottom-Right Slot Container**
**GameObject Setup:**
- Name: `BottomRightContainer`
- Component: `SlotContainer`
- Position: Bottom-right of screen (e.g., anchored to bottom-right)
- Recommended position: X=800, Y=-400 (adjust for your canvas size)
**SlotContainer Settings:**
- Layout Type: `Vertical`
- Spacing: `120`
- Center Slots: ✓
- Auto Register Children: ✓
**Child Slots (Slot_0, Slot_1, Slot_2):**
- Add component: `DraggableSlot` to each
- Size: 150x200 (adjust to your booster size)
- Settings:
- Filter By Type: ✓
- Allowed Type Names: `BoosterPackDraggable`
- Apply Scale To Occupant: ☐ (leave normal size)
---
### **B. Center Opening Slot**
**GameObject Setup:**
- Name: `CenterSlot`
- Component: `DraggableSlot`
- Position: Center of screen (X=0, Y=0)
- Size: 300x400 (larger than boosters)
**DraggableSlot Settings:**
- Filter By Type: ✓
- Allowed Type Names: `BoosterPackDraggable`
- **Apply Scale To Occupant: ✓** ← Important!
- **Occupant Scale: (2, 2, 1)** ← Makes booster 2x bigger
- Scale Transition Duration: `0.3`
---
### **C. Card Display Container**
**GameObject Setup:**
- Name: `CardDisplayContainer`
- Component: `RectTransform` only
- Position: Center of screen (X=0, Y=-100)
- This is where revealed cards will spawn
---
### **D. Booster Pack Instances**
You have two options: **Prefabs** or **Scene Instances**
#### **Option 1: Using Prefabs (Recommended)**
1. **Create BoosterPack Prefab:**
```
BoosterPackPrefab
├── BoosterPackDraggable (component)
├── Image (base - raycastTarget = false)
└── Visual (BoosterPackVisual)
├── Canvas
├── CanvasGroup
├── TiltParent (Transform)
│ └── PackImage (Image - your booster sprite)
├── ShakeParent (Transform)
└── GlowEffect (ParticleSystem - optional)
```
2. **BoosterPackDraggable Settings:**
- Visual Prefab: Assign the Visual prefab
- Can Open On Drop: ☐
- Can Open On Double Click: ☐
- **Can Tap To Open: ✓**
- **Max Taps To Open: 3** (or your preference)
3. **In Scene:**
- Under `BoosterInstances` container, create 3 instances
- Name them: `BoosterPack_0`, `BoosterPack_1`, `BoosterPack_2`
- They will start inactive and be shown based on available count
#### **Option 2: Scene Instances**
- Place 3 booster GameObjects directly in the scene
- Configure as above
- Set initial Active state to `false`
---
### **E. Flippable Card Prefab**
**For now (placeholder):**
1. Create a simple prefab named `FlippableCardPrefab`
2. Structure:
```
FlippableCardPrefab
├── RectTransform (200x300)
├── CardDisplay (component)
├── Image (card back sprite initially)
└── Button (will be added at runtime if not present)
```
3. **Important:**
- The card should show a "back" sprite initially
- When clicked, `CardDisplay.SetupCard()` will be called
- In the future, add flip animation here
---
### **F. BoosterOpeningPage Component Configuration**
**In the Inspector:**
**UI References:**
- Canvas Group: Auto-assigned or drag the CanvasGroup
- Close Button: Drag your close button
**Booster Management:**
- Booster Pack Instances: **Array size = 3**
- Element 0: `BoosterPack_0`
- Element 1: `BoosterPack_1`
- Element 2: `BoosterPack_2`
- Bottom Right Slots: Drag `BottomRightContainer`
- Center Opening Slot: Drag `CenterSlot`
**Card Display:**
- Card Display Container: Drag `CardDisplayContainer`
- Flippable Card Prefab: Drag your card prefab
- Card Spacing: `150` (distance between cards)
**Settings:**
- Card Reveal Delay: `0.5`
- Booster Disappear Duration: `0.5`
---
## 🎨 Creating the Booster Pack Visual Prefab
### **Booster Visual Structure:**
```
BoosterVisual (BoosterPackVisual component)
├── Canvas (sort order will be controlled at runtime)
├── CanvasGroup (for fading)
├── TiltParent (empty Transform for 3D tilt effect)
│ ├── PackImage (Image - your booster pack sprite)
│ └── FrameImage (Image - optional frame/border)
├── ShakeParent (empty Transform for shake effects)
└── GlowEffect (Particle System - optional sparkles)
```
### **Component Settings:**
**BoosterPackVisual:**
- Canvas: Auto-assigned
- Canvas Group: Auto-assigned
- Tilt Parent: Drag `TiltParent`
- Shake Parent: Drag `ShakeParent`
- Pack Image: Drag the `PackImage`
- Pack Sprite: Assign your booster sprite asset
**Visual Animation Settings:**
- Follow Speed: `30`
- Rotation Amount: `20`
- Tilt Speed: `20`
- Use Idle Animation: ✓
- Idle Animation Speed: `1`
- Opening Scale Punch: `0.5`
- Opening Rotation Punch: `360`
- Opening Duration: `0.5`
---
## 🔗 Connecting to AlbumViewPage
**In your AlbumViewPage Inspector:**
- Booster Opening Page: Drag your `BoosterOpeningPage` GameObject
- Booster Pack Buttons: Array of booster button GameObjects (already configured)
**That's it!** The AlbumViewPage script already has the updated code to pass booster count.
---
## 🎮 Testing the Flow
### **Test Checklist:**
1. **Setup Test:**
- ✓ BoosterOpeningPage exists in scene
- ✓ All slots configured
- ✓ Booster instances assigned
- ✓ AlbumViewPage has reference to opening page
2. **Manual Booster Count:**
- In CardSystemManager, set initial booster count to 3
- Or use the editor tool to add boosters
3. **Test Flow:**
1. Open album → Should see 3 booster buttons
2. Click a booster button → Opens BoosterOpeningPage
3. Should see 3 boosters in bottom-right
4. **Drag Test:** Drag a booster to center → Should scale up 2x
5. **Tap Test:** Tap the booster 3 times:
- Tap 1: Small shake
- Tap 2: Medium shake
- Tap 3: Big shake → Booster disappears
6. Should see 3 card backs appear
7. Click each card → Should reveal (show CardDisplay)
8. After all revealed → Should show next booster OR close page
---
## ⚙️ Configuration Options
### **Adjusting Tap Count:**
- Select any BoosterPackDraggable
- Change `Max Taps To Open` (3 is default)
### **Adjusting Booster Cap:**
- In BoosterOpeningPage, change `Booster Pack Instances` array size
- Add/remove booster instances
- Bottom-right slots should match (add more Slot children)
### **Adjusting Center Slot Scale:**
- Select `CenterSlot`
- Change `Occupant Scale` (2,2,1 = 2x size)
### **Card Spacing:**
- In BoosterOpeningPage, adjust `Card Spacing` (150 default)
---
## 🐛 Troubleshooting
### **Problem: Boosters don't appear**
- Check: `Booster Pack Instances` array is filled
- Check: CardSystemManager has boosters available
- Check: AlbumViewPage passes booster count correctly
### **Problem: Can't drag booster to center**
- Check: CenterSlot has `Filter By Type` with `BoosterPackDraggable`
- Check: CenterSlot is not locked initially
- Check: Booster has Image with `raycastTarget = true` on base object
### **Problem: Tapping doesn't work**
- Check: BoosterPackDraggable has `Can Tap To Open` enabled
- Check: Booster is in the center slot (tap only works when slotted)
- Check: Visual children have `raycastTarget = false` so taps reach the base
### **Problem: Booster doesn't scale up in center**
- Check: CenterSlot has `Apply Scale To Occupant` enabled
- Check: `Occupant Scale` is set (e.g., 2,2,1)
### **Problem: Cards don't reveal**
- Check: `Flippable Card Prefab` is assigned
- Check: Prefab has CardDisplay component
- Check: CardDisplay can receive card data
---
## 🎨 Visual Polish (Optional Next Steps)
### **Enhance Card Flip:**
- Add rotation animation when revealing
- Use DOTween or Tween for 3D flip effect
- Particle effects on reveal
### **Booster Opening Effects:**
- More dramatic particles when opening
- Sound effects on taps and opening
- Screen shake on final tap
### **Transition Polish:**
- Boosters fly in on page open
- Cards fly out after revealing
- Smooth transitions between boosters
---
## 📝 Summary
**You now have:**
✅ Complete booster opening flow
✅ Tap-to-shake interaction (3 taps default)
✅ Drag-and-drop alternative
✅ Card reveal system (click to flip)
✅ Auto-progression to next booster
✅ Auto-close when no boosters left
✅ Scalable system (adjust array size for more boosters)
**Next Steps:**
1. Create your visual assets (booster sprites, card backs)
2. Set up the scene structure as outlined
3. Configure the BoosterOpeningPage component
4. Test the flow
5. Polish with animations and effects
**Need Help?**
- Reference the existing drag-and-drop documentation
- Check CardSystem documentation for card data structure
- Test individual components in isolation first
---
🎉 **Happy Booster Opening!** 🎉

View File

@@ -0,0 +1,342 @@
# Booster Pack Prefab Structure Guide
## 🎨 Complete Booster Pack Prefab Setup
This guide shows you exactly how to structure your booster pack prefabs for the opening system.
---
## 📦 BoosterPackDraggable Prefab Structure
```
BoosterPackPrefab (RectTransform)
├── [Components on Root]
│ ├── RectTransform (200x300 size recommended)
│ ├── Image (IMPORTANT: raycastTarget = TRUE - for clicking/dragging)
│ ├── BoosterPackDraggable (script)
│ └── Canvas Group (optional - for fading)
└── Visual (Child GameObject)
└── [BoosterPackVisual Prefab Instance]
```
---
## 🎭 BoosterPackVisual Prefab Structure
```
BoosterVisual (RectTransform)
├── [Components on Root]
│ ├── RectTransform (same size as parent)
│ ├── Canvas (will be controlled at runtime)
│ ├── CanvasGroup (for alpha transitions)
│ └── BoosterPackVisual (script)
├── TiltParent (Empty Transform)
│ │ [This rotates for 3D tilt effect]
│ │
│ ├── PackImage (Image)
│ │ ├── Sprite: Your booster pack sprite
│ │ ├── raycastTarget: FALSE
│ │ └── Size: Slightly smaller than parent
│ │
│ ├── FrameImage (Image - optional)
│ │ ├── Sprite: Border/frame decoration
│ │ └── raycastTarget: FALSE
│ │
│ └── RarityIndicator (Image - optional)
│ ├── Sprite: Rarity gem/star
│ └── raycastTarget: FALSE
├── ShakeParent (Empty Transform)
│ │ [This is used for shake offset]
│ └── (Currently empty, reserved for effects)
└── GlowEffect (Particle System - optional)
├── Shape: Circle
├── Start Size: 5-10
├── Start Color: Golden/sparkly
├── Emission Rate: 10-20
└── Renderer: Sort Order = 1 (above images)
```
---
## ⚙️ Component Configuration
### **BoosterPackDraggable Settings:**
```yaml
[Draggable Settings]
Move Speed: 50
Smooth Movement:
Snap Duration: 0.3
[Visual]
Visual Prefab: (Assign BoosterVisual prefab)
Instantiate Visual:
Visual Parent: (Leave empty - uses canvas)
[Selection]
Is Selectable:
Selection Offset: 50
[Booster Pack Settings]
Can Open On Drop:
Can Open On Double Click:
[Tap to Open]
Can Tap To Open:
Max Taps To Open: 3
```
### **BoosterPackVisual Settings:**
```yaml
[References]
Canvas: (Auto-assigned)
Canvas Group: (Auto-assigned)
Tilt Parent: (Drag TiltParent object)
Shake Parent: (Drag ShakeParent object)
[Follow Parameters]
Follow Speed: 30
Use Follow Delay:
[Rotation/Tilt Parameters]
Rotation Amount: 20
Rotation Speed: 20
Auto Tilt Amount: 30
Manual Tilt Amount: 20
Tilt Speed: 20
[Scale Parameters]
Use Scale Animations:
Scale On Hover: 1.15
Scale On Drag: 1.25
Scale Transition Duration: 0.15
[Idle Animation]
Use Idle Animation:
Idle Animation Speed: 1
[Booster Pack Visual]
Pack Image: (Drag PackImage)
Pack Sprite: (Assign your sprite asset)
Glow Effect: (Drag particle system if using)
Glow Transform: (Drag particle transform if using)
[Opening Animation]
Opening Scale Punch: 0.5
Opening Rotation Punch: 360
Opening Duration: 0.5
```
---
## 🎨 Creating in Unity (Step-by-Step)
### **Step 1: Create Root GameObject**
1. Right-click in Hierarchy → UI → Image
2. Name it: `BoosterPackPrefab`
3. Set Size: 200x300 (Width x Height)
4. Add sprite: Temporary placeholder or your booster sprite
5. **IMPORTANT:** Ensure `raycastTarget` is ✓ checked
### **Step 2: Add BoosterPackDraggable**
1. Add Component → `BoosterPackDraggable`
2. Configure settings (see above)
3. Leave Visual Prefab empty for now
### **Step 3: Create Visual Child**
1. Right-click BoosterPackPrefab → Create Empty
2. Name it: `BoosterVisual`
3. Add Component → `Canvas`
4. Add Component → `Canvas Group`
5. Add Component → `BoosterPackVisual`
### **Step 4: Create TiltParent**
1. Right-click BoosterVisual → Create Empty
2. Name it: `TiltParent`
3. Position: (0, 0, 0)
### **Step 5: Add Images under TiltParent**
1. Right-click TiltParent → UI → Image
2. Name it: `PackImage`
3. Assign your booster sprite
4. **Set raycastTarget to ☐ UNCHECKED**
5. Size: 180x280 (slightly smaller)
### **Step 6: Create ShakeParent**
1. Right-click BoosterVisual → Create Empty
2. Name it: `ShakeParent`
3. Position: (0, 0, 0)
### **Step 7: Add Glow Effect (Optional)**
1. Right-click BoosterVisual → Effects → Particle System
2. Name it: `GlowEffect`
3. Configure:
- Duration: 1
- Looping: ✓
- Start Lifetime: 1-2
- Start Speed: 0
- Start Size: 5-10
- Start Color: Gold/Yellow with alpha
- Shape: Circle, Radius: 1
- Emission: Rate over Time = 15
- Renderer: Material = Default Particle, Sort Order = 1
### **Step 8: Wire Up References**
1. Select `BoosterVisual`
2. In BoosterPackVisual component:
- Tilt Parent: Drag `TiltParent`
- Shake Parent: Drag `ShakeParent`
- Pack Image: Drag `PackImage`
- Pack Sprite: Assign sprite asset
- Glow Effect: Drag `GlowEffect` (if using)
### **Step 9: Make Prefab**
1. Drag `BoosterPackPrefab` to Project folder
2. Delete from scene
3. You now have a reusable prefab!
---
## 🎯 Instantiation in Scene
### **Option A: Prefab Instances (Recommended)**
In your BoosterOpeningPage scene:
1. Create empty GameObject: `BoosterInstances`
2. Drag 3 instances of your prefab into it
3. Name them: `BoosterPack_0`, `BoosterPack_1`, `BoosterPack_2`
4. Set all to Active = ☐ (unchecked)
5. Position doesn't matter - they'll be assigned to slots
### **Option B: Runtime Instantiation**
- Assign the prefab to BoosterOpeningPage
- Script will instantiate as needed
- (Not implemented yet, but easy to add)
---
## 🧪 Testing Your Prefab
### **Test 1: Visual Check**
1. Place one instance in scene (active)
2. Enter Play Mode
3. Should see: Sprite, idle wobble animation
4. Hover over it: Should scale up slightly
### **Test 2: Drag Test**
1. Create a SlotContainer with a slot
2. Try dragging the booster to the slot
3. Should snap smoothly
4. Visual should lerp-follow with delay
### **Test 3: Tap Test**
1. Place booster in a slot
2. Set Can Tap To Open = ✓, Max Taps = 3
3. Click 3 times
4. Should see increasing shakes
---
## 📐 Size Recommendations
### **For Mobile:**
- Booster: 200x300
- Center Slot Scale: 2x → 400x600
- Card: 180x250
### **For Desktop:**
- Booster: 250x350
- Center Slot Scale: 1.5x → 375x525
- Card: 200x280
### **Spacing:**
- Bottom slots: 120-150 units apart
- Cards: 150-200 units apart
---
## 🎨 Sprite Requirements
### **Booster Pack Sprite:**
- Recommended: 512x768 or 1024x1536
- Format: PNG with transparency
- Style: Vertical rectangle (2:3 ratio)
- Should have clear visual identity
### **Card Back Sprite:**
- Same aspect ratio as cards
- Clearly distinct from front
- Can match booster theme
---
## 🔧 Troubleshooting Prefabs
**Problem: Can't click/drag booster**
→ Check: Root Image has raycastTarget = ✓
→ Check: Visual children have raycastTarget = ☐
**Problem: Visual doesn't follow smoothly**
→ Check: BoosterPackVisual is initialized
→ Check: Follow Speed > 0, Use Follow Delay = ✓
**Problem: No shake animation**
→ Check: BoosterPackVisual subscribes to OnTapped
→ Check: TiltParent and ShakeParent are assigned
**Problem: Booster looks weird when dragging**
→ Check: TiltParent contains the images
→ Check: Rotation parameters are reasonable (10-30)
---
## 💡 Advanced Customization
### **Rarity-Based Visuals:**
Add different sprites or effects based on rarity:
```csharp
// In BoosterPackDraggable or Visual:
public enum BoosterRarity { Common, Rare, Legendary }
public BoosterRarity rarity;
// Change sprite/effects based on rarity
```
### **Opening Sequence:**
Customize the opening animation:
- Adjust Opening Scale Punch (0.5 default)
- Adjust Opening Rotation Punch (360 default)
- Add sound effects in `PlayOpeningAnimation()`
### **Hover Effects:**
Make boosters more responsive:
- Increase Scale On Hover (1.15 → 1.3)
- Add glow intensity on hover
- Tilt toward mouse more (Manual Tilt Amount)
---
## ✅ Final Checklist
Before using your prefab:
- [ ] Root has BoosterPackDraggable
- [ ] Root Image has raycastTarget = TRUE
- [ ] Visual child has BoosterPackVisual
- [ ] TiltParent exists and contains sprites
- [ ] ShakeParent exists
- [ ] All image children have raycastTarget = FALSE
- [ ] References are wired up in Visual
- [ ] Prefab is saved in Project
- [ ] Can Tap To Open = ✓
- [ ] Max Taps To Open = 3 (or your choice)
---
🎉 **You're ready to create beautiful, interactive booster packs!** 🎉

View File

@@ -0,0 +1,309 @@
# Drag and Drop Card System
A robust, touch-compatible drag-and-drop system for Unity UI, inspired by Balatro's visual feel. Supports cards, booster packs, and any other draggable objects with smooth visual effects.
## Architecture Overview
The system is built on a separation of concerns:
- **Logic Layer** (`DraggableObject`) - Handles dragging, slot snapping, events
- **Visual Layer** (`DraggableVisual`) - Follows the logic object with lerping, tilting, animations
- **Slot System** (`DraggableSlot` + `SlotContainer`) - Manages positions and layout
## Core Components
### 1. DraggableObject (Abstract Base Class)
Base class for any draggable UI element.
**Key Features:**
- Touch-compatible via Unity's pointer event system
- Smooth movement toward pointer (configurable)
- Automatic slot snapping on release
- Selection support with visual offset
- Comprehensive event system
**Usage:**
```csharp
public class MyDraggable : DraggableObject
{
protected override void OnDragStartedHook()
{
// Custom logic when drag starts
}
}
```
### 2. DraggableVisual (Abstract Base Class)
Visual representation that follows the DraggableObject.
**Key Features:**
- Lerps toward parent position (not instant)
- Tilt based on movement velocity
- Auto-tilt idle animation (sine/cosine wobble)
- Manual tilt when hovering
- Scale animations on hover/drag/select
**Usage:**
```csharp
public class MyVisual : DraggableVisual
{
protected override void UpdateVisualContent()
{
// Update your visual elements here
}
}
```
### 3. DraggableSlot
Represents a position where draggables can snap.
**Key Features:**
- Occupancy management (one object per slot)
- Type filtering (restrict which types can occupy)
- Lock/unlock functionality
- Swap support
### 4. SlotContainer
Manages a collection of slots.
**Key Features:**
- Multiple layout types (Horizontal, Vertical, Grid, Custom)
- Curve-based positioning for horizontal layouts
- Automatic slot registration
- Find closest slot algorithm
**Layout Types:**
- **Horizontal** - Slots arranged in a horizontal line (with optional curve)
- **Vertical** - Slots arranged in a vertical line
- **Grid** - Slots arranged in a grid pattern
- **Custom** - Manually position slots
## Card-Specific Implementations
### CardDraggable
Card-specific draggable implementation.
**Features:**
- Holds `CardData` reference
- Events for card data changes
- Integrates with `CardSystemManager`
**Example:**
```csharp
CardDraggable card = GetComponent<CardDraggable>();
card.SetCardData(myCardData);
```
### CardDraggableVisual
Visual representation for cards.
**Features:**
- Uses existing `CardDisplay` component
- Shadow effects on press
- Automatic visual refresh when card data changes
### BoosterPackDraggable
Booster pack implementation.
**Features:**
- Double-click to open support
- Opening state management
- Events for opening
### BoosterPackVisual
Visual representation for booster packs.
**Features:**
- Glow particle effects
- Opening animation (scale + rotation)
- Sprite customization
## Setup Guide
### Basic Setup
1. **Create Slot Container:**
```
GameObject → UI → Panel (rename to "CardSlotContainer")
Add Component → SlotContainer
```
2. **Create Slots:**
```
Under CardSlotContainer:
GameObject → UI → Empty (rename to "Slot_01")
Add Component → DraggableSlot
```
Duplicate for as many slots as needed.
3. **Create Draggable Card:**
```
GameObject → UI → Image (rename to "CardDraggable")
Add Component → CardDraggable
```
4. **Create Visual Prefab:**
```
Create a prefab with:
- Root: Empty GameObject with CardDraggableVisual component
- Child: Canvas (for sorting control)
- Child: TiltParent (Transform for tilt effects)
- Child: ShakeParent (Transform for punch effects)
- Child: CardDisplay (your existing card visual)
```
5. **Link Visual to Draggable:**
```
On CardDraggable:
- Assign your visual prefab to "Visual Prefab"
- Set "Instantiate Visual" to true
```
### Advanced: Curved Hand Layout
For a Balatro-style curved card hand:
1. On SlotContainer:
- Set Layout Type to "Horizontal"
- Enable "Use Curve Layout"
- Edit "Position Curve" (try: keys at 0,0.5,1 with values 0,1,0)
- Set "Curve Height" (e.g., 50)
- Enable "Center Slots"
2. Adjust spacing to fit your card size
## Event System
### DraggableObject Events:
```csharp
draggable.OnDragStarted += (obj) => { };
draggable.OnDragEnded += (obj) => { };
draggable.OnPointerEntered += (obj) => { };
draggable.OnPointerExited += (obj) => { };
draggable.OnPointerDowned += (obj) => { };
draggable.OnPointerUpped += (obj, longPress) => { };
draggable.OnSelected += (obj, selected) => { };
draggable.OnSlotChanged += (obj, slot) => { };
```
### CardDraggable Events:
```csharp
cardDraggable.OnCardDataChanged += (card, data) => { };
```
### BoosterPackDraggable Events:
```csharp
boosterDraggable.OnBoosterOpened += (pack) => { };
```
## Touch Support
The system is fully touch-compatible out of the box! Unity's Event System automatically routes touch events through the pointer interfaces.
**Supported Gestures:**
- Single touch drag
- Tap to select
- Double-tap (on booster packs)
- Long press detection
**Note:** For multi-touch support, the system uses PointerEventData which handles the first touch automatically. Additional touch support can be added by extending the pointer event handlers.
## Performance Tips
1. **Disable Idle Animations** if you have many cards:
```
On DraggableVisual: useIdleAnimation = false
```
2. **Reduce Follow Speed** for smoother performance:
```
On DraggableVisual: followSpeed = 20 (default: 30)
```
3. **Disable Scale Animations** if needed:
```
On DraggableVisual: useScaleAnimations = false
```
4. **Use Object Pooling** for spawning many cards
## Extending the System
### Creating Custom Draggable Types
1. Inherit from `DraggableObject`:
```csharp
public class MyCustomDraggable : DraggableObject
{
protected override void OnDragStartedHook()
{
// Your logic
}
}
```
2. Inherit from `DraggableVisual`:
```csharp
public class MyCustomVisual : DraggableVisual
{
protected override void UpdateVisualContent()
{
// Update your visuals
}
}
```
### Custom Slot Filtering
```csharp
// On DraggableSlot component:
filterByType = true
allowedTypeNames = { "CardDraggable", "BoosterPackDraggable" }
```
## Troubleshooting
**Cards don't snap to slots:**
- Ensure SlotContainer has slots registered
- Check that slots aren't locked
- Verify type filtering isn't blocking the card
**Visuals don't follow smoothly:**
- Check followSpeed value (try 20-40)
- Ensure TiltParent and ShakeParent are assigned
- Verify the visual prefab has correct hierarchy
**Touch not working:**
- Ensure EventSystem exists in scene
- Check Canvas Raycast Target is enabled
- Verify GraphicRaycaster is on Canvas
**Cards jitter or shake:**
- Reduce followSpeed
- Disable idle animation
- Check for conflicting tweens
## Integration with Card System
The drag-and-drop system integrates seamlessly with your existing `CardSystemManager`:
```csharp
// Spawn a draggable card from CardData
CardData cardData = CardSystemManager.Instance.GetAllCollectedCards()[0];
GameObject cardObj = Instantiate(cardDraggablePrefab, slotContainer.transform);
CardDraggable card = cardObj.GetComponent<CardDraggable>();
card.SetCardData(cardData);
// Assign to first available slot
DraggableSlot slot = slotContainer.GetAvailableSlots().FirstOrDefault();
if (slot != null)
{
card.AssignToSlot(slot, false);
}
```
## Credits
Inspired by the excellent feel of Balatro's card system (mixandjam/balatro-feel on GitHub).
Adapted for AppleHills card collection game with full touch support and Unity UI integration.