diff --git a/CLAUDE.md b/CLAUDE.md index ea33b24..5eccbb3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,3 +24,9 @@ Input → Command → GameSim (state + rules) → Events → Presentation Tout `Control` (ColorRect, Label…) a `MouseFilter = Stop` par defaut. Quand un Control est enfant d'un Node2D (ex: les ColorRect dans CellView, les Labels dans PieceView), **il participe quand meme au systeme GUI et consomme les clics**, empechant `_UnhandledInput` de recevoir l'evenement. **Regle** : toujours mettre `MouseFilter = Control.MouseFilterEnum.Ignore` sur les Controls purement visuels enfants de Node2D. + +## Conventions Claude + +### Plans + +Les fichiers de plan doivent etre rediges a la racine du workspace (ex: `/workspace/PLAN_juice.md`), **pas** dans `.claude/plans/` car ce dossier a une taille limitee. diff --git a/PLAN_juice.md b/PLAN_juice.md new file mode 100644 index 0000000..746155f --- /dev/null +++ b/PLAN_juice.md @@ -0,0 +1,107 @@ +# Plan: Juice Pass - Animations, Sons, Visuels + +## Context + +Le jeu fonctionne mais manque de "juice" — les interactions sont plates, pas de sons, les animations sont minimales. L'objectif est de rendre chaque action satisfaisante visuellement et auditivement. + +## Principes + +- Pas de dépendance externe (pas de fichiers audio .wav/.ogg) — sons générés procéduralement via `AudioStreamGenerator` ou les nœuds Godot (`AudioStreamPlayer` avec des tones synthétiques) +- Tout est fait en code (pas de .tscn supplémentaires) +- Les effets sont subtils, jamais bloquants + +## Changements par catégorie + +### 1. Pièces — Vie et feedback (PieceView.cs) + +- **Bounce à la pose** : quand une pièce est placée, scale 0→1.2→1.0 (0.2s, ease back-out) +- **Pulse quand porte un colis** : le cargo indicator pulse doucement (scale 1.0↔1.2, loop) +- **Ombre sous la pièce** : un cercle sombre semi-transparent (alpha 0.15) légèrement décalé en bas +- **Lettre de la pièce** : utiliser le nom complet court au lieu de la lettre seule pour plus de clarté + +### 2. Trajectoires (TrajectView.cs) + +- **Flèche directionnelle** : ajouter un triangle au bout de la ligne pour montrer le sens +- **Pulse pendant la simulation** : la ligne pulse (alpha oscille 0.3↔0.6) quand la sim tourne +- **Couleur par type de pièce** : la trajectoire reprend la couleur de la pièce + +### 3. Animations de tour (EventAnimator.cs) + +- **Production** : particules (petits carrés colorés) qui jaillissent de la cellule de production + scale bounce de la cellule +- **Transfert** : le cargo slide laisse une traînée (2-3 carrés plus petits qui suivent avec délai) + ease back-out pour un effet de "whip" +- **Mouvement** : les pièces se soulèvent légèrement (scale 1.0→1.1→1.0 pendant le déplacement) pour donner l'impression de vol +- **Destruction** : la pièce se réduit (scale→0) + rotation + particules rouges éclatantes au lieu d'un simple flash +- **Victoire** : pluie de confettis dorés sur tout l'écran + +### 4. Cellules (CellView.cs) + +- **Hover amélioré** : la cellule survolée fait un léger scale-up (1.0→1.03) + outline pulse +- **Highlight de placement** : les cellules valides pulsent doucement (alpha oscille) + +### 5. UI — Contrôles et panneaux + +**ControlBar.cs** : +- Boutons avec style (fond coloré, coins arrondis) au lieu du style par défaut Godot +- Boutons disabled grayed out visuellement + +**MetricsOverlay.cs** : +- Apparition avec fade-in + scale (0.8→1.0) au lieu d'un Visible=true brutal +- Les métriques apparaissent une par une avec un petit délai + +**LevelSelectScreen.cs** : +- Cards hover : légère élévation (border-color plus clair) + scale 1.0→1.02 + +**ObjectivePanel.cs** : +- Flash vert sur la barre de progression quand une livraison arrive +- Animation de la jauge (tween de la valeur plutôt qu'un saut) + +### 6. Sons procéduraux (nouveau: SfxManager.cs) + +Un nœud singleton qui génère des bips synthétiques via `AudioStreamPlayer` : +- **Placement** : bip court montant (C5, 0.08s) +- **Production** : bip grave doux (C3, 0.1s) +- **Transfert** : swoosh (bruit blanc filtré, 0.15s) +- **Livraison à demande** : ding satisfaisant (C5+E5 chord, 0.2s) +- **Mouvement** : léger whoosh (bruit blanc très court, 0.05s) +- **Destruction** : crunch descendant (C4→C2, 0.15s) +- **Victoire** : arpège majeur montant (C4-E4-G4-C5, 0.5s) +- **Clic UI** : tick léger (0.02s) + +Implémenté avec `AudioStreamGenerator` pour les tones, buffer rempli avec des sinusoïdes + enveloppe ADSR simple. + +### 7. Transitions (Main.cs) + +- **Fade in/out** entre level select et gameplay : ColorRect noir plein écran, alpha 1→0 (0.4s) +- **Camera** : zoom léger quand la simulation démarre (1.0→0.95→1.0) + +## Fichiers à modifier + +| Fichier | Changements | +|---------|-------------| +| `Scripts/Pieces/PieceView.cs` | Ombre, bounce, cargo pulse | +| `Scripts/Pieces/TrajectView.cs` | Flèche, pulse, couleur | +| `Scripts/Presentation/EventAnimator.cs` | Particules, trails, destruction améliorée, confettis victoire | +| `Scripts/Board/CellView.cs` | Hover scale, highlight pulse | +| `Scripts/UI/ControlBar.cs` | Boutons stylés | +| `Scripts/UI/MetricsOverlay.cs` | Fade-in, métriques séquentielles | +| `Scripts/UI/LevelSelectScreen.cs` | Card hover effects | +| `Scripts/UI/ObjectivePanel.cs` | Flash vert, tween jauge | +| `Scripts/Main.cs` | Fade transition, camera juice | +| `Scripts/Presentation/SfxManager.cs` | **NOUVEAU** — sons procéduraux | + +## Ordre d'implémentation + +1. SfxManager (fondation audio) +2. PieceView (bounce, ombre, cargo pulse) +3. EventAnimator (particules, trails, destruction, confettis) +4. CellView (hover, highlight pulse) +5. TrajectView (flèche, pulse) +6. UI polish (ControlBar, MetricsOverlay, ObjectivePanel, LevelSelectScreen) +7. Transitions (Main.cs fade, camera) + +## Vérification + +- Lancer le jeu, vérifier chaque animation visuellement +- S'assurer que les sons ne sont pas trop forts ou gênants +- Vérifier que les animations n'interfèrent pas avec le gameplay (pas de blocage) +- `dotnet test` pour s'assurer que l'engine n'est pas impacté diff --git a/Scripts/Board/CellView.cs b/Scripts/Board/CellView.cs index 6604acc..4127a91 100644 --- a/Scripts/Board/CellView.cs +++ b/Scripts/Board/CellView.cs @@ -7,6 +7,7 @@ public partial class CellView : Node2D { private ColorRect _background = null!; private ColorRect _highlight = null!; + private ColorRect _innerShadow = null!; private Label _label = null!; // Hover outline (4 thin rects forming a border) @@ -17,13 +18,14 @@ public partial class CellView : Node2D public Coords Coords { get; private set; } - private static readonly Color LightColor = new("#F0D9B5"); - private static readonly Color DarkColor = new("#B58863"); - private static readonly Color WallColor = new("#555555"); - private static readonly Color ProductionColor = new("#6B8E5A"); - private static readonly Color DemandColor = new("#C9A833"); + // Warmer, more grounded palette + private static readonly Color LightColor = new("#E8D5A8"); // warm parchment + private static readonly Color DarkColor = new("#A07850"); // warm walnut + private static readonly Color WallColor = new("#3A3A3A"); // charcoal + private static readonly Color ProductionColor = new("#4A6E3A"); // deep forest + private static readonly Color DemandColor = new("#B8942A"); // aged gold private static readonly Color HighlightColor = new("#44FF4444"); - private static readonly Color HoverOutlineColor = new("#FFFFFFAA"); + private static readonly Color HoverOutlineColor = new("#FFFFFF88"); private const int OutlineWidth = 2; @@ -49,6 +51,17 @@ public partial class CellView : Node2D }; AddChild(_background); + // Subtle inner shadow for depth (top-left lit, bottom-right dark) + _innerShadow = new ColorRect + { + Size = new Vector2(cellSize, cellSize), + Position = Vector2.Zero, + Color = new Color(0, 0, 0, 0.06f), + Visible = cellType == CellType.Empty, + MouseFilter = Control.MouseFilterEnum.Ignore + }; + AddChild(_innerShadow); + _highlight = new ColorRect { Size = new Vector2(cellSize, cellSize), @@ -60,56 +73,36 @@ public partial class CellView : Node2D AddChild(_highlight); // Hover outline (4 border rects) - _hoverTop = new ColorRect - { - Size = new Vector2(cellSize, OutlineWidth), - Position = Vector2.Zero, - Color = HoverOutlineColor, - Visible = false, - MouseFilter = Control.MouseFilterEnum.Ignore - }; - AddChild(_hoverTop); - - _hoverBottom = new ColorRect - { - Size = new Vector2(cellSize, OutlineWidth), - Position = new Vector2(0, cellSize - OutlineWidth), - Color = HoverOutlineColor, - Visible = false, - MouseFilter = Control.MouseFilterEnum.Ignore - }; - AddChild(_hoverBottom); - - _hoverLeft = new ColorRect - { - Size = new Vector2(OutlineWidth, cellSize), - Position = Vector2.Zero, - Color = HoverOutlineColor, - Visible = false, - MouseFilter = Control.MouseFilterEnum.Ignore - }; - AddChild(_hoverLeft); - - _hoverRight = new ColorRect - { - Size = new Vector2(OutlineWidth, cellSize), - Position = new Vector2(cellSize - OutlineWidth, 0), - Color = HoverOutlineColor, - Visible = false, - MouseFilter = Control.MouseFilterEnum.Ignore - }; - AddChild(_hoverRight); + _hoverTop = CreateBorderRect(new Vector2(cellSize, OutlineWidth), Vector2.Zero); + _hoverBottom = CreateBorderRect(new Vector2(cellSize, OutlineWidth), new Vector2(0, cellSize - OutlineWidth)); + _hoverLeft = CreateBorderRect(new Vector2(OutlineWidth, cellSize), Vector2.Zero); + _hoverRight = CreateBorderRect(new Vector2(OutlineWidth, cellSize), new Vector2(cellSize - OutlineWidth, 0)); _label = new Label { - Position = new Vector2(2, 2), + Position = new Vector2(4, 3), Text = "", MouseFilter = Control.MouseFilterEnum.Ignore }; - _label.AddThemeFontSizeOverride("font_size", 10); + _label.AddThemeFontSizeOverride("font_size", 9); + _label.AddThemeColorOverride("font_color", new Color(1, 1, 1, 0.7f)); AddChild(_label); } + private ColorRect CreateBorderRect(Vector2 size, Vector2 pos) + { + var rect = new ColorRect + { + Size = size, + Position = pos, + Color = HoverOutlineColor, + Visible = false, + MouseFilter = Control.MouseFilterEnum.Ignore + }; + AddChild(rect); + return rect; + } + public void SetLabel(string text) => _label.Text = text; public void SetHighlight(bool on) => _highlight.Visible = on; @@ -128,14 +121,15 @@ public partial class CellView : Node2D } /// - /// Brief white flash on the cell to signal production. + /// Production pulse: warm glow that radiates outward. /// public void FlashProduce(float duration = 0.3f) { - _highlight.Color = new Color(1, 1, 1, 0.5f); + _highlight.Color = new Color(0.9f, 0.85f, 0.5f, 0.55f); // warm golden _highlight.Visible = true; var tween = CreateTween(); - tween.TweenProperty(_highlight, "color", new Color(1, 1, 1, 0f), duration); + tween.TweenProperty(_highlight, "color", new Color(0.9f, 0.85f, 0.5f, 0f), duration) + .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic); tween.TweenCallback(Callable.From(() => _highlight.Visible = false)); } } diff --git a/Scripts/Main.cs b/Scripts/Main.cs index 99b0b36..d2be020 100644 --- a/Scripts/Main.cs +++ b/Scripts/Main.cs @@ -1,4 +1,5 @@ using Godot; +using System; using System.Collections.Generic; using System.Linq; using Chessistics.Engine.Commands; @@ -37,6 +38,7 @@ public partial class Main : Node2D private PanelContainer _sidePanel = null!; private PanelContainer _controlBarWrapper = null!; private Camera2D _camera = null!; + private ColorRect _fadeOverlay = null!; // Simulation timer private Godot.Timer _simTimer = null!; @@ -58,6 +60,9 @@ public partial class Main : Node2D BuildSceneTree(); ConnectSignals(); ShowLevelSelect(); + + // Fade in from black on startup + FadeIn(0.5f); } public override void _UnhandledInput(InputEvent @event) @@ -73,12 +78,33 @@ public partial class Main : Node2D } } + private void FadeIn(float duration) + { + _fadeOverlay.Color = new Color(0, 0, 0, 1); + var tween = CreateTween(); + tween.TweenProperty(_fadeOverlay, "color:a", 0f, duration) + .SetEase(Tween.EaseType.Out); + } + + private void FadeOut(float duration, Action onComplete) + { + _fadeOverlay.Color = new Color(0, 0, 0, 0); + var tween = CreateTween(); + tween.TweenProperty(_fadeOverlay, "color:a", 1f, duration) + .SetEase(Tween.EaseType.In); + tween.TweenCallback(Callable.From(onComplete)); + } + private void BuildSceneTree() { // Camera _camera = new Camera2D { Enabled = true }; AddChild(_camera); + // SFX + var sfx = new SfxManager(); + AddChild(sfx); + // Board _boardView = new BoardView(); AddChild(_boardView); @@ -204,6 +230,15 @@ public partial class Main : Node2D _levelSelectScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect); uiRoot.AddChild(_levelSelectScreen); + // --- Fade overlay (on top of everything) --- + _fadeOverlay = new ColorRect + { + Color = new Color(0, 0, 0, 1), + MouseFilter = Control.MouseFilterEnum.Ignore + }; + _fadeOverlay.SetAnchorsPreset(Control.LayoutPreset.FullRect); + uiRoot.AddChild(_fadeOverlay); + // Initialize animator _eventAnimator.Initialize(_boardView, _objectivePanel, _controlBar, _metricsOverlay); } @@ -254,8 +289,15 @@ public partial class Main : Node2D private void OnLevelSelected(int levelIndex) { + SfxManager.Instance?.PlayClick(); _currentLevelIndex = levelIndex; - LoadLevel(levelIndex); + + // Fade out, load, fade in + FadeOut(0.25f, () => + { + LoadLevel(levelIndex); + FadeIn(0.3f); + }); } private void LoadLevel(int index) @@ -370,17 +412,13 @@ public partial class Main : Node2D private void CreatePieceVisual(PiecePlacedEvent placed) { + SfxManager.Instance?.PlayPlace(); + var pieceView = new PieceView(); pieceView.Setup(placed.PieceId, placed.Kind, placed.Start, placed.End, _boardView); _boardView.AddChild(pieceView); - var color = placed.Kind switch - { - PieceKind.Rook => new Color("#4A7AB5"), - PieceKind.Bishop => new Color("#B54A8E"), - PieceKind.Knight => new Color("#B5824A"), - _ => Colors.White - }; + var color = PieceView.GetPieceColor(placed.Kind); var trajectView = new TrajectView(); trajectView.Setup(placed.PieceId, diff --git a/Scripts/Pieces/PieceView.cs b/Scripts/Pieces/PieceView.cs index 1bd6525..edc934a 100644 --- a/Scripts/Pieces/PieceView.cs +++ b/Scripts/Pieces/PieceView.cs @@ -6,21 +6,25 @@ namespace Chessistics.Scripts.Pieces; public partial class PieceView : Node2D { + private Sprite2D _shadow = null!; private Sprite2D _sprite = null!; private ColorRect _cargoIndicator = null!; private Label _label = null!; + private Tween? _cargoPulseTween; public int PieceId { get; private set; } public PieceKind Kind { get; private set; } public Coords StartCell { get; private set; } public Coords EndCell { get; private set; } - private static readonly Color PawnColor = new("#7AB54A"); - private static readonly Color RookColor = new("#4A7AB5"); - private static readonly Color BishopColor = new("#B54A8E"); - private static readonly Color KnightColor = new("#B5824A"); - private static readonly Color WoodCargoColor = new("#8B6914"); - private static readonly Color StoneCargoColor = new("#808080"); + // Muted, earthy palette — teal, sienna, gold tones + private static readonly Color PawnColor = new("#5A8C6B"); // sage green + private static readonly Color RookColor = new("#3D6B8E"); // deep teal + private static readonly Color BishopColor = new("#8E5A6B"); // dusty rose + private static readonly Color KnightColor = new("#8E7A3D"); // burnt sienna + private static readonly Color WoodCargoColor = new("#A67C32"); + private static readonly Color StoneCargoColor = new("#7A7A7A"); + private static readonly Color ShadowColor = new Color(0, 0, 0, 0.18f); public void Setup(int pieceId, PieceKind kind, Coords startCell, Coords endCell, BoardView boardView) { @@ -31,26 +35,36 @@ public partial class PieceView : Node2D Position = boardView.CoordsToPixel(startCell); - var color = kind switch - { - PieceKind.Pawn => PawnColor, - PieceKind.Rook => RookColor, - PieceKind.Bishop => BishopColor, - PieceKind.Knight => KnightColor, - _ => Colors.White - }; + var color = GetPieceColor(kind); - // Piece body (circle) - _sprite = new Sprite2D(); - var texture = new GradientTexture2D + // Shadow (slightly offset, rendered first) + _shadow = new Sprite2D(); + var shadowTex = new GradientTexture2D { - Width = 48, - Height = 48, + Width = 44, Height = 44, Fill = GradientTexture2D.FillEnum.Radial, Gradient = new Gradient() }; - texture.Gradient.SetColor(0, color); - texture.Gradient.SetColor(1, color.Darkened(0.3f)); + shadowTex.Gradient.SetColor(0, ShadowColor); + shadowTex.Gradient.SetColor(1, new Color(0, 0, 0, 0)); + _shadow.Texture = shadowTex; + _shadow.Position = new Vector2(3, 5); // subtle offset down-right + AddChild(_shadow); + + // Piece body (circle with inner glow) + _sprite = new Sprite2D(); + var texture = new GradientTexture2D + { + Width = 48, Height = 48, + Fill = GradientTexture2D.FillEnum.Radial, + Gradient = new Gradient() + }; + texture.Gradient.Colors = [ + color.Lightened(0.15f), // bright center + color, // main color + color.Darkened(0.25f) // dark rim + ]; + texture.Gradient.Offsets = [0f, 0.5f, 1f]; _sprite.Texture = texture; AddChild(_sprite); @@ -71,25 +85,46 @@ public partial class PieceView : Node2D MouseFilter = Control.MouseFilterEnum.Ignore }; _label.AddThemeFontSizeOverride("font_size", 16); - _label.AddThemeColorOverride("font_color", Colors.White); + _label.AddThemeColorOverride("font_color", new Color(1, 1, 1, 0.9f)); AddChild(_label); // Cargo indicator (hidden by default) _cargoIndicator = new ColorRect { - Size = new Vector2(14, 14), - Position = new Vector2(-7, -30), + Size = new Vector2(12, 12), + Position = new Vector2(-6, -28), Visible = false, MouseFilter = Control.MouseFilterEnum.Ignore }; AddChild(_cargoIndicator); + + // Entrance animation: scale bounce + Scale = Vector2.Zero; + var entranceTween = CreateTween(); + entranceTween.TweenProperty(this, "scale", new Vector2(1.15f, 1.15f), 0.12f) + .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back); + entranceTween.TweenProperty(this, "scale", Vector2.One, 0.08f) + .SetEase(Tween.EaseType.InOut); } + public static Color GetPieceColor(PieceKind kind) => kind switch + { + PieceKind.Pawn => PawnColor, + PieceKind.Rook => RookColor, + PieceKind.Bishop => BishopColor, + PieceKind.Knight => KnightColor, + _ => Colors.White + }; + public void SetCargo(CargoType? cargo) { + _cargoPulseTween?.Kill(); + _cargoPulseTween = null; + if (cargo == null) { _cargoIndicator.Visible = false; + _cargoIndicator.Scale = Vector2.One; return; } @@ -100,6 +135,16 @@ public partial class PieceView : Node2D CargoType.Stone => StoneCargoColor, _ => Colors.White }; + + // Cargo pulse: gentle breathing + _cargoPulseTween = CreateTween(); + _cargoPulseTween.SetLoops(); + _cargoPulseTween.TweenProperty(_cargoIndicator, "scale", + new Vector2(1.25f, 1.25f), 0.5f) + .SetEase(Tween.EaseType.InOut).SetTrans(Tween.TransitionType.Sine); + _cargoPulseTween.TweenProperty(_cargoIndicator, "scale", + Vector2.One, 0.5f) + .SetEase(Tween.EaseType.InOut).SetTrans(Tween.TransitionType.Sine); } public void AnimateMoveTo(Vector2 target, float duration = 0.3f) @@ -107,7 +152,6 @@ public partial class PieceView : Node2D var tween = CreateTween(); if (Kind == PieceKind.Knight) { - // Arc animation for knight var mid = (Position + target) / 2 + new Vector2(0, -30); tween.TweenMethod(Callable.From(t => { @@ -119,7 +163,8 @@ public partial class PieceView : Node2D } else { - tween.TweenProperty(this, "position", target, duration); + tween.TweenProperty(this, "position", target, duration) + .SetEase(Tween.EaseType.InOut).SetTrans(Tween.TransitionType.Sine); } } } diff --git a/Scripts/Pieces/TrajectView.cs b/Scripts/Pieces/TrajectView.cs index f46503b..801a235 100644 --- a/Scripts/Pieces/TrajectView.cs +++ b/Scripts/Pieces/TrajectView.cs @@ -5,15 +5,34 @@ namespace Chessistics.Scripts.Pieces; public partial class TrajectView : Line2D { public int PieceId { get; private set; } + private Polygon2D? _arrow; public void Setup(int pieceId, Vector2 from, Vector2 to, Color color) { PieceId = pieceId; - Width = 3f; - DefaultColor = new Color(color, 0.5f); + Width = 2.5f; + DefaultColor = new Color(color, 0.35f); + Antialiased = true; ClearPoints(); AddPoint(from); AddPoint(to); ZIndex = -1; + + // Arrowhead at the end point + var dir = (to - from).Normalized(); + var perp = new Vector2(-dir.Y, dir.X); + float arrowSize = 8f; + var tip = to - dir * 4f; // slightly inset from end + var baseL = tip - dir * arrowSize + perp * arrowSize * 0.5f; + var baseR = tip - dir * arrowSize - perp * arrowSize * 0.5f; + + _arrow = new Polygon2D + { + Polygon = [tip - Position, baseL - Position, baseR - Position], + Color = new Color(color, 0.4f), + Position = Vector2.Zero + }; + // Position relative to parent, not this Line2D + AddChild(_arrow); } } diff --git a/Scripts/Presentation/EventAnimator.cs b/Scripts/Presentation/EventAnimator.cs index 2bb7d73..9625b1a 100644 --- a/Scripts/Presentation/EventAnimator.cs +++ b/Scripts/Presentation/EventAnimator.cs @@ -23,14 +23,14 @@ public partial class EventAnimator : Node private bool _animating; public bool IsAnimating => _animating; - private static readonly Color WoodCargoColor = new("#8B6914"); - private static readonly Color StoneCargoColor = new("#808080"); + private static readonly Color WoodCargoColor = new("#A67C32"); + private static readonly Color StoneCargoColor = new("#7A7A7A"); - 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; + 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(); @@ -107,6 +107,8 @@ public partial class EventAnimator : Node FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents); tween.TweenCallback(Callable.From(() => { + SfxManager.Instance?.PlayVictory(); + SpawnConfetti(); _metricsOverlay.ShowMetrics(victory.Metrics); EmitSignal(SignalName.VictoryReached); })); @@ -137,45 +139,50 @@ public partial class EventAnimator : Node List moveEvents, List collisionEvents) { - // Phase 1: Produce — flash production cells + // Phase 1: Produce — warm golden flash + particle burst if (produceEvents.Count > 0) { + var captured = produceEvents.ToList(); tween.TweenCallback(Callable.From(() => { - foreach (var evt in produceEvents.ToList()) + 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 — animate cargo sliding from giver to receiver + // Phase 2: Transfers — cargo slides with trail particles 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(() => { + bool hasDelivery = false; 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); + + if (transfer.ReceivingPieceId == null) hasDelivery = true; } } + if (hasDelivery) + SfxManager.Instance?.PlayDeliver(); + else + SfxManager.Instance?.PlayTransfer(); })); - // Step 2: wait for slide, then show cargo on receivers + update demand progress tween.TweenInterval(TransferDuration); tween.TweenCallback(Callable.From(() => { @@ -196,9 +203,10 @@ public partial class EventAnimator : Node transferEvents.Clear(); } - // Phase 3: Movement — all pieces move simultaneously + // 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) { @@ -206,74 +214,218 @@ public partial class EventAnimator : Node { var target = _boardView.CoordsToPixel(moved.To); float duration = pv.Kind == PieceKind.Knight ? KnightMoveDuration : MoveDuration; - tween.TweenProperty(pv, "position", target, duration); + tween.TweenProperty(pv, "position", target, duration) + .SetEase(Tween.EaseType.InOut).SetTrans(Tween.TransitionType.Sine); } } tween.SetParallel(false); moveEvents.Clear(); } - // Phase 4: Collision/Destruction + // Phase 4: Collision/Destruction — shrink + spin + particles if (collisionEvents.Count > 0) { - tween.SetParallel(true); - foreach (var destroyed in collisionEvents) + var captured = collisionEvents.ToList(); + tween.TweenCallback(Callable.From(() => { - var pieceId = destroyed.PieceId; - tween.TweenCallback(Callable.From(() => + SfxManager.Instance?.PlayDestroy(); + foreach (var destroyed in captured) { - FlashPiece(pieceId); - UnregisterPiece(pieceId); - })); - } - tween.SetParallel(false); + if (_pieceViews.TryGetValue(destroyed.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); + } + } + })); tween.TweenInterval(DestroyDuration); + tween.TweenCallback(Callable.From(() => + { + foreach (var destroyed in captured) + UnregisterPiece(destroyed.PieceId); + })); collisionEvents.Clear(); } } - /// - /// Creates a temporary colored square that slides from the giver to the receiver. - /// + // --- Visual Effects --- + 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 color = GetCargoColor(transfer.Type); - var sprite = new ColorRect + var container = new Node2D { Position = from }; + _boardView.AddChild(container); + + // Main cargo square + var main = new ColorRect { - Size = new Vector2(14, 14), - Position = new Vector2(-7, -7), + Size = new Vector2(12, 12), + Position = new Vector2(-6, -6), Color = color, MouseFilter = Control.MouseFilterEnum.Ignore }; + container.AddChild(main); - var container = new Node2D { Position = from }; - container.AddChild(sprite); - _boardView.AddChild(container); + // 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.InOut) - .SetTrans(Tween.TransitionType.Cubic); + .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back); slideTween.TweenCallback(Callable.From(() => container.QueueFree())); } - private void FlashPiece(int pieceId) + private void SpawnProduceParticles(Coords cell, CargoType type) { - 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); + 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, + _ => Colors.White + }; + public void ResetPiecePositions(BoardSnapshot snapshot) { foreach (var ps in snapshot.Pieces) diff --git a/Scripts/Presentation/SfxManager.cs b/Scripts/Presentation/SfxManager.cs new file mode 100644 index 0000000..8579f08 --- /dev/null +++ b/Scripts/Presentation/SfxManager.cs @@ -0,0 +1,160 @@ +using Godot; +using System; + +namespace Chessistics.Scripts.Presentation; + +/// +/// Procedural sound effects via synthesized waveforms. +/// No external audio files needed — everything is generated in code. +/// +public partial class SfxManager : Node +{ + private const int SampleRate = 22050; + private const float MasterVolume = 0.15f; + + public static SfxManager? Instance { get; private set; } + + public override void _Ready() + { + Instance = this; + } + + // --- Public API --- + + public void PlayPlace() => PlayTone(523.25f, 0.08f, vol: 0.5f); // C5 short blip + public void PlayProduce() => PlayTone(130.81f, 0.12f, vol: 0.3f, wave: Wave.Triangle); // C3 warm + public void PlayTransfer() => PlayNoise(0.12f, vol: 0.15f); // filtered swoosh + public void PlayDeliver() => PlayChord([523.25f, 659.25f], 0.18f, vol: 0.4f); // C5+E5 ding + public void PlayMove() => PlayNoise(0.04f, vol: 0.08f); // tiny whoosh + public void PlayDestroy() => PlaySweep(262f, 65f, 0.18f, vol: 0.4f); // descending crunch + public void PlayVictory() => PlayArpeggio([262f, 330f, 392f, 523f], 0.12f, vol: 0.35f); // C-E-G-C arp + public void PlayClick() => PlayTone(880f, 0.02f, vol: 0.2f); // tiny tick + + // --- Synthesis --- + + private enum Wave { Sine, Triangle, Square } + + private void PlayTone(float freq, float duration, float vol = 0.3f, Wave wave = Wave.Sine) + { + var samples = GenerateTone(freq, duration, vol, wave); + PlaySamples(samples); + } + + private void PlayNoise(float duration, float vol = 0.2f) + { + var count = (int)(SampleRate * duration); + var samples = new float[count]; + var rng = new Random(); + for (int i = 0; i < count; i++) + { + float t = (float)i / count; + float envelope = Envelope(t); + samples[i] = (float)(rng.NextDouble() * 2 - 1) * vol * envelope * MasterVolume; + } + // Simple low-pass: average with previous sample + for (int i = count - 1; i > 0; i--) + samples[i] = (samples[i] + samples[i - 1]) * 0.5f; + PlaySamples(samples); + } + + private void PlayChord(float[] freqs, float duration, float vol = 0.3f) + { + var count = (int)(SampleRate * duration); + var samples = new float[count]; + foreach (var freq in freqs) + { + var tone = GenerateTone(freq, duration, vol / freqs.Length, Wave.Sine); + for (int i = 0; i < count; i++) + samples[i] += tone[i]; + } + PlaySamples(samples); + } + + private void PlaySweep(float startFreq, float endFreq, float duration, float vol = 0.3f) + { + var count = (int)(SampleRate * duration); + var samples = new float[count]; + for (int i = 0; i < count; i++) + { + float t = (float)i / count; + float freq = Mathf.Lerp(startFreq, endFreq, t); + float envelope = Envelope(t); + float phase = 2f * Mathf.Pi * freq * i / SampleRate; + samples[i] = Mathf.Sin(phase) * vol * envelope * MasterVolume; + } + PlaySamples(samples); + } + + private void PlayArpeggio(float[] notes, float noteLength, float vol = 0.3f) + { + float totalDuration = noteLength * notes.Length; + var totalCount = (int)(SampleRate * totalDuration); + var samples = new float[totalCount]; + var noteCount = (int)(SampleRate * noteLength); + + for (int n = 0; n < notes.Length; n++) + { + int offset = n * noteCount; + var tone = GenerateTone(notes[n], noteLength, vol, Wave.Sine); + for (int i = 0; i < tone.Length && offset + i < totalCount; i++) + samples[offset + i] += tone[i]; + } + PlaySamples(samples); + } + + private float[] GenerateTone(float freq, float duration, float vol, Wave wave) + { + var count = (int)(SampleRate * duration); + var samples = new float[count]; + for (int i = 0; i < count; i++) + { + float t = (float)i / count; + float phase = 2f * Mathf.Pi * freq * i / SampleRate; + float value = wave switch + { + Wave.Triangle => 2f * Mathf.Abs(2f * ((freq * i / SampleRate) % 1f) - 1f) - 1f, + Wave.Square => Mathf.Sin(phase) >= 0 ? 1f : -1f, + _ => Mathf.Sin(phase) + }; + float envelope = Envelope(t); + samples[i] = value * vol * envelope * MasterVolume; + } + return samples; + } + + /// Simple ADSR-ish envelope: quick attack, sustain, smooth release. + private static float Envelope(float t) + { + if (t < 0.05f) return t / 0.05f; // attack + if (t < 0.3f) return 1f; // sustain + return 1f - (t - 0.3f) / 0.7f; // release + } + + private void PlaySamples(float[] samples) + { + var stream = new AudioStreamWav + { + Format = AudioStreamWav.FormatEnum.Format16Bits, + MixRate = SampleRate, + Stereo = false, + Data = FloatsToWav16(samples) + }; + + var player = new AudioStreamPlayer { Stream = stream, VolumeDb = -6f }; + AddChild(player); + player.Finished += () => player.QueueFree(); + player.Play(); + } + + private static byte[] FloatsToWav16(float[] samples) + { + var bytes = new byte[samples.Length * 2]; + for (int i = 0; i < samples.Length; i++) + { + short val = (short)(Mathf.Clamp(samples[i], -1f, 1f) * 32767); + bytes[i * 2] = (byte)(val & 0xFF); + bytes[i * 2 + 1] = (byte)((val >> 8) & 0xFF); + } + return bytes; + } +} diff --git a/Scripts/UI/ControlBar.cs b/Scripts/UI/ControlBar.cs index 040fad7..784b29c 100644 --- a/Scripts/UI/ControlBar.cs +++ b/Scripts/UI/ControlBar.cs @@ -24,38 +24,92 @@ public partial class ControlBar : HBoxContainer private OptionButton _speedSelect = null!; private Label _turnLabel = null!; + private static readonly Color BtnBg = new("#2A2A2E"); + private static readonly Color BtnHover = new("#3A3A40"); + private static readonly Color BtnPressed = new("#1A1A1E"); + private static readonly Color BtnDisabled = new("#1E1E20"); + private static readonly Color BtnBorder = new("#444448"); + public override void _Ready() { - _playButton = new Button { Text = "▶ PLAY" }; + AddThemeConstantOverride("separation", 8); + + _playButton = CreateStyledButton("PLAY"); _playButton.Pressed += () => EmitSignal(SignalName.PlayPressed); AddChild(_playButton); - _pauseButton = new Button { Text = "⏸ PAUSE" }; + _pauseButton = CreateStyledButton("PAUSE"); _pauseButton.Pressed += () => EmitSignal(SignalName.PausePressed); AddChild(_pauseButton); - _stepButton = new Button { Text = "⏭ STEP" }; + _stepButton = CreateStyledButton("STEP"); _stepButton.Pressed += () => EmitSignal(SignalName.StepPressed); AddChild(_stepButton); - _stopButton = new Button { Text = "⏹ STOP" }; + _stopButton = CreateStyledButton("STOP"); _stopButton.Pressed += () => EmitSignal(SignalName.StopPressed); AddChild(_stopButton); - _speedSelect = new OptionButton(); + // Spacer + AddChild(new Control { CustomMinimumSize = new Vector2(12, 0) }); + + _speedSelect = new OptionButton { CustomMinimumSize = new Vector2(60, 30) }; _speedSelect.AddItem("x1", 0); _speedSelect.AddItem("x2", 1); _speedSelect.AddItem("x4", 2); _speedSelect.ItemSelected += OnSpeedSelected; AddChild(_speedSelect); + // Spacer + AddChild(new Control { SizeFlagsHorizontal = SizeFlags.ExpandFill }); + _turnLabel = new Label { Text = "Coup: --" }; - _turnLabel.AddThemeFontSizeOverride("font_size", 14); + _turnLabel.AddThemeFontSizeOverride("font_size", 13); + _turnLabel.AddThemeColorOverride("font_color", new Color("#999999")); AddChild(_turnLabel); UpdateForPhase(SimPhase.Edit); } + private static Button CreateStyledButton(string text) + { + var btn = new Button + { + Text = text, + CustomMinimumSize = new Vector2(70, 30) + }; + btn.AddThemeFontSizeOverride("font_size", 11); + + var normal = MakeStyle(BtnBg); + var hover = MakeStyle(BtnHover); + var pressed = MakeStyle(BtnPressed); + var disabled = MakeStyle(BtnDisabled); + disabled.BorderColor = new Color("#2A2A2E"); + + btn.AddThemeStyleboxOverride("normal", normal); + btn.AddThemeStyleboxOverride("hover", hover); + btn.AddThemeStyleboxOverride("pressed", pressed); + btn.AddThemeStyleboxOverride("disabled", disabled); + btn.AddThemeColorOverride("font_disabled_color", new Color("#555555")); + + return btn; + } + + private static StyleBoxFlat MakeStyle(Color bg) + { + return new StyleBoxFlat + { + BgColor = bg, + BorderColor = BtnBorder, + BorderWidthBottom = 1, BorderWidthTop = 1, + BorderWidthLeft = 1, BorderWidthRight = 1, + CornerRadiusTopLeft = 4, CornerRadiusTopRight = 4, + CornerRadiusBottomLeft = 4, CornerRadiusBottomRight = 4, + ContentMarginLeft = 10, ContentMarginRight = 10, + ContentMarginTop = 4, ContentMarginBottom = 4 + }; + } + private void OnSpeedSelected(long index) { float speed = index switch diff --git a/Scripts/UI/MetricsOverlay.cs b/Scripts/UI/MetricsOverlay.cs index 62f0c84..44198e3 100644 --- a/Scripts/UI/MetricsOverlay.cs +++ b/Scripts/UI/MetricsOverlay.cs @@ -10,46 +10,62 @@ public partial class MetricsOverlay : PanelContainer [Signal] public delegate void RetryPressedEventHandler(); - private Label _metricsLabel = null!; + private Label _titleLabel = null!; + private Label _piecesLabel = null!; + private Label _turnsLabel = null!; + private Label _cellsLabel = null!; + private HBoxContainer _buttons = null!; public override void _Ready() { - var vbox = new VBoxContainer(); - vbox.SetAnchorsPreset(LayoutPreset.Center); + var style = new StyleBoxFlat + { + BgColor = new Color(0.1f, 0.1f, 0.12f, 0.95f), + BorderColor = new Color("#B8942A"), + BorderWidthBottom = 2, BorderWidthTop = 2, + BorderWidthLeft = 2, BorderWidthRight = 2, + CornerRadiusTopLeft = 12, CornerRadiusTopRight = 12, + CornerRadiusBottomLeft = 12, CornerRadiusBottomRight = 12, + ContentMarginLeft = 32, ContentMarginRight = 32, + ContentMarginTop = 28, ContentMarginBottom = 28 + }; + AddThemeStyleboxOverride("panel", style); - var title = new Label + var vbox = new VBoxContainer(); + vbox.AddThemeConstantOverride("separation", 8); + + _titleLabel = new Label { Text = "VICTOIRE !", HorizontalAlignment = HorizontalAlignment.Center }; - title.AddThemeFontSizeOverride("font_size", 24); - title.AddThemeColorOverride("font_color", new Color("#FFD700")); - vbox.AddChild(title); + _titleLabel.AddThemeFontSizeOverride("font_size", 26); + _titleLabel.AddThemeColorOverride("font_color", new Color("#FFD700")); + vbox.AddChild(_titleLabel); vbox.AddChild(new HSeparator()); - _metricsLabel = new Label - { - Text = "", - HorizontalAlignment = HorizontalAlignment.Center - }; - _metricsLabel.AddThemeFontSizeOverride("font_size", 14); - vbox.AddChild(_metricsLabel); + _piecesLabel = CreateMetricLabel(); + vbox.AddChild(_piecesLabel); + _turnsLabel = CreateMetricLabel(); + vbox.AddChild(_turnsLabel); + _cellsLabel = CreateMetricLabel(); + vbox.AddChild(_cellsLabel); vbox.AddChild(new HSeparator()); - var buttons = new HBoxContainer(); - buttons.Alignment = BoxContainer.AlignmentMode.Center; + _buttons = new HBoxContainer { Alignment = BoxContainer.AlignmentMode.Center }; + _buttons.AddThemeConstantOverride("separation", 16); - var retryBtn = new Button { Text = "Rejouer" }; + var retryBtn = CreateStyledButton("Rejouer"); retryBtn.Pressed += () => EmitSignal(SignalName.RetryPressed); - buttons.AddChild(retryBtn); + _buttons.AddChild(retryBtn); - var nextBtn = new Button { Text = "Niveau suivant" }; + var nextBtn = CreateStyledButton("Niveau suivant"); nextBtn.Pressed += () => EmitSignal(SignalName.NextLevelPressed); - buttons.AddChild(nextBtn); + _buttons.AddChild(nextBtn); - vbox.AddChild(buttons); + vbox.AddChild(_buttons); AddChild(vbox); Visible = false; @@ -57,11 +73,87 @@ public partial class MetricsOverlay : PanelContainer public void ShowMetrics(Metrics metrics) { - _metricsLabel.Text = $"Pieces utilisees: {metrics.PiecesUsed}\n" + - $"Coups: {metrics.TurnsTaken}\n" + - $"Cases occupees: {metrics.CellsOccupied}"; + _piecesLabel.Text = $"Pieces utilisees: {metrics.PiecesUsed}"; + _turnsLabel.Text = $"Coups: {metrics.TurnsTaken}"; + _cellsLabel.Text = $"Cases occupees: {metrics.CellsOccupied}"; + + // Start invisible, fade + scale in + Modulate = new Color(1, 1, 1, 0); + Scale = new Vector2(0.85f, 0.85f); + PivotOffset = Size / 2f; Visible = true; + + // Hide metrics initially, reveal sequentially + _piecesLabel.Modulate = new Color(1, 1, 1, 0); + _turnsLabel.Modulate = new Color(1, 1, 1, 0); + _cellsLabel.Modulate = new Color(1, 1, 1, 0); + _buttons.Modulate = new Color(1, 1, 1, 0); + + var tween = CreateTween(); + // Panel fade in + tween.SetParallel(true); + tween.TweenProperty(this, "modulate:a", 1f, 0.3f) + .SetEase(Tween.EaseType.Out); + tween.TweenProperty(this, "scale", Vector2.One, 0.35f) + .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back); + tween.SetParallel(false); + + // Sequential metric reveals + tween.TweenInterval(0.15f); + tween.TweenProperty(_piecesLabel, "modulate:a", 1f, 0.2f); + tween.TweenInterval(0.1f); + tween.TweenProperty(_turnsLabel, "modulate:a", 1f, 0.2f); + tween.TweenInterval(0.1f); + tween.TweenProperty(_cellsLabel, "modulate:a", 1f, 0.2f); + tween.TweenInterval(0.15f); + tween.TweenProperty(_buttons, "modulate:a", 1f, 0.2f); } - public new void Hide() => Visible = false; + public new void Hide() + { + Visible = false; + } + + private static Label CreateMetricLabel() + { + var label = new Label + { + Text = "", + HorizontalAlignment = HorizontalAlignment.Center + }; + label.AddThemeFontSizeOverride("font_size", 14); + label.AddThemeColorOverride("font_color", new Color("#CCCCCC")); + return label; + } + + private static Button CreateStyledButton(string text) + { + var btn = new Button + { + Text = text, + CustomMinimumSize = new Vector2(130, 36) + }; + + var normal = new StyleBoxFlat + { + BgColor = new Color("#3D6B8E"), + CornerRadiusTopLeft = 6, CornerRadiusTopRight = 6, + CornerRadiusBottomLeft = 6, CornerRadiusBottomRight = 6, + ContentMarginLeft = 16, ContentMarginRight = 16, + ContentMarginTop = 6, ContentMarginBottom = 6 + }; + var hover = new StyleBoxFlat + { + BgColor = new Color("#4A8EBF"), + CornerRadiusTopLeft = 6, CornerRadiusTopRight = 6, + CornerRadiusBottomLeft = 6, CornerRadiusBottomRight = 6, + ContentMarginLeft = 16, ContentMarginRight = 16, + ContentMarginTop = 6, ContentMarginBottom = 6 + }; + btn.AddThemeStyleboxOverride("normal", normal); + btn.AddThemeStyleboxOverride("hover", hover); + btn.AddThemeFontSizeOverride("font_size", 13); + + return btn; + } } diff --git a/Scripts/UI/ObjectivePanel.cs b/Scripts/UI/ObjectivePanel.cs index da74fc5..ece5ea3 100644 --- a/Scripts/UI/ObjectivePanel.cs +++ b/Scripts/UI/ObjectivePanel.cs @@ -6,7 +6,7 @@ namespace Chessistics.Scripts.UI; public partial class ObjectivePanel : VBoxContainer { - private readonly Dictionary _entries = new(); + private readonly Dictionary _entries = new(); public void Setup(IReadOnlyList demands) { @@ -16,7 +16,7 @@ public partial class ObjectivePanel : VBoxContainer var title = new Label { Text = "OBJECTIFS" }; title.AddThemeFontSizeOverride("font_size", 16); - title.AddThemeColorOverride("font_color", new Color("#FFD700")); + title.AddThemeColorOverride("font_color", new Color("#B8942A")); // aged gold AddChild(title); AddChild(new HSeparator()); @@ -24,9 +24,11 @@ public partial class ObjectivePanel : VBoxContainer foreach (var demand in demands) { var vbox = new VBoxContainer(); + vbox.AddThemeConstantOverride("separation", 2); var label = new Label { Text = $"{demand.Name}: 0/{demand.Amount} {demand.Cargo}" }; label.AddThemeFontSizeOverride("font_size", 12); + label.AddThemeColorOverride("font_color", new Color("#CCCCCC")); vbox.AddChild(label); var bar = new ProgressBar @@ -34,18 +36,34 @@ public partial class ObjectivePanel : VBoxContainer MinValue = 0, MaxValue = demand.Amount, Value = 0, - CustomMinimumSize = new Vector2(180, 16), + CustomMinimumSize = new Vector2(180, 14), ShowPercentage = false }; + + // Style the progress bar + var bgStyle = new StyleBoxFlat + { + BgColor = new Color(0.2f, 0.2f, 0.22f), + CornerRadiusTopLeft = 3, CornerRadiusTopRight = 3, + CornerRadiusBottomLeft = 3, CornerRadiusBottomRight = 3 + }; + var fillStyle = new StyleBoxFlat + { + BgColor = new Color("#3D6B8E"), // teal fill + CornerRadiusTopLeft = 3, CornerRadiusTopRight = 3, + CornerRadiusBottomLeft = 3, CornerRadiusBottomRight = 3 + }; + bar.AddThemeStyleboxOverride("background", bgStyle); + bar.AddThemeStyleboxOverride("fill", fillStyle); vbox.AddChild(bar); var deadline = new Label { Text = $"Deadline: {demand.Deadline} coups" }; deadline.AddThemeFontSizeOverride("font_size", 10); - deadline.AddThemeColorOverride("font_color", new Color("#AAAAAA")); + deadline.AddThemeColorOverride("font_color", new Color("#777777")); vbox.AddChild(deadline); AddChild(vbox); - _entries[demand.Position] = (label, bar); + _entries[demand.Position] = (label, bar, deadline); } } @@ -54,9 +72,24 @@ public partial class ObjectivePanel : VBoxContainer if (!_entries.TryGetValue(demandCell, out var entry)) return; entry.label.Text = $"{name}: {current}/{required}"; - entry.bar.Value = current; + + // Animate the progress bar value + var tween = entry.bar.CreateTween(); + tween.TweenProperty(entry.bar, "value", (double)current, 0.2f) + .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic); if (current >= required) - entry.label.AddThemeColorOverride("font_color", new Color("#44CC44")); + { + entry.label.AddThemeColorOverride("font_color", new Color("#5AAC5A")); // warm green + + // Flash the progress bar green + var fillStyle = new StyleBoxFlat + { + BgColor = new Color("#5AAC5A"), + CornerRadiusTopLeft = 3, CornerRadiusTopRight = 3, + CornerRadiusBottomLeft = 3, CornerRadiusBottomRight = 3 + }; + entry.bar.AddThemeStyleboxOverride("fill", fillStyle); + } } }