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; } private sealed class ActiveMove { public Card3D Card; public float Remaining; } 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) { 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, }); } 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); continue; } // Страховка: карта могла быть снята с рендера до // завершения Tick. Не держим InputLock вечно. if (active.Remaining <= 0f) { card.StopMove(snap: true); _active.RemoveAt(i); } } } if (_queue.Count > 0) { _delay -= Mathf.Max(0f, deltaTime); if (_delay <= 0f) StartNext(); } if (!IsBusy) ReleaseLock(); } public void CancelAll(bool snapCards = false) { _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 remaining = new Queue(); while (_queue.Count > 0) { var request = _queue.Dequeue(); if (request.Card != card) remaining.Enqueue(request); } while (remaining.Count > 0) _queue.Enqueue(remaining.Dequeue()); 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; var request = _queue.Dequeue(); var card = request.Card; if (card == null || card.IsDragging) { _delay = 0f; return; } card.StartArcMove( request.FromPosition, request.FromRotation, request.FromScale, request.Duration, request.ArcHeight); if (card.IsMoving) { _active.Add(new ActiveMove { Card = card, Remaining = Mathf.Max(0.1f, request.Duration) + 0.05f, }); } _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; } } }