Merge branch 'main' into DamianBranch

This commit is contained in:
2025-11-06 17:11:57 +01:00
65 changed files with 11318 additions and 717 deletions

View File

@@ -1,4 +1,6 @@
using Pixelplacement;
using Bootstrap;
using Data.CardSystem;
using Pixelplacement;
using UI.Core;
using UnityEngine;
using UnityEngine.UI;
@@ -7,12 +9,20 @@ namespace UI.CardSystem
{
/// <summary>
/// UI page for viewing the player's card collection in an album.
/// Manages booster pack button visibility and opening flow.
/// </summary>
public class AlbumViewPage : UIPage
{
[Header("UI References")]
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private Button exitButton;
[SerializeField] private BookCurlPro.BookPro book;
[Header("Booster Pack UI")]
[SerializeField] private GameObject[] boosterPackButtons;
[SerializeField] private BoosterOpeningPage boosterOpeningPage;
private Input.InputMode _previousInputMode;
private void Awake()
{
@@ -28,16 +38,73 @@ namespace UI.CardSystem
exitButton.onClick.AddListener(OnExitButtonClicked);
}
// Set up booster pack button listeners
SetupBoosterButtonListeners();
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
// UI pages should start disabled
gameObject.SetActive(false);
}
private void InitializePostBoot()
{
// Subscribe to CardSystemManager events
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged += OnBoosterCountChanged;
// Update initial button visibility
int initialCount = CardSystemManager.Instance.GetBoosterPackCount();
UpdateBoosterButtons(initialCount);
}
}
private void SetupBoosterButtonListeners()
{
if (boosterPackButtons == null) return;
for (int i = 0; i < boosterPackButtons.Length; i++)
{
if (boosterPackButtons[i] == null) continue;
Button button = boosterPackButtons[i].GetComponent<Button>();
if (button != null)
{
button.onClick.AddListener(OnBoosterButtonClicked);
}
}
}
private void OnDestroy()
{
// Unsubscribe from CardSystemManager
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged;
}
// Clean up exit button
if (exitButton != null)
{
exitButton.onClick.RemoveListener(OnExitButtonClicked);
}
// Clean up booster button listeners
if (boosterPackButtons != null)
{
foreach (var buttonObj in boosterPackButtons)
{
if (buttonObj == null) continue;
Button button = buttonObj.GetComponent<Button>();
if (button != null)
{
button.onClick.RemoveListener(OnBoosterButtonClicked);
}
}
}
}
private void OnExitButtonClicked()
@@ -57,12 +124,72 @@ namespace UI.CardSystem
else
{
// Already on page 0 or no book reference, exit
// Restore input mode before popping
if (Input.InputManager.Instance != null)
{
Input.InputManager.Instance.SetInputMode(_previousInputMode);
Debug.Log($"[AlbumViewPage] Restored input mode to {_previousInputMode} on exit");
}
if (UIPageController.Instance != null)
{
UIPageController.Instance.PopPage();
}
}
}
private void OnBoosterCountChanged(int newCount)
{
UpdateBoosterButtons(newCount);
}
private void UpdateBoosterButtons(int boosterCount)
{
if (boosterPackButtons == null || boosterPackButtons.Length == 0) return;
int visibleCount = Mathf.Min(boosterCount, boosterPackButtons.Length);
for (int i = 0; i < boosterPackButtons.Length; i++)
{
if (boosterPackButtons[i] != null)
{
boosterPackButtons[i].SetActive(i < visibleCount);
}
}
}
private void OnBoosterButtonClicked()
{
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);
}
}
public override void TransitionIn()
{
// Only store and switch input mode if this is the first time entering
// (when _previousInputMode hasn't been set yet)
if (Input.InputManager.Instance != null)
{
// Store the current input mode before switching
_previousInputMode = Input.InputMode.GameAndUI;
Input.InputManager.Instance.SetInputMode(Input.InputMode.UI);
Debug.Log("[AlbumViewPage] Switched to UI-only input mode on first entry");
}
base.TransitionIn();
}
public override void TransitionOut()
{
// Don't restore input mode here - only restore when actually exiting (in OnExitButtonClicked)
base.TransitionOut();
}
protected override void DoTransitionIn(System.Action onComplete)
{

View File

@@ -0,0 +1,225 @@
using Bootstrap;
using Data.CardSystem;
using Pixelplacement;
using Pixelplacement.TweenSystem;
using TMPro;
using UnityEngine;
namespace UI.CardSystem
{
/// <summary>
/// Manages a notification dot that displays a count (e.g., booster packs)
/// Can be reused across different UI elements that need to show numeric notifications
/// Automatically syncs with CardSystemManager to display booster pack count
/// </summary>
public class BoosterNotificationDot : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private GameObject dotBackground;
[SerializeField] private TextMeshProUGUI countText;
[Header("Settings")]
[SerializeField] private bool hideWhenZero = true;
[SerializeField] private bool useAnimation = false;
[SerializeField] private string textPrefix = "";
[SerializeField] private string textSuffix = "";
[SerializeField] private Color textColor = Color.white;
[Header("Animation")]
[SerializeField] private bool useTween = true;
[SerializeField] private float pulseDuration = 0.3f;
[SerializeField] private float pulseScale = 1.2f;
// Optional animator reference
[SerializeField] private Animator animator;
[SerializeField] private string animationTrigger = "Update";
// Current count value
private int _currentCount;
private Vector3 _originalScale;
private TweenBase _activeTween;
private void Awake()
{
// Store original scale for pulse animation
if (dotBackground != null)
{
_originalScale = dotBackground.transform.localScale;
}
// Apply text color
if (countText != null)
{
countText.color = textColor;
}
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void InitializePostBoot()
{
// Subscribe to CardSystemManager events
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged += OnBoosterCountChanged;
// Poll initial count and display it
int initialCount = CardSystemManager.Instance.GetBoosterPackCount();
SetCount(initialCount);
}
else
{
// If CardSystemManager isn't available yet, set to default count
SetCount(_currentCount);
}
}
private void OnDestroy()
{
// Unsubscribe from CardSystemManager events to prevent memory leaks
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged;
}
}
/// <summary>
/// Callback when booster count changes in CardSystemManager
/// </summary>
private void OnBoosterCountChanged(int newCount)
{
SetCount(newCount);
}
/// <summary>
/// Sets the count displayed on the notification dot
/// Also handles visibility based on settings
/// </summary>
public void SetCount(int count)
{
bool countChanged = count != _currentCount;
_currentCount = count;
// Update text
if (countText != null)
{
countText.text = textPrefix + count.ToString() + textSuffix;
}
// Handle visibility
if (hideWhenZero)
{
SetVisibility(count > 0);
}
// Play animation if value changed and animation is enabled
if (countChanged && count > 0)
{
if (useAnimation)
{
Animate();
}
}
}
/// <summary>
/// Gets the current count value
/// </summary>
public int GetCount()
{
return _currentCount;
}
/// <summary>
/// Set text formatting options
/// </summary>
public void SetFormatting(string prefix, string suffix, Color color)
{
textPrefix = prefix;
textSuffix = suffix;
textColor = color;
if (countText != null)
{
countText.color = color;
// Update text with new formatting
countText.text = textPrefix + _currentCount.ToString() + textSuffix;
}
}
/// <summary>
/// Explicitly control the notification dot visibility
/// </summary>
public void SetVisibility(bool isVisible)
{
if (dotBackground != null)
{
dotBackground.SetActive(isVisible);
}
if (countText != null)
{
countText.gameObject.SetActive(isVisible);
}
}
/// <summary>
/// Show the notification dot
/// </summary>
public void Show()
{
SetVisibility(true);
}
/// <summary>
/// Hide the notification dot
/// </summary>
public void Hide()
{
SetVisibility(false);
}
/// <summary>
/// Play animation manually - either using Animator or Tween
/// </summary>
public void Animate()
{
if (useAnimation)
{
if (animator != null)
{
animator.SetTrigger(animationTrigger);
}
else if (useTween && dotBackground != null)
{
// Cancel any existing tweens on this transform
if(_activeTween != null)
_activeTween.Cancel();
// Reset to original scale
dotBackground.transform.localScale = _originalScale;
// Pulse animation using Tween
_activeTween = Tween.LocalScale(dotBackground.transform,
_originalScale * pulseScale,
pulseDuration/2,
0,
Tween.EaseOut,
Tween.LoopType.None,
null,
() => {
// Scale back to original size
Tween.LocalScale(dotBackground.transform,
_originalScale,
pulseDuration/2,
0,
Tween.EaseIn);
},
obeyTimescale: false);
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5845ed3764635fe429b6f1063effdd8a

View File

@@ -0,0 +1,529 @@
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;
namespace UI.CardSystem
{
/// <summary>
/// UI page for opening booster packs and displaying the cards received.
/// Manages the entire booster opening flow with drag-and-drop interaction.
/// </summary>
public class BoosterOpeningPage : UIPage
{
[Header("UI References")]
[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 GameObject flippableCardPrefab; // Placeholder for card backs
[SerializeField] private float cardSpacing = 150f;
[Header("Settings")]
[SerializeField] private float cardRevealDelay = 0.5f;
[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()
{
// Make sure we have a CanvasGroup for transitions
if (canvasGroup == null)
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
// Set up close button
if (closeButton != null)
{
closeButton.onClick.AddListener(OnCloseButtonClicked);
}
// UI pages should start disabled
gameObject.SetActive(false);
}
private void OnDestroy()
{
if (closeButton != null)
{
closeButton.onClick.RemoveListener(OnCloseButtonClicked);
}
// Unsubscribe from slot events
if (centerOpeningSlot != null)
{
centerOpeningSlot.OnOccupied -= OnBoosterPlacedInCenter;
}
// Unsubscribe from booster events
UnsubscribeFromAllBoosters();
}
private void OnCloseButtonClicked()
{
if (UIPageController.Instance != null)
{
UIPageController.Instance.PopPage();
}
}
/// <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()
{
Debug.Log($"[BoosterOpeningPage] InitializeBoosterDisplay called with {_availableBoosterCount} boosters available");
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);
Debug.Log($"[BoosterOpeningPage] Will show {visibleCount} boosters out of {boosterPackInstances.Length} instances");
// Show/hide boosters and assign to slots
for (int i = 0; i < boosterPackInstances.Length; i++)
{
if (boosterPackInstances[i] == null) continue;
bool shouldShow = i < visibleCount;
Debug.Log($"[BoosterOpeningPage] Booster {i} ({boosterPackInstances[i].name}): shouldShow={shouldShow}, position={boosterPackInstances[i].transform.position}");
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)
{
Debug.Log($"[BoosterOpeningPage] Assigning booster {i} to slot {slot.name} at {slot.transform.position}");
booster.AssignToSlot(slot, false);
}
else
{
Debug.LogWarning($"[BoosterOpeningPage] Slot {i} is null in bottomRightSlots!");
}
}
else
{
Debug.LogWarning($"[BoosterOpeningPage] No slot available for booster {i}. bottomRightSlots={bottomRightSlots}, SlotCount={bottomRightSlots?.SlotCount}");
}
}
else
{
Debug.LogWarning($"[BoosterOpeningPage] Booster {i} has no BoosterPackDraggable component!");
}
}
}
// Subscribe to center slot events
if (centerOpeningSlot != null)
{
centerOpeningSlot.OnOccupied += OnBoosterPlacedInCenter;
Debug.Log($"[BoosterOpeningPage] Subscribed to center slot {centerOpeningSlot.name} at {centerOpeningSlot.transform.position}");
}
else
{
Debug.LogWarning("[BoosterOpeningPage] centerOpeningSlot is null!");
}
}
/// <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);
// Configure booster for opening (disables drag, enables tapping, resets tap count)
booster.SetInOpeningSlot(true);
// Subscribe to tap events for visual feedback
booster.OnTapped += OnBoosterTapped;
booster.OnReadyToOpen += OnBoosterReadyToOpen;
Debug.Log($"[BoosterOpeningPage] Booster placed in center, ready for {booster.CurrentTapCount} taps");
}
private void OnBoosterTapped(BoosterPackDraggable booster, int currentTaps, int maxTaps)
{
Debug.Log($"[BoosterOpeningPage] Booster tapped: {currentTaps}/{maxTaps}");
// Calculate shake intensity (increases with each tap)
float shakeIntensity = currentTaps / (float)maxTaps;
float shakeAmount = 10f + (shakeIntensity * 30f); // 10 to 40 units
// TODO: Shake visual feedback
// This would be handled by BoosterPackVisual if we add a shake method
}
/// <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;
Debug.Log($"[BoosterOpeningPage] Booster ready to open!");
// Trigger the actual opening sequence
booster.TriggerOpen();
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
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
Tween.Value(0f, 1f, (value) => canvasGroup.alpha = value, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, onComplete, obeyTimescale: false);
}
else
{
// Fallback if no CanvasGroup
onComplete?.Invoke();
}
}
protected override void DoTransitionOut(System.Action onComplete)
{
// Simple fade out animation
if (canvasGroup != null)
{
Tween.Value(canvasGroup.alpha, 0f, (value) => canvasGroup.alpha = value, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, onComplete, obeyTimescale: false);
}
else
{
// Fallback if no CanvasGroup
onComplete?.Invoke();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 91691a5efb1346b5b34482dd8200c868
timeCreated: 1762418615

View File

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

View File

@@ -0,0 +1,167 @@
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;
[SerializeField] private float tapPulseScale = 1.15f;
[SerializeField] private float tapPulseDuration = 0.2f;
[SerializeField] private ParticleSystem openingParticleSystem;
// ...existing code...
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 a long press)
if (canTapToOpen && !longPress && CurrentSlot != null)
{
_currentTapCount++;
// Pulse effect on tap (scales visual up and back down)
if (Visual != null)
{
// Calculate pulse intensity based on tap progress
float tapProgress = _currentTapCount / (float)maxTapsToOpen;
float currentPulseScale = 1f + (tapPulseScale - 1f) * (0.5f + tapProgress * 0.5f); // Increases from 1.075 to 1.15
// Save the current scale before pulsing
Vector3 baseScale = Visual.transform.localScale;
Pixelplacement.Tween.Cancel(Visual.transform.GetInstanceID());
Pixelplacement.Tween.LocalScale(Visual.transform, baseScale * currentPulseScale, tapPulseDuration * 0.5f, 0f,
Pixelplacement.Tween.EaseOutBack, completeCallback: () =>
{
// Return to the base scale we had before pulsing
Pixelplacement.Tween.LocalScale(Visual.transform, baseScale, tapPulseDuration * 0.5f, 0f, Pixelplacement.Tween.EaseInBack);
});
}
OnTapped?.Invoke(this, _currentTapCount, maxTapsToOpen);
if (_currentTapCount >= maxTapsToOpen)
{
OnReadyToOpen?.Invoke(this);
}
return; // Don't process double-click if tap-to-open is active
}
// ...existing code...
if (canOpenOnDoubleClick && !longPress)
{
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;
// Play particle effect
if (openingParticleSystem != null)
{
openingParticleSystem.Play();
}
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;
_currentTapCount = 0;
}
/// <summary>
/// Set whether this booster is in the opening slot (disables dragging, enables tapping)
/// </summary>
public void SetInOpeningSlot(bool inSlot)
{
SetDraggingEnabled(!inSlot); // Disable dragging when in opening slot
canTapToOpen = inSlot; // Enable tap-to-open when in opening slot
if (inSlot)
{
_currentTapCount = 0; // Reset tap counter when placed
}
else
{
ResetOpeningState(); // Reset completely when removed
}
}
/// <summary>
/// Reset tap count (useful when starting a new opening sequence)
/// </summary>
public void ResetTapCount()
{
_currentTapCount = 0;
}
/// <summary>
/// Enable or disable tap-to-open functionality at runtime
/// </summary>
public void SetTapToOpenEnabled(bool enabled)
{
canTapToOpen = enabled;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,577 @@
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.
/// Note: Optionally uses Image or CanvasGroup for automatic raycast toggling during drag.
/// </summary>
public abstract class DraggableObject : MonoBehaviour,
IBeginDragHandler, IDragHandler, IEndDragHandler,
IPointerEnterHandler, IPointerExitHandler,
IPointerUpHandler, IPointerDownHandler
{
[Header("Draggable Settings")]
[SerializeField] protected float moveSpeed = 50f;
[SerializeField] protected bool smoothMovement = false; // Disabled for instant cursor tracking
[SerializeField] protected float snapDuration = 0.3f;
[Header("Visual")]
[SerializeField] protected DraggableVisual visual;
[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;
protected bool _isDraggingEnabled = true;
// References
protected Canvas _canvas;
protected Image _imageComponent;
protected CanvasGroup _canvasGroup;
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 Awake()
{
Initialize();
}
protected virtual void Initialize()
{
Debug.Log($"[DraggableObject] Initializing {name} at world pos {transform.position}, local pos {transform.localPosition}, parent: {(transform.parent != null ? transform.parent.name : "NULL")}");
_canvas = GetComponentInParent<Canvas>();
Debug.Log($"[DraggableObject] {name} found canvas: {(_canvas != null ? _canvas.name : "NULL")}, canvas pos: {(_canvas != null ? _canvas.transform.position.ToString() : "N/A")}");
_imageComponent = GetComponent<Image>();
_canvasGroup = GetComponent<CanvasGroup>();
_raycaster = _canvas?.GetComponent<GraphicRaycaster>();
// If no Image component exists, add an invisible one for raycast detection
// Unity UI requires a Graphic component to receive pointer events
if (_imageComponent == null && _canvasGroup != null)
{
_imageComponent = gameObject.AddComponent<Image>();
_imageComponent.color = new Color(1, 1, 1, 0.01f); // Nearly transparent (0 doesn't work)
_imageComponent.raycastTarget = true;
Debug.Log($"[DraggableObject] Added invisible Image to {name} for raycast detection");
}
// Use assigned visual, or find in children recursively if not assigned
if (visual != null)
{
_visualInstance = visual;
}
else
{
_visualInstance = GetComponentInChildren<DraggableVisual>(true);
}
// Initialize the visual if found
if (_visualInstance != null)
{
_visualInstance.Initialize(this);
}
// If we're already in a slot, register with it
DraggableSlot parentSlot = GetComponentInParent<DraggableSlot>();
if (parentSlot != null)
{
AssignToSlot(parentSlot, false);
}
}
protected virtual void Update()
{
if (_isDragging && smoothMovement)
{
SmoothMoveTowardPointer();
}
// Only clamp for non-overlay canvases (WorldSpace/ScreenSpaceCamera)
if (_canvas != null && _canvas.renderMode != RenderMode.ScreenSpaceOverlay)
{
ClampToScreen();
}
}
protected virtual void SmoothMoveTowardPointer()
{
if (RectTransform == null)
return;
// For ScreenSpaceOverlay, work with screen/anchoredPosition
if (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
Vector2 targetPos = (Vector2)_lastPointerPosition - (Vector2)_dragOffset;
Vector2 currentPos = RectTransform.position;
Vector2 direction = (targetPos - currentPos).normalized;
float distance = Vector2.Distance(currentPos, targetPos);
float speed = Mathf.Min(moveSpeed, distance / Time.deltaTime);
RectTransform.position = currentPos + direction * speed * Time.deltaTime;
}
else
{
// For WorldSpace/ScreenSpaceCamera, use world coordinates
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()
{
// This method is only called for WorldSpace/ScreenSpaceCamera canvases
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;
// Check if dragging is enabled BEFORE setting any state
if (!_isDraggingEnabled)
return;
_isDragging = true;
_wasDragged = true;
// ...existing code...
if (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay && RectTransform != null)
{
// For overlay, use screen position directly
_dragOffset = (Vector3)eventData.position - RectTransform.position;
_lastPointerPosition = eventData.position;
}
else
{
// For WorldSpace/ScreenSpaceCamera, convert to world coords
Vector3 worldPointer = GetWorldPosition(eventData);
_dragOffset = worldPointer - transform.position;
_lastPointerPosition = worldPointer;
}
// Reset base rotation to identity (0°) for clean dragging
Tween.Rotation(transform, Quaternion.identity, 0.2f, 0f, Tween.EaseOutBack);
// Disable raycasting to allow detecting slots underneath
if (_raycaster != null)
_raycaster.enabled = false;
if (_imageComponent != null)
_imageComponent.raycastTarget = false;
if (_canvasGroup != null)
_canvasGroup.blocksRaycasts = 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;
// Update last pointer position based on canvas type
if (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
_lastPointerPosition = eventData.position;
if (!smoothMovement && RectTransform != null)
{
RectTransform.position = (Vector2)_lastPointerPosition - (Vector2)_dragOffset;
}
}
else
{
_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;
if (_canvasGroup != null)
_canvasGroup.blocksRaycasts = true;
// Find closest slot and snap
FindAndSnapToSlot();
// Snap base rotation back to slot rotation (if in a slot)
if (_currentSlot != null)
{
Tween.Rotation(transform, _currentSlot.transform.rotation, 0.3f, 0f, Tween.EaseOutBack);
}
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;
// Use RectTransform.position for overlay, transform.position for others
Vector3 myPosition = (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay && RectTransform != null)
? RectTransform.position
: transform.position;
foreach (var container in containers)
{
DraggableSlot slot = container.FindClosestSlot(myPosition, this);
if (slot != null)
{
Vector3 slotPosition = slot.RectTransform != null ? slot.RectTransform.position : slot.transform.position;
float distance = Vector3.Distance(myPosition, slotPosition);
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;
Debug.Log($"[DraggableObject] Assigning {name} to slot {slot.name}, animate={animate}, current pos={transform.position}, slot pos={slot.transform.position}");
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;
transform.localRotation = Quaternion.identity;
Debug.Log($"[DraggableObject] {name} assigned to slot {slot.name}, new world pos={transform.position}, local pos={transform.localPosition}");
}
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);
Tween.LocalRotation(transform, Quaternion.identity, snapDuration, 0f, Tween.EaseOutBack);
}
else
{
transform.localPosition = targetLocalPos;
transform.localRotation = Quaternion.identity;
}
}
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 Dragging Control
/// <summary>
/// Enable or disable dragging functionality
/// </summary>
public virtual void SetDraggingEnabled(bool enabled)
{
_isDraggingEnabled = enabled;
}
#endregion
#region Helper Methods
protected Vector3 GetWorldPosition(PointerEventData eventData)
{
if (Camera.main == null)
return Vector3.zero;
// For screen space overlay canvas
if (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
return eventData.position;
}
// For world space or camera space
return Camera.main.ScreenToWorldPoint(new Vector3(eventData.position.x, eventData.position.y, _canvas.planeDistance));
}
protected IEnumerator ResetWasDraggedFlag()
{
yield return new WaitForEndOfFrame();
_wasDragged = false;
}
#endregion
#region Abstract/Virtual Hooks for Subclasses
protected virtual void OnDragStartedHook() { }
protected virtual void OnDragEndedHook() { }
protected virtual void OnPointerEnterHook() { }
protected virtual void OnPointerExitHook() { }
protected virtual void OnPointerDownHook() { }
protected virtual void OnPointerUpHook(bool longPress) { }
protected virtual void OnSelectionChangedHook(bool selected) { }
protected virtual void OnSlotChangedHook(DraggableSlot previousSlot, DraggableSlot newSlot) { }
#endregion
protected virtual void OnDestroy()
{
if (_visualInstance != null)
{
Destroy(_visualInstance.gameObject);
}
}
public int GetSiblingCount()
{
return _currentSlot != null && _currentSlot.transform.parent != null
? _currentSlot.transform.parent.childCount - 1
: 0;
}
public int GetSlotIndex()
{
return _currentSlot != null ? _currentSlot.SlotIndex : 0;
}
public float GetNormalizedSlotPosition()
{
if (_currentSlot == null || _currentSlot.transform.parent == null)
return 0f;
int siblingCount = _currentSlot.transform.parent.childCount - 1;
if (siblingCount <= 0)
return 0f;
return (float)_currentSlot.SlotIndex / siblingCount;
}
}
}

View File

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

View File

@@ -0,0 +1,182 @@
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
{
public enum OccupantSizeMode
{
None, // Don't modify occupant size
MatchSlotSize, // Set occupant RectTransform size to match slot size
Scale // Apply scale multiplier to occupant
}
[Header("Slot Settings")]
[SerializeField] private int slotIndex;
[SerializeField] private bool isLocked;
[SerializeField] private bool hideImageOnPlay = false;
[Header("Type Filtering")]
[SerializeField] private bool filterByType;
[SerializeField] private string[] allowedTypeNames;
[Header("Occupant Size Control")]
[SerializeField] private OccupantSizeMode occupantSizeMode = OccupantSizeMode.None;
[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;
private void Start()
{
if (hideImageOnPlay)
{
UnityEngine.UI.Image image = GetComponent<UnityEngine.UI.Image>();
if (image != null)
{
Destroy(image);
}
}
}
/// <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 size modification based on mode
switch (occupantSizeMode)
{
case OccupantSizeMode.MatchSlotSize:
if (draggable.RectTransform != null && RectTransform != null)
{
Vector2 targetSize = RectTransform.sizeDelta;
Tween.Value(draggable.RectTransform.sizeDelta, targetSize,
(val) => draggable.RectTransform.sizeDelta = val,
scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
break;
case OccupantSizeMode.Scale:
Tween.LocalScale(draggable.transform, occupantScale, scaleTransitionDuration, 0f, Tween.EaseOutBack);
// Also scale the visual if it exists (since visual is now independent)
if (draggable.Visual != null)
{
Tween.LocalScale(draggable.Visual.transform, occupantScale, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
break;
case OccupantSizeMode.None:
default:
// Don't modify size
break;
}
OnOccupied?.Invoke(draggable);
return true;
}
/// <summary>
/// Vacate this slot, removing the current occupant
/// </summary>
public void Vacate()
{
if (_occupant != null)
{
DraggableObject previousOccupant = _occupant;
_occupant = null;
OnVacated?.Invoke(previousOccupant);
}
}
/// <summary>
/// Check if this slot can accept a specific draggable type
/// </summary>
public bool CanAccept(DraggableObject draggable)
{
if (!filterByType || allowedTypeNames == null || allowedTypeNames.Length == 0)
return true;
string draggableTypeName = draggable.GetType().Name;
foreach (string allowedType in allowedTypeNames)
{
if (draggableTypeName == allowedType)
return true;
}
return false;
}
/// <summary>
/// Swap occupants with another slot
/// </summary>
public void SwapWith(DraggableSlot otherSlot)
{
if (otherSlot == null || otherSlot == this)
return;
DraggableObject thisOccupant = _occupant;
DraggableObject otherOccupant = otherSlot._occupant;
// Vacate both slots
Vacate();
otherSlot.Vacate();
// Occupy with swapped objects
if (otherOccupant != null)
Occupy(otherOccupant);
if (thisOccupant != null)
otherSlot.Occupy(thisOccupant);
}
/// <summary>
/// Lock/unlock this slot
/// </summary>
public void SetLocked(bool locked)
{
isLocked = locked;
}
/// <summary>
/// Set the slot index
/// </summary>
public void SetSlotIndex(int index)
{
slotIndex = index;
}
}
}

View File

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

View File

@@ -0,0 +1,529 @@
using Pixelplacement;
using UnityEngine;
using UnityEngine.InputSystem; // Added for new Input System
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 = 10f; // Reduced from 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 = 0.5f; // Slowed down from 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;
Canvas parentCanvas = parent.GetComponentInParent<Canvas>();
Debug.Log($"[DraggableVisual] Initializing visual for {parent.name} at world pos {parent.transform.position}, parent canvas: {(parentCanvas != null ? parentCanvas.name + " (renderMode: " + parentCanvas.renderMode + ")" : "NULL")}");
// CRITICAL: Reparent visual to canvas (not base) so it can move independently
// This enables the delayed follow effect
if (parentCanvas != null)
{
transform.SetParent(parentCanvas.transform, true); // worldPositionStays = true
Debug.Log($"[DraggableVisual] Reparented visual {name} to canvas {parentCanvas.name} for independent movement");
}
// Get components if assigned (don't auto-create Canvas to avoid Unity's auto-reparenting)
if (canvas == null)
canvas = GetComponent<Canvas>();
if (canvasGroup == null)
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
// Subscribe to parent events
SubscribeToParentEvents();
// Initial position to match parent
transform.position = parent.transform.position;
_lastPosition = transform.position;
// Set rotation to match base's current rotation (will be maintained via separate rotation management)
transform.rotation = parent.transform.rotation;
// Reset shake and tilt parent rotations to zero (local space) for clean wobble
if (shakeParent != null)
{
shakeParent.localRotation = Quaternion.identity;
}
if (tiltParent != null)
{
tiltParent.localRotation = Quaternion.identity;
}
Debug.Log($"[DraggableVisual] Visual {name} initialized at world pos {transform.position}, local pos {transform.localPosition}, local rotation {transform.localRotation.eulerAngles}, parent at world pos {parent.transform.position}, local pos {parent.transform.localPosition}, rotation {parent.transform.rotation.eulerAngles}");
_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();
UpdateFollowRotation(); // Track base rotation changes
UpdateRotation();
UpdateTilt();
UpdateVisualContent();
}
#region Position & Movement
protected virtual void UpdateFollowRotation()
{
if (_parentDraggable == null)
return;
// Smoothly follow base rotation (since we're no longer a child)
// Base rotation changes when picking up (→ 0°) or dropping (→ slot rotation)
transform.rotation = Quaternion.Lerp(transform.rotation, _parentDraggable.transform.rotation, 10f * Time.deltaTime);
}
protected virtual void UpdateFollowPosition()
{
if (_parentDraggable == null)
return;
// For ScreenSpaceOverlay, use RectTransform.position consistently
Canvas parentCanvas = _parentDraggable.GetComponentInParent<Canvas>();
bool isOverlay = parentCanvas != null && parentCanvas.renderMode == RenderMode.ScreenSpaceOverlay;
Vector3 targetPosition;
Vector3 currentPosition;
if (isOverlay && _parentDraggable.RectTransform != null && GetComponent<RectTransform>() != null)
{
// Use RectTransform.position for overlay (screen space)
RectTransform myRect = GetComponent<RectTransform>();
targetPosition = _parentDraggable.RectTransform.position;
currentPosition = myRect.position;
}
else
{
// Use transform.position for WorldSpace/ScreenSpaceCamera
targetPosition = _parentDraggable.transform.position;
currentPosition = transform.position;
}
// Debug log if position is drastically different (likely a clustering issue)
float distance = Vector3.Distance(currentPosition, targetPosition);
if (distance > 500f)
{
Debug.LogWarning($"[DraggableVisual] Large position delta detected! Visual {name} at {currentPosition}, target {targetPosition}, parent {_parentDraggable.name}, distance: {distance}");
}
// Apply follow logic with snappy easing
if (useFollowDelay)
{
// Calculate lerp factor with snappy ease
float rawT = followSpeed * Time.deltaTime;
// Apply EaseOutCubic for snappier movement: 1 - (1-t)^3
float t = 1f - Mathf.Pow(1f - rawT, 3f);
Vector3 newPosition = Vector3.Lerp(currentPosition, targetPosition, t);
if (isOverlay && GetComponent<RectTransform>() != null)
{
GetComponent<RectTransform>().position = newPosition;
}
else
{
transform.position = newPosition;
}
}
else
{
if (isOverlay && GetComponent<RectTransform>() != null)
{
GetComponent<RectTransform>().position = targetPosition;
}
else
{
transform.position = targetPosition;
}
}
// Calculate movement delta for tilt
Vector3 actualCurrentPos = isOverlay && GetComponent<RectTransform>() != null
? GetComponent<RectTransform>().position
: transform.position;
Vector3 movement = actualCurrentPos - _lastPosition;
_movementDelta = Vector3.Lerp(_movementDelta, movement, 25f * Time.deltaTime);
_lastPosition = actualCurrentPos;
}
protected virtual Vector3 GetTargetPosition()
{
Canvas parentCanvas = _parentDraggable.GetComponentInParent<Canvas>();
bool isOverlay = parentCanvas != null && parentCanvas.renderMode == RenderMode.ScreenSpaceOverlay;
if (isOverlay && _parentDraggable.RectTransform != null)
{
return _parentDraggable.RectTransform.position;
}
return _parentDraggable.transform.position;
}
#endregion
#region Rotation & Tilt
protected virtual void UpdateRotation()
{
if (_parentDraggable == null || shakeParent == null)
return;
// Apply rotation based on movement to shakeParent (not main transform)
// This way it's additive on top of the base rotation in transform.rotation
// Negated to make the bottom "drag behind" instead of leading
Vector3 movementRotation = _movementDelta * -rotationAmount; // Flipped direction
_rotationDelta = Vector3.Lerp(_rotationDelta, movementRotation, rotationSpeed * Time.deltaTime);
// Apply Z-axis rotation to shakeParent as local rotation
float clampedZ = Mathf.Clamp(_rotationDelta.x, -60f, 60f);
shakeParent.localEulerAngles = new Vector3(0, 0, 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 with different frequencies)
float idleMultiplier = _parentDraggable.IsHovering ? 0.2f : 1f;
float time = Time.time * idleAnimationSpeed + _savedSlotIndex;
// Use sine for X wobble, cosine for Y wobble (different phases)
float sineWobbleX = Mathf.Sin(time) * idleMultiplier;
float cosineWobbleY = Mathf.Cos(time) * idleMultiplier;
// Use slower cosine for Z rotation wobble (half speed for subtle rotation)
float cosineWobbleZ = Mathf.Cos(time * 0.5f) * idleMultiplier * 0.3f; // Much subtler
// Manual tilt based on pointer position (when hovering)
float manualTiltX = 0f;
float manualTiltY = 0f;
if (_parentDraggable.IsHovering)
{
Canvas parentCanvas = _parentDraggable.GetComponentInParent<Canvas>();
bool isOverlay = parentCanvas != null && parentCanvas.renderMode == RenderMode.ScreenSpaceOverlay;
Vector3 mousePos;
Vector3 myPos;
// Get mouse position using new Input System
Vector2 mouseScreenPos = Mouse.current != null ? Mouse.current.position.ReadValue() : Vector2.zero;
if (isOverlay)
{
// For overlay, use screen coordinates directly
mousePos = mouseScreenPos;
myPos = GetComponent<RectTransform>() != null
? GetComponent<RectTransform>().position
: transform.position;
}
else if (Camera.main != null)
{
// For WorldSpace/ScreenSpaceCamera, convert to world coords
mousePos = Camera.main.ScreenToWorldPoint(mouseScreenPos);
myPos = transform.position;
}
else
{
mousePos = myPos = Vector3.zero;
}
Vector3 offset = myPos - mousePos;
manualTiltX = (offset.y * -1f) * manualTiltAmount;
manualTiltY = offset.x * manualTiltAmount;
}
// Combine auto and manual tilt
// X uses sine wobble, Y uses cosine wobble, Z uses slower cosine for rotation
float targetTiltX = manualTiltX + (useIdleAnimation ? sineWobbleX * autoTiltAmount : 0f);
float targetTiltY = manualTiltY + (useIdleAnimation ? cosineWobbleY * autoTiltAmount : 0f);
float targetTiltZ = _parentDraggable.IsDragging ? tiltParent.localEulerAngles.z : (useIdleAnimation ? cosineWobbleZ * autoTiltAmount : 0f);
// Lerp to target tilt using LOCAL rotation
float lerpX = Mathf.LerpAngle(tiltParent.localEulerAngles.x, targetTiltX, tiltSpeed * Time.deltaTime);
float lerpY = Mathf.LerpAngle(tiltParent.localEulerAngles.y, targetTiltY, tiltSpeed * Time.deltaTime);
float lerpZ = Mathf.LerpAngle(tiltParent.localEulerAngles.z, targetTiltZ, (tiltSpeed / 2f) * Time.deltaTime);
tiltParent.localEulerAngles = 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;
}
// Reset shake parent rotation (movement wobble)
if (shakeParent != null)
{
Tween.LocalRotation(shakeParent, Quaternion.identity, 0.2f, 0f, Tween.EaseOutBack);
}
// Reset tilt parent rotation (idle wobble)
if (tiltParent != null)
{
Tween.LocalRotation(tiltParent, Quaternion.identity, 0.2f, 0f, Tween.EaseOutBack);
}
// Reset rotation delta for fresh movement wobble
_rotationDelta = Vector3.zero;
OnDragStartedVisual();
}
protected virtual void HandleDragEnded(DraggableObject draggable)
{
if (canvas != null)
{
canvas.overrideSorting = false;
}
// Only reset scale if NOT in a slot (let slots handle their own scaling)
if (draggable.CurrentSlot == null)
{
Tween.LocalScale(transform, Vector3.one, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
// Reset shake parent (movement wobble) to zero for fresh start
if (shakeParent != null)
{
Tween.LocalRotation(shakeParent, Quaternion.identity, 0.3f, 0f, Tween.EaseOutBack);
}
// Reset tilt parent (idle wobble) to zero for fresh start
if (tiltParent != null)
{
Tween.LocalRotation(tiltParent, Quaternion.identity, 0.3f, 0f, Tween.EaseOutBack);
}
OnDragEndedVisual();
}
protected virtual void HandlePointerEnter(DraggableObject draggable)
{
if (useScaleAnimations)
{
Tween.LocalScale(transform, Vector3.one * scaleOnHover, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
// Punch rotation effect
if (shakeParent != null)
{
Tween.Rotation(shakeParent, shakeParent.eulerAngles + Vector3.forward * 5f, 0.15f, 0f, Tween.EaseOutBack);
}
OnPointerEnterVisual();
}
protected virtual void HandlePointerExit(DraggableObject draggable)
{
if (!draggable.WasDragged && useScaleAnimations)
{
Tween.LocalScale(transform, Vector3.one, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
OnPointerExitVisual();
}
protected virtual void HandlePointerDown(DraggableObject draggable)
{
if (useScaleAnimations)
{
Tween.LocalScale(transform, Vector3.one * scaleOnDrag, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
OnPointerDownVisual();
}
protected virtual void HandlePointerUp(DraggableObject draggable, bool longPress)
{
float targetScale = longPress ? scaleOnHover : scaleOnDrag;
if (useScaleAnimations)
{
Tween.LocalScale(transform, Vector3.one * targetScale, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
OnPointerUpVisual(longPress);
}
protected virtual void HandleSelection(DraggableObject draggable, bool selected)
{
// Punch effect on selection
if (shakeParent != null && selected)
{
Vector3 punchAmount = shakeParent.up * 20f;
Tween.Position(shakeParent, shakeParent.position + punchAmount, 0.15f, 0f, Tween.EaseOutBack,
completeCallback: () => Tween.Position(shakeParent, shakeParent.position - punchAmount, 0.15f, 0f, Tween.EaseInBack));
}
if (useScaleAnimations)
{
float targetScale = selected ? scaleOnDrag : scaleOnHover;
Tween.LocalScale(transform, Vector3.one * targetScale, scaleTransitionDuration, 0f, Tween.EaseOutBack);
}
OnSelectionVisual(selected);
}
#endregion
#region Abstract/Virtual Hooks for Subclasses
/// <summary>
/// Called after initialization is complete
/// </summary>
protected virtual void OnInitialized() { }
/// <summary>
/// Update the actual visual content (sprites, UI elements, etc.)
/// Called every frame
/// </summary>
protected abstract void UpdateVisualContent();
/// <summary>
/// Visual-specific behavior when drag starts
/// </summary>
protected virtual void OnDragStartedVisual() { }
/// <summary>
/// Visual-specific behavior when drag ends
/// </summary>
protected virtual void OnDragEndedVisual() { }
/// <summary>
/// Visual-specific behavior when pointer enters
/// </summary>
protected virtual void OnPointerEnterVisual() { }
/// <summary>
/// Visual-specific behavior when pointer exits
/// </summary>
protected virtual void OnPointerExitVisual() { }
/// <summary>
/// Visual-specific behavior when pointer down
/// </summary>
protected virtual void OnPointerDownVisual() { }
/// <summary>
/// Visual-specific behavior when pointer up
/// </summary>
protected virtual void OnPointerUpVisual(bool longPress) { }
/// <summary>
/// Visual-specific behavior when selection changes
/// </summary>
protected virtual void OnSelectionVisual(bool selected) { }
#endregion
protected virtual void OnDestroy()
{
UnsubscribeFromParentEvents();
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4b9473a4ece8425d97676fe7630d25ac
timeCreated: 1762428015

View File

@@ -0,0 +1,95 @@
using UnityEditor;
using UnityEngine;
using UI.DragAndDrop.Core;
namespace UI.DragAndDrop.Editor
{
[CustomEditor(typeof(DraggableObject), true)]
public class DraggableObjectEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
DraggableObject draggable = (DraggableObject)target;
// Only show button in edit mode
if (!Application.isPlaying)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Editor Tools", EditorStyles.boldLabel);
if (GUILayout.Button("Snap to Parent Slot"))
{
SnapToParentSlot(draggable);
}
}
}
private void SnapToParentSlot(DraggableObject draggable)
{
// Find parent slot
DraggableSlot parentSlot = draggable.GetComponentInParent<DraggableSlot>();
if (parentSlot == null)
{
Debug.LogWarning("No parent DraggableSlot found!");
return;
}
Undo.RecordObject(draggable.transform, "Snap to Parent Slot");
// Reset position and rotation
draggable.transform.localPosition = Vector3.zero;
draggable.transform.localRotation = Quaternion.identity;
// Apply slot's size mode
RectTransform draggableRect = draggable.GetComponent<RectTransform>();
RectTransform slotRect = parentSlot.GetComponent<RectTransform>();
if (draggableRect != null && slotRect != null)
{
// Use reflection to access private fields
System.Reflection.FieldInfo modeField = typeof(DraggableSlot).GetField("occupantSizeMode",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
System.Reflection.FieldInfo scaleField = typeof(DraggableSlot).GetField("occupantScale",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (modeField != null && scaleField != null)
{
var sizeMode = modeField.GetValue(parentSlot);
var occupantScale = (Vector3)scaleField.GetValue(parentSlot);
// Get enum type
System.Type enumType = sizeMode.GetType();
string modeName = System.Enum.GetName(enumType, sizeMode);
Undo.RecordObject(draggableRect, "Apply Slot Size Mode");
switch (modeName)
{
case "MatchSlotSize":
draggableRect.sizeDelta = slotRect.sizeDelta;
draggableRect.localScale = Vector3.one;
Debug.Log($"Matched slot size: {slotRect.sizeDelta}");
break;
case "Scale":
draggableRect.localScale = occupantScale;
Debug.Log($"Applied scale: {occupantScale}");
break;
case "None":
default:
// Keep current size
break;
}
}
}
EditorUtility.SetDirty(draggable.gameObject);
Debug.Log($"Snapped {draggable.name} to parent slot {parentSlot.name}");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 290619d598ef4b199862482abc7188a3
timeCreated: 1762428015

View File

@@ -5,16 +5,16 @@ public class ScrapbookController : MonoBehaviour
{
private void OnEnable()
{
InputManager.Instance.SetInputMode(InputMode.UI);
// InputManager.Instance.SetInputMode(InputMode.UI);
}
private void OnDisable()
{
InputManager.Instance.SetInputMode (InputMode.Game);
// InputManager.Instance.SetInputMode (InputMode.Game);
}
public void DebugClick()
{
Debug.Log("Yey I was clicked!");
// Debug.Log("Yey I was clicked!");
}
}