Code up the card part
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
3
Assets/Scripts/UI/CardSystem/DragDrop.meta
Normal file
3
Assets/Scripts/UI/CardSystem/DragDrop.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 053a2ff2538541699b134b07a07edecb
|
||||
timeCreated: 1762420654
|
||||
120
Assets/Scripts/UI/CardSystem/DragDrop/BoosterPackDraggable.cs
Normal file
120
Assets/Scripts/UI/CardSystem/DragDrop/BoosterPackDraggable.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f95c1542aaa549d1867b43f6dc21e90f
|
||||
timeCreated: 1762420681
|
||||
189
Assets/Scripts/UI/CardSystem/DragDrop/BoosterPackVisual.cs
Normal file
189
Assets/Scripts/UI/CardSystem/DragDrop/BoosterPackVisual.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7d9474ece3b4d2ebad19ae178b22f4d
|
||||
timeCreated: 1762420699
|
||||
62
Assets/Scripts/UI/CardSystem/DragDrop/CardDraggable.cs
Normal file
62
Assets/Scripts/UI/CardSystem/DragDrop/CardDraggable.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a2741bb7299441b9f9bd44d746ebb4b
|
||||
timeCreated: 1762420654
|
||||
121
Assets/Scripts/UI/CardSystem/DragDrop/CardDraggableVisual.cs
Normal file
121
Assets/Scripts/UI/CardSystem/DragDrop/CardDraggableVisual.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a4c3884410d44f98182cd8119a972a4
|
||||
timeCreated: 1762420668
|
||||
3
Assets/Scripts/UI/DragAndDrop.meta
Normal file
3
Assets/Scripts/UI/DragAndDrop.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9818aa1de299458b8b1fc95cdabc3f7f
|
||||
timeCreated: 1762420597
|
||||
3
Assets/Scripts/UI/DragAndDrop/Core.meta
Normal file
3
Assets/Scripts/UI/DragAndDrop/Core.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de2fa1660c564a13ab22715e94b45e4c
|
||||
timeCreated: 1762420597
|
||||
476
Assets/Scripts/UI/DragAndDrop/Core/DraggableObject.cs
Normal file
476
Assets/Scripts/UI/DragAndDrop/Core/DraggableObject.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 062198d83a0940538c140da0999f4de9
|
||||
timeCreated: 1762420597
|
||||
139
Assets/Scripts/UI/DragAndDrop/Core/DraggableSlot.cs
Normal file
139
Assets/Scripts/UI/DragAndDrop/Core/DraggableSlot.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/UI/DragAndDrop/Core/DraggableSlot.cs.meta
Normal file
3
Assets/Scripts/UI/DragAndDrop/Core/DraggableSlot.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee43b700f9dd44dba39deb8c5bcd688c
|
||||
timeCreated: 1762420738
|
||||
367
Assets/Scripts/UI/DragAndDrop/Core/DraggableVisual.cs
Normal file
367
Assets/Scripts/UI/DragAndDrop/Core/DraggableVisual.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 203c649ed73845c6999682bcf8383ee8
|
||||
timeCreated: 1762420644
|
||||
258
Assets/Scripts/UI/DragAndDrop/Core/SlotContainer.cs
Normal file
258
Assets/Scripts/UI/DragAndDrop/Core/SlotContainer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/UI/DragAndDrop/Core/SlotContainer.cs.meta
Normal file
3
Assets/Scripts/UI/DragAndDrop/Core/SlotContainer.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1347da8005d4687a85dbc4db9a1bbbb
|
||||
timeCreated: 1762420766
|
||||
365
docs/booster_opening_setup_guide.md
Normal file
365
docs/booster_opening_setup_guide.md
Normal 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!** 🎉
|
||||
|
||||
342
docs/booster_prefab_structure_guide.md
Normal file
342
docs/booster_prefab_structure_guide.md
Normal 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!** 🎉
|
||||
|
||||
309
docs/drag_drop_system_readme.md
Normal file
309
docs/drag_drop_system_readme.md
Normal 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.
|
||||
|
||||
Reference in New Issue
Block a user