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);
+ }
}
}