Files
AppleHillsProduction/Assets/Scripts/CardSystem/StateMachine/CardAnimator.cs
tschesky 235fa04eba Merge a card refresh (#59)
- **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
2025-11-18 08:40:59 +00:00

407 lines
16 KiB
C#

using Pixelplacement;
using Pixelplacement.TweenSystem;
using UnityEngine;
using System;
using AppleHills.Core.Settings;
using Core;
namespace UI.CardSystem.StateMachine
{
/// <summary>
/// Handles common card animations that can be reused across states.
/// Centralizes animation logic to avoid duplication.
/// Animates the CARD ROOT TRANSFORM (all states follow the card).
/// </summary>
public class CardAnimator : MonoBehaviour
{
private Transform _transform;
private RectTransform _rectTransform;
private ICardSystemSettings _settings;
private TweenBase _activeIdleHoverTween;
private void Awake()
{
_transform = transform;
_rectTransform = GetComponent<RectTransform>();
_settings = GameManager.GetSettingsObject<ICardSystemSettings>();
}
#region Scale Animations
/// <summary>
/// Animate scale to target value
/// </summary>
public TweenBase AnimateScale(Vector3 targetScale, float? duration = null, Action onComplete = null)
{
return Tween.LocalScale(_transform, targetScale, duration ?? _settings.DefaultAnimationDuration, 0f,
Tween.EaseInOut, completeCallback: onComplete);
}
/// <summary>
/// Pulse scale animation (scale up then back to normal)
/// </summary>
public void PulseScale(float pulseAmount = 1.1f, float duration = 0.2f, Action onComplete = null)
{
Vector3 originalScale = _transform.localScale;
Vector3 pulseScale = originalScale * pulseAmount;
Tween.LocalScale(_transform, pulseScale, duration, 0f, Tween.EaseOutBack,
completeCallback: () =>
{
Tween.LocalScale(_transform, originalScale, duration, 0f, Tween.EaseInBack,
completeCallback: onComplete);
});
}
/// <summary>
/// Pop-in animation (scale from 0 to 1 with overshoot)
/// </summary>
public TweenBase PopIn(float duration = 0.5f, Action onComplete = null)
{
_transform.localScale = Vector3.zero;
return Tween.LocalScale(_transform, Vector3.one, duration, 0f,
Tween.EaseOutBack, completeCallback: onComplete);
}
/// <summary>
/// Pop-out animation (scale from current to 0)
/// </summary>
public TweenBase PopOut(float duration = 0.3f, Action onComplete = null)
{
return Tween.LocalScale(_transform, Vector3.zero, duration, 0f,
Tween.EaseInBack, completeCallback: onComplete);
}
#endregion
#region Position Animations (RectTransform)
/// <summary>
/// Animate anchored position (for UI elements)
/// </summary>
public TweenBase AnimateAnchoredPosition(Vector2 targetPosition, float? duration = null, Action onComplete = null)
{
if (_rectTransform == null)
{
Debug.LogWarning("CardAnimator: No RectTransform found for anchored position animation");
return null;
}
return Tween.AnchoredPosition(_rectTransform, targetPosition, duration ?? _settings.DefaultAnimationDuration, 0f,
Tween.EaseInOut, completeCallback: onComplete);
}
/// <summary>
/// Animate local position
/// </summary>
public TweenBase AnimateLocalPosition(Vector3 targetPosition, float? duration = null, Action onComplete = null)
{
return Tween.LocalPosition(_transform, targetPosition, duration ?? _settings.DefaultAnimationDuration, 0f,
Tween.EaseInOut, completeCallback: onComplete);
}
#endregion
#region Rotation Animations
/// <summary>
/// Animate local rotation to target
/// </summary>
public TweenBase AnimateLocalRotation(Quaternion targetRotation, float? duration = null, Action onComplete = null)
{
return Tween.LocalRotation(_transform, targetRotation, duration ?? _settings.DefaultAnimationDuration, 0f,
Tween.EaseInOut, completeCallback: onComplete);
}
/// <summary>
/// Rotate a child object (typically used by states for CardBackVisual, etc.)
/// </summary>
public TweenBase AnimateChildRotation(Transform childTransform, Quaternion targetRotation,
float duration, Action onComplete = null)
{
return Tween.LocalRotation(childTransform, targetRotation, duration, 0f,
Tween.EaseInOut, completeCallback: onComplete);
}
#endregion
#region Flip Animations
/// <summary>
/// Play card flip animation - rotates card back from 0° to 90°, then card front from 180° to 0°
/// Based on FlippableCard.FlipToReveal()
/// </summary>
public void PlayFlip(Transform cardBack, Transform cardFront, float? duration = null, Action onComplete = null)
{
float flipDuration = duration ?? _settings.FlipDuration;
// Phase 1: Rotate both to 90 degrees (edge view)
if (cardBack != null)
{
Tween.LocalRotation(cardBack, Quaternion.Euler(0, 90, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut);
}
if (cardFront != null)
{
Tween.LocalRotation(cardFront, Quaternion.Euler(0, 90, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut,
completeCallback: () =>
{
// At edge (90°), switch visibility
if (cardBack != null)
cardBack.gameObject.SetActive(false);
if (cardFront != null)
cardFront.gameObject.SetActive(true);
// Phase 2: Rotate front from 90 to 0 (show at correct orientation)
Tween.LocalRotation(cardFront, Quaternion.Euler(0, 0, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut,
completeCallback: onComplete);
});
}
}
/// <summary>
/// Play scale punch during flip animation for extra juice
/// Based on FlippableCard.FlipToReveal()
/// </summary>
public void PlayFlipScalePunch(float? punchScale = null, float? duration = null)
{
float punch = punchScale ?? _settings.FlipScalePunch;
float flipDuration = duration ?? _settings.FlipDuration;
Vector3 originalScale = _transform.localScale;
Tween.LocalScale(_transform, originalScale * punch, flipDuration * 0.5f, 0f, Tween.EaseOutBack,
completeCallback: () =>
{
Tween.LocalScale(_transform, originalScale, flipDuration * 0.5f, 0f, Tween.EaseInBack);
});
}
#endregion
#region Enlarge/Shrink Animations
/// <summary>
/// Enlarge card to specified scale
/// Based on FlippableCard.EnlargeCard() and AlbumCard.EnlargeCard()
/// </summary>
public void PlayEnlarge(float? targetScale = null, float? duration = null, Action onComplete = null)
{
float scale = targetScale ?? _settings.NewCardEnlargedScale;
float scaleDuration = duration ?? _settings.ScaleDuration;
Tween.LocalScale(_transform, Vector3.one * scale, scaleDuration, 0f, Tween.EaseOutBack,
completeCallback: onComplete);
}
/// <summary>
/// Shrink card back to original scale
/// Based on AlbumCard.ShrinkCard() and FlippableCard.ReturnToNormalSize()
/// </summary>
public void PlayShrink(Vector3 targetScale, float? duration = null, Action onComplete = null)
{
float scaleDuration = duration ?? _settings.ScaleDuration;
Tween.LocalScale(_transform, targetScale, scaleDuration, 0f, Tween.EaseInBack,
completeCallback: onComplete);
}
#endregion
#region Combined Animations
/// <summary>
/// Hover enter animation (lift and scale)
/// For RectTransform UI elements
/// </summary>
public void HoverEnter(float liftAmount = 20f, float scaleMultiplier = 1.05f,
float duration = 0.2f, Action onComplete = null)
{
if (_rectTransform != null)
{
Vector2 currentPos = _rectTransform.anchoredPosition;
Vector2 targetPos = currentPos + Vector2.up * liftAmount;
Tween.AnchoredPosition(_rectTransform, targetPos, duration, 0f, Tween.EaseOutBack);
Tween.LocalScale(_transform, Vector3.one * scaleMultiplier, duration, 0f,
Tween.EaseOutBack, completeCallback: onComplete);
}
else
{
// Fallback for non-RectTransform
Vector3 currentPos = _transform.localPosition;
Vector3 targetPos = currentPos + Vector3.up * liftAmount;
Tween.LocalPosition(_transform, targetPos, duration, 0f, Tween.EaseOutBack);
Tween.LocalScale(_transform, Vector3.one * scaleMultiplier, duration, 0f,
Tween.EaseOutBack, completeCallback: onComplete);
}
}
/// <summary>
/// Hover exit animation (return to original position and scale)
/// </summary>
public void HoverExit(Vector2 originalPosition, float duration = 0.2f, Action onComplete = null)
{
if (_rectTransform != null)
{
Tween.AnchoredPosition(_rectTransform, originalPosition, duration, 0f, Tween.EaseInBack);
Tween.LocalScale(_transform, Vector3.one, duration, 0f,
Tween.EaseInBack, completeCallback: onComplete);
}
else
{
Tween.LocalPosition(_transform, originalPosition, duration, 0f, Tween.EaseInBack);
Tween.LocalScale(_transform, Vector3.one, duration, 0f,
Tween.EaseInBack, completeCallback: onComplete);
}
}
/// <summary>
/// Idle hover animation (gentle bobbing loop)
/// Returns the TweenBase so caller can stop it later.
/// Only starts if not already running, or kills and restarts.
/// </summary>
public TweenBase StartIdleHover(float hoverHeight = 10f, float duration = 1.5f, bool restartIfActive = false)
{
// If already running, either skip or restart
if (_activeIdleHoverTween != null)
{
if (!restartIfActive)
{
// Already running, skip
return _activeIdleHoverTween;
}
// Kill existing and restart
_activeIdleHoverTween.Stop();
_activeIdleHoverTween = null;
}
if (_rectTransform != null)
{
Vector2 originalPos = _rectTransform.anchoredPosition;
Vector2 targetPos = originalPos + Vector2.up * hoverHeight;
_activeIdleHoverTween = Tween.Value(0f, 1f,
(val) =>
{
if (_rectTransform != null)
{
float t = Mathf.Sin(val * Mathf.PI * 2f) * 0.5f + 0.5f;
_rectTransform.anchoredPosition = Vector2.Lerp(originalPos, targetPos, t);
}
},
duration, 0f, Tween.EaseInOut, Tween.LoopType.Loop);
return _activeIdleHoverTween;
}
return null;
}
/// <summary>
/// Stop idle hover animation and return to original position
/// </summary>
public void StopIdleHover(Vector2 originalPosition, float duration = 0.3f)
{
// Stop the tracked tween if it exists
if (_activeIdleHoverTween != null)
{
_activeIdleHoverTween.Stop();
_activeIdleHoverTween = null;
}
// Return to original position
if (_rectTransform != null)
{
Tween.AnchoredPosition(_rectTransform, originalPosition, duration, 0f, Tween.EaseInOut);
}
}
#endregion
#region Flip Animations (Two-Phase)
/// <summary>
/// Flip animation: Phase 1 - Rotate card back to edge (0° to 90°)
/// Used by FlippingState to hide the back
/// </summary>
public void FlipPhase1_HideBack(Transform cardBackTransform, float duration, Action onHalfwayComplete)
{
Tween.LocalRotation(cardBackTransform, Quaternion.Euler(0, 90, 0), duration, 0f,
Tween.EaseInOut, completeCallback: onHalfwayComplete);
}
/// <summary>
/// Flip animation: Phase 2 - Rotate card front from back to face (180° to 90° to 0°)
/// Used by FlippingState to reveal the front
/// </summary>
public void FlipPhase2_RevealFront(Transform cardFrontTransform, float duration, Action onComplete)
{
// First rotate from 180 to 90 (edge)
Tween.LocalRotation(cardFrontTransform, Quaternion.Euler(0, 90, 0), duration, 0f,
Tween.EaseInOut,
completeCallback: () =>
{
// Then rotate from 90 to 0 (face)
Tween.LocalRotation(cardFrontTransform, Quaternion.Euler(0, 0, 0), duration, 0f,
Tween.EaseInOut, completeCallback: onComplete);
});
}
/// <summary>
/// Scale punch during flip (makes flip more juicy)
/// </summary>
public void FlipScalePunch(float punchMultiplier = 1.1f, float totalDuration = 0.6f)
{
Vector3 originalScale = _transform.localScale;
Vector3 punchScale = originalScale * punchMultiplier;
Tween.LocalScale(_transform, punchScale, totalDuration * 0.5f, 0f, Tween.EaseOutBack,
completeCallback: () =>
{
Tween.LocalScale(_transform, originalScale, totalDuration * 0.5f, 0f, Tween.EaseInBack);
});
}
#endregion
#region Utility
/// <summary>
/// Stop all active tweens on this transform
/// </summary>
public void StopAllAnimations()
{
Tween.Stop(_transform.GetInstanceID());
if (_rectTransform != null)
Tween.Stop(_rectTransform.GetInstanceID());
}
/// <summary>
/// Reset transform to default values
/// </summary>
public void ResetTransform()
{
StopAllAnimations();
_transform.localPosition = Vector3.zero;
_transform.localRotation = Quaternion.identity;
_transform.localScale = Vector3.one;
if (_rectTransform != null)
_rectTransform.anchoredPosition = Vector2.zero;
}
/// <summary>
/// Get current anchored position (useful for saving before hover)
/// </summary>
public Vector2 GetAnchoredPosition()
{
return _rectTransform != null ? _rectTransform.anchoredPosition : Vector2.zero;
}
#endregion
}
}