using System;
using System.Collections.Generic;
using UnityEngine;
using Vanguard.Battle;
using Vanguard.Board3D;
namespace Vanguard.Board3D
{
///
/// Единый планировщик карточных перемещений. Карточки анимируются
/// Card3D, аниматор отвечает за stagger, очередь и InputLock.
///
public sealed class CardMoveAnimator
{
private sealed class Request
{
public Card3D Card;
public Vector3 FromPosition;
public Quaternion FromRotation;
public Vector3 FromScale;
public float Duration;
public float ArcHeight;
public bool Sequential;
public Action OnCompleted;
public Action OnStart;
public bool FromCurrent;
}
private sealed class ActiveMove
{
public Card3D Card;
public float Remaining;
public Action OnCompleted;
}
private readonly Queue _queue = new Queue();
private readonly List _active = new List();
private readonly InputLockService _inputLock;
private AnimationConfig _config;
private float _delay;
private bool _locked;
public CardMoveAnimator(
InputLockService inputLock,
AnimationConfig config)
{
_inputLock = inputLock;
_config = config;
}
public bool IsBusy => _queue.Count > 0 || _active.Count > 0;
public int PendingCount => _queue.Count;
public int ActiveCount => _active.Count;
public bool IsCardQueued(Card3D card)
{
if (card == null)
return false;
foreach (var request in _queue)
{
if (request.Card == card)
return true;
}
return false;
}
public bool IsCardActive(Card3D card)
{
if (card == null)
return false;
foreach (var active in _active)
{
if (active.Card == card)
return true;
}
return false;
}
public void SetConfig(AnimationConfig config)
{
_config = config;
}
public void Enqueue(
Card3D card,
Vector3 fromPosition,
Quaternion fromRotation,
Vector3 fromScale,
float duration,
float arcHeight,
bool sequential = false,
Action onCompleted = null,
Action onStart = null,
bool fromCurrent = false)
{
if (card == null || card.IsDragging)
return;
// У одной карты не может быть нескольких активных/ожидающих
// запросов одновременно: старый отменяем, новый стартует из
// текущей визуальной позиции.
CancelCard(card);
EnsureLock();
_queue.Enqueue(new Request
{
Card = card,
FromPosition = fromPosition,
FromRotation = fromRotation,
FromScale = fromScale,
Duration = duration,
ArcHeight = arcHeight,
Sequential = sequential,
OnCompleted = onCompleted,
OnStart = onStart,
FromCurrent = fromCurrent,
});
}
///
/// Запускает перемещение сразу, без очереди и stagger. Нужен для
/// парных доборов, где две карты должны лететь одновременно.
///
public void EnqueueNow(
Card3D card,
Vector3 fromPosition,
Quaternion fromRotation,
Vector3 fromScale,
float duration,
float arcHeight)
{
if (card == null || card.IsDragging)
return;
CancelCard(card);
EnsureLock();
card.StartArcMove(
fromPosition,
fromRotation,
fromScale,
duration,
arcHeight);
if (card.IsMoving)
{
_active.Add(new ActiveMove
{
Card = card,
Remaining = Mathf.Max(0.1f, duration) + 0.05f,
});
}
}
public void Tick(float deltaTime)
{
if (_active.Count > 0)
{
var delta = Mathf.Max(0f, deltaTime);
for (var i = _active.Count - 1; i >= 0; i--)
{
var active = _active[i];
var card = active?.Card;
if (active == null
|| card == null
|| !card.gameObject.activeInHierarchy)
{
if (card != null)
card.StopMove(snap: false);
_active.RemoveAt(i);
continue;
}
active.Remaining -= delta;
if (!card.IsMoving)
{
_active.RemoveAt(i);
active.OnCompleted?.Invoke(card);
continue;
}
// Страховка: карта могла быть снята с рендера до
// завершения Tick. Не держим InputLock вечно.
if (active.Remaining <= 0f)
{
card.StopMove(snap: true);
_active.RemoveAt(i);
active.OnCompleted?.Invoke(card);
}
}
}
if (_queue.Count > 0)
{
_delay -= Mathf.Max(0f, deltaTime);
if (_delay <= 0f)
StartNext();
}
if (!IsBusy)
ReleaseLock();
}
public void CancelAll(bool snapCards = false)
{
foreach (var request in _queue)
{
if (request?.Card != null)
request.Card.CancelHold();
}
_queue.Clear();
foreach (var active in _active)
{
if (active?.Card != null)
active.Card.StopMove(snapCards);
}
_active.Clear();
_delay = 0f;
ReleaseLock();
}
public void CancelCard(Card3D card)
{
if (card == null)
return;
var wasQueued = false;
var remaining = new Queue();
while (_queue.Count > 0)
{
var request = _queue.Dequeue();
if (request.Card != card)
{
remaining.Enqueue(request);
}
else
{
wasQueued = true;
}
}
while (remaining.Count > 0)
_queue.Enqueue(remaining.Dequeue());
if (wasQueued)
card.CancelHold();
for (var i = _active.Count - 1; i >= 0; i--)
{
if (_active[i]?.Card != card)
continue;
card.StopMove(snap: false);
_active.RemoveAt(i);
}
if (!IsBusy)
ReleaseLock();
}
private void StartNext()
{
if (_queue.Count == 0)
return;
// Последовательные запросы (добор, возврат в колоду) идут строго
// по одной карте: следующая стартует только после предыдущей.
if (_queue.Peek().Sequential && _active.Count > 0)
{
_delay = 0f;
return;
}
var request = _queue.Dequeue();
var card = request.Card;
if (card == null || card.IsDragging)
{
_delay = 0f;
return;
}
request.OnStart?.Invoke(card);
card.StartArcMove(
request.FromCurrent
? card.transform.position
: request.FromPosition,
request.FromCurrent
? card.transform.rotation
: request.FromRotation,
request.FromCurrent
? card.transform.localScale
: request.FromScale,
request.Duration,
request.ArcHeight);
if (card.IsMoving)
{
_active.Add(new ActiveMove
{
Card = card,
Remaining = Mathf.Max(0.1f, request.Duration) + 0.05f,
OnCompleted = request.OnCompleted,
});
}
else
{
request.OnCompleted?.Invoke(card);
}
_delay = Mathf.Max(0.01f, _config != null
? _config.moveDelayBetweenCards
: 0.18f);
}
private void EnsureLock()
{
if (_locked)
return;
_inputLock?.Acquire("card-move");
_locked = true;
}
private void ReleaseLock()
{
if (!_locked)
return;
_inputLock?.Release("card-move");
_locked = false;
}
}
}