VanguardReMaster/client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs

340 lines
9.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using UnityEngine;
using Vanguard.Battle;
using Vanguard.Board3D;
namespace Vanguard.Board3D
{
/// <summary>
/// Единый планировщик карточных перемещений. Карточки анимируются
/// Card3D, аниматор отвечает за stagger, очередь и InputLock.
/// </summary>
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<Card3D> OnCompleted;
public Action<Card3D> OnStart;
public bool FromCurrent;
}
private sealed class ActiveMove
{
public Card3D Card;
public float Remaining;
public Action<Card3D> OnCompleted;
}
private readonly Queue<Request> _queue = new Queue<Request>();
private readonly List<ActiveMove> _active = new List<ActiveMove>();
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<Card3D> onCompleted = null,
Action<Card3D> 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,
});
}
/// <summary>
/// Запускает перемещение сразу, без очереди и stagger. Нужен для
/// парных доборов, где две карты должны лететь одновременно.
/// </summary>
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<Request>();
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;
}
}
}