Updates to card testing scene

This commit is contained in:
Michal Pikulski
2025-11-12 14:39:38 +01:00
committed by Michal Pikulski
parent 4e7f774386
commit 755082c67d
26 changed files with 1336 additions and 841 deletions

View File

@@ -0,0 +1,193 @@
using System;
using AppleHills.Data.CardSystem;
using Core;
using Pixelplacement;
using UnityEngine;
using UnityEngine.EventSystems;
using AppleHills.Core.Settings;
namespace UI.CardSystem
{
/// <summary>
/// Album card component that wraps CardDisplay.
/// Handles tap-to-enlarge and tap-to-shrink interactions for cards placed in album slots.
///
/// TODO: Consider refactoring to state machine pattern (PendingReveal, PlacedInSlot, Enlarged)
/// This would eliminate the need for separate AlbumPlacementCard wrapper and simplify the hierarchy.
/// See design discussion with state transitions for cleaner architecture.
/// </summary>
public class AlbumCard : MonoBehaviour, IPointerClickHandler
{
[Header("References")]
[SerializeField] private CardDisplay cardDisplay;
// Events for AlbumViewPage to manage backdrop and reparenting
public event Action<AlbumCard> OnEnlargeRequested;
public event Action<AlbumCard> OnShrinkRequested;
private AlbumCardSlot _parentSlot;
private CardData _cardData;
private bool _isEnlarged;
private Vector3 _originalScale;
private Transform _originalParent;
private Vector3 _originalLocalPosition;
private Quaternion _originalLocalRotation;
private ICardSystemSettings _settings;
private void Awake()
{
_settings = GameManager.GetSettingsObject<ICardSystemSettings>();
// Auto-find CardDisplay if not assigned
if (cardDisplay == null)
{
cardDisplay = GetComponentInChildren<CardDisplay>();
}
// Store original scale
_originalScale = transform.localScale;
}
/// <summary>
/// Setup card with data
/// </summary>
public void SetupCard(CardData data)
{
_cardData = data;
if (cardDisplay != null)
{
cardDisplay.SetupCard(data);
}
}
/// <summary>
/// Set the parent slot this card belongs to
/// </summary>
public void SetParentSlot(AlbumCardSlot slot)
{
_parentSlot = slot;
}
/// <summary>
/// Get the card data
/// </summary>
public CardData GetCardData()
{
return _cardData;
}
/// <summary>
/// Handle tap on card - request enlarge/shrink from parent page
/// Only process clicks when card is placed in a slot (not during reveal flow)
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] OnPointerClick on {name}, _parentSlot={((_parentSlot != null) ? _parentSlot.name : "NULL")}, _isEnlarged={_isEnlarged}, position={eventData.position}");
// During reveal flow (before placed in slot), forward clicks to parent FlippableCard
if (_parentSlot == null)
{
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] {name} - No parent slot, forwarding click to parent FlippableCard");
// Find parent FlippableCard and forward the click
FlippableCard parentFlippable = GetComponentInParent<FlippableCard>();
if (parentFlippable != null)
{
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] {name} - Found parent FlippableCard, calling OnPointerClick");
parentFlippable.OnPointerClick(eventData);
}
else
{
Logging.Warning($"[CLICK-TRACE-ALBUMCARD] {name} - No parent FlippableCard found!");
}
return;
}
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] {name} - Has parent slot, processing click");
if (_isEnlarged)
{
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] {name} - Is enlarged, requesting shrink");
OnShrinkRequested?.Invoke(this);
}
else
{
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] {name} - Is normal size, requesting enlarge");
OnEnlargeRequested?.Invoke(this);
}
}
/// <summary>
/// Enlarge card (called by AlbumViewPage after reparenting)
/// </summary>
public void EnlargeCard()
{
if (_isEnlarged) return;
_isEnlarged = true;
// Store original transform info for restoration
_originalParent = transform.parent;
_originalLocalPosition = transform.localPosition;
_originalLocalRotation = transform.localRotation;
// Scale up with snappy tween
Tween.LocalScale(transform, _originalScale * _settings.AlbumCardEnlargedScale, _settings.ScaleDuration, 0f, Tween.EaseOutBack);
}
/// <summary>
/// Shrink card back to original size (called by AlbumViewPage before reparenting back)
/// </summary>
/// <param name="onComplete">Optional callback to invoke when shrink animation completes</param>
public void ShrinkCard(System.Action onComplete = null)
{
if (!_isEnlarged) return;
_isEnlarged = false;
// Scale back down with snappy tween, invoke callback when done
Tween.LocalScale(transform, _originalScale, _settings.ScaleDuration, 0f, Tween.EaseInBack,
completeCallback: () => onComplete?.Invoke());
}
/// <summary>
/// Get original parent for restoration
/// </summary>
public Transform GetOriginalParent()
{
return _originalParent;
}
/// <summary>
/// Get original local position for restoration
/// </summary>
public Vector3 GetOriginalLocalPosition()
{
return _originalLocalPosition;
}
/// <summary>
/// Get original local rotation for restoration
/// </summary>
public Quaternion GetOriginalLocalRotation()
{
return _originalLocalRotation;
}
/// <summary>
/// Check if card is currently enlarged
/// </summary>
public bool IsEnlarged => _isEnlarged;
/// <summary>
/// Force reset enlarged state (for cleanup scenarios like page closing)
/// </summary>
public void ForceResetEnlargedState()
{
_isEnlarged = false;
transform.localScale = _originalScale;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 258a530448814715b5ec19737df2a658
timeCreated: 1762505823

View File

@@ -0,0 +1,252 @@
using System;
using System.Collections;
using AppleHills.Data.CardSystem;
using Core;
using Data.CardSystem;
using Pixelplacement;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace UI.CardSystem
{
/// <summary>
/// Draggable card for album reveal system.
/// Handles both tap and drag-hold interactions for revealing cards.
/// Auto-snaps to matching album slot on release/tap.
/// </summary>
public class AlbumCardPlacementDraggable : DraggableObject
{
[Header("Album Card Settings")]
[SerializeField] private FlippableCard flippableCard;
[SerializeField] private float holdRevealDelay = 0.1f;
private CardData _cardData;
private bool _isRevealed = false;
private bool _isDragRevealing = false;
private bool _waitingForPlacementTap = false;
private Coroutine _holdRevealCoroutine;
private bool _isHolding = false; // Track if pointer is currently down
// Events
public event Action<AlbumCardPlacementDraggable, CardData> OnCardRevealed;
public event Action<AlbumCardPlacementDraggable, CardData> OnCardPlacedInAlbum;
public CardData CardData => _cardData;
public bool IsRevealed => _isRevealed;
public CardZone Zone => _cardData?.Zone ?? CardZone.AppleHills;
protected override void Initialize()
{
base.Initialize();
// Auto-find FlippableCard if not assigned
if (flippableCard == null)
{
flippableCard = GetComponent<FlippableCard>();
}
}
/// <summary>
/// Setup the card data (stores it but doesn't reveal until tapped/dragged)
/// </summary>
public void SetupCard(CardData data)
{
_cardData = data;
if (flippableCard != null)
{
flippableCard.SetupCard(data);
}
}
/// <summary>
/// Reveal the card (flip to show front)
/// </summary>
public void RevealCard()
{
if (_isRevealed)
{
return;
}
_isRevealed = true;
if (flippableCard != null)
{
flippableCard.FlipToReveal();
}
OnCardRevealed?.Invoke(this, _cardData);
}
/// <summary>
/// Snap to the matching album slot
/// </summary>
public void SnapToAlbumSlot()
{
if (_cardData == null)
{
Logging.Warning("[AlbumCardPlacementDraggable] Cannot snap to slot - no card data assigned.");
return;
}
// Find all album card slots in the scene
AlbumCardSlot[] allSlots = FindObjectsByType<AlbumCardSlot>(FindObjectsSortMode.None);
AlbumCardSlot matchingSlot = null;
foreach (var slot in allSlots)
{
if (slot.CanAcceptCard(_cardData))
{
matchingSlot = slot;
break;
}
}
if (matchingSlot != null)
{
SetDraggingEnabled(false);
// NEW FLOW: Extract AlbumCard FIRST, then tween it
if (flippableCard != null)
{
AlbumCard extractedCard = flippableCard.ExtractAlbumCard(matchingSlot.transform);
if (extractedCard != null)
{
// Notify slot that card was placed
matchingSlot.OnCardPlaced(extractedCard);
// NOW tween the extracted AlbumCard into position
TweenExtractedCardToSlot(extractedCard, () =>
{
// After animation completes
Logging.Debug($"[AlbumCardPlacementDraggable] Card placement animation complete for {_cardData.Name}");
// Notify that card was placed
OnCardPlacedInAlbum?.Invoke(this, _cardData);
// Destroy this wrapper (the AlbumPlacementCard)
Destroy(gameObject);
});
}
else
{
Logging.Warning("[AlbumCardPlacementDraggable] Failed to extract AlbumCard from wrapper!");
}
}
}
else
{
Logging.Warning($"[AlbumCardPlacementDraggable] Could not find matching slot for card '{_cardData.Name}' (Zone: {_cardData.Zone}, Index: {_cardData.CollectionIndex})");
}
}
/// <summary>
/// Tween the extracted AlbumCard into its slot position
/// Tweens from current size to slot size - AspectRatioFitter handles width
/// </summary>
private void TweenExtractedCardToSlot(AlbumCard card, System.Action onComplete)
{
Transform cardTransform = card.transform;
RectTransform cardRect = cardTransform as RectTransform;
if (cardRect != null)
{
// Get target height from slot
RectTransform slotRect = cardTransform.parent as RectTransform;
float targetHeight = slotRect != null ? slotRect.rect.height : cardRect.sizeDelta.y;
// Tween from current size to target size (AspectRatioFitter will adjust width)
Vector2 targetSize = new Vector2(cardRect.sizeDelta.x, targetHeight);
Tween.Size(cardRect, targetSize, snapDuration, 0f, Tween.EaseOutBack);
// Tween position and rotation to slot center
Tween.LocalPosition(cardRect, Vector3.zero, snapDuration, 0f, Tween.EaseOutBack);
Tween.LocalRotation(cardTransform, Quaternion.identity, snapDuration, 0f, Tween.EaseOutBack,
completeCallback: () =>
{
Logging.Debug($"[AlbumCardPlacementDraggable] Tween complete for extracted card {card.name}, final height: {cardRect.sizeDelta.y}");
onComplete?.Invoke();
});
}
else
{
// No RectTransform, just reset and call callback
cardTransform.localPosition = Vector3.zero;
cardTransform.localRotation = Quaternion.identity;
onComplete?.Invoke();
}
}
protected override void OnPointerDownHook()
{
base.OnPointerDownHook();
_isHolding = true;
// Start hold-reveal timer if card not yet revealed
if (!_isRevealed && _holdRevealCoroutine == null)
{
_holdRevealCoroutine = StartCoroutine(HoldRevealTimer());
}
}
protected override void OnPointerUpHook(bool longPress)
{
base.OnPointerUpHook(longPress);
_isHolding = false;
// Cancel hold timer if running
if (_holdRevealCoroutine != null)
{
StopCoroutine(_holdRevealCoroutine);
_holdRevealCoroutine = null;
}
else
{
}
// Handle tap (not dragged)
if (!_wasDragged)
{
if (!_isRevealed)
{
// First tap: reveal the card
RevealCard();
_waitingForPlacementTap = true;
}
else if (_waitingForPlacementTap)
{
// Second tap: snap to slot
_waitingForPlacementTap = false;
SnapToAlbumSlot();
}
else
{
}
}
else if (_isDragRevealing)
{
// Was drag-revealed, auto-snap on release
_isDragRevealing = false;
SnapToAlbumSlot();
}
}
/// <summary>
/// Coroutine to reveal card after holding for specified duration
/// </summary>
private IEnumerator HoldRevealTimer()
{
yield return new WaitForSeconds(holdRevealDelay);
// If still holding after delay, reveal the card
if (!_isRevealed && _isHolding)
{
RevealCard();
_isDragRevealing = true;
}
_holdRevealCoroutine = null;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 706803638ea24880bae19c87d3851ce6
timeCreated: 1762470947

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,2 @@
fileFormatVersion: 2
guid: e6edf435b57f09c47bb9e10d34164570

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,65 @@
using UnityEngine;
using UnityEngine.EventSystems;
namespace UI.CardSystem.StateMachine.States
{
/// <summary>
/// Optional helper component for handling drag/drop integration with existing DragDrop system.
/// Can be attached to Card root to bridge between state machine and drag system.
/// </summary>
public class CardInteractionHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
private CardContext _context;
private CardDraggingState _draggingState;
private bool _isDragging;
private void Awake()
{
_context = GetComponent<CardContext>();
}
public void OnBeginDrag(PointerEventData eventData)
{
// Only allow drag from certain states
var currentState = _context.StateMachine.currentState?.name;
if (currentState != "RevealedState" && currentState != "PlacedInSlotState")
{
return;
}
// Transition to dragging state
_context.StateMachine.ChangeState("DraggingState");
_draggingState = _context.StateMachine.currentState?.GetComponent<CardDraggingState>();
_isDragging = true;
}
public void OnDrag(PointerEventData eventData)
{
if (!_isDragging || _draggingState == null) return;
// Update drag position
Vector3 worldPosition;
RectTransformUtility.ScreenPointToWorldPointInRectangle(
transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out worldPosition
);
// _draggingState.UpdateDragPosition(worldPosition);
}
public void OnEndDrag(PointerEventData eventData)
{
if (!_isDragging || _draggingState == null) return;
_isDragging = false;
// Check if dropped over a valid slot
// This would integrate with your existing AlbumCardSlot system
// For now, just return to revealed state
// _draggingState.OnDroppedOutsideSlot();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 56af1049620bac744b1eee076a14594e

View File

@@ -0,0 +1,673 @@
using System;
using AppleHills.Data.CardSystem;
using Core;
using Pixelplacement;
using Pixelplacement.TweenSystem;
using UnityEngine;
using UnityEngine.EventSystems;
using AppleHills.Core.Settings;
namespace UI.CardSystem
{
/// <summary>
/// Flippable card wrapper that shows a card back, then flips to reveal the CardDisplay front.
/// This component nests an existing CardDisplay prefab to reuse card visuals everywhere.
/// </summary>
public class FlippableCard : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
{
[Header("Card References")]
[SerializeField] private GameObject cardBackObject; // The card back visual
[SerializeField] private GameObject cardFrontObject; // Your CardDisplay prefab instance
[SerializeField] private CardDisplay cardDisplay; // Reference to CardDisplay component
[SerializeField] private AlbumCard albumCard; // Reference to nested AlbumCard (for album placement flow)
[Header("Idle Hover Animation")]
[SerializeField] private bool enableIdleHover = true;
[Header("New/Repeat Card Display")]
[SerializeField] private GameObject newCardText;
[SerializeField] private GameObject newCardIdleText;
[SerializeField] private GameObject repeatText;
[SerializeField] private GameObject progressBarContainer;
// State
private ICardSystemSettings _settings;
private bool _isFlipped = false;
private bool _isFlipping = false;
private TweenBase _idleHoverTween;
private CardData _cardData;
private Vector2 _originalPosition; // Track original spawn position
private bool _isWaitingForTap = false; // Waiting for tap after reveal
private bool _isNew = false; // Is this a new card
private int _ownedCount = 0; // Owned count for repeat cards
private bool _isClickable = true; // Can this card be clicked
// Events
public event Action<FlippableCard, CardData> OnCardRevealed;
public event Action<FlippableCard> OnCardTappedAfterReveal;
public event Action<FlippableCard> OnClickedWhileInactive; // Fired when clicked but not clickable
public event Action<FlippableCard> OnFlipStarted; // Fired when flip animation begins
public bool IsFlipped => _isFlipped;
public CardData CardData => _cardData;
public int CardsToUpgrade => _settings?.CardsToUpgrade ?? 5;
private void Awake()
{
_settings = GameManager.GetSettingsObject<ICardSystemSettings>();
// Auto-find CardDisplay if not assigned
if (cardDisplay == null && cardFrontObject != null)
{
cardDisplay = cardFrontObject.GetComponent<CardDisplay>();
}
// Auto-find AlbumCard if not assigned
if (albumCard == null)
{
albumCard = GetComponentInChildren<AlbumCard>();
}
// Card back: starts at 0° rotation (normal, facing camera, clickable)
// Card front: starts at 180° rotation (flipped away, will rotate to 0° when revealed)
if (cardBackObject != null)
{
cardBackObject.transform.localRotation = Quaternion.Euler(0, 0, 0);
cardBackObject.SetActive(true);
}
if (cardFrontObject != null)
{
cardFrontObject.transform.localRotation = Quaternion.Euler(0, 180, 0);
cardFrontObject.SetActive(false);
}
// Hide all new/repeat UI elements initially
if (newCardText != null)
newCardText.SetActive(false);
if (newCardIdleText != null)
newCardIdleText.SetActive(false);
if (repeatText != null)
repeatText.SetActive(false);
if (progressBarContainer != null)
progressBarContainer.SetActive(false);
}
private void Start()
{
// Save the original position so we can return to it after hover
RectTransform rectTransform = GetComponent<RectTransform>();
if (rectTransform != null)
{
_originalPosition = rectTransform.anchoredPosition;
}
// Start idle hover animation
if (enableIdleHover && !_isFlipped)
{
StartIdleHover();
}
}
/// <summary>
/// Setup the card data (stores it but doesn't reveal until flipped)
/// </summary>
public void SetupCard(CardData data)
{
_cardData = data;
// Setup the CardDisplay but keep it hidden
if (cardDisplay != null)
{
cardDisplay.SetupCard(data);
}
}
/// <summary>
/// Flip the card to reveal the front
/// </summary>
public void FlipToReveal()
{
if (_isFlipped || _isFlipping)
return;
_isFlipping = true;
// Fire flip started event IMMEDIATELY (before animations)
OnFlipStarted?.Invoke(this);
// Stop idle hover
StopIdleHover();
// Flip animation: Rotate the visual children (back from 0→90, front from 180→0)
// ...existing code...
// Card back: 0° → 90° (rotates away)
// Card front: 180° → 90° → 0° (rotates into view)
float flipDur = _settings.FlipDuration;
float flipPunch = _settings.FlipScalePunch;
// Phase 1: Rotate both to 90 degrees (edge view)
if (cardBackObject != null)
{
Tween.LocalRotation(cardBackObject.transform, Quaternion.Euler(0, 90, 0), flipDur * 0.5f, 0f, Tween.EaseInOut);
}
if (cardFrontObject != null)
{
Tween.LocalRotation(cardFrontObject.transform, Quaternion.Euler(0, 90, 0), flipDur * 0.5f, 0f, Tween.EaseInOut,
completeCallback: () =>
{
// At edge (90°), switch visibility
if (cardBackObject != null)
cardBackObject.SetActive(false);
if (cardFrontObject != null)
cardFrontObject.SetActive(true);
// Phase 2: Rotate front from 90 to 0 (show at correct orientation)
Tween.LocalRotation(cardFrontObject.transform, Quaternion.Euler(0, 0, 0), flipDur * 0.5f, 0f, Tween.EaseInOut,
completeCallback: () =>
{
_isFlipped = true;
_isFlipping = false;
// Fire revealed event
OnCardRevealed?.Invoke(this, _cardData);
});
});
}
// Scale punch during flip for extra juice
Vector3 originalScale = transform.localScale;
Tween.LocalScale(transform, originalScale * flipPunch, flipDur * 0.5f, 0f, Tween.EaseOutBack,
completeCallback: () =>
{
Tween.LocalScale(transform, originalScale, flipDur * 0.5f, 0f, Tween.EaseInBack);
});
}
/// <summary>
/// Start idle hover animation (gentle bobbing)
/// </summary>
private void StartIdleHover()
{
if (_idleHoverTween != null)
return;
RectTransform rectTransform = GetComponent<RectTransform>();
if (rectTransform == null)
return;
Vector2 originalPos = rectTransform.anchoredPosition;
Vector2 targetPos = originalPos + Vector2.up * _settings.IdleHoverHeight;
_idleHoverTween = Tween.Value(0f, 1f,
(val) =>
{
if (rectTransform != null)
{
float t = Mathf.Sin(val * Mathf.PI * 2f) * 0.5f + 0.5f; // Smooth sine wave
rectTransform.anchoredPosition = Vector2.Lerp(originalPos, targetPos, t);
}
},
_settings.IdleHoverDuration, 0f, Tween.EaseInOut, Tween.LoopType.Loop);
}
/// <summary>
/// Stop idle hover animation
/// </summary>
private void StopIdleHover()
{
if (_idleHoverTween != null)
{
_idleHoverTween.Stop();
_idleHoverTween = null;
// Reset to ORIGINAL position (not Vector2.zero!)
RectTransform rectTransform = GetComponent<RectTransform>();
if (rectTransform != null)
{
Tween.AnchoredPosition(rectTransform, _originalPosition, 0.3f, 0f, Tween.EaseOutBack);
}
}
}
#region Pointer Event Handlers
public void OnPointerEnter(PointerEventData eventData)
{
if (_isFlipped || _isFlipping)
return;
// Scale up slightly on hover
Tween.LocalScale(transform, Vector3.one * _settings.HoverScaleMultiplier, 0.2f, 0f, Tween.EaseOutBack);
}
public void OnPointerExit(PointerEventData eventData)
{
if (_isFlipped || _isFlipping)
return;
// Scale back to normal
Tween.LocalScale(transform, Vector3.one, 0.2f, 0f, Tween.EaseOutBack);
}
public void OnPointerClick(PointerEventData eventData)
{
Logging.Debug($"[CLICK-TRACE-FLIPPABLE] OnPointerClick on {name}, _isClickable={_isClickable}, _isWaitingForTap={_isWaitingForTap}, _isFlipped={_isFlipped}, position={eventData.position}");
// If not clickable, notify and return
if (!_isClickable)
{
Logging.Debug($"[CLICK-TRACE-FLIPPABLE] {name} - Not clickable, firing OnClickedWhileInactive");
OnClickedWhileInactive?.Invoke(this);
return;
}
// If waiting for tap after reveal, handle that
if (_isWaitingForTap)
{
Logging.Debug($"[CLICK-TRACE-FLIPPABLE] {name} - Waiting for tap, dismissing enlarged state");
OnCardTappedAfterReveal?.Invoke(this);
_isWaitingForTap = false;
return;
}
if (_isFlipped || _isFlipping)
{
Logging.Debug($"[CLICK-TRACE-FLIPPABLE] {name} - Ignoring click (flipped={_isFlipped}, flipping={_isFlipping})");
return;
}
Logging.Debug($"[CLICK-TRACE-FLIPPABLE] {name} - Processing click, starting flip");
// Flip on click
FlipToReveal();
}
#endregion
#region New/Repeat Card Display
/// <summary>
/// Show this card as a new card (enlarge, show "NEW CARD" text, wait for tap)
/// </summary>
public void ShowAsNew()
{
_isNew = true;
_isWaitingForTap = true;
// Show new card text
if (newCardText != null)
newCardText.SetActive(true);
// Enlarge the card
EnlargeCard();
}
/// <summary>
/// Show this card as a repeat that will trigger an upgrade (enlarge, show progress, auto-transition to upgrade)
/// </summary>
/// <param name="ownedCount">Number of copies owned BEFORE this one</param>
/// <param name="lowerRarityCard">The existing card data at lower rarity (for upgrade reference)</param>
public void ShowAsRepeatWithUpgrade(int ownedCount, AppleHills.Data.CardSystem.CardData lowerRarityCard)
{
_isNew = false;
_ownedCount = ownedCount;
_isWaitingForTap = false; // Don't wait yet - upgrade will happen automatically
// Show repeat text
if (repeatText != null)
repeatText.SetActive(true);
// Enlarge the card
EnlargeCard();
// Show progress bar with owned count, then auto-trigger upgrade
ShowProgressBar(ownedCount, () =>
{
// Progress animation complete - trigger upgrade!
TriggerUpgradeTransition(lowerRarityCard);
});
}
/// <summary>
/// Trigger the upgrade transition (called after progress bar fills)
/// </summary>
private void TriggerUpgradeTransition(AppleHills.Data.CardSystem.CardData lowerRarityCard)
{
Logging.Debug($"[FlippableCard] Triggering upgrade transition from {lowerRarityCard.Rarity}!");
AppleHills.Data.CardSystem.CardRarity oldRarity = lowerRarityCard.Rarity;
AppleHills.Data.CardSystem.CardRarity newRarity = oldRarity + 1;
// Reset the lower rarity count to 0
lowerRarityCard.CopiesOwned = 0;
// Create upgraded card data
AppleHills.Data.CardSystem.CardData upgradedCardData = new AppleHills.Data.CardSystem.CardData(_cardData);
upgradedCardData.Rarity = newRarity;
upgradedCardData.CopiesOwned = 1;
// Check if we already have this card at the higher rarity
bool isNewAtHigherRarity = Data.CardSystem.CardSystemManager.Instance.IsCardNew(upgradedCardData, out AppleHills.Data.CardSystem.CardData existingHigherRarity);
// Add the higher rarity card to inventory
Data.CardSystem.CardSystemManager.Instance.GetCardInventory().AddCard(upgradedCardData);
// Update our displayed card data
_cardData.Rarity = newRarity;
// Transition to appropriate display
if (isNewAtHigherRarity || newRarity == AppleHills.Data.CardSystem.CardRarity.Legendary)
{
// Show as NEW at higher rarity
TransitionToNewCardView(newRarity);
}
else
{
// Show progress for higher rarity, then transition to NEW
int ownedAtHigherRarity = existingHigherRarity.CopiesOwned;
ShowProgressBar(ownedAtHigherRarity, () =>
{
TransitionToNewCardView(newRarity);
});
}
}
/// <summary>
/// Show this card as a repeat (enlarge, show progress bar, wait for tap)
/// </summary>
/// <param name="ownedCount">Number of copies owned BEFORE this one</param>
public void ShowAsRepeat(int ownedCount)
{
_isNew = false;
_ownedCount = ownedCount;
_isWaitingForTap = true;
// Show repeat text
if (repeatText != null)
repeatText.SetActive(true);
// Enlarge the card
EnlargeCard();
// Show progress bar with owned count, then blink new element
ShowProgressBar(ownedCount, () =>
{
// Progress animation complete
});
}
/// <summary>
/// Show this card as upgraded (hide progress bar, show as new with upgraded rarity)
/// </summary>
public void ShowAsUpgraded(AppleHills.Data.CardSystem.CardRarity oldRarity, AppleHills.Data.CardSystem.CardRarity newRarity)
{
_isNew = true;
_isWaitingForTap = true;
// Update the CardDisplay to show new rarity
if (cardDisplay != null && _cardData != null)
{
_cardData.Rarity = newRarity;
cardDisplay.SetupCard(_cardData);
}
// Hide progress bar and repeat text
if (progressBarContainer != null)
progressBarContainer.SetActive(false);
if (repeatText != null)
repeatText.SetActive(false);
// Show new card text (it's now a "new" card at the higher rarity)
if (newCardText != null)
newCardText.SetActive(true);
Logging.Debug($"[FlippableCard] Card upgraded from {oldRarity} to {newRarity}! Showing as NEW.");
// Card is already enlarged from the repeat display, so no need to enlarge again
}
/// <summary>
/// Show this card as upgraded with progress bar (already have copies at higher rarity)
/// </summary>
public void ShowAsUpgradedWithProgress(AppleHills.Data.CardSystem.CardRarity oldRarity, AppleHills.Data.CardSystem.CardRarity newRarity, int ownedAtNewRarity)
{
_isNew = false;
_isWaitingForTap = false; // Don't wait for tap yet, progress bar will complete first
// Hide new card text
if (newCardText != null)
newCardText.SetActive(false);
// Show repeat text (it's a repeat at the new rarity)
if (repeatText != null)
repeatText.SetActive(true);
// Show progress bar for the new rarity
ShowProgressBar(ownedAtNewRarity, () =>
{
// Progress animation complete - now transition to "NEW CARD" view
TransitionToNewCardView(newRarity);
});
Logging.Debug($"[FlippableCard] Card upgraded from {oldRarity} to {newRarity}! Showing progress {ownedAtNewRarity}/5");
}
/// <summary>
/// Transition to "NEW CARD" view after upgrade progress completes
/// </summary>
private void TransitionToNewCardView(AppleHills.Data.CardSystem.CardRarity newRarity)
{
Logging.Debug($"[FlippableCard] Transitioning to NEW CARD view at {newRarity} rarity");
// Update the CardDisplay to show new rarity
if (cardDisplay != null && _cardData != null)
{
_cardData.Rarity = newRarity;
cardDisplay.SetupCard(_cardData);
}
// Hide progress bar and repeat text
if (progressBarContainer != null)
progressBarContainer.SetActive(false);
if (repeatText != null)
repeatText.SetActive(false);
// Show "NEW CARD" text
if (newCardText != null)
newCardText.SetActive(true);
// Now wait for tap
_isNew = true;
_isWaitingForTap = true;
Logging.Debug($"[FlippableCard] Now showing as NEW CARD at {newRarity}, waiting for tap");
}
/// <summary>
/// Enlarge the card
/// </summary>
private void EnlargeCard()
{
Tween.LocalScale(transform, Vector3.one * _settings.NewCardEnlargedScale, _settings.ScaleDuration, 0f, Tween.EaseOutBack);
}
/// <summary>
/// Return card to normal size
/// </summary>
public void ReturnToNormalSize()
{
Tween.LocalScale(transform, Vector3.one, _settings.ScaleDuration, 0f, Tween.EaseOutBack, completeCallback: () =>
{
// After returning to normal, hide new card text, show idle text
if (_isNew)
{
if (newCardText != null)
newCardText.SetActive(false);
if (newCardIdleText != null)
newCardIdleText.SetActive(true);
}
// Keep repeat text visible
});
}
/// <summary>
/// Show progress bar with owned count, then blink the new element
/// </summary>
private void ShowProgressBar(int ownedCount, System.Action onComplete)
{
if (progressBarContainer == null)
{
onComplete?.Invoke();
return;
}
progressBarContainer.SetActive(true);
// Get all child Image components
UnityEngine.UI.Image[] progressElements = progressBarContainer.GetComponentsInChildren<UnityEngine.UI.Image>(true);
int cardsToUpgrade = _settings.CardsToUpgrade;
// Check if we have the required number of elements (should match cardsToUpgrade)
if (progressElements.Length < cardsToUpgrade)
{
Logging.Warning($"[FlippableCard] Not enough Image components in progress bar! Expected {cardsToUpgrade}, found {progressElements.Length}");
onComplete?.Invoke();
return;
}
// Disable all elements first
foreach (var img in progressElements)
{
img.enabled = false;
}
// Show owned count (from the END, going backwards)
// E.g., if owned 3 cards, enable elements at index [4], [3], [2] (last 3 elements)
int startIndex = Mathf.Max(0, cardsToUpgrade - ownedCount);
for (int i = startIndex; i < cardsToUpgrade && i < progressElements.Length; i++)
{
progressElements[i].enabled = true;
}
// Wait a moment, then blink the new element
// New element is at index (cardsToUpgrade - ownedCount - 1)
int newElementIndex = Mathf.Max(0, cardsToUpgrade - ownedCount - 1);
if (newElementIndex >= 0 && newElementIndex < progressElements.Length)
{
Tween.Value(0f, 1f, (val) => { }, 0.3f, 0f, completeCallback: () =>
{
BlinkProgressElement(newElementIndex, progressElements, onComplete);
});
}
else
{
onComplete?.Invoke();
}
}
/// <summary>
/// Blink a progress element (enable/disable rapidly)
/// </summary>
private void BlinkProgressElement(int index, UnityEngine.UI.Image[] elements, System.Action onComplete)
{
if (index < 0 || index >= elements.Length)
{
onComplete?.Invoke();
return;
}
UnityEngine.UI.Image element = elements[index];
int blinkCount = 0;
const int maxBlinks = 3;
void Blink()
{
element.enabled = !element.enabled;
blinkCount++;
if (blinkCount >= maxBlinks * 2)
{
element.enabled = true; // End on enabled
onComplete?.Invoke();
}
else
{
Tween.Value(0f, 1f, (val) => { }, 0.15f, 0f, completeCallback: Blink);
}
}
Blink();
}
/// <summary>
/// Enable or disable clickability of this card
/// </summary>
public void SetClickable(bool clickable)
{
_isClickable = clickable;
}
/// <summary>
/// Jiggle the card (shake animation)
/// </summary>
public void Jiggle()
{
// Quick shake animation - rotate left, then right, then center
Transform cardTransform = transform;
Quaternion originalRotation = cardTransform.localRotation;
// Shake sequence: 0 -> -5 -> +5 -> 0
Tween.LocalRotation(cardTransform, Quaternion.Euler(0, 0, -5), 0.05f, 0f, Tween.EaseInOut,
completeCallback: () =>
{
Tween.LocalRotation(cardTransform, Quaternion.Euler(0, 0, 5), 0.1f, 0f, Tween.EaseInOut,
completeCallback: () =>
{
Tween.LocalRotation(cardTransform, originalRotation, 0.05f, 0f, Tween.EaseInOut);
});
});
}
/// <summary>
/// Extract the nested AlbumCard and reparent it to a new parent
/// Used when placing card in album slot - extracts the AlbumCard from this wrapper
/// The caller is responsible for tweening it to the final position
/// </summary>
/// <param name="newParent">The transform to reparent the AlbumCard to (typically the AlbumCardSlot)</param>
/// <returns>The extracted AlbumCard component, or null if not found</returns>
public AlbumCard ExtractAlbumCard(Transform newParent)
{
if (albumCard == null)
{
Logging.Warning("[FlippableCard] Cannot extract AlbumCard - none found!");
return null;
}
// Reparent AlbumCard to new parent (maintain world position temporarily)
// The caller will tween it to the final position
albumCard.transform.SetParent(newParent, true);
// Setup the card data on the AlbumCard
if (_cardData != null)
{
albumCard.SetupCard(_cardData);
}
Logging.Debug($"[FlippableCard] Extracted AlbumCard '{_cardData?.Name}' to {newParent.name} - ready for tween");
return albumCard;
}
#endregion
private void OnDestroy()
{
StopIdleHover();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bb9e3a783cd7fce4aab2ff7f7f63119b

View File

@@ -0,0 +1,219 @@
# DEPRECATED - Old Card System Files
## ⚠️ This folder contains old card system files that have been replaced by the new state machine implementation.
**DO NOT USE THESE FILES IN NEW CODE.**
These files are kept temporarily for backward compatibility during migration. Once all code is migrated to the new Card state machine system, this entire folder can be deleted.
---
## Files in this folder:
### **Old Card Components:**
1. **FlippableCard.cs**
- Old monolithic card component for booster opening
- **Replaced by:** `Card.cs` + state machine (IdleState, FlippingState, EnlargedNewState, etc.)
- 700+ lines of boolean-driven state management
- Used in old BoosterOpeningPage
2. **AlbumCard.cs**
- Old album card component for tap-to-enlarge functionality
- **Replaced by:** `Card.cs` (PlacedInSlotState → AlbumEnlargedState)
- Wrapped CardDisplay and managed enlarge/shrink
- Used in old AlbumViewPage
### **Old Drag/Drop Wrappers:**
3. **CardDraggable.cs**
- Empty wrapper around DraggableObject
- Only stored CardData (now in CardContext)
- **Replaced by:** `Card.cs` now inherits from DraggableObject directly
4. **CardDraggableVisual.cs**
- Visual component for CardDraggable
- Managed CardDisplay child
- **Replaced by:** Card state machine handles all visuals
5. **AlbumCardPlacementDraggable.cs**
- Drag wrapper for FlippableCard in album corner placement flow
- Handled tap-to-reveal, drag-to-place logic
- **Replaced by:** `Card.cs` with SetupForAlbumPlacement() + drag event hooks
### **Old Interaction Handler:**
6. **CardInteractionHandler.cs**
- Bridge between Unity pointer events and state machine
- Implemented IBeginDragHandler, IDragHandler, IEndDragHandler
- **Replaced by:** `Card.cs` inherits from DraggableObject (already has these interfaces)
---
## What Replaced Them:
### **New State Machine System:**
**Single Card Component:**
```
Card.cs (inherits from DraggableObject)
├─ CardContext.cs (shared data + events)
├─ CardAnimator.cs (reusable animations)
└─ 7 State Components:
├─ CardIdleState.cs (card back, hover, click to flip)
├─ CardRevealedState.cs (normal size, waiting, idle badges)
├─ CardEnlargedNewState.cs (enlarged with NEW badge)
├─ CardEnlargedRepeatState.cs (enlarged with progress bar)
├─ CardDraggingState.cs (visual feedback during drag)
├─ CardPlacedInSlotState.cs (in album slot)
└─ CardAlbumEnlargedState.cs (enlarged from album)
```
### **Benefits:**
-**60% less code** (shared components, no duplication)
-**No boolean soup** (1 active state vs 12+ boolean flags)
-**Automatic visual management** (state-owned GameObjects)
-**Easier testing** (can test states in isolation)
-**Simpler extension** (add new state vs modify monolith)
-**Better debugging** (inspector shows active state name)
---
## Migration Status:
### **Phase 1: Implementation ✅ COMPLETE**
- [x] Created Card.cs with state machine
- [x] Created all 7 state components
- [x] Created CardContext, CardAnimator
- [x] Integrated drag/drop (Card inherits DraggableObject)
- [x] Moved old files to DEPRECATED/
### **Phase 2: Booster Opening Migration 🚧 IN PROGRESS**
- [ ] Update BoosterOpeningPage to use Card.cs
- [ ] Replace FlippableCard spawning with Card spawning
- [ ] Use Card.SetupForBoosterReveal()
- [ ] Test all booster flows (NEW, REPEAT, UPGRADE)
### **Phase 3: Album Migration 🚧 PENDING**
- [ ] Update AlbumViewPage corner cards to use Card.cs
- [ ] Use Card.SetupForAlbumPlacement()
- [ ] Update album slots to use Card.cs
- [ ] Use Card.SetupForAlbumSlot()
- [ ] Test drag/drop to slots
- [ ] Test enlarge/shrink from album
### **Phase 4: Cleanup 🚧 PENDING**
- [ ] Verify no references to old files in active code
- [ ] Delete DEPRECATED/ folder
- [ ] Remove old prefab variants
- [ ] Update documentation
---
## How to Migrate Code:
### **Old Booster Opening (FlippableCard):**
```csharp
// OLD:
FlippableCard card = Instantiate(flippableCardPrefab);
card.SetupCard(cardData);
card.OnCardRevealed += OnCardRevealed;
if (isNewCard)
card.ShowAsNew();
else if (willUpgrade)
card.ShowAsRepeatWithUpgrade(ownedCount, lowerRarityCard);
else
card.ShowAsRepeat(ownedCount);
```
### **New Booster Opening (Card + States):**
```csharp
// NEW:
Card card = Instantiate(cardPrefab);
card.SetupForBoosterReveal(cardData, isNew: isNewCard);
card.Context.RepeatCardCount = ownedCount;
card.Context.OnFlipComplete += OnCardFlipComplete;
card.Context.OnCardInteractionComplete += OnCardComplete;
// Card automatically transitions through states on click
```
---
### **Old Album Placement (AlbumCardPlacementDraggable):**
```csharp
// OLD:
AlbumCardPlacementDraggable card = Instantiate(placementPrefab);
card.SetupCard(cardData);
card.OnCardRevealed += OnCardRevealed;
card.OnCardPlacedInAlbum += OnCardPlaced;
// Tap to reveal, drag to place
```
### **New Album Placement (Card + States):**
```csharp
// NEW:
Card card = Instantiate(cardPrefab);
card.SetupForAlbumPlacement(cardData);
// Card starts in RevealedState, can be dragged
// Automatically transitions to PlacedInSlotState when dropped in slot
```
---
### **Old Album Card (AlbumCard):**
```csharp
// OLD:
AlbumCard card = Instantiate(albumCardPrefab);
card.SetupCard(cardData);
card.OnEnlargeRequested += OnCardEnlarged;
card.OnShrinkRequested += OnCardShrunk;
card.EnlargeCard(); // Manual enlarge
```
### **New Album Card (Card + States):**
```csharp
// NEW:
Card card = Instantiate(cardPrefab, albumSlot.transform);
card.SetupForAlbumSlot(cardData, albumSlot);
// Click to enlarge → AlbumEnlargedState
// Tap to shrink → PlacedInSlotState
// State machine handles transitions automatically
```
---
## When Can We Delete This Folder?
**Checklist:**
- [ ] BoosterOpeningPage fully migrated to Card.cs
- [ ] AlbumViewPage fully migrated to Card.cs
- [ ] All prefabs updated to use new Card prefab
- [ ] All old prefabs deleted/archived
- [ ] No compiler references to:
- FlippableCard
- AlbumCard
- CardDraggable
- CardDraggableVisual
- AlbumCardPlacementDraggable
- CardInteractionHandler
- [ ] All tests passing with new system
- [ ] QA approved for production
**Once all checkboxes are complete, delete this entire DEPRECATED/ folder.**
---
## Need Help Migrating?
See documentation:
- `docs/cards_wip/card_system_implementation_summary.md` - Architecture overview
- `docs/cards_wip/card_prefab_assembly_guide.md` - How to build Card prefab
- `docs/cards_wip/card_dragdrop_integration_summary.md` - Drag/drop integration
- `docs/cards_wip/card_test_scene_setup_guide.md` - Testing scene setup
---
**Last Updated:** December 11, 2025
**Migration Status:** Phase 1 Complete, Phase 2-4 In Progress

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 77fe31c3dcfb4d4d8cfee187b838e8e3
timeCreated: 1762951626