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("#A67C32"); private static readonly Color StoneCargoColor = new("#7A7A7A"); private static readonly Color ToolsCargoColor = new("#C87533"); private static readonly Color ArmsCargoColor = new("#8B0000"); private static readonly Color GoldCargoColor = new("#FFD700"); private const float ProduceDuration = 0.35f; private const float TransferDuration = 0.28f; private const float MoveDuration = 0.32f; private const float KnightMoveDuration = 0.42f; private const float DestroyDuration = 0.45f; [Signal] public delegate void TurnAnimationCompletedEventHandler(); [Signal] public delegate void VictoryReachedEventHandler(); [Signal] public delegate void MissionAdvancedEventHandler(); [Signal] public delegate void CollisionOccurredEventHandler(int col, int row, string victimKind, int destroyerId); 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 transformerEvents = new List(); var transferEvents = new List(); var moveEvents = new List(); var collisionEvents = new List(); // Pre-scan: if MissionStartedEvent follows MissionCompleteEvent, it's an auto-advance (not last mission) bool hasAutoAdvance = events.Any(e => e is MissionStartedEvent); foreach (var evt in events) { switch (evt) { case TurnStartedEvent ts: FlushPhases(tween, produceEvents, transformerEvents, transferEvents, moveEvents, collisionEvents); tween.TweenCallback(Callable.From(() => _controlBar.UpdateTurn(ts.TurnNumber))); break; case CargoProducedEvent produced: produceEvents.Add(produced); break; case CargoConvertedEvent converted: transformerEvents.Add(converted); break; case CargoTransferredEvent: case DemandProgressEvent: transferEvents.Add(evt); break; case PieceMovedEvent moved: moveEvents.Add(moved); break; case PieceReturnedToStockEvent returned: collisionEvents.Add(returned); break; case MissionCompleteEvent: FlushPhases(tween, produceEvents, transformerEvents, transferEvents, moveEvents, collisionEvents); tween.TweenCallback(Callable.From(() => { SfxManager.Instance?.PlayVictory(); SpawnConfetti(); if (!hasAutoAdvance) EmitSignal(SignalName.VictoryReached); })); break; case MissionStartedEvent: FlushPhases(tween, produceEvents, transformerEvents, transferEvents, moveEvents, collisionEvents); tween.TweenCallback(Callable.From(() => { EmitSignal(SignalName.MissionAdvanced); })); break; case SimulationPausedEvent: // Auto-pause from collision — handled by FlushPhases break; case TurnEndedEvent: FlushPhases(tween, produceEvents, transformerEvents, transferEvents, moveEvents, collisionEvents); break; default: break; } } FlushPhases(tween, produceEvents, transformerEvents, transferEvents, moveEvents, collisionEvents); tween.TweenCallback(Callable.From(() => { _animating = false; EmitSignal(SignalName.TurnAnimationCompleted); })); } private void FlushPhases( Tween tween, List produceEvents, List transformerEvents, List transferEvents, List moveEvents, List collisionEvents) { // Phase 1a: Transformer conversions — copper flash, distinct from production if (transformerEvents.Count > 0) { var captured = transformerEvents.ToList(); tween.TweenCallback(Callable.From(() => { SfxManager.Instance?.PlayProduce(); foreach (var evt in captured) { var cell = _boardView.GetCellView(evt.TransformerCell); cell?.FlashTransform(0.45f); SpawnProduceParticles(evt.TransformerCell, evt.OutputCargo); } })); tween.TweenInterval(0.45f); transformerEvents.Clear(); } // Phase 1: Produce — warm golden flash + particle burst if (produceEvents.Count > 0) { var captured = produceEvents.ToList(); tween.TweenCallback(Callable.From(() => { SfxManager.Instance?.PlayProduce(); foreach (var evt in captured) { var cell = _boardView.GetCellView(evt.ProductionCell); cell?.FlashProduce(ProduceDuration); SpawnProduceParticles(evt.ProductionCell, evt.Type); } })); tween.TweenInterval(ProduceDuration); produceEvents.Clear(); } // Phase 2: Transfers — cargo slides with trail particles if (transferEvents.Count > 0) { var eventsToAnimate = transferEvents.ToList(); tween.TweenCallback(Callable.From(() => { bool hasDelivery = false; foreach (var evt in eventsToAnimate) { if (evt is CargoTransferredEvent transfer) { if (transfer.GivingPieceId != null && _pieceViews.ContainsKey(transfer.GivingPieceId.Value)) _pieceViews[transfer.GivingPieceId.Value].SetCargo(null); SpawnCargoSlide(transfer); if (transfer.ReceivingPieceId == null) hasDelivery = true; } } if (hasDelivery) SfxManager.Instance?.PlayDeliver(); else SfxManager.Instance?.PlayTransfer(); })); 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 — simultaneous, with sfx if (moveEvents.Count > 0) { tween.TweenCallback(Callable.From(() => SfxManager.Instance?.PlayMove())); 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) .SetEase(Tween.EaseType.InOut).SetTrans(Tween.TransitionType.Sine); } } tween.SetParallel(false); moveEvents.Clear(); } // Phase 4: Collision — piece returned to stock (shrink + spin + particles) if (collisionEvents.Count > 0) { var captured = collisionEvents.ToList(); tween.TweenCallback(Callable.From(() => { SfxManager.Instance?.PlayDestroy(); foreach (var returned in captured) { if (_pieceViews.TryGetValue(returned.PieceId, out var pv)) { SpawnDestroyParticles(pv.Position); var dt = pv.CreateTween(); dt.SetParallel(true); dt.TweenProperty(pv, "scale", Vector2.Zero, DestroyDuration) .SetEase(Tween.EaseType.In).SetTrans(Tween.TransitionType.Back); dt.TweenProperty(pv, "rotation", Mathf.Pi * 2f, DestroyDuration); dt.TweenProperty(pv, "modulate:a", 0f, DestroyDuration); } } // Emit signal for Main to pan camera + show toast var first = captured[0]; EmitSignal(SignalName.CollisionOccurred, first.Cell.Col, first.Cell.Row, first.Kind.ToString(), first.DestroyerPieceId ?? -1); })); tween.TweenInterval(DestroyDuration); tween.TweenCallback(Callable.From(() => { foreach (var returned in captured) UnregisterPiece(returned.PieceId); })); collisionEvents.Clear(); } } // --- Visual Effects --- private void SpawnCargoSlide(CargoTransferredEvent transfer) { var from = _boardView.CoordsToPixel(transfer.From); var to = _boardView.CoordsToPixel(transfer.To); var color = GetCargoColor(transfer.Type); var container = new Node2D { Position = from }; _boardView.AddChild(container); // Main cargo square var main = new ColorRect { Size = new Vector2(12, 12), Position = new Vector2(-6, -6), Color = color, MouseFilter = Control.MouseFilterEnum.Ignore }; container.AddChild(main); // Trail particles (2 smaller squares that follow with delay) for (int i = 1; i <= 2; i++) { float trailDelay = i * 0.04f; float trailSize = 12f - i * 3f; float trailAlpha = 0.6f - i * 0.2f; var trail = new ColorRect { Size = new Vector2(trailSize, trailSize), Position = new Vector2(-trailSize / 2f, -trailSize / 2f), Color = new Color(color, trailAlpha), MouseFilter = Control.MouseFilterEnum.Ignore }; var trailContainer = new Node2D { Position = from }; trailContainer.AddChild(trail); _boardView.AddChild(trailContainer); var trailTween = trailContainer.CreateTween(); trailTween.TweenInterval(trailDelay); trailTween.TweenProperty(trailContainer, "position", to, TransferDuration - trailDelay) .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back); trailTween.TweenCallback(Callable.From(() => trailContainer.QueueFree())); } // Main slide with whip easing var slideTween = container.CreateTween(); slideTween.TweenProperty(container, "position", to, TransferDuration) .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back); slideTween.TweenCallback(Callable.From(() => container.QueueFree())); } private void SpawnProduceParticles(Coords cell, CargoType type) { var center = _boardView.CoordsToPixel(cell); var color = GetCargoColor(type); var rng = new Random(); for (int i = 0; i < 6; i++) { float angle = i * Mathf.Pi * 2f / 6f + (float)rng.NextDouble() * 0.5f; float dist = 25f + (float)rng.NextDouble() * 15f; float size = 4f + (float)rng.NextDouble() * 4f; var particle = new ColorRect { Size = new Vector2(size, size), Position = new Vector2(-size / 2f, -size / 2f), Color = new Color(color, 0.8f), MouseFilter = Control.MouseFilterEnum.Ignore }; var pNode = new Node2D { Position = center }; pNode.AddChild(particle); _boardView.AddChild(pNode); var target = center + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * dist; var pt = pNode.CreateTween(); pt.SetParallel(true); pt.TweenProperty(pNode, "position", target, ProduceDuration * 0.8f) .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic); pt.TweenProperty(pNode, "modulate:a", 0f, ProduceDuration); pt.SetParallel(false); pt.TweenCallback(Callable.From(() => pNode.QueueFree())); } } private void SpawnDestroyParticles(Vector2 position) { var rng = new Random(); var red = new Color(0.9f, 0.25f, 0.2f); for (int i = 0; i < 10; i++) { float angle = (float)rng.NextDouble() * Mathf.Pi * 2f; float dist = 20f + (float)rng.NextDouble() * 30f; float size = 3f + (float)rng.NextDouble() * 5f; var particle = new ColorRect { Size = new Vector2(size, size), Position = new Vector2(-size / 2f, -size / 2f), Color = new Color(red, 0.9f), MouseFilter = Control.MouseFilterEnum.Ignore }; var pNode = new Node2D { Position = position }; pNode.AddChild(particle); _boardView.AddChild(pNode); var target = position + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * dist; var pt = pNode.CreateTween(); pt.SetParallel(true); pt.TweenProperty(pNode, "position", target, DestroyDuration * 0.7f) .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic); pt.TweenProperty(pNode, "modulate:a", 0f, DestroyDuration); pt.TweenProperty(pNode, "scale", Vector2.Zero, DestroyDuration) .SetEase(Tween.EaseType.In); pt.SetParallel(false); pt.TweenCallback(Callable.From(() => pNode.QueueFree())); } } private void SpawnConfetti() { var rng = new Random(); var viewport = GetViewport().GetVisibleRect().Size; Color[] confettiColors = [ new("#FFD700"), new("#FF6B35"), new("#3D8E5A"), new("#4A7AB5"), new("#B54A8E"), new("#E8D5A8") ]; for (int i = 0; i < 40; i++) { float x = (float)rng.NextDouble() * viewport.X; float startY = -20f; float endY = viewport.Y + 50f; float size = 4f + (float)rng.NextDouble() * 6f; float duration = 1.5f + (float)rng.NextDouble() * 1.5f; float delay = (float)rng.NextDouble() * 0.5f; float drift = ((float)rng.NextDouble() - 0.5f) * 80f; var confetti = new ColorRect { Size = new Vector2(size, size * 0.5f), Color = confettiColors[rng.Next(confettiColors.Length)], MouseFilter = Control.MouseFilterEnum.Ignore }; // Confetti needs to be in screen space (CanvasLayer) var parent = GetTree().Root; var cNode = new Node2D { Position = new Vector2(x, startY) }; cNode.AddChild(confetti); parent.AddChild(cNode); var ct = cNode.CreateTween(); ct.TweenInterval(delay); ct.SetParallel(true); ct.TweenProperty(cNode, "position", new Vector2(x + drift, endY), duration) .SetEase(Tween.EaseType.In).SetTrans(Tween.TransitionType.Sine); ct.TweenProperty(cNode, "rotation", (float)rng.NextDouble() * Mathf.Pi * 4f, duration); ct.SetParallel(false); ct.TweenCallback(Callable.From(() => cNode.QueueFree())); } } private static Color GetCargoColor(CargoType type) => type switch { CargoType.Wood => WoodCargoColor, CargoType.Stone => StoneCargoColor, CargoType.Tools => ToolsCargoColor, CargoType.Arms => ArmsCargoColor, CargoType.Gold => GoldCargoColor, _ => Colors.White }; 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(); } }