- **Refactored Card Placement Flow** - Separated card presentation from orchestration logic - Extracted `CornerCardManager` for pending card lifecycle (spawn, shuffle, rebuild) - Extracted `AlbumNavigationService` for book page navigation and zone mapping - Extracted `CardEnlargeController` for backdrop management and card reparenting - Implemented controller pattern (non-MonoBehaviour) for complex logic - Cards now unparent from slots before rebuild to prevent premature destruction - **Improved Corner Card Display** - Fixed cards spawning on top of each other during rebuild - Implemented shuffle-to-front logic (remaining cards occupy slots 0→1→2) - Added smart card selection (prioritizes cards matching current album page) - Pending cards now removed from queue immediately on drag start - Corner cards rebuild after each placement with proper slot reassignment - **Enhanced Album Card Viewing** - Added dramatic scale increase when viewing cards from album slots - Implemented shrink animation when dismissing enlarged cards - Cards transition: `PlacedInSlotState` → `AlbumEnlargedState` → `PlacedInSlotState` - Backdrop shows/hides with card enlarge/shrink cycle - Cards reparent to enlarged container while viewing, return to slot after - **State Machine Improvements** - Added `CardStateNames` constants for type-safe state transitions - Implemented `ICardClickHandler` and `ICardStateDragHandler` interfaces - State transitions now use cached property indices - `BoosterCardContext` separated from `CardContext` for single responsibility - **Code Cleanup** - Identified unused `SlotContainerHelper.cs` (superseded by `CornerCardManager`) - Identified unused `BoosterPackDraggable.canOpenOnDrop` field - Identified unused `AlbumViewPage._previousInputMode` (hardcoded value) - Identified unused `Card.OnPlacedInAlbumSlot` event (no subscribers) Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Reviewed-on: #59
226 lines
8.9 KiB
C#
226 lines
8.9 KiB
C#
using Core;
|
|
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;
|
|
|
|
// ...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();
|
|
|
|
// Find closest slot and assign to it (replaces removed auto-slotting from base class)
|
|
SlotContainer[] containers = FindObjectsByType<SlotContainer>(FindObjectsSortMode.None);
|
|
DraggableSlot closestSlot = null;
|
|
float closestDistance = float.MaxValue;
|
|
|
|
// Get position (handle both overlay and world space canvas)
|
|
Vector3 myPosition = (RectTransform != null &&
|
|
GetComponentInParent<Canvas>()?.renderMode == RenderMode.ScreenSpaceOverlay)
|
|
? RectTransform.position
|
|
: transform.position;
|
|
|
|
// Find closest slot among all containers
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Assign to closest slot if found
|
|
if (closestSlot != null)
|
|
{
|
|
Logging.Debug($"[BoosterPackDraggable] Drag ended, assigning to closest slot: {closestSlot.name}");
|
|
AssignToSlot(closestSlot, true);
|
|
}
|
|
else if (CurrentSlot != null)
|
|
{
|
|
// No valid slot found, return to current slot
|
|
Logging.Debug($"[BoosterPackDraggable] No valid slot found, snapping back to current slot");
|
|
AssignToSlot(CurrentSlot, true);
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
_currentTapCount = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set whether this booster is in the opening slot (disables dragging, enables tapping)
|
|
/// </summary>
|
|
public void SetInOpeningSlot(bool inSlot)
|
|
{
|
|
Logging.Debug($"[BoosterPackDraggable] SetInOpeningSlot({inSlot}) called on {name}");
|
|
|
|
SetDraggingEnabled(!inSlot); // Disable dragging when in opening slot
|
|
Logging.Debug($"[BoosterPackDraggable] SetDraggingEnabled({!inSlot}) called");
|
|
|
|
canTapToOpen = inSlot; // Enable tap-to-open when in opening slot
|
|
|
|
if (inSlot)
|
|
{
|
|
_currentTapCount = 0; // Reset tap counter when placed
|
|
|
|
// Suppress visual effects (idle animations, glow, etc.) when in opening slot
|
|
// But allow slot tween and tap pulse to still work
|
|
if (Visual != null)
|
|
{
|
|
Visual.SuppressEffects();
|
|
|
|
// Play one-time placement tween to animate into slot
|
|
// The visual will follow the parent to its slot position, then lock in place
|
|
// Get target scale from current slot if it has scale mode
|
|
Vector3 targetScale = Vector3.one;
|
|
if (CurrentSlot != null && CurrentSlot.GetComponent<DraggableSlot>() != null)
|
|
{
|
|
// Access the slot's occupant scale if it's in Scale mode
|
|
// For now, use Vector3.one as default
|
|
targetScale = Vector3.one;
|
|
}
|
|
|
|
Visual.PlayPlacementTween(0.3f, targetScale);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ResetOpeningState(); // Reset completely when removed
|
|
|
|
// Resume visual effects when removed from opening slot
|
|
if (Visual != null)
|
|
{
|
|
Visual.StopPlacementTween(); // Stop any ongoing placement tween
|
|
Visual.ResumeEffects();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
}
|
|
}
|
|
|