using Godot; using System; using System.Collections.Generic; using System.Linq; using Chessistics.Engine.Events; using Chessistics.Engine.Model; using Chessistics.Scripts.Board; using Chessistics.Scripts.Pieces; using Chessistics.Scripts.UI; namespace Chessistics.Scripts.Presentation; public partial class EventAnimator : Node { private BoardView _boardView = null!; private ObjectivePanel _objectivePanel = null!; private ControlBar _controlBar = null!; private MetricsOverlay _metricsOverlay = null!; private readonly Dictionary _pieceViews = new(); private readonly Dictionary _trajectViews = new(); private bool _animating; public bool IsAnimating => _animating; private static readonly Color WoodCargoColor = new("#8B6914"); private static readonly Color StoneCargoColor = new("#808080"); private const float ProduceDuration = 0.3f; private const float TransferDuration = 0.25f; private const float MoveDuration = 0.3f; private const float KnightMoveDuration = 0.4f; private const float DestroyDuration = 0.3f; [Signal] public delegate void TurnAnimationCompletedEventHandler(); [Signal] public delegate void VictoryReachedEventHandler(); public void Initialize(BoardView boardView, ObjectivePanel objectivePanel, ControlBar controlBar, MetricsOverlay metricsOverlay) { _boardView = boardView; _objectivePanel = objectivePanel; _controlBar = controlBar; _metricsOverlay = metricsOverlay; } public void RegisterPiece(int pieceId, PieceView pieceView, TrajectView trajectView) { _pieceViews[pieceId] = pieceView; _trajectViews[pieceId] = trajectView; } public void UnregisterPiece(int pieceId) { if (_pieceViews.TryGetValue(pieceId, out var pv)) { pv.QueueFree(); _pieceViews.Remove(pieceId); } if (_trajectViews.TryGetValue(pieceId, out var tv)) { tv.QueueFree(); _trajectViews.Remove(pieceId); } } public void ProcessEvents(IReadOnlyList events) { _animating = true; var tween = CreateTween(); tween.SetParallel(false); var produceEvents = new List(); var transferEvents = new List(); var moveEvents = new List(); var collisionEvents = new List(); foreach (var evt in events) { switch (evt) { case TurnStartedEvent ts: FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents); tween.TweenCallback(Callable.From(() => _controlBar.UpdateTurn(ts.TurnNumber))); break; case CargoProducedEvent produced: produceEvents.Add(produced); break; case CargoTransferredEvent: case DemandProgressEvent: transferEvents.Add(evt); break; case PieceMovedEvent moved: moveEvents.Add(moved); break; case PieceDestroyedEvent destroyed: collisionEvents.Add(destroyed); break; case VictoryEvent victory: FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents); tween.TweenCallback(Callable.From(() => { _metricsOverlay.ShowMetrics(victory.Metrics); EmitSignal(SignalName.VictoryReached); })); break; case TurnEndedEvent: FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents); break; default: break; } } FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents); tween.TweenCallback(Callable.From(() => { _animating = false; EmitSignal(SignalName.TurnAnimationCompleted); })); } private void FlushPhases( Tween tween, List produceEvents, List transferEvents, List moveEvents, List collisionEvents) { // Phase 1: Produce — flash production cells if (produceEvents.Count > 0) { tween.TweenCallback(Callable.From(() => { foreach (var evt in produceEvents.ToList()) { var cell = _boardView.GetCellView(evt.ProductionCell); cell?.FlashProduce(ProduceDuration); } })); tween.TweenInterval(ProduceDuration); produceEvents.Clear(); } // Phase 2: Transfers — animate cargo sliding from giver to receiver if (transferEvents.Count > 0) { // Capture the events list before clearing var eventsToAnimate = transferEvents.ToList(); // Step 1: remove cargo from givers + spawn sliding cargo sprites tween.TweenCallback(Callable.From(() => { foreach (var evt in eventsToAnimate) { if (evt is CargoTransferredEvent transfer) { // Remove cargo indicator from giver if (transfer.GivingPieceId != null && _pieceViews.ContainsKey(transfer.GivingPieceId.Value)) _pieceViews[transfer.GivingPieceId.Value].SetCargo(null); // Create sliding cargo sprite SpawnCargoSlide(transfer); } } })); // Step 2: wait for slide, then show cargo on receivers + update demand progress tween.TweenInterval(TransferDuration); tween.TweenCallback(Callable.From(() => { foreach (var evt in eventsToAnimate) { if (evt is CargoTransferredEvent transfer) { if (transfer.ReceivingPieceId != null && _pieceViews.ContainsKey(transfer.ReceivingPieceId.Value)) _pieceViews[transfer.ReceivingPieceId.Value].SetCargo(transfer.Type); } else if (evt is DemandProgressEvent progress) { _objectivePanel.UpdateProgress(progress.DemandCell, progress.Name, progress.Current, progress.Required); } } })); transferEvents.Clear(); } // Phase 3: Movement — all pieces move simultaneously if (moveEvents.Count > 0) { tween.SetParallel(true); foreach (var moved in moveEvents) { if (_pieceViews.TryGetValue(moved.PieceId, out var pv)) { var target = _boardView.CoordsToPixel(moved.To); float duration = pv.Kind == PieceKind.Knight ? KnightMoveDuration : MoveDuration; tween.TweenProperty(pv, "position", target, duration); } } tween.SetParallel(false); moveEvents.Clear(); } // Phase 4: Collision/Destruction if (collisionEvents.Count > 0) { tween.SetParallel(true); foreach (var destroyed in collisionEvents) { var pieceId = destroyed.PieceId; tween.TweenCallback(Callable.From(() => { FlashPiece(pieceId); UnregisterPiece(pieceId); })); } tween.SetParallel(false); tween.TweenInterval(DestroyDuration); collisionEvents.Clear(); } } /// /// Creates a temporary colored square that slides from the giver to the receiver. /// private void SpawnCargoSlide(CargoTransferredEvent transfer) { var from = _boardView.CoordsToPixel(transfer.From); var to = _boardView.CoordsToPixel(transfer.To); var color = transfer.Type switch { CargoType.Wood => WoodCargoColor, CargoType.Stone => StoneCargoColor, _ => Colors.White }; var sprite = new ColorRect { Size = new Vector2(14, 14), Position = new Vector2(-7, -7), Color = color, MouseFilter = Control.MouseFilterEnum.Ignore }; var container = new Node2D { Position = from }; container.AddChild(sprite); _boardView.AddChild(container); var slideTween = container.CreateTween(); slideTween.TweenProperty(container, "position", to, TransferDuration) .SetEase(Tween.EaseType.InOut) .SetTrans(Tween.TransitionType.Cubic); slideTween.TweenCallback(Callable.From(() => container.QueueFree())); } private void FlashPiece(int pieceId) { if (!_pieceViews.TryGetValue(pieceId, out var pv)) return; var tween = pv.CreateTween(); tween.TweenProperty(pv, "modulate", new Color(1, 0.2f, 0.2f), 0.1f); tween.TweenProperty(pv, "modulate", Colors.White, 0.1f); tween.SetLoops(3); } public void ResetPiecePositions(BoardSnapshot snapshot) { foreach (var ps in snapshot.Pieces) { if (_pieceViews.TryGetValue(ps.Id, out var pv)) { pv.Position = _boardView.CoordsToPixel(ps.StartCell); pv.SetCargo(null); } } } public void ClearAll() { foreach (var pv in _pieceViews.Values) pv.QueueFree(); foreach (var tv in _trajectViews.Values) tv.QueueFree(); _pieceViews.Clear(); _trajectViews.Clear(); } }